From 568a0ac3974d837d7114e4480691b1d717451be2 Mon Sep 17 00:00:00 2001 From: lat9nq Date: Sun, 10 Jul 2022 11:46:53 -0400 Subject: [PATCH 01/50] yuzu: Rename check_vulkan to startup_checks --- src/yuzu/CMakeLists.txt | 4 ++-- src/yuzu/main.cpp | 2 +- src/yuzu/{check_vulkan.cpp => startup_checks.cpp} | 0 src/yuzu/{check_vulkan.h => startup_checks.h} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename src/yuzu/{check_vulkan.cpp => startup_checks.cpp} (100%) rename src/yuzu/{check_vulkan.h => startup_checks.h} (100%) diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 242867a4f0..534e553550 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -30,8 +30,6 @@ add_executable(yuzu applets/qt_web_browser_scripts.h bootmanager.cpp bootmanager.h - check_vulkan.cpp - check_vulkan.h compatdb.ui compatibility_list.cpp compatibility_list.h @@ -155,6 +153,8 @@ add_executable(yuzu main.cpp main.h main.ui + startup_checks.cpp + startup_checks.h uisettings.cpp uisettings.h util/controller_navigation.cpp diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index b460020b16..64be8bf614 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -115,7 +115,6 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "video_core/shader_notify.h" #include "yuzu/about_dialog.h" #include "yuzu/bootmanager.h" -#include "yuzu/check_vulkan.h" #include "yuzu/compatdb.h" #include "yuzu/compatibility_list.h" #include "yuzu/configuration/config.h" @@ -131,6 +130,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "yuzu/install_dialog.h" #include "yuzu/loading_screen.h" #include "yuzu/main.h" +#include "yuzu/startup_checks.h" #include "yuzu/uisettings.h" using namespace Common::Literals; diff --git a/src/yuzu/check_vulkan.cpp b/src/yuzu/startup_checks.cpp similarity index 100% rename from src/yuzu/check_vulkan.cpp rename to src/yuzu/startup_checks.cpp diff --git a/src/yuzu/check_vulkan.h b/src/yuzu/startup_checks.h similarity index 100% rename from src/yuzu/check_vulkan.h rename to src/yuzu/startup_checks.h From 4f15d9ed6fba4fa1804c5be3f9378e3ad3d32688 Mon Sep 17 00:00:00 2001 From: lat9nq Date: Sun, 10 Jul 2022 14:08:20 -0400 Subject: [PATCH 02/50] yuzu: Check Vulkan on startup with a child --- src/yuzu/main.cpp | 15 +++++++++ src/yuzu/startup_checks.cpp | 61 ++++++++++++++++++++++++++++++++++++- src/yuzu/startup_checks.h | 3 ++ 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 64be8bf614..f2e449560f 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -3853,6 +3853,21 @@ void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { #endif int main(int argc, char* argv[]) { +#ifdef _WIN32 + char variable_contents[32]; + const DWORD startup_check_var = + GetEnvironmentVariable(STARTUP_CHECK_ENV_VAR, variable_contents, 32); + if (startup_check_var != 0) { + std::fprintf(stderr, "perform statup checks\n"); + CheckVulkan(); + return 0; + } else { + std::fprintf(stderr, "%d\n", StartupChecks()); + } +#elif YUZU_UNIX +#error "Unimplemented" +#endif + Common::DetachedTasks detached_tasks; MicroProfileOnThreadCreate("Frontend"); SCOPE_EXIT({ MicroProfileShutdown(); }); diff --git a/src/yuzu/startup_checks.cpp b/src/yuzu/startup_checks.cpp index e6d66ab349..cfd14a6d5f 100644 --- a/src/yuzu/startup_checks.cpp +++ b/src/yuzu/startup_checks.cpp @@ -3,6 +3,15 @@ #include "video_core/vulkan_common/vulkan_wrapper.h" +#ifdef _WIN32 +#include // for memset, strncpy +#include +#include +#elif defined(YUZU_UNIX) +#include +#endif + +#include #include #include #include "common/fs/fs.h" @@ -10,7 +19,7 @@ #include "common/logging/log.h" #include "video_core/vulkan_common/vulkan_instance.h" #include "video_core/vulkan_common/vulkan_library.h" -#include "yuzu/check_vulkan.h" +#include "yuzu/startup_checks.h" #include "yuzu/uisettings.h" constexpr char TEMP_FILE_NAME[] = "vulkan_check"; @@ -51,3 +60,53 @@ bool CheckVulkan() { std::filesystem::remove(temp_file_loc); return true; } + +bool StartupChecks() { +#ifdef _WIN32 + const bool env_var_set = SetEnvironmentVariableA(STARTUP_CHECK_ENV_VAR, "ON"); + if (!env_var_set) { + LOG_ERROR(Frontend, "SetEnvironmentVariableA failed to set {}, {}", STARTUP_CHECK_ENV_VAR, + GetLastError()); + return false; + } + + STARTUPINFOA startup_info; + PROCESS_INFORMATION process_info; + + std::memset(&startup_info, '\0', sizeof(startup_info)); + std::memset(&process_info, '\0', sizeof(process_info)); + startup_info.cb = sizeof(startup_info); + + char p_name[255]; + std::strncpy(p_name, "yuzu.exe", 255); + + // TODO: use argv[0] instead of yuzu.exe + const bool process_created = CreateProcessA(nullptr, // lpApplicationName + p_name, // lpCommandLine + nullptr, // lpProcessAttributes + nullptr, // lpThreadAttributes + false, // bInheritHandles + 0, // dwCreationFlags + nullptr, // lpEnvironment + nullptr, // lpCurrentDirectory + &startup_info, // lpStartupInfo + &process_info // lpProcessInformation + ); + if (!process_created) { + LOG_ERROR(Frontend, "CreateProcessA failed, {}", GetLastError()); + return false; + } + + // wait until the processs exits + DWORD exit_code = STILL_ACTIVE; + while (exit_code == STILL_ACTIVE) { + GetExitCodeProcess(process_info.hProcess, &exit_code); + } + + std::fprintf(stderr, "exit code: %d\n", exit_code); + + CloseHandle(process_info.hProcess); + CloseHandle(process_info.hThread); +#endif + return true; +} diff --git a/src/yuzu/startup_checks.h b/src/yuzu/startup_checks.h index e4ea935822..98bd5f4bff 100644 --- a/src/yuzu/startup_checks.h +++ b/src/yuzu/startup_checks.h @@ -3,4 +3,7 @@ #pragma once +constexpr char STARTUP_CHECK_ENV_VAR[] = "YUZU_DO_STARTUP_CHECKS"; + bool CheckVulkan(); +bool StartupChecks(); From 33abdfff9bf0a549b0d1d327e914e9a1ab4b799b Mon Sep 17 00:00:00 2001 From: lat9nq Date: Sun, 10 Jul 2022 16:10:35 -0400 Subject: [PATCH 03/50] yuzu: Simplify broken Vulkan handling --- src/yuzu/configuration/config.cpp | 8 -- src/yuzu/configuration/configure_graphics.cpp | 37 ++------- src/yuzu/configuration/configure_graphics.h | 2 +- src/yuzu/configuration/configure_graphics.ui | 9 +-- src/yuzu/main.cpp | 30 ++++--- src/yuzu/main.h | 2 +- src/yuzu/startup_checks.cpp | 78 ++++++++----------- src/yuzu/startup_checks.h | 12 ++- src/yuzu/uisettings.h | 2 +- 9 files changed, 65 insertions(+), 115 deletions(-) diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 9686412d07..ca10e9f236 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -682,12 +682,6 @@ void Config::ReadRendererValues() { ReadGlobalSetting(Settings::values.bg_green); ReadGlobalSetting(Settings::values.bg_blue); - if (!global && UISettings::values.has_broken_vulkan && - Settings::values.renderer_backend.GetValue() == Settings::RendererBackend::Vulkan && - !Settings::values.renderer_backend.UsingGlobal()) { - Settings::values.renderer_backend.SetGlobal(true); - } - if (global) { ReadBasicSetting(Settings::values.renderer_debug); ReadBasicSetting(Settings::values.renderer_shader_feedback); @@ -807,7 +801,6 @@ void Config::ReadUIValues() { ReadBasicSetting(UISettings::values.pause_when_in_background); ReadBasicSetting(UISettings::values.mute_when_in_background); ReadBasicSetting(UISettings::values.hide_mouse); - ReadBasicSetting(UISettings::values.has_broken_vulkan); ReadBasicSetting(UISettings::values.disable_web_applet); qt_config->endGroup(); @@ -1355,7 +1348,6 @@ void Config::SaveUIValues() { WriteBasicSetting(UISettings::values.pause_when_in_background); WriteBasicSetting(UISettings::values.mute_when_in_background); WriteBasicSetting(UISettings::values.hide_mouse); - WriteBasicSetting(UISettings::values.has_broken_vulkan); WriteBasicSetting(UISettings::values.disable_web_applet); qt_config->endGroup(); diff --git a/src/yuzu/configuration/configure_graphics.cpp b/src/yuzu/configuration/configure_graphics.cpp index 85f34dc35f..6b33c45351 100644 --- a/src/yuzu/configuration/configure_graphics.cpp +++ b/src/yuzu/configuration/configure_graphics.cpp @@ -58,24 +58,9 @@ ConfigureGraphics::ConfigureGraphics(const Core::System& system_, QWidget* paren UpdateBackgroundColorButton(new_bg_color); }); - connect(ui->button_check_vulkan, &QAbstractButton::clicked, this, [this] { - UISettings::values.has_broken_vulkan = false; - - if (RetrieveVulkanDevices()) { - ui->api->setEnabled(true); - ui->button_check_vulkan->hide(); - - for (const auto& device : vulkan_devices) { - ui->device->addItem(device); - } - } else { - UISettings::values.has_broken_vulkan = true; - } - }); - - ui->api->setEnabled(!UISettings::values.has_broken_vulkan.GetValue()); - ui->button_check_vulkan->setVisible(UISettings::values.has_broken_vulkan.GetValue()); - + ui->api->setEnabled(!UISettings::values.has_broken_vulkan); + ui->api_widget->setEnabled(!UISettings::values.has_broken_vulkan || + Settings::IsConfiguringGlobal()); ui->bg_label->setVisible(Settings::IsConfiguringGlobal()); ui->bg_combobox->setVisible(!Settings::IsConfiguringGlobal()); } @@ -315,7 +300,7 @@ void ConfigureGraphics::UpdateAPILayout() { vulkan_device = Settings::values.vulkan_device.GetValue(true); shader_backend = Settings::values.shader_backend.GetValue(true); ui->device_widget->setEnabled(false); - ui->backend_widget->setEnabled(UISettings::values.has_broken_vulkan.GetValue()); + ui->backend_widget->setEnabled(false); } else { vulkan_device = Settings::values.vulkan_device.GetValue(); shader_backend = Settings::values.shader_backend.GetValue(); @@ -337,9 +322,9 @@ void ConfigureGraphics::UpdateAPILayout() { } } -bool ConfigureGraphics::RetrieveVulkanDevices() try { +void ConfigureGraphics::RetrieveVulkanDevices() try { if (UISettings::values.has_broken_vulkan) { - return false; + return; } using namespace Vulkan; @@ -355,11 +340,8 @@ bool ConfigureGraphics::RetrieveVulkanDevices() try { const std::string name = vk::PhysicalDevice(device, dld).GetProperties().deviceName; vulkan_devices.push_back(QString::fromStdString(name)); } - - return true; } catch (const Vulkan::vk::Exception& exception) { LOG_ERROR(Frontend, "Failed to enumerate devices with error: {}", exception.what()); - return false; } Settings::RendererBackend ConfigureGraphics::GetCurrentGraphicsBackend() const { @@ -440,11 +422,4 @@ void ConfigureGraphics::SetupPerGameUI() { ui->api, static_cast(Settings::values.renderer_backend.GetValue(true))); ConfigurationShared::InsertGlobalItem( ui->nvdec_emulation, static_cast(Settings::values.nvdec_emulation.GetValue(true))); - - if (UISettings::values.has_broken_vulkan) { - ui->backend_widget->setEnabled(true); - ConfigurationShared::SetColoredComboBox( - ui->backend, ui->backend_widget, - static_cast(Settings::values.shader_backend.GetValue(true))); - } } diff --git a/src/yuzu/configuration/configure_graphics.h b/src/yuzu/configuration/configure_graphics.h index 8438f01876..1b101c9405 100644 --- a/src/yuzu/configuration/configure_graphics.h +++ b/src/yuzu/configuration/configure_graphics.h @@ -41,7 +41,7 @@ private: void UpdateDeviceSelection(int device); void UpdateShaderBackendSelection(int backend); - bool RetrieveVulkanDevices(); + void RetrieveVulkanDevices(); void SetupPerGameUI(); diff --git a/src/yuzu/configuration/configure_graphics.ui b/src/yuzu/configuration/configure_graphics.ui index 2f94c94bca..1e4f74704b 100644 --- a/src/yuzu/configuration/configure_graphics.ui +++ b/src/yuzu/configuration/configure_graphics.ui @@ -6,7 +6,7 @@ 0 0 - 471 + 541 759 @@ -574,13 +574,6 @@ - - - - Check for Working Vulkan - - - diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index f2e449560f..a2b11fdbf4 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -252,7 +252,7 @@ static QString PrettyProductName() { return QSysInfo::prettyProductName(); } -GMainWindow::GMainWindow() +GMainWindow::GMainWindow(bool has_broken_vulkan) : ui{std::make_unique()}, system{std::make_unique()}, input_subsystem{std::make_shared()}, config{std::make_unique(*system)}, @@ -352,17 +352,15 @@ GMainWindow::GMainWindow() MigrateConfigFiles(); - if (!CheckVulkan()) { - config->Save(); + if (has_broken_vulkan) { + UISettings::values.has_broken_vulkan = true; + + QMessageBox::warning(this, tr("Broken Vulkan Installation Detected"), + tr("Vulkan initialization failed during boot.

Click " + "here for instructions to fix the issue.")); - QMessageBox::warning( - this, tr("Broken Vulkan Installation Detected"), - tr("Vulkan initialization failed on the previous boot.

Click here for " - "instructions to fix the issue.")); - } - if (UISettings::values.has_broken_vulkan) { Settings::values.renderer_backend = Settings::RendererBackend::OpenGL; renderer_status_button->setDisabled(true); @@ -3853,17 +3851,17 @@ void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { #endif int main(int argc, char* argv[]) { + bool has_broken_vulkan = false; #ifdef _WIN32 char variable_contents[32]; const DWORD startup_check_var = GetEnvironmentVariable(STARTUP_CHECK_ENV_VAR, variable_contents, 32); - if (startup_check_var != 0) { - std::fprintf(stderr, "perform statup checks\n"); + const std::string variable_contents_s{variable_contents}; + if (startup_check_var > 0 && variable_contents_s == "ON") { CheckVulkan(); return 0; - } else { - std::fprintf(stderr, "%d\n", StartupChecks()); } + StartupChecks(argv[0], &has_broken_vulkan); #elif YUZU_UNIX #error "Unimplemented" #endif @@ -3907,7 +3905,7 @@ int main(int argc, char* argv[]) { // generating shaders setlocale(LC_ALL, "C"); - GMainWindow main_window{}; + GMainWindow main_window{has_broken_vulkan}; // After settings have been loaded by GMainWindow, apply the filter main_window.show(); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 8cf224c9c2..7df6560025 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -118,7 +118,7 @@ class GMainWindow : public QMainWindow { public: void filterBarSetChecked(bool state); void UpdateUITheme(); - explicit GMainWindow(); + explicit GMainWindow(bool has_broken_vulkan); ~GMainWindow() override; bool DropAction(QDropEvent* event); diff --git a/src/yuzu/startup_checks.cpp b/src/yuzu/startup_checks.cpp index cfd14a6d5f..c860f0aa03 100644 --- a/src/yuzu/startup_checks.cpp +++ b/src/yuzu/startup_checks.cpp @@ -22,29 +22,7 @@ #include "yuzu/startup_checks.h" #include "yuzu/uisettings.h" -constexpr char TEMP_FILE_NAME[] = "vulkan_check"; - -bool CheckVulkan() { - if (UISettings::values.has_broken_vulkan) { - return true; - } - - LOG_DEBUG(Frontend, "Checking presence of Vulkan"); - - const auto fs_config_loc = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir); - const auto temp_file_loc = fs_config_loc / TEMP_FILE_NAME; - - if (std::filesystem::exists(temp_file_loc)) { - LOG_WARNING(Frontend, "Detected recovery from previous failed Vulkan initialization"); - - UISettings::values.has_broken_vulkan = true; - std::filesystem::remove(temp_file_loc); - return false; - } - - std::ofstream temp_file_handle(temp_file_loc); - temp_file_handle.close(); - +void CheckVulkan() { try { Vulkan::vk::InstanceDispatch dld; const Common::DynamicLibrary library = Vulkan::OpenLibrary(); @@ -53,32 +31,48 @@ bool CheckVulkan() { } catch (const Vulkan::vk::Exception& exception) { LOG_ERROR(Frontend, "Failed to initialize Vulkan: {}", exception.what()); - // Don't set has_broken_vulkan to true here: we care when loading Vulkan crashes the - // application, not when we can handle it. } - - std::filesystem::remove(temp_file_loc); - return true; } -bool StartupChecks() { +bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { #ifdef _WIN32 const bool env_var_set = SetEnvironmentVariableA(STARTUP_CHECK_ENV_VAR, "ON"); if (!env_var_set) { - LOG_ERROR(Frontend, "SetEnvironmentVariableA failed to set {}, {}", STARTUP_CHECK_ENV_VAR, - GetLastError()); + std::fprintf(stderr, "SetEnvironmentVariableA failed to set %s, %d\n", + STARTUP_CHECK_ENV_VAR, GetLastError()); return false; } - STARTUPINFOA startup_info; PROCESS_INFORMATION process_info; + std::memset(&process_info, '\0', sizeof(process_info)); + + if (!SpawnChild(arg0, &process_info)) { + return false; + } + + // wait until the processs exits + DWORD exit_code = STILL_ACTIVE; + while (exit_code == STILL_ACTIVE) { + GetExitCodeProcess(process_info.hProcess, &exit_code); + } + + *has_broken_vulkan = (exit_code != 0); + + CloseHandle(process_info.hProcess); + CloseHandle(process_info.hThread); +#endif + return true; +} + +#ifdef _WIN32 +bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi) { + STARTUPINFOA startup_info; std::memset(&startup_info, '\0', sizeof(startup_info)); - std::memset(&process_info, '\0', sizeof(process_info)); startup_info.cb = sizeof(startup_info); char p_name[255]; - std::strncpy(p_name, "yuzu.exe", 255); + std::strncpy(p_name, arg0, 255); // TODO: use argv[0] instead of yuzu.exe const bool process_created = CreateProcessA(nullptr, // lpApplicationName @@ -90,23 +84,13 @@ bool StartupChecks() { nullptr, // lpEnvironment nullptr, // lpCurrentDirectory &startup_info, // lpStartupInfo - &process_info // lpProcessInformation + pi // lpProcessInformation ); if (!process_created) { - LOG_ERROR(Frontend, "CreateProcessA failed, {}", GetLastError()); + std::fprintf(stderr, "CreateProcessA failed, %d\n", GetLastError()); return false; } - // wait until the processs exits - DWORD exit_code = STILL_ACTIVE; - while (exit_code == STILL_ACTIVE) { - GetExitCodeProcess(process_info.hProcess, &exit_code); - } - - std::fprintf(stderr, "exit code: %d\n", exit_code); - - CloseHandle(process_info.hProcess); - CloseHandle(process_info.hThread); -#endif return true; } +#endif diff --git a/src/yuzu/startup_checks.h b/src/yuzu/startup_checks.h index 98bd5f4bff..096dd54a89 100644 --- a/src/yuzu/startup_checks.h +++ b/src/yuzu/startup_checks.h @@ -3,7 +3,15 @@ #pragma once +#ifdef _WIN32 +#include +#endif + constexpr char STARTUP_CHECK_ENV_VAR[] = "YUZU_DO_STARTUP_CHECKS"; -bool CheckVulkan(); -bool StartupChecks(); +void CheckVulkan(); +bool StartupChecks(const char* arg0, bool* has_broken_vulkan); + +#ifdef _WIN32 +bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi); +#endif diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h index 044d88ca6a..2f6948243d 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h @@ -78,7 +78,7 @@ struct Values { Settings::Setting mute_when_in_background{false, "muteWhenInBackground"}; Settings::Setting hide_mouse{true, "hideInactiveMouse"}; // Set when Vulkan is known to crash the application - Settings::Setting has_broken_vulkan{false, "has_broken_vulkan"}; + bool has_broken_vulkan = false; Settings::Setting select_user_on_boot{false, "select_user_on_boot"}; From fd4e48f96e8328bbace2d4df4f2a6f7ab9a699f1 Mon Sep 17 00:00:00 2001 From: lat9nq Date: Sun, 10 Jul 2022 17:01:37 -0400 Subject: [PATCH 04/50] startup_checks: Implement unix side code Wow fork() is nice, isn't it? --- src/yuzu/main.cpp | 12 +-------- src/yuzu/startup_checks.cpp | 53 ++++++++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index a2b11fdbf4..69f9cdd9ca 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -3852,19 +3852,9 @@ void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { int main(int argc, char* argv[]) { bool has_broken_vulkan = false; -#ifdef _WIN32 - char variable_contents[32]; - const DWORD startup_check_var = - GetEnvironmentVariable(STARTUP_CHECK_ENV_VAR, variable_contents, 32); - const std::string variable_contents_s{variable_contents}; - if (startup_check_var > 0 && variable_contents_s == "ON") { - CheckVulkan(); + if (StartupChecks(argv[0], &has_broken_vulkan)) { return 0; } - StartupChecks(argv[0], &has_broken_vulkan); -#elif YUZU_UNIX -#error "Unimplemented" -#endif Common::DetachedTasks detached_tasks; MicroProfileOnThreadCreate("Frontend"); diff --git a/src/yuzu/startup_checks.cpp b/src/yuzu/startup_checks.cpp index c860f0aa03..6bd895400a 100644 --- a/src/yuzu/startup_checks.cpp +++ b/src/yuzu/startup_checks.cpp @@ -8,6 +8,8 @@ #include #include #elif defined(YUZU_UNIX) +#include +#include #include #endif @@ -36,9 +38,20 @@ void CheckVulkan() { bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { #ifdef _WIN32 + // Check environment variable to see if we are the child + char variable_contents[32]; + const DWORD startup_check_var = + GetEnvironmentVariable(STARTUP_CHECK_ENV_VAR, variable_contents, 32); + const std::string variable_contents_s{variable_contents}; + if (startup_check_var > 0 && variable_contents_s == "ON") { + CheckVulkan(); + return true; + } + + // Set the startup variable for child processes const bool env_var_set = SetEnvironmentVariableA(STARTUP_CHECK_ENV_VAR, "ON"); if (!env_var_set) { - std::fprintf(stderr, "SetEnvironmentVariableA failed to set %s, %d\n", + std::fprintf(stderr, "SetEnvironmentVariableA failed to set %s with error %d\n", STARTUP_CHECK_ENV_VAR, GetLastError()); return false; } @@ -53,15 +66,43 @@ bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { // wait until the processs exits DWORD exit_code = STILL_ACTIVE; while (exit_code == STILL_ACTIVE) { - GetExitCodeProcess(process_info.hProcess, &exit_code); + const int err = GetExitCodeProcess(process_info.hProcess, &exit_code); + if (err == 0) { + std::fprintf(stderr, "GetExitCodeProcess failed with error %d\n", GetLastError()); + break; + } } *has_broken_vulkan = (exit_code != 0); - CloseHandle(process_info.hProcess); - CloseHandle(process_info.hThread); + if (CloseHandle(process_info.hProcess) == 0) { + std::fprintf(stderr, "CloseHandle failed with error %d\n", GetLastError()); + } + if (CloseHandle(process_info.hThread) == 0) { + std::fprintf(stderr, "CloseHandle failed with error %d\n", GetLastError()); + } + +#elif defined(YUZU_UNIX) + const pid_t pid = fork(); + if (pid == 0) { + CheckVulkan(); + return true; + } else if (pid == -1) { + const int err = errno; + std::fprintf(stderr, "fork failed with error %d\n", err); + return false; + } + + int status; + const int r_val = wait(&status); + if (r_val == -1) { + const int err = errno; + std::fprintf(stderr, "wait failed with error %d\n", err); + return false; + } + *has_broken_vulkan = (status != 0); #endif - return true; + return false; } #ifdef _WIN32 @@ -87,7 +128,7 @@ bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi) { pi // lpProcessInformation ); if (!process_created) { - std::fprintf(stderr, "CreateProcessA failed, %d\n", GetLastError()); + std::fprintf(stderr, "CreateProcessA failed with error %d\n", GetLastError()); return false; } From d57cd8bcddab1dadec76dc7274f63e8bdf30a32b Mon Sep 17 00:00:00 2001 From: lat9nq Date: Sun, 10 Jul 2022 17:18:31 -0400 Subject: [PATCH 05/50] startup_checks: Clean up Adds some comments, removes unused includes, and removes last bits of logging since this is before the logging backend starts up. --- src/yuzu/startup_checks.cpp | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/yuzu/startup_checks.cpp b/src/yuzu/startup_checks.cpp index 6bd895400a..b27b9c8d93 100644 --- a/src/yuzu/startup_checks.cpp +++ b/src/yuzu/startup_checks.cpp @@ -14,17 +14,12 @@ #endif #include -#include -#include -#include "common/fs/fs.h" -#include "common/fs/path_util.h" -#include "common/logging/log.h" #include "video_core/vulkan_common/vulkan_instance.h" #include "video_core/vulkan_common/vulkan_library.h" #include "yuzu/startup_checks.h" -#include "yuzu/uisettings.h" void CheckVulkan() { + // Just start the Vulkan loader, this will crash if something is wrong try { Vulkan::vk::InstanceDispatch dld; const Common::DynamicLibrary library = Vulkan::OpenLibrary(); @@ -32,7 +27,7 @@ void CheckVulkan() { Vulkan::CreateInstance(library, dld, VK_API_VERSION_1_0); } catch (const Vulkan::vk::Exception& exception) { - LOG_ERROR(Frontend, "Failed to initialize Vulkan: {}", exception.what()); + std::fprintf(stderr, "Failed to initialize Vulkan: %s\n", exception.what()); } } @@ -63,7 +58,7 @@ bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { return false; } - // wait until the processs exits + // Wait until the processs exits and get exit code from it DWORD exit_code = STILL_ACTIVE; while (exit_code == STILL_ACTIVE) { const int err = GetExitCodeProcess(process_info.hProcess, &exit_code); @@ -73,6 +68,7 @@ bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { } } + // Vulkan is broken if the child crashed (return value is not zero) *has_broken_vulkan = (exit_code != 0); if (CloseHandle(process_info.hProcess) == 0) { @@ -93,6 +89,7 @@ bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { return false; } + // Get exit code from child process int status; const int r_val = wait(&status); if (r_val == -1) { @@ -100,6 +97,7 @@ bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { std::fprintf(stderr, "wait failed with error %d\n", err); return false; } + // Vulkan is broken if the child crashed (return value is not zero) *has_broken_vulkan = (status != 0); #endif return false; @@ -115,7 +113,6 @@ bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi) { char p_name[255]; std::strncpy(p_name, arg0, 255); - // TODO: use argv[0] instead of yuzu.exe const bool process_created = CreateProcessA(nullptr, // lpApplicationName p_name, // lpCommandLine nullptr, // lpProcessAttributes From 2d2a69ab5bec849c923b743186e827460221a4b4 Mon Sep 17 00:00:00 2001 From: lat9nq Date: Sun, 10 Jul 2022 20:24:10 -0400 Subject: [PATCH 06/50] startup_checks: Use GetEnvironmentVariableA Solves MSVC compile error. Also drops need string use for comparison. --- src/yuzu/startup_checks.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/yuzu/startup_checks.cpp b/src/yuzu/startup_checks.cpp index b27b9c8d93..0919d89c65 100644 --- a/src/yuzu/startup_checks.cpp +++ b/src/yuzu/startup_checks.cpp @@ -34,11 +34,10 @@ void CheckVulkan() { bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { #ifdef _WIN32 // Check environment variable to see if we are the child - char variable_contents[32]; + char variable_contents[8]; const DWORD startup_check_var = - GetEnvironmentVariable(STARTUP_CHECK_ENV_VAR, variable_contents, 32); - const std::string variable_contents_s{variable_contents}; - if (startup_check_var > 0 && variable_contents_s == "ON") { + GetEnvironmentVariableA(STARTUP_CHECK_ENV_VAR, variable_contents, 8); + if (startup_check_var > 0 && std::strncmp(variable_contents, "ON", 8) == 0) { CheckVulkan(); return true; } From 7d9369d15ea6061e4b3a48cc8dbe442501a86ba1 Mon Sep 17 00:00:00 2001 From: lat9nq Date: Tue, 12 Jul 2022 14:23:50 -0400 Subject: [PATCH 07/50] startup_checks: Use WaitForSingleObject and more cleanup --- src/yuzu/startup_checks.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/yuzu/startup_checks.cpp b/src/yuzu/startup_checks.cpp index 0919d89c65..8421280bf2 100644 --- a/src/yuzu/startup_checks.cpp +++ b/src/yuzu/startup_checks.cpp @@ -58,13 +58,11 @@ bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { } // Wait until the processs exits and get exit code from it + WaitForSingleObject(process_info.hProcess, INFINITE); DWORD exit_code = STILL_ACTIVE; - while (exit_code == STILL_ACTIVE) { - const int err = GetExitCodeProcess(process_info.hProcess, &exit_code); - if (err == 0) { - std::fprintf(stderr, "GetExitCodeProcess failed with error %d\n", GetLastError()); - break; - } + const int err = GetExitCodeProcess(process_info.hProcess, &exit_code); + if (err == 0) { + std::fprintf(stderr, "GetExitCodeProcess failed with error %d\n", GetLastError()); } // Vulkan is broken if the child crashed (return value is not zero) @@ -77,6 +75,11 @@ bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { std::fprintf(stderr, "CloseHandle failed with error %d\n", GetLastError()); } + if (!SetEnvironmentVariableA(STARTUP_CHECK_ENV_VAR, nullptr)) { + std::fprintf(stderr, "SetEnvironmentVariableA failed to clear %s with error %d\n", + STARTUP_CHECK_ENV_VAR, GetLastError()); + } + #elif defined(YUZU_UNIX) const pid_t pid = fork(); if (pid == 0) { From 0624c880bd5af45ae9095465e079fa55458515f6 Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 26 Jun 2022 18:52:16 -0400 Subject: [PATCH 08/50] kernel: use KScheduler from mesosphere --- src/core/arm/arm_interface.cpp | 3 +- src/core/cpu_manager.cpp | 169 ++-- src/core/cpu_manager.h | 14 +- .../hle/kernel/global_scheduler_context.cpp | 2 +- src/core/hle/kernel/k_interrupt_manager.cpp | 7 + src/core/hle/kernel/k_scheduler.cpp | 733 +++++++++--------- src/core/hle/kernel/k_scheduler.h | 220 +++--- src/core/hle/kernel/k_scheduler_lock.h | 2 + src/core/hle/kernel/k_thread.cpp | 11 +- src/core/hle/kernel/k_thread.h | 5 + src/core/hle/kernel/kernel.cpp | 10 +- src/core/hle/kernel/svc.cpp | 7 +- 12 files changed, 572 insertions(+), 611 deletions(-) diff --git a/src/core/arm/arm_interface.cpp b/src/core/arm/arm_interface.cpp index cef79b245a..cdf388fb9b 100644 --- a/src/core/arm/arm_interface.cpp +++ b/src/core/arm/arm_interface.cpp @@ -155,9 +155,10 @@ void ARM_Interface::Run() { break; } - // Handle syscalls and scheduling (this may change the current thread) + // Handle syscalls and scheduling (this may change the current thread/core) if (Has(hr, svc_call)) { Kernel::Svc::Call(system, GetSvcNumber()); + break; } if (Has(hr, break_loop) || !uses_wall_clock) { break; diff --git a/src/core/cpu_manager.cpp b/src/core/cpu_manager.cpp index 37d3d83b98..4281941291 100644 --- a/src/core/cpu_manager.cpp +++ b/src/core/cpu_manager.cpp @@ -8,6 +8,7 @@ #include "core/core.h" #include "core/core_timing.h" #include "core/cpu_manager.h" +#include "core/hle/kernel/k_interrupt_manager.h" #include "core/hle/kernel/k_scheduler.h" #include "core/hle/kernel/k_thread.h" #include "core/hle/kernel/kernel.h" @@ -41,6 +42,14 @@ void CpuManager::Shutdown() { } } +void CpuManager::GuestActivateFunction() { + if (is_multicore) { + MultiCoreGuestActivate(); + } else { + SingleCoreGuestActivate(); + } +} + void CpuManager::GuestThreadFunction() { if (is_multicore) { MultiCoreRunGuestThread(); @@ -49,36 +58,49 @@ void CpuManager::GuestThreadFunction() { } } -void CpuManager::GuestRewindFunction() { - if (is_multicore) { - MultiCoreRunGuestLoop(); - } else { - SingleCoreRunGuestLoop(); - } -} - -void CpuManager::IdleThreadFunction() { - if (is_multicore) { - MultiCoreRunIdleThread(); - } else { - SingleCoreRunIdleThread(); - } -} - void CpuManager::ShutdownThreadFunction() { ShutdownThread(); } +void CpuManager::WaitForAndHandleInterrupt() { + auto& kernel = system.Kernel(); + auto& physical_core = kernel.CurrentPhysicalCore(); + + ASSERT(Kernel::GetCurrentThread(kernel).GetDisableDispatchCount() == 1); + + if (!physical_core.IsInterrupted()) { + physical_core.Idle(); + } + + HandleInterrupt(); +} + +void CpuManager::HandleInterrupt() { + auto& kernel = system.Kernel(); + auto core_index = kernel.CurrentPhysicalCoreIndex(); + + Kernel::KInterruptManager::HandleInterrupt(kernel, static_cast(core_index)); +} + /////////////////////////////////////////////////////////////////////////////// /// MultiCore /// /////////////////////////////////////////////////////////////////////////////// -void CpuManager::MultiCoreRunGuestThread() { +void CpuManager::MultiCoreGuestActivate() { + // Similar to the HorizonKernelMain callback in HOS auto& kernel = system.Kernel(); - kernel.CurrentScheduler()->OnThreadStart(); - auto* thread = kernel.CurrentScheduler()->GetSchedulerCurrentThread(); - auto& host_context = thread->GetHostContext(); - host_context->SetRewindPoint([this] { GuestRewindFunction(); }); + auto* scheduler = kernel.CurrentScheduler(); + + scheduler->Activate(); + UNREACHABLE(); +} + +void CpuManager::MultiCoreRunGuestThread() { + // Similar to UserModeThreadStarter in HOS + auto& kernel = system.Kernel(); + auto* thread = kernel.GetCurrentEmuThread(); + thread->EnableDispatch(); + MultiCoreRunGuestLoop(); } @@ -91,18 +113,8 @@ void CpuManager::MultiCoreRunGuestLoop() { physical_core->Run(); physical_core = &kernel.CurrentPhysicalCore(); } - { - Kernel::KScopedDisableDispatch dd(kernel); - physical_core->ArmInterface().ClearExclusiveState(); - } - } -} -void CpuManager::MultiCoreRunIdleThread() { - auto& kernel = system.Kernel(); - while (true) { - Kernel::KScopedDisableDispatch dd(kernel); - kernel.CurrentPhysicalCore().Idle(); + HandleInterrupt(); } } @@ -110,83 +122,20 @@ void CpuManager::MultiCoreRunIdleThread() { /// SingleCore /// /////////////////////////////////////////////////////////////////////////////// -void CpuManager::SingleCoreRunGuestThread() { - auto& kernel = system.Kernel(); - kernel.CurrentScheduler()->OnThreadStart(); - auto* thread = kernel.CurrentScheduler()->GetSchedulerCurrentThread(); - auto& host_context = thread->GetHostContext(); - host_context->SetRewindPoint([this] { GuestRewindFunction(); }); - SingleCoreRunGuestLoop(); -} +void CpuManager::SingleCoreGuestActivate() {} -void CpuManager::SingleCoreRunGuestLoop() { - auto& kernel = system.Kernel(); - while (true) { - auto* physical_core = &kernel.CurrentPhysicalCore(); - if (!physical_core->IsInterrupted()) { - physical_core->Run(); - physical_core = &kernel.CurrentPhysicalCore(); - } - kernel.SetIsPhantomModeForSingleCore(true); - system.CoreTiming().Advance(); - kernel.SetIsPhantomModeForSingleCore(false); - physical_core->ArmInterface().ClearExclusiveState(); - PreemptSingleCore(); - auto& scheduler = kernel.Scheduler(current_core); - scheduler.RescheduleCurrentCore(); - } -} +void CpuManager::SingleCoreRunGuestThread() {} -void CpuManager::SingleCoreRunIdleThread() { - auto& kernel = system.Kernel(); - while (true) { - auto& physical_core = kernel.CurrentPhysicalCore(); - PreemptSingleCore(false); - system.CoreTiming().AddTicks(1000U); - idle_count++; - auto& scheduler = physical_core.Scheduler(); - scheduler.RescheduleCurrentCore(); - } -} +void CpuManager::SingleCoreRunGuestLoop() {} -void CpuManager::PreemptSingleCore(bool from_running_enviroment) { - { - auto& kernel = system.Kernel(); - auto& scheduler = kernel.Scheduler(current_core); - Kernel::KThread* current_thread = scheduler.GetSchedulerCurrentThread(); - if (idle_count >= 4 || from_running_enviroment) { - if (!from_running_enviroment) { - system.CoreTiming().Idle(); - idle_count = 0; - } - kernel.SetIsPhantomModeForSingleCore(true); - system.CoreTiming().Advance(); - kernel.SetIsPhantomModeForSingleCore(false); - } - current_core.store((current_core + 1) % Core::Hardware::NUM_CPU_CORES); - system.CoreTiming().ResetTicks(); - scheduler.Unload(scheduler.GetSchedulerCurrentThread()); - - auto& next_scheduler = kernel.Scheduler(current_core); - Common::Fiber::YieldTo(current_thread->GetHostContext(), *next_scheduler.ControlContext()); - } - - // May have changed scheduler - { - auto& scheduler = system.Kernel().Scheduler(current_core); - scheduler.Reload(scheduler.GetSchedulerCurrentThread()); - if (!scheduler.IsIdle()) { - idle_count = 0; - } - } -} +void CpuManager::PreemptSingleCore(bool from_running_enviroment) {} void CpuManager::ShutdownThread() { auto& kernel = system.Kernel(); + auto* thread = kernel.GetCurrentEmuThread(); auto core = is_multicore ? kernel.CurrentPhysicalCoreIndex() : 0; - auto* current_thread = kernel.GetCurrentEmuThread(); - Common::Fiber::YieldTo(current_thread->GetHostContext(), *core_data[core].host_context); + Common::Fiber::YieldTo(thread->GetHostContext(), *core_data[core].host_context); UNREACHABLE(); } @@ -218,9 +167,21 @@ void CpuManager::RunThread(std::size_t core) { system.GPU().ObtainContext(); } - auto* current_thread = system.Kernel().CurrentScheduler()->GetIdleThread(); - Kernel::SetCurrentThread(system.Kernel(), current_thread); - Common::Fiber::YieldTo(data.host_context, *current_thread->GetHostContext()); + auto& kernel = system.Kernel(); + + auto* main_thread = Kernel::KThread::Create(kernel); + main_thread->SetName(fmt::format("MainThread:{}", core)); + ASSERT(Kernel::KThread::InitializeMainThread(system, main_thread, static_cast(core)) + .IsSuccess()); + + auto* idle_thread = Kernel::KThread::Create(kernel); + ASSERT(Kernel::KThread::InitializeIdleThread(system, idle_thread, static_cast(core)) + .IsSuccess()); + + kernel.SetCurrentEmuThread(main_thread); + kernel.CurrentScheduler()->Initialize(idle_thread); + + Common::Fiber::YieldTo(data.host_context, *main_thread->GetHostContext()); } } // namespace Core diff --git a/src/core/cpu_manager.h b/src/core/cpu_manager.h index 76dc58ee1e..8143424af2 100644 --- a/src/core/cpu_manager.h +++ b/src/core/cpu_manager.h @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -47,10 +48,14 @@ public: gpu_barrier->Sync(); } + void WaitForAndHandleInterrupt(); void Initialize(); void Shutdown(); - std::function GetGuestThreadStartFunc() { + std::function GetGuestActivateFunc() { + return [this] { GuestActivateFunction(); }; + } + std::function GetGuestThreadFunc() { return [this] { GuestThreadFunction(); }; } std::function GetIdleThreadStartFunc() { @@ -67,21 +72,22 @@ public: } private: + void GuestActivateFunction(); void GuestThreadFunction(); - void GuestRewindFunction(); void IdleThreadFunction(); void ShutdownThreadFunction(); + void MultiCoreGuestActivate(); void MultiCoreRunGuestThread(); void MultiCoreRunGuestLoop(); - void MultiCoreRunIdleThread(); + void SingleCoreGuestActivate(); void SingleCoreRunGuestThread(); void SingleCoreRunGuestLoop(); - void SingleCoreRunIdleThread(); static void ThreadStart(std::stop_token stop_token, CpuManager& cpu_manager, std::size_t core); + void HandleInterrupt(); void ShutdownThread(); void RunThread(std::size_t core); diff --git a/src/core/hle/kernel/global_scheduler_context.cpp b/src/core/hle/kernel/global_scheduler_context.cpp index 164436b261..21fd5cb678 100644 --- a/src/core/hle/kernel/global_scheduler_context.cpp +++ b/src/core/hle/kernel/global_scheduler_context.cpp @@ -41,7 +41,7 @@ void GlobalSchedulerContext::PreemptThreads() { ASSERT(IsLocked()); for (u32 core_id = 0; core_id < Core::Hardware::NUM_CPU_CORES; core_id++) { const u32 priority = preemption_priorities[core_id]; - kernel.Scheduler(core_id).RotateScheduledQueue(core_id, priority); + KScheduler::RotateScheduledQueue(kernel, core_id, priority); // Signal an interrupt occurred. For core 3, this is a certainty, as preemption will result // in the rotator thread being scheduled. For cores 0-2, this is to simulate or system diff --git a/src/core/hle/kernel/k_interrupt_manager.cpp b/src/core/hle/kernel/k_interrupt_manager.cpp index d606a7f865..1b577a5b3e 100644 --- a/src/core/hle/kernel/k_interrupt_manager.cpp +++ b/src/core/hle/kernel/k_interrupt_manager.cpp @@ -6,6 +6,7 @@ #include "core/hle/kernel/k_scheduler.h" #include "core/hle/kernel/k_thread.h" #include "core/hle/kernel/kernel.h" +#include "core/hle/kernel/physical_core.h" namespace Kernel::KInterruptManager { @@ -15,6 +16,9 @@ void HandleInterrupt(KernelCore& kernel, s32 core_id) { return; } + // Acknowledge the interrupt. + kernel.PhysicalCore(core_id).ClearInterrupt(); + auto& current_thread = GetCurrentThread(kernel); // If the user disable count is set, we may need to pin the current thread. @@ -27,6 +31,9 @@ void HandleInterrupt(KernelCore& kernel, s32 core_id) { // Set the interrupt flag for the thread. GetCurrentThread(kernel).SetInterruptFlag(); } + + // Request interrupt scheduling. + kernel.CurrentScheduler()->RequestScheduleOnInterrupt(); } } // namespace Kernel::KInterruptManager diff --git a/src/core/hle/kernel/k_scheduler.cpp b/src/core/hle/kernel/k_scheduler.cpp index d599d2bcb9..13915dbd99 100644 --- a/src/core/hle/kernel/k_scheduler.cpp +++ b/src/core/hle/kernel/k_scheduler.cpp @@ -27,69 +27,162 @@ static void IncrementScheduledCount(Kernel::KThread* thread) { } } -void KScheduler::RescheduleCores(KernelCore& kernel, u64 cores_pending_reschedule) { - auto scheduler = kernel.CurrentScheduler(); - - u32 current_core{0xF}; - bool must_context_switch{}; - if (scheduler) { - current_core = scheduler->core_id; - // TODO(bunnei): Should be set to true when we deprecate single core - must_context_switch = !kernel.IsPhantomModeForSingleCore(); - } - - while (cores_pending_reschedule != 0) { - const auto core = static_cast(std::countr_zero(cores_pending_reschedule)); - ASSERT(core < Core::Hardware::NUM_CPU_CORES); - if (!must_context_switch || core != current_core) { - auto& phys_core = kernel.PhysicalCore(core); - phys_core.Interrupt(); +KScheduler::KScheduler(KernelCore& kernel_) : kernel{kernel_} { + m_idle_stack = std::make_shared([this] { + while (true) { + ScheduleImplOffStack(); } - cores_pending_reschedule &= ~(1ULL << core); - } + }); - for (std::size_t core_id = 0; core_id < Core::Hardware::NUM_CPU_CORES; ++core_id) { - if (kernel.PhysicalCore(core_id).IsInterrupted()) { - KInterruptManager::HandleInterrupt(kernel, static_cast(core_id)); - } - } + m_state.needs_scheduling = true; +} - if (must_context_switch) { - auto core_scheduler = kernel.CurrentScheduler(); - kernel.ExitSVCProfile(); - core_scheduler->RescheduleCurrentCore(); - kernel.EnterSVCProfile(); +KScheduler::~KScheduler() = default; + +void KScheduler::SetInterruptTaskRunnable() { + m_state.interrupt_task_runnable = true; + m_state.needs_scheduling = true; +} + +void KScheduler::RequestScheduleOnInterrupt() { + m_state.needs_scheduling = true; + + if (CanSchedule(kernel)) { + ScheduleOnInterrupt(); } } -u64 KScheduler::UpdateHighestPriorityThread(KThread* highest_thread) { - KScopedSpinLock lk{guard}; - if (KThread* prev_highest_thread = state.highest_priority_thread; - prev_highest_thread != highest_thread) { - if (prev_highest_thread != nullptr) { - IncrementScheduledCount(prev_highest_thread); - prev_highest_thread->SetLastScheduledTick(system.CoreTiming().GetCPUTicks()); +void KScheduler::DisableScheduling(KernelCore& kernel) { + ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() >= 0); + GetCurrentThread(kernel).DisableDispatch(); +} + +void KScheduler::EnableScheduling(KernelCore& kernel, u64 cores_needing_scheduling) { + ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() >= 1); + + auto* scheduler = kernel.CurrentScheduler(); + + if (!scheduler) { + // HACK: we cannot schedule from this thread, it is not a core thread + RescheduleCores(kernel, cores_needing_scheduling); + if (GetCurrentThread(kernel).GetDisableDispatchCount() == 1) { + // Special case to ensure dummy threads that are waiting block + GetCurrentThread(kernel).IfDummyThreadTryWait(); } - if (state.should_count_idle) { - if (highest_thread != nullptr) { + GetCurrentThread(kernel).EnableDispatch(); + return; + } + + scheduler->RescheduleOtherCores(cores_needing_scheduling); + + if (GetCurrentThread(kernel).GetDisableDispatchCount() > 1) { + GetCurrentThread(kernel).EnableDispatch(); + } else { + scheduler->RescheduleCurrentCore(); + } +} + +u64 KScheduler::UpdateHighestPriorityThreads(KernelCore& kernel) { + if (IsSchedulerUpdateNeeded(kernel)) { + return UpdateHighestPriorityThreadsImpl(kernel); + } else { + return 0; + } +} + +void KScheduler::Schedule() { + ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() == 1); + ASSERT(m_core_id == GetCurrentCoreId(kernel)); + + ScheduleImpl(); +} + +void KScheduler::ScheduleOnInterrupt() { + GetCurrentThread(kernel).DisableDispatch(); + Schedule(); + GetCurrentThread(kernel).EnableDispatch(); +} + +void KScheduler::RescheduleCurrentCore() { + ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() == 1); + + GetCurrentThread(kernel).EnableDispatch(); + + if (m_state.needs_scheduling.load()) { + // Disable interrupts, and then check again if rescheduling is needed. + // KScopedInterruptDisable intr_disable; + + kernel.CurrentScheduler()->RescheduleCurrentCoreImpl(); + } +} + +void KScheduler::RescheduleCurrentCoreImpl() { + // Check that scheduling is needed. + if (m_state.needs_scheduling.load()) [[likely]] { + GetCurrentThread(kernel).DisableDispatch(); + Schedule(); + GetCurrentThread(kernel).EnableDispatch(); + } +} + +void KScheduler::Initialize(KThread* idle_thread) { + // Set core ID/idle thread/interrupt task manager. + m_core_id = GetCurrentCoreId(kernel); + m_idle_thread = idle_thread; + // m_state.idle_thread_stack = m_idle_thread->GetStackTop(); + // m_state.interrupt_task_manager = &kernel.GetInterruptTaskManager(); + + // Insert the main thread into the priority queue. + // { + // KScopedSchedulerLock lk{kernel}; + // GetPriorityQueue(kernel).PushBack(GetCurrentThreadPointer(kernel)); + // SetSchedulerUpdateNeeded(kernel); + // } + + // Bind interrupt handler. + // kernel.GetInterruptManager().BindHandler( + // GetSchedulerInterruptHandler(kernel), KInterruptName::Scheduler, m_core_id, + // KInterruptController::PriorityLevel_Scheduler, false, false); + + // Set the current thread. + m_current_thread = GetCurrentThreadPointer(kernel); +} + +void KScheduler::Activate() { + ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() == 1); + + // m_state.should_count_idle = KTargetSystem::IsDebugMode(); + m_is_active = true; + RescheduleCurrentCore(); +} + +u64 KScheduler::UpdateHighestPriorityThread(KThread* highest_thread) { + if (KThread* prev_highest_thread = m_state.highest_priority_thread; + prev_highest_thread != highest_thread) [[likely]] { + if (prev_highest_thread != nullptr) [[likely]] { + IncrementScheduledCount(prev_highest_thread); + prev_highest_thread->SetLastScheduledTick(kernel.System().CoreTiming().GetCPUTicks()); + } + if (m_state.should_count_idle) { + if (highest_thread != nullptr) [[likely]] { if (KProcess* process = highest_thread->GetOwnerProcess(); process != nullptr) { - process->SetRunningThread(core_id, highest_thread, state.idle_count); + process->SetRunningThread(m_core_id, highest_thread, m_state.idle_count); } } else { - state.idle_count++; + m_state.idle_count++; } } - state.highest_priority_thread = highest_thread; - state.needs_scheduling.store(true); - return (1ULL << core_id); + m_state.highest_priority_thread = highest_thread; + m_state.needs_scheduling = true; + return (1ULL << m_core_id); } else { return 0; } } u64 KScheduler::UpdateHighestPriorityThreadsImpl(KernelCore& kernel) { - ASSERT(kernel.GlobalSchedulerContext().IsLocked()); + ASSERT(IsSchedulerLockedByCurrentThread(kernel)); // Clear that we need to update. ClearSchedulerUpdateNeeded(kernel); @@ -98,18 +191,20 @@ u64 KScheduler::UpdateHighestPriorityThreadsImpl(KernelCore& kernel) { KThread* top_threads[Core::Hardware::NUM_CPU_CORES]; auto& priority_queue = GetPriorityQueue(kernel); - /// We want to go over all cores, finding the highest priority thread and determining if - /// scheduling is needed for that core. + // We want to go over all cores, finding the highest priority thread and determining if + // scheduling is needed for that core. for (size_t core_id = 0; core_id < Core::Hardware::NUM_CPU_CORES; core_id++) { KThread* top_thread = priority_queue.GetScheduledFront(static_cast(core_id)); if (top_thread != nullptr) { - // If the thread has no waiters, we need to check if the process has a thread pinned. - if (top_thread->GetNumKernelWaiters() == 0) { - if (KProcess* parent = top_thread->GetOwnerProcess(); parent != nullptr) { - if (KThread* pinned = parent->GetPinnedThread(static_cast(core_id)); - pinned != nullptr && pinned != top_thread) { - // We prefer our parent's pinned thread if possible. However, we also don't - // want to schedule un-runnable threads. + // We need to check if the thread's process has a pinned thread. + if (KProcess* parent = top_thread->GetOwnerProcess()) { + // Check that there's a pinned thread other than the current top thread. + if (KThread* pinned = parent->GetPinnedThread(static_cast(core_id)); + pinned != nullptr && pinned != top_thread) { + // We need to prefer threads with kernel waiters to the pinned thread. + if (top_thread->GetNumKernelWaiters() == + 0 /* && top_thread != parent->GetExceptionThread() */) { + // If the pinned thread is runnable, use it. if (pinned->GetRawState() == ThreadState::Runnable) { top_thread = pinned; } else { @@ -129,7 +224,8 @@ u64 KScheduler::UpdateHighestPriorityThreadsImpl(KernelCore& kernel) { // Idle cores are bad. We're going to try to migrate threads to each idle core in turn. while (idle_cores != 0) { - const auto core_id = static_cast(std::countr_zero(idle_cores)); + const s32 core_id = static_cast(std::countr_zero(idle_cores)); + if (KThread* suggested = priority_queue.GetSuggestedFront(core_id); suggested != nullptr) { s32 migration_candidates[Core::Hardware::NUM_CPU_CORES]; size_t num_candidates = 0; @@ -150,7 +246,6 @@ u64 KScheduler::UpdateHighestPriorityThreadsImpl(KernelCore& kernel) { // The suggested thread isn't bound to its core, so we can migrate it! suggested->SetActiveCore(core_id); priority_queue.ChangeCore(suggested_core, suggested); - top_threads[core_id] = suggested; cores_needing_scheduling |= kernel.Scheduler(core_id).UpdateHighestPriorityThread(top_threads[core_id]); @@ -183,7 +278,6 @@ u64 KScheduler::UpdateHighestPriorityThreadsImpl(KernelCore& kernel) { // Perform the migration. suggested->SetActiveCore(core_id); priority_queue.ChangeCore(candidate_core, suggested); - top_threads[core_id] = suggested; cores_needing_scheduling |= kernel.Scheduler(core_id).UpdateHighestPriorityThread( @@ -200,24 +294,223 @@ u64 KScheduler::UpdateHighestPriorityThreadsImpl(KernelCore& kernel) { return cores_needing_scheduling; } +void KScheduler::SwitchThread(KThread* next_thread) { + KProcess* const cur_process = kernel.CurrentProcess(); + KThread* const cur_thread = GetCurrentThreadPointer(kernel); + + // We never want to schedule a null thread, so use the idle thread if we don't have a next. + if (next_thread == nullptr) { + next_thread = m_idle_thread; + } + + if (next_thread->GetCurrentCore() != m_core_id) { + next_thread->SetCurrentCore(m_core_id); + } + + // If we're not actually switching thread, there's nothing to do. + if (next_thread == cur_thread) { + return; + } + + // Next thread is now known not to be nullptr, and must not be dispatchable. + ASSERT(next_thread->GetDisableDispatchCount() == 1); + ASSERT(!next_thread->IsDummyThread()); + + // Update the CPU time tracking variables. + const s64 prev_tick = m_last_context_switch_time; + const s64 cur_tick = kernel.System().CoreTiming().GetCPUTicks(); + const s64 tick_diff = cur_tick - prev_tick; + cur_thread->AddCpuTime(m_core_id, tick_diff); + if (cur_process != nullptr) { + cur_process->UpdateCPUTimeTicks(tick_diff); + } + m_last_context_switch_time = cur_tick; + + // Update our previous thread. + if (cur_process != nullptr) { + if (!cur_thread->IsTerminationRequested() && cur_thread->GetActiveCore() == m_core_id) + [[likely]] { + m_state.prev_thread = cur_thread; + } else { + m_state.prev_thread = nullptr; + } + } + + // Switch the current process, if we're switching processes. + // if (KProcess *next_process = next_thread->GetOwnerProcess(); next_process != cur_process) { + // KProcess::Switch(cur_process, next_process); + // } + + // Set the new thread. + SetCurrentThread(kernel, next_thread); + m_current_thread = next_thread; + + // Set the new Thread Local region. + // cpu::SwitchThreadLocalRegion(GetInteger(next_thread->GetThreadLocalRegionAddress())); +} + +void KScheduler::ScheduleImpl() { + // First, clear the needs scheduling bool. + m_state.needs_scheduling.store(false, std::memory_order_seq_cst); + + // Load the appropriate thread pointers for scheduling. + KThread* const cur_thread{GetCurrentThreadPointer(kernel)}; + KThread* highest_priority_thread{m_state.highest_priority_thread}; + + // Check whether there are runnable interrupt tasks. + if (m_state.interrupt_task_runnable) { + // The interrupt task is runnable. + // We want to switch to the interrupt task/idle thread. + highest_priority_thread = nullptr; + } + + // If there aren't, we want to check if the highest priority thread is the same as the current + // thread. + if (highest_priority_thread == cur_thread) { + // If they're the same, then we can just return. + return; + } + + // The highest priority thread is not the same as the current thread. + // Switch to the idle thread stack and continue executing from there. + m_idle_cur_thread = cur_thread; + m_idle_highest_priority_thread = highest_priority_thread; + Common::Fiber::YieldTo(cur_thread->host_context, *m_idle_stack); + + // Returning from ScheduleImpl occurs after this thread has been scheduled again. +} + +void KScheduler::ScheduleImplOffStack() { + KThread* const cur_thread{m_idle_cur_thread}; + KThread* highest_priority_thread{m_idle_highest_priority_thread}; + + // Get a reference to the current thread's stack parameters. + auto& sp{cur_thread->GetStackParameters()}; + + // Save the original thread context. + { + auto& physical_core = kernel.System().CurrentPhysicalCore(); + auto& cpu_core = physical_core.ArmInterface(); + cpu_core.SaveContext(cur_thread->GetContext32()); + cpu_core.SaveContext(cur_thread->GetContext64()); + // Save the TPIDR_EL0 system register in case it was modified. + cur_thread->SetTPIDR_EL0(cpu_core.GetTPIDR_EL0()); + cpu_core.ClearExclusiveState(); + } + + // Check if the thread is terminated by checking the DPC flags. + if ((sp.dpc_flags & static_cast(DpcFlag::Terminated)) == 0) { + // The thread isn't terminated, so we want to unlock it. + sp.m_lock.store(false, std::memory_order_seq_cst); + } + + // The current thread's context has been entirely taken care of. + // Now we want to loop until we successfully switch the thread context. + while (true) { + // We're starting to try to do the context switch. + // Check if the highest priority thread is null. + if (!highest_priority_thread) { + // The next thread is nullptr! + // Switch to nullptr. This will actually switch to the idle thread. + SwitchThread(nullptr); + + // We've switched to the idle thread, so we want to process interrupt tasks until we + // schedule a non-idle thread. + while (!m_state.interrupt_task_runnable) { + // Check if we need scheduling. + if (m_state.needs_scheduling.load(std::memory_order_seq_cst)) { + goto retry; + } + + // Clear the previous thread. + m_state.prev_thread = nullptr; + + // Wait for an interrupt before checking again. + kernel.System().GetCpuManager().WaitForAndHandleInterrupt(); + } + + // Execute any pending interrupt tasks. + // m_state.interrupt_task_manager->DoTasks(); + + // Clear the interrupt task thread as runnable. + m_state.interrupt_task_runnable = false; + + // Retry the scheduling loop. + goto retry; + } else { + // We want to try to lock the highest priority thread's context. + // Try to take it. + bool expected{false}; + while (!highest_priority_thread->stack_parameters.m_lock.compare_exchange_strong( + expected, true, std::memory_order_seq_cst)) { + // The highest priority thread's context is already locked. + // Check if we need scheduling. If we don't, we can retry directly. + if (m_state.needs_scheduling.load(std::memory_order_seq_cst)) { + // If we do, another core is interfering, and we must start again. + goto retry; + } + expected = false; + } + + // It's time to switch the thread. + // Switch to the highest priority thread. + SwitchThread(highest_priority_thread); + + // Check if we need scheduling. If we do, then we can't complete the switch and should + // retry. + if (m_state.needs_scheduling.load(std::memory_order_seq_cst)) { + // Our switch failed. + // We should unlock the thread context, and then retry. + highest_priority_thread->stack_parameters.m_lock.store(false, + std::memory_order_seq_cst); + goto retry; + } else { + break; + } + } + + retry: + + // We failed to successfully do the context switch, and need to retry. + // Clear needs_scheduling. + m_state.needs_scheduling.store(false, std::memory_order_seq_cst); + + // Refresh the highest priority thread. + highest_priority_thread = m_state.highest_priority_thread; + } + + // Reload the guest thread context. + { + auto& cpu_core = kernel.System().CurrentArmInterface(); + cpu_core.LoadContext(highest_priority_thread->GetContext32()); + cpu_core.LoadContext(highest_priority_thread->GetContext64()); + cpu_core.SetTlsAddress(highest_priority_thread->GetTLSAddress()); + cpu_core.SetTPIDR_EL0(highest_priority_thread->GetTPIDR_EL0()); + cpu_core.LoadWatchpointArray(highest_priority_thread->GetOwnerProcess()->GetWatchpoints()); + cpu_core.ClearExclusiveState(); + } + + // Reload the host thread. + Common::Fiber::YieldTo(m_idle_stack, *highest_priority_thread->host_context); +} + void KScheduler::ClearPreviousThread(KernelCore& kernel, KThread* thread) { - ASSERT(kernel.GlobalSchedulerContext().IsLocked()); + ASSERT(IsSchedulerLockedByCurrentThread(kernel)); for (size_t i = 0; i < Core::Hardware::NUM_CPU_CORES; ++i) { // Get an atomic reference to the core scheduler's previous thread. - std::atomic_ref prev_thread(kernel.Scheduler(static_cast(i)).prev_thread); - static_assert(std::atomic_ref::is_always_lock_free); + auto& prev_thread{kernel.Scheduler(i).m_state.prev_thread}; // Atomically clear the previous thread if it's our target. KThread* compare = thread; - prev_thread.compare_exchange_strong(compare, nullptr); + prev_thread.compare_exchange_strong(compare, nullptr, std::memory_order_seq_cst); } } void KScheduler::OnThreadStateChanged(KernelCore& kernel, KThread* thread, ThreadState old_state) { - ASSERT(kernel.GlobalSchedulerContext().IsLocked()); + ASSERT(IsSchedulerLockedByCurrentThread(kernel)); // Check if the state has changed, because if it hasn't there's nothing to do. - const auto cur_state = thread->GetRawState(); + const ThreadState cur_state = thread->GetRawState(); if (cur_state == old_state) { return; } @@ -237,12 +530,12 @@ void KScheduler::OnThreadStateChanged(KernelCore& kernel, KThread* thread, Threa } void KScheduler::OnThreadPriorityChanged(KernelCore& kernel, KThread* thread, s32 old_priority) { - ASSERT(kernel.GlobalSchedulerContext().IsLocked()); + ASSERT(IsSchedulerLockedByCurrentThread(kernel)); // If the thread is runnable, we want to change its priority in the queue. if (thread->GetRawState() == ThreadState::Runnable) { GetPriorityQueue(kernel).ChangePriority(old_priority, - thread == kernel.GetCurrentEmuThread(), thread); + thread == GetCurrentThreadPointer(kernel), thread); IncrementScheduledCount(thread); SetSchedulerUpdateNeeded(kernel); } @@ -250,7 +543,7 @@ void KScheduler::OnThreadPriorityChanged(KernelCore& kernel, KThread* thread, s3 void KScheduler::OnThreadAffinityMaskChanged(KernelCore& kernel, KThread* thread, const KAffinityMask& old_affinity, s32 old_core) { - ASSERT(kernel.GlobalSchedulerContext().IsLocked()); + ASSERT(IsSchedulerLockedByCurrentThread(kernel)); // If the thread is runnable, we want to change its affinity in the queue. if (thread->GetRawState() == ThreadState::Runnable) { @@ -260,15 +553,14 @@ void KScheduler::OnThreadAffinityMaskChanged(KernelCore& kernel, KThread* thread } } -void KScheduler::RotateScheduledQueue(s32 cpu_core_id, s32 priority) { - ASSERT(system.GlobalSchedulerContext().IsLocked()); +void KScheduler::RotateScheduledQueue(KernelCore& kernel, s32 core_id, s32 priority) { + ASSERT(IsSchedulerLockedByCurrentThread(kernel)); // Get a reference to the priority queue. - auto& kernel = system.Kernel(); auto& priority_queue = GetPriorityQueue(kernel); // Rotate the front of the queue to the end. - KThread* top_thread = priority_queue.GetScheduledFront(cpu_core_id, priority); + KThread* top_thread = priority_queue.GetScheduledFront(core_id, priority); KThread* next_thread = nullptr; if (top_thread != nullptr) { next_thread = priority_queue.MoveToScheduledBack(top_thread); @@ -280,7 +572,7 @@ void KScheduler::RotateScheduledQueue(s32 cpu_core_id, s32 priority) { // While we have a suggested thread, try to migrate it! { - KThread* suggested = priority_queue.GetSuggestedFront(cpu_core_id, priority); + KThread* suggested = priority_queue.GetSuggestedFront(core_id, priority); while (suggested != nullptr) { // Check if the suggested thread is the top thread on its core. const s32 suggested_core = suggested->GetActiveCore(); @@ -301,7 +593,7 @@ void KScheduler::RotateScheduledQueue(s32 cpu_core_id, s32 priority) { // to the front of the queue. if (top_on_suggested_core == nullptr || top_on_suggested_core->GetPriority() >= HighestCoreMigrationAllowedPriority) { - suggested->SetActiveCore(cpu_core_id); + suggested->SetActiveCore(core_id); priority_queue.ChangeCore(suggested_core, suggested, true); IncrementScheduledCount(suggested); break; @@ -309,22 +601,21 @@ void KScheduler::RotateScheduledQueue(s32 cpu_core_id, s32 priority) { } // Get the next suggestion. - suggested = priority_queue.GetSamePriorityNext(cpu_core_id, suggested); + suggested = priority_queue.GetSamePriorityNext(core_id, suggested); } } // Now that we might have migrated a thread with the same priority, check if we can do better. - { - KThread* best_thread = priority_queue.GetScheduledFront(cpu_core_id); + KThread* best_thread = priority_queue.GetScheduledFront(core_id); if (best_thread == GetCurrentThreadPointer(kernel)) { - best_thread = priority_queue.GetScheduledNext(cpu_core_id, best_thread); + best_thread = priority_queue.GetScheduledNext(core_id, best_thread); } // If the best thread we can choose has a priority the same or worse than ours, try to // migrate a higher priority thread. if (best_thread != nullptr && best_thread->GetPriority() >= priority) { - KThread* suggested = priority_queue.GetSuggestedFront(cpu_core_id); + KThread* suggested = priority_queue.GetSuggestedFront(core_id); while (suggested != nullptr) { // If the suggestion's priority is the same as ours, don't bother. if (suggested->GetPriority() >= best_thread->GetPriority()) { @@ -343,7 +634,7 @@ void KScheduler::RotateScheduledQueue(s32 cpu_core_id, s32 priority) { if (top_on_suggested_core == nullptr || top_on_suggested_core->GetPriority() >= HighestCoreMigrationAllowedPriority) { - suggested->SetActiveCore(cpu_core_id); + suggested->SetActiveCore(core_id); priority_queue.ChangeCore(suggested_core, suggested, true); IncrementScheduledCount(suggested); break; @@ -351,7 +642,7 @@ void KScheduler::RotateScheduledQueue(s32 cpu_core_id, s32 priority) { } // Get the next suggestion. - suggested = priority_queue.GetSuggestedNext(cpu_core_id, suggested); + suggested = priority_queue.GetSuggestedNext(core_id, suggested); } } } @@ -360,64 +651,6 @@ void KScheduler::RotateScheduledQueue(s32 cpu_core_id, s32 priority) { SetSchedulerUpdateNeeded(kernel); } -bool KScheduler::CanSchedule(KernelCore& kernel) { - return kernel.GetCurrentEmuThread()->GetDisableDispatchCount() <= 1; -} - -bool KScheduler::IsSchedulerUpdateNeeded(const KernelCore& kernel) { - return kernel.GlobalSchedulerContext().scheduler_update_needed.load(std::memory_order_acquire); -} - -void KScheduler::SetSchedulerUpdateNeeded(KernelCore& kernel) { - kernel.GlobalSchedulerContext().scheduler_update_needed.store(true, std::memory_order_release); -} - -void KScheduler::ClearSchedulerUpdateNeeded(KernelCore& kernel) { - kernel.GlobalSchedulerContext().scheduler_update_needed.store(false, std::memory_order_release); -} - -void KScheduler::DisableScheduling(KernelCore& kernel) { - // If we are shutting down the kernel, none of this is relevant anymore. - if (kernel.IsShuttingDown()) { - return; - } - - ASSERT(GetCurrentThreadPointer(kernel)->GetDisableDispatchCount() >= 0); - GetCurrentThreadPointer(kernel)->DisableDispatch(); -} - -void KScheduler::EnableScheduling(KernelCore& kernel, u64 cores_needing_scheduling) { - // If we are shutting down the kernel, none of this is relevant anymore. - if (kernel.IsShuttingDown()) { - return; - } - - auto* current_thread = GetCurrentThreadPointer(kernel); - - ASSERT(current_thread->GetDisableDispatchCount() >= 1); - - if (current_thread->GetDisableDispatchCount() > 1) { - current_thread->EnableDispatch(); - } else { - RescheduleCores(kernel, cores_needing_scheduling); - } - - // Special case to ensure dummy threads that are waiting block. - current_thread->IfDummyThreadTryWait(); -} - -u64 KScheduler::UpdateHighestPriorityThreads(KernelCore& kernel) { - if (IsSchedulerUpdateNeeded(kernel)) { - return UpdateHighestPriorityThreadsImpl(kernel); - } else { - return 0; - } -} - -KSchedulerPriorityQueue& KScheduler::GetPriorityQueue(KernelCore& kernel) { - return kernel.GlobalSchedulerContext().priority_queue; -} - void KScheduler::YieldWithoutCoreMigration(KernelCore& kernel) { // Validate preconditions. ASSERT(CanSchedule(kernel)); @@ -437,7 +670,7 @@ void KScheduler::YieldWithoutCoreMigration(KernelCore& kernel) { // Perform the yield. { - KScopedSchedulerLock lock(kernel); + KScopedSchedulerLock sl{kernel}; const auto cur_state = cur_thread.GetRawState(); if (cur_state == ThreadState::Runnable) { @@ -476,7 +709,7 @@ void KScheduler::YieldWithCoreMigration(KernelCore& kernel) { // Perform the yield. { - KScopedSchedulerLock lock(kernel); + KScopedSchedulerLock sl{kernel}; const auto cur_state = cur_thread.GetRawState(); if (cur_state == ThreadState::Runnable) { @@ -496,7 +729,7 @@ void KScheduler::YieldWithCoreMigration(KernelCore& kernel) { if (KThread* running_on_suggested_core = (suggested_core >= 0) - ? kernel.Scheduler(suggested_core).state.highest_priority_thread + ? kernel.Scheduler(suggested_core).m_state.highest_priority_thread : nullptr; running_on_suggested_core != suggested) { // If the current thread's priority is higher than our suggestion's we prefer @@ -564,7 +797,7 @@ void KScheduler::YieldToAnyThread(KernelCore& kernel) { // Perform the yield. { - KScopedSchedulerLock lock(kernel); + KScopedSchedulerLock sl{kernel}; const auto cur_state = cur_thread.GetRawState(); if (cur_state == ThreadState::Runnable) { @@ -621,223 +854,19 @@ void KScheduler::YieldToAnyThread(KernelCore& kernel) { } } -KScheduler::KScheduler(Core::System& system_, s32 core_id_) : system{system_}, core_id{core_id_} { - switch_fiber = std::make_shared([this] { SwitchToCurrent(); }); - state.needs_scheduling.store(true); - state.interrupt_task_thread_runnable = false; - state.should_count_idle = false; - state.idle_count = 0; - state.idle_thread_stack = nullptr; - state.highest_priority_thread = nullptr; -} - -void KScheduler::Finalize() { - if (idle_thread) { - idle_thread->Close(); - idle_thread = nullptr; +void KScheduler::RescheduleOtherCores(u64 cores_needing_scheduling) { + if (const u64 core_mask = cores_needing_scheduling & ~(1ULL << m_core_id); core_mask != 0) { + RescheduleCores(kernel, core_mask); } } -KScheduler::~KScheduler() { - ASSERT(!idle_thread); -} - -KThread* KScheduler::GetSchedulerCurrentThread() const { - if (auto result = current_thread.load(); result) { - return result; - } - return idle_thread; -} - -u64 KScheduler::GetLastContextSwitchTicks() const { - return last_context_switch_time; -} - -void KScheduler::RescheduleCurrentCore() { - ASSERT(GetCurrentThread(system.Kernel()).GetDisableDispatchCount() == 1); - - auto& phys_core = system.Kernel().PhysicalCore(core_id); - if (phys_core.IsInterrupted()) { - phys_core.ClearInterrupt(); - } - - guard.Lock(); - if (state.needs_scheduling.load()) { - Schedule(); - } else { - GetCurrentThread(system.Kernel()).EnableDispatch(); - guard.Unlock(); - } -} - -void KScheduler::OnThreadStart() { - SwitchContextStep2(); -} - -void KScheduler::Unload(KThread* thread) { - ASSERT(thread); - - LOG_TRACE(Kernel, "core {}, unload thread {}", core_id, thread ? thread->GetName() : "nullptr"); - - if (thread->IsCallingSvc()) { - thread->ClearIsCallingSvc(); - } - - auto& physical_core = system.Kernel().PhysicalCore(core_id); - if (!physical_core.IsInitialized()) { - return; - } - - Core::ARM_Interface& cpu_core = physical_core.ArmInterface(); - cpu_core.SaveContext(thread->GetContext32()); - cpu_core.SaveContext(thread->GetContext64()); - // Save the TPIDR_EL0 system register in case it was modified. - thread->SetTPIDR_EL0(cpu_core.GetTPIDR_EL0()); - cpu_core.ClearExclusiveState(); - - if (!thread->IsTerminationRequested() && thread->GetActiveCore() == core_id) { - prev_thread = thread; - } else { - prev_thread = nullptr; - } - - thread->context_guard.unlock(); -} - -void KScheduler::Reload(KThread* thread) { - LOG_TRACE(Kernel, "core {}, reload thread {}", core_id, thread->GetName()); - - Core::ARM_Interface& cpu_core = system.ArmInterface(core_id); - cpu_core.LoadContext(thread->GetContext32()); - cpu_core.LoadContext(thread->GetContext64()); - cpu_core.LoadWatchpointArray(thread->GetOwnerProcess()->GetWatchpoints()); - cpu_core.SetTlsAddress(thread->GetTLSAddress()); - cpu_core.SetTPIDR_EL0(thread->GetTPIDR_EL0()); - cpu_core.ClearExclusiveState(); -} - -void KScheduler::SwitchContextStep2() { - // Load context of new thread - Reload(GetCurrentThreadPointer(system.Kernel())); - - RescheduleCurrentCore(); -} - -void KScheduler::Schedule() { - ASSERT(GetCurrentThread(system.Kernel()).GetDisableDispatchCount() == 1); - this->ScheduleImpl(); -} - -void KScheduler::ScheduleImpl() { - KThread* previous_thread = GetCurrentThreadPointer(system.Kernel()); - KThread* next_thread = state.highest_priority_thread; - - state.needs_scheduling.store(false); - - // We never want to schedule a null thread, so use the idle thread if we don't have a next. - if (next_thread == nullptr) { - next_thread = idle_thread; - } - - if (next_thread->GetCurrentCore() != core_id) { - next_thread->SetCurrentCore(core_id); - } - - // We never want to schedule a dummy thread, as these are only used by host threads for locking. - if (next_thread->GetThreadType() == ThreadType::Dummy) { - ASSERT_MSG(false, "Dummy threads should never be scheduled!"); - next_thread = idle_thread; - } - - // If we're not actually switching thread, there's nothing to do. - if (next_thread == current_thread.load()) { - previous_thread->EnableDispatch(); - guard.Unlock(); - return; - } - - // Update the CPU time tracking variables. - KProcess* const previous_process = system.Kernel().CurrentProcess(); - UpdateLastContextSwitchTime(previous_thread, previous_process); - - // Save context for previous thread - Unload(previous_thread); - - std::shared_ptr* old_context; - old_context = &previous_thread->GetHostContext(); - - // Set the new thread. - SetCurrentThread(system.Kernel(), next_thread); - current_thread.store(next_thread); - - guard.Unlock(); - - Common::Fiber::YieldTo(*old_context, *switch_fiber); - /// When a thread wakes up, the scheduler may have changed to other in another core. - auto& next_scheduler = *system.Kernel().CurrentScheduler(); - next_scheduler.SwitchContextStep2(); -} - -void KScheduler::SwitchToCurrent() { - while (true) { - { - KScopedSpinLock lk{guard}; - current_thread.store(state.highest_priority_thread); - state.needs_scheduling.store(false); +void KScheduler::RescheduleCores(KernelCore& kernel, u64 core_mask) { + // Send IPI + for (size_t i = 0; i < Core::Hardware::NUM_CPU_CORES; i++) { + if (core_mask & (1ULL << i)) { + kernel.PhysicalCore(i).Interrupt(); } - const auto is_switch_pending = [this] { - KScopedSpinLock lk{guard}; - return state.needs_scheduling.load(); - }; - do { - auto next_thread = current_thread.load(); - if (next_thread != nullptr) { - const auto locked = next_thread->context_guard.try_lock(); - if (state.needs_scheduling.load()) { - next_thread->context_guard.unlock(); - break; - } - if (next_thread->GetActiveCore() != core_id) { - next_thread->context_guard.unlock(); - break; - } - if (!locked) { - continue; - } - } - auto thread = next_thread ? next_thread : idle_thread; - SetCurrentThread(system.Kernel(), thread); - Common::Fiber::YieldTo(switch_fiber, *thread->GetHostContext()); - } while (!is_switch_pending()); } } -void KScheduler::UpdateLastContextSwitchTime(KThread* thread, KProcess* process) { - const u64 prev_switch_ticks = last_context_switch_time; - const u64 most_recent_switch_ticks = system.CoreTiming().GetCPUTicks(); - const u64 update_ticks = most_recent_switch_ticks - prev_switch_ticks; - - if (thread != nullptr) { - thread->AddCpuTime(core_id, update_ticks); - } - - if (process != nullptr) { - process->UpdateCPUTimeTicks(update_ticks); - } - - last_context_switch_time = most_recent_switch_ticks; -} - -void KScheduler::Initialize() { - idle_thread = KThread::Create(system.Kernel()); - ASSERT(KThread::InitializeIdleThread(system, idle_thread, core_id).IsSuccess()); - idle_thread->SetName(fmt::format("IdleThread:{}", core_id)); - idle_thread->EnableDispatch(); -} - -KScopedSchedulerLock::KScopedSchedulerLock(KernelCore& kernel) - : KScopedLock(kernel.GlobalSchedulerContext().SchedulerLock()) {} - -KScopedSchedulerLock::~KScopedSchedulerLock() = default; - } // namespace Kernel diff --git a/src/core/hle/kernel/k_scheduler.h b/src/core/hle/kernel/k_scheduler.h index 6a4760eca1..8f4eebf6ac 100644 --- a/src/core/hle/kernel/k_scheduler.h +++ b/src/core/hle/kernel/k_scheduler.h @@ -11,6 +11,7 @@ #include "core/hle/kernel/k_scheduler_lock.h" #include "core/hle/kernel/k_scoped_lock.h" #include "core/hle/kernel/k_spin_lock.h" +#include "core/hle/kernel/k_thread.h" namespace Common { class Fiber; @@ -23,184 +24,139 @@ class System; namespace Kernel { class KernelCore; +class KInterruptTaskManager; class KProcess; -class SchedulerLock; class KThread; +class KScopedDisableDispatch; +class KScopedSchedulerLock; +class KScopedSchedulerLockAndSleep; class KScheduler final { public: - explicit KScheduler(Core::System& system_, s32 core_id_); + YUZU_NON_COPYABLE(KScheduler); + YUZU_NON_MOVEABLE(KScheduler); + + using LockType = KAbstractSchedulerLock; + + explicit KScheduler(KernelCore& kernel); ~KScheduler(); - void Finalize(); + void Initialize(KThread* idle_thread); + void Activate(); - /// Reschedules to the next available thread (call after current thread is suspended) - void RescheduleCurrentCore(); + void SetInterruptTaskRunnable(); + void RequestScheduleOnInterrupt(); - /// Reschedules cores pending reschedule, to be called on EnableScheduling. - static void RescheduleCores(KernelCore& kernel, u64 cores_pending_reschedule); - - /// The next two are for SingleCore Only. - /// Unload current thread before preempting core. - void Unload(KThread* thread); - - /// Reload current thread after core preemption. - void Reload(KThread* thread); - - /// Gets the current running thread - [[nodiscard]] KThread* GetSchedulerCurrentThread() const; - - /// Gets the idle thread - [[nodiscard]] KThread* GetIdleThread() const { - return idle_thread; + u64 GetIdleCount() { + return m_state.idle_count; } - /// Returns true if the scheduler is idle - [[nodiscard]] bool IsIdle() const { - return GetSchedulerCurrentThread() == idle_thread; + KThread* GetIdleThread() const { + return m_idle_thread; } - /// Gets the timestamp for the last context switch in ticks. - [[nodiscard]] u64 GetLastContextSwitchTicks() const; - - [[nodiscard]] bool ContextSwitchPending() const { - return state.needs_scheduling.load(std::memory_order_relaxed); + KThread* GetPreviousThread() const { + return m_state.prev_thread; } - void Initialize(); - - void OnThreadStart(); - - [[nodiscard]] std::shared_ptr& ControlContext() { - return switch_fiber; + KThread* GetSchedulerCurrentThread() const { + return m_current_thread.load(); } - [[nodiscard]] const std::shared_ptr& ControlContext() const { - return switch_fiber; + s64 GetLastContextSwitchTime() const { + return m_last_context_switch_time; } - [[nodiscard]] u64 UpdateHighestPriorityThread(KThread* highest_thread); + // Static public API. + static bool CanSchedule(KernelCore& kernel) { + return kernel.GetCurrentEmuThread()->GetDisableDispatchCount() == 0; + } + static bool IsSchedulerLockedByCurrentThread(KernelCore& kernel) { + return kernel.GlobalSchedulerContext().scheduler_lock.IsLockedByCurrentThread(); + } - /** - * Takes a thread and moves it to the back of the it's priority list. - * - * @note This operation can be redundant and no scheduling is changed if marked as so. - */ - static void YieldWithoutCoreMigration(KernelCore& kernel); + static bool IsSchedulerUpdateNeeded(KernelCore& kernel) { + return kernel.GlobalSchedulerContext().scheduler_update_needed; + } + static void SetSchedulerUpdateNeeded(KernelCore& kernel) { + kernel.GlobalSchedulerContext().scheduler_update_needed = true; + } + static void ClearSchedulerUpdateNeeded(KernelCore& kernel) { + kernel.GlobalSchedulerContext().scheduler_update_needed = false; + } - /** - * Takes a thread and moves it to the back of the it's priority list. - * Afterwards, tries to pick a suggested thread from the suggested queue that has worse time or - * a better priority than the next thread in the core. - * - * @note This operation can be redundant and no scheduling is changed if marked as so. - */ - static void YieldWithCoreMigration(KernelCore& kernel); + static void DisableScheduling(KernelCore& kernel); + static void EnableScheduling(KernelCore& kernel, u64 cores_needing_scheduling); - /** - * Takes a thread and moves it out of the scheduling queue. - * and into the suggested queue. If no thread can be scheduled afterwards in that core, - * a suggested thread is obtained instead. - * - * @note This operation can be redundant and no scheduling is changed if marked as so. - */ - static void YieldToAnyThread(KernelCore& kernel); + static u64 UpdateHighestPriorityThreads(KernelCore& kernel); static void ClearPreviousThread(KernelCore& kernel, KThread* thread); - /// Notify the scheduler a thread's status has changed. static void OnThreadStateChanged(KernelCore& kernel, KThread* thread, ThreadState old_state); - - /// Notify the scheduler a thread's priority has changed. static void OnThreadPriorityChanged(KernelCore& kernel, KThread* thread, s32 old_priority); - - /// Notify the scheduler a thread's core and/or affinity mask has changed. static void OnThreadAffinityMaskChanged(KernelCore& kernel, KThread* thread, const KAffinityMask& old_affinity, s32 old_core); - static bool CanSchedule(KernelCore& kernel); - static bool IsSchedulerUpdateNeeded(const KernelCore& kernel); - static void SetSchedulerUpdateNeeded(KernelCore& kernel); - static void ClearSchedulerUpdateNeeded(KernelCore& kernel); - static void DisableScheduling(KernelCore& kernel); - static void EnableScheduling(KernelCore& kernel, u64 cores_needing_scheduling); - [[nodiscard]] static u64 UpdateHighestPriorityThreads(KernelCore& kernel); + static void RotateScheduledQueue(KernelCore& kernel, s32 core_id, s32 priority); + static void RescheduleCores(KernelCore& kernel, u64 cores_needing_scheduling); + + static void YieldWithoutCoreMigration(KernelCore& kernel); + static void YieldWithCoreMigration(KernelCore& kernel); + static void YieldToAnyThread(KernelCore& kernel); private: - friend class GlobalSchedulerContext; + // Static private API. + static KSchedulerPriorityQueue& GetPriorityQueue(KernelCore& kernel) { + return kernel.GlobalSchedulerContext().priority_queue; + } + static u64 UpdateHighestPriorityThreadsImpl(KernelCore& kernel); - /** - * Takes care of selecting the new scheduled threads in three steps: - * - * 1. First a thread is selected from the top of the priority queue. If no thread - * is obtained then we move to step two, else we are done. - * - * 2. Second we try to get a suggested thread that's not assigned to any core or - * that is not the top thread in that core. - * - * 3. Third is no suggested thread is found, we do a second pass and pick a running - * thread in another core and swap it with its current thread. - * - * returns the cores needing scheduling. - */ - [[nodiscard]] static u64 UpdateHighestPriorityThreadsImpl(KernelCore& kernel); - - [[nodiscard]] static KSchedulerPriorityQueue& GetPriorityQueue(KernelCore& kernel); - - void RotateScheduledQueue(s32 cpu_core_id, s32 priority); + // Instanced private API. + void ScheduleImpl(); + void ScheduleImplOffStack(); + void SwitchThread(KThread* next_thread); void Schedule(); + void ScheduleOnInterrupt(); - /// Switches the CPU's active thread context to that of the specified thread - void ScheduleImpl(); + void RescheduleOtherCores(u64 cores_needing_scheduling); + void RescheduleCurrentCore(); + void RescheduleCurrentCoreImpl(); - /// When a thread wakes up, it must run this through it's new scheduler - void SwitchContextStep2(); + u64 UpdateHighestPriorityThread(KThread* thread); - /** - * Called on every context switch to update the internal timestamp - * This also updates the running time ticks for the given thread and - * process using the following difference: - * - * ticks += most_recent_ticks - last_context_switch_ticks - * - * The internal tick timestamp for the scheduler is simply the - * most recent tick count retrieved. No special arithmetic is - * applied to it. - */ - void UpdateLastContextSwitchTime(KThread* thread, KProcess* process); - - void SwitchToCurrent(); - - KThread* prev_thread{}; - std::atomic current_thread{}; - - KThread* idle_thread{}; - - std::shared_ptr switch_fiber{}; +private: + friend class KScopedDisableDispatch; struct SchedulingState { - std::atomic needs_scheduling{}; - bool interrupt_task_thread_runnable{}; - bool should_count_idle{}; - u64 idle_count{}; - KThread* highest_priority_thread{}; - void* idle_thread_stack{}; + std::atomic needs_scheduling{false}; + bool interrupt_task_runnable{false}; + bool should_count_idle{false}; + u64 idle_count{0}; + KThread* highest_priority_thread{nullptr}; + void* idle_thread_stack{nullptr}; + std::atomic prev_thread{nullptr}; + KInterruptTaskManager* interrupt_task_manager{nullptr}; }; - SchedulingState state; + KernelCore& kernel; + SchedulingState m_state; + bool m_is_active{false}; + s32 m_core_id{0}; + s64 m_last_context_switch_time{0}; + KThread* m_idle_thread{nullptr}; + std::atomic m_current_thread{nullptr}; - Core::System& system; - u64 last_context_switch_time{}; - const s32 core_id; - - KSpinLock guard{}; + std::shared_ptr m_idle_stack{}; + KThread* m_idle_cur_thread{}; + KThread* m_idle_highest_priority_thread{}; }; -class [[nodiscard]] KScopedSchedulerLock : KScopedLock { +class KScopedSchedulerLock : public KScopedLock { public: - explicit KScopedSchedulerLock(KernelCore& kernel); - ~KScopedSchedulerLock(); + explicit KScopedSchedulerLock(KernelCore& kernel) + : KScopedLock(kernel.GlobalSchedulerContext().scheduler_lock) {} + ~KScopedSchedulerLock() = default; }; } // namespace Kernel diff --git a/src/core/hle/kernel/k_scheduler_lock.h b/src/core/hle/kernel/k_scheduler_lock.h index 4fa2569701..73314b45e9 100644 --- a/src/core/hle/kernel/k_scheduler_lock.h +++ b/src/core/hle/kernel/k_scheduler_lock.h @@ -5,9 +5,11 @@ #include #include "common/assert.h" +#include "core/hle/kernel/k_interrupt_manager.h" #include "core/hle/kernel/k_spin_lock.h" #include "core/hle/kernel/k_thread.h" #include "core/hle/kernel/kernel.h" +#include "core/hle/kernel/physical_core.h" namespace Kernel { diff --git a/src/core/hle/kernel/k_thread.cpp b/src/core/hle/kernel/k_thread.cpp index 90de867707..9daa589b5a 100644 --- a/src/core/hle/kernel/k_thread.cpp +++ b/src/core/hle/kernel/k_thread.cpp @@ -261,9 +261,14 @@ Result KThread::InitializeDummyThread(KThread* thread) { return thread->Initialize({}, {}, {}, DummyThreadPriority, 3, {}, ThreadType::Dummy); } +Result KThread::InitializeMainThread(Core::System& system, KThread* thread, s32 virt_core) { + return InitializeThread(thread, {}, {}, {}, IdleThreadPriority, virt_core, {}, ThreadType::Main, + system.GetCpuManager().GetGuestActivateFunc()); +} + Result KThread::InitializeIdleThread(Core::System& system, KThread* thread, s32 virt_core) { return InitializeThread(thread, {}, {}, {}, IdleThreadPriority, virt_core, {}, ThreadType::Main, - system.GetCpuManager().GetIdleThreadStartFunc()); + abort); } Result KThread::InitializeHighPriorityThread(Core::System& system, KThread* thread, @@ -277,7 +282,7 @@ Result KThread::InitializeUserThread(Core::System& system, KThread* thread, KThr KProcess* owner) { system.Kernel().GlobalSchedulerContext().AddThread(thread); return InitializeThread(thread, func, arg, user_stack_top, prio, virt_core, owner, - ThreadType::User, system.GetCpuManager().GetGuestThreadStartFunc()); + ThreadType::User, system.GetCpuManager().GetGuestThreadFunc()); } void KThread::PostDestroy(uintptr_t arg) { @@ -1058,6 +1063,8 @@ void KThread::Exit() { // Register the thread as a work task. KWorkerTaskManager::AddTask(kernel, KWorkerTaskManager::WorkerType::Exit, this); } + + UNREACHABLE_MSG("KThread::Exit() would return"); } Result KThread::Sleep(s64 timeout) { diff --git a/src/core/hle/kernel/k_thread.h b/src/core/hle/kernel/k_thread.h index 28cd7ecb09..416a861a94 100644 --- a/src/core/hle/kernel/k_thread.h +++ b/src/core/hle/kernel/k_thread.h @@ -110,6 +110,7 @@ void SetCurrentThread(KernelCore& kernel, KThread* thread); [[nodiscard]] KThread* GetCurrentThreadPointer(KernelCore& kernel); [[nodiscard]] KThread& GetCurrentThread(KernelCore& kernel); [[nodiscard]] s32 GetCurrentCoreId(KernelCore& kernel); +size_t CaptureBacktrace(void** buffer, size_t max); class KThread final : public KAutoObjectWithSlabHeapAndContainer, public boost::intrusive::list_base_hook<> { @@ -413,6 +414,9 @@ public: [[nodiscard]] static Result InitializeDummyThread(KThread* thread); + [[nodiscard]] static Result InitializeMainThread(Core::System& system, KThread* thread, + s32 virt_core); + [[nodiscard]] static Result InitializeIdleThread(Core::System& system, KThread* thread, s32 virt_core); @@ -435,6 +439,7 @@ public: bool is_pinned; s32 disable_count; KThread* cur_thread; + std::atomic m_lock; }; [[nodiscard]] StackParameters& GetStackParameters() { diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp index 7307cf262b..10e1f47f65 100644 --- a/src/core/hle/kernel/kernel.cpp +++ b/src/core/hle/kernel/kernel.cpp @@ -75,7 +75,6 @@ struct KernelCore::Impl { InitializeSystemResourceLimit(kernel, system.CoreTiming()); InitializeMemoryLayout(); Init::InitializeKPageBufferSlabHeap(system); - InitializeSchedulers(); InitializeShutdownThreads(); InitializePreemption(kernel); @@ -148,7 +147,6 @@ struct KernelCore::Impl { shutdown_threads[core_id] = nullptr; } - schedulers[core_id]->Finalize(); schedulers[core_id].reset(); } @@ -195,17 +193,11 @@ struct KernelCore::Impl { exclusive_monitor = Core::MakeExclusiveMonitor(system.Memory(), Core::Hardware::NUM_CPU_CORES); for (u32 i = 0; i < Core::Hardware::NUM_CPU_CORES; i++) { - schedulers[i] = std::make_unique(system, i); + schedulers[i] = std::make_unique(system.Kernel()); cores.emplace_back(i, system, *schedulers[i], interrupts); } } - void InitializeSchedulers() { - for (u32 i = 0; i < Core::Hardware::NUM_CPU_CORES; i++) { - cores[i].Scheduler().Initialize(); - } - } - // Creates the default system resource limit void InitializeSystemResourceLimit(KernelCore& kernel, const Core::Timing::CoreTiming& core_timing) { diff --git a/src/core/hle/kernel/svc.cpp b/src/core/hle/kernel/svc.cpp index 8655506b07..27e5a805db 100644 --- a/src/core/hle/kernel/svc.cpp +++ b/src/core/hle/kernel/svc.cpp @@ -887,7 +887,7 @@ static Result GetInfo(Core::System& system, u64* result, u64 info_id, Handle han const auto* const current_thread = GetCurrentThreadPointer(system.Kernel()); const bool same_thread = current_thread == thread.GetPointerUnsafe(); - const u64 prev_ctx_ticks = scheduler.GetLastContextSwitchTicks(); + const u64 prev_ctx_ticks = scheduler.GetLastContextSwitchTime(); u64 out_ticks = 0; if (same_thread && info_sub_id == 0xFFFFFFFFFFFFFFFF) { const u64 thread_ticks = current_thread->GetCpuTime(); @@ -3026,11 +3026,6 @@ void Call(Core::System& system, u32 immediate) { } kernel.ExitSVCProfile(); - - if (!thread->IsCallingSvc()) { - auto* host_context = thread->GetHostContext().get(); - host_context->Rewind(); - } } } // namespace Kernel::Svc From 21945ae127480c8332c1110ceada2df4a42a5848 Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 5 Jul 2022 23:27:25 -0400 Subject: [PATCH 09/50] kernel: fix issues with single core mode --- src/core/cpu_manager.cpp | 160 ++++++++++------ src/core/cpu_manager.h | 11 +- .../hle/kernel/global_scheduler_context.cpp | 5 - src/core/hle/kernel/k_scheduler.cpp | 173 +++++++++--------- src/core/hle/kernel/k_scheduler.h | 24 ++- src/core/hle/kernel/k_thread.cpp | 5 +- src/core/hle/kernel/k_thread.h | 24 --- src/core/hle/kernel/kernel.cpp | 19 +- src/core/hle/kernel/physical_core.cpp | 1 + 9 files changed, 229 insertions(+), 193 deletions(-) diff --git a/src/core/cpu_manager.cpp b/src/core/cpu_manager.cpp index 4281941291..838d6be21c 100644 --- a/src/core/cpu_manager.cpp +++ b/src/core/cpu_manager.cpp @@ -42,14 +42,6 @@ void CpuManager::Shutdown() { } } -void CpuManager::GuestActivateFunction() { - if (is_multicore) { - MultiCoreGuestActivate(); - } else { - SingleCoreGuestActivate(); - } -} - void CpuManager::GuestThreadFunction() { if (is_multicore) { MultiCoreRunGuestThread(); @@ -58,21 +50,16 @@ void CpuManager::GuestThreadFunction() { } } -void CpuManager::ShutdownThreadFunction() { - ShutdownThread(); +void CpuManager::IdleThreadFunction() { + if (is_multicore) { + MultiCoreRunIdleThread(); + } else { + SingleCoreRunIdleThread(); + } } -void CpuManager::WaitForAndHandleInterrupt() { - auto& kernel = system.Kernel(); - auto& physical_core = kernel.CurrentPhysicalCore(); - - ASSERT(Kernel::GetCurrentThread(kernel).GetDisableDispatchCount() == 1); - - if (!physical_core.IsInterrupted()) { - physical_core.Idle(); - } - - HandleInterrupt(); +void CpuManager::ShutdownThreadFunction() { + ShutdownThread(); } void CpuManager::HandleInterrupt() { @@ -86,26 +73,10 @@ void CpuManager::HandleInterrupt() { /// MultiCore /// /////////////////////////////////////////////////////////////////////////////// -void CpuManager::MultiCoreGuestActivate() { - // Similar to the HorizonKernelMain callback in HOS - auto& kernel = system.Kernel(); - auto* scheduler = kernel.CurrentScheduler(); - - scheduler->Activate(); - UNREACHABLE(); -} - void CpuManager::MultiCoreRunGuestThread() { // Similar to UserModeThreadStarter in HOS auto& kernel = system.Kernel(); - auto* thread = kernel.GetCurrentEmuThread(); - thread->EnableDispatch(); - - MultiCoreRunGuestLoop(); -} - -void CpuManager::MultiCoreRunGuestLoop() { - auto& kernel = system.Kernel(); + kernel.CurrentScheduler()->OnThreadStart(); while (true) { auto* physical_core = &kernel.CurrentPhysicalCore(); @@ -118,17 +89,105 @@ void CpuManager::MultiCoreRunGuestLoop() { } } +void CpuManager::MultiCoreRunIdleThread() { + // Not accurate to HOS. Remove this entire method when singlecore is removed. + // See notes in KScheduler::ScheduleImpl for more information about why this + // is inaccurate. + + auto& kernel = system.Kernel(); + kernel.CurrentScheduler()->OnThreadStart(); + + while (true) { + auto& physical_core = kernel.CurrentPhysicalCore(); + if (!physical_core.IsInterrupted()) { + physical_core.Idle(); + } + + HandleInterrupt(); + } +} + /////////////////////////////////////////////////////////////////////////////// /// SingleCore /// /////////////////////////////////////////////////////////////////////////////// -void CpuManager::SingleCoreGuestActivate() {} +void CpuManager::SingleCoreRunGuestThread() { + auto& kernel = system.Kernel(); + kernel.CurrentScheduler()->OnThreadStart(); -void CpuManager::SingleCoreRunGuestThread() {} + while (true) { + auto* physical_core = &kernel.CurrentPhysicalCore(); + if (!physical_core->IsInterrupted()) { + physical_core->Run(); + physical_core = &kernel.CurrentPhysicalCore(); + } -void CpuManager::SingleCoreRunGuestLoop() {} + kernel.SetIsPhantomModeForSingleCore(true); + system.CoreTiming().Advance(); + kernel.SetIsPhantomModeForSingleCore(false); -void CpuManager::PreemptSingleCore(bool from_running_enviroment) {} + PreemptSingleCore(); + HandleInterrupt(); + } +} + +void CpuManager::SingleCoreRunIdleThread() { + auto& kernel = system.Kernel(); + kernel.CurrentScheduler()->OnThreadStart(); + + while (true) { + PreemptSingleCore(false); + system.CoreTiming().AddTicks(1000U); + idle_count++; + HandleInterrupt(); + } +} + +void CpuManager::PreemptSingleCore(bool from_running_environment) { + { + auto& kernel = system.Kernel(); + auto& scheduler = kernel.Scheduler(current_core); + + Kernel::KThread* current_thread = scheduler.GetSchedulerCurrentThread(); + if (idle_count >= 4 || from_running_environment) { + if (!from_running_environment) { + system.CoreTiming().Idle(); + idle_count = 0; + } + kernel.SetIsPhantomModeForSingleCore(true); + system.CoreTiming().Advance(); + kernel.SetIsPhantomModeForSingleCore(false); + } + current_core.store((current_core + 1) % Core::Hardware::NUM_CPU_CORES); + system.CoreTiming().ResetTicks(); + scheduler.Unload(scheduler.GetSchedulerCurrentThread()); + + auto& next_scheduler = kernel.Scheduler(current_core); + + // Disable dispatch. We're about to preempt this thread. + Kernel::KScopedDisableDispatch dd{kernel}; + Common::Fiber::YieldTo(current_thread->GetHostContext(), *next_scheduler.GetSwitchFiber()); + } + + // We've now been scheduled again, and we may have exchanged schedulers. + // Reload the scheduler in case it's different. + { + auto& scheduler = system.Kernel().Scheduler(current_core); + scheduler.Reload(scheduler.GetSchedulerCurrentThread()); + if (!scheduler.IsIdle()) { + idle_count = 0; + } + } +} + +void CpuManager::GuestActivate() { + // Similar to the HorizonKernelMain callback in HOS + auto& kernel = system.Kernel(); + auto* scheduler = kernel.CurrentScheduler(); + + scheduler->Activate(); + UNREACHABLE(); +} void CpuManager::ShutdownThread() { auto& kernel = system.Kernel(); @@ -168,20 +227,11 @@ void CpuManager::RunThread(std::size_t core) { } auto& kernel = system.Kernel(); + auto& scheduler = *kernel.CurrentScheduler(); + auto* thread = scheduler.GetSchedulerCurrentThread(); + Kernel::SetCurrentThread(kernel, thread); - auto* main_thread = Kernel::KThread::Create(kernel); - main_thread->SetName(fmt::format("MainThread:{}", core)); - ASSERT(Kernel::KThread::InitializeMainThread(system, main_thread, static_cast(core)) - .IsSuccess()); - - auto* idle_thread = Kernel::KThread::Create(kernel); - ASSERT(Kernel::KThread::InitializeIdleThread(system, idle_thread, static_cast(core)) - .IsSuccess()); - - kernel.SetCurrentEmuThread(main_thread); - kernel.CurrentScheduler()->Initialize(idle_thread); - - Common::Fiber::YieldTo(data.host_context, *main_thread->GetHostContext()); + Common::Fiber::YieldTo(data.host_context, *thread->GetHostContext()); } } // namespace Core diff --git a/src/core/cpu_manager.h b/src/core/cpu_manager.h index 8143424af2..835505b922 100644 --- a/src/core/cpu_manager.h +++ b/src/core/cpu_manager.h @@ -48,12 +48,11 @@ public: gpu_barrier->Sync(); } - void WaitForAndHandleInterrupt(); void Initialize(); void Shutdown(); std::function GetGuestActivateFunc() { - return [this] { GuestActivateFunction(); }; + return [this] { GuestActivate(); }; } std::function GetGuestThreadFunc() { return [this] { GuestThreadFunction(); }; @@ -72,21 +71,19 @@ public: } private: - void GuestActivateFunction(); void GuestThreadFunction(); void IdleThreadFunction(); void ShutdownThreadFunction(); - void MultiCoreGuestActivate(); void MultiCoreRunGuestThread(); - void MultiCoreRunGuestLoop(); + void MultiCoreRunIdleThread(); - void SingleCoreGuestActivate(); void SingleCoreRunGuestThread(); - void SingleCoreRunGuestLoop(); + void SingleCoreRunIdleThread(); static void ThreadStart(std::stop_token stop_token, CpuManager& cpu_manager, std::size_t core); + void GuestActivate(); void HandleInterrupt(); void ShutdownThread(); void RunThread(std::size_t core); diff --git a/src/core/hle/kernel/global_scheduler_context.cpp b/src/core/hle/kernel/global_scheduler_context.cpp index 21fd5cb678..65576b8c4b 100644 --- a/src/core/hle/kernel/global_scheduler_context.cpp +++ b/src/core/hle/kernel/global_scheduler_context.cpp @@ -42,11 +42,6 @@ void GlobalSchedulerContext::PreemptThreads() { for (u32 core_id = 0; core_id < Core::Hardware::NUM_CPU_CORES; core_id++) { const u32 priority = preemption_priorities[core_id]; KScheduler::RotateScheduledQueue(kernel, core_id, priority); - - // Signal an interrupt occurred. For core 3, this is a certainty, as preemption will result - // in the rotator thread being scheduled. For cores 0-2, this is to simulate or system - // interrupts that may have occurred. - kernel.PhysicalCore(core_id).Interrupt(); } } diff --git a/src/core/hle/kernel/k_scheduler.cpp b/src/core/hle/kernel/k_scheduler.cpp index 13915dbd99..cac96a7806 100644 --- a/src/core/hle/kernel/k_scheduler.cpp +++ b/src/core/hle/kernel/k_scheduler.cpp @@ -28,9 +28,9 @@ static void IncrementScheduledCount(Kernel::KThread* thread) { } KScheduler::KScheduler(KernelCore& kernel_) : kernel{kernel_} { - m_idle_stack = std::make_shared([this] { + m_switch_fiber = std::make_shared([this] { while (true) { - ScheduleImplOffStack(); + ScheduleImplFiber(); } }); @@ -60,9 +60,9 @@ void KScheduler::DisableScheduling(KernelCore& kernel) { void KScheduler::EnableScheduling(KernelCore& kernel, u64 cores_needing_scheduling) { ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() >= 1); - auto* scheduler = kernel.CurrentScheduler(); + auto* scheduler{kernel.CurrentScheduler()}; - if (!scheduler) { + if (!scheduler || kernel.IsPhantomModeForSingleCore()) { // HACK: we cannot schedule from this thread, it is not a core thread RescheduleCores(kernel, cores_needing_scheduling); if (GetCurrentThread(kernel).GetDisableDispatchCount() == 1) { @@ -125,9 +125,9 @@ void KScheduler::RescheduleCurrentCoreImpl() { } } -void KScheduler::Initialize(KThread* idle_thread) { +void KScheduler::Initialize(KThread* main_thread, KThread* idle_thread, s32 core_id) { // Set core ID/idle thread/interrupt task manager. - m_core_id = GetCurrentCoreId(kernel); + m_core_id = core_id; m_idle_thread = idle_thread; // m_state.idle_thread_stack = m_idle_thread->GetStackTop(); // m_state.interrupt_task_manager = &kernel.GetInterruptTaskManager(); @@ -142,10 +142,10 @@ void KScheduler::Initialize(KThread* idle_thread) { // Bind interrupt handler. // kernel.GetInterruptManager().BindHandler( // GetSchedulerInterruptHandler(kernel), KInterruptName::Scheduler, m_core_id, - // KInterruptController::PriorityLevel_Scheduler, false, false); + // KInterruptController::PriorityLevel::Scheduler, false, false); // Set the current thread. - m_current_thread = GetCurrentThreadPointer(kernel); + m_current_thread = main_thread; } void KScheduler::Activate() { @@ -156,6 +156,10 @@ void KScheduler::Activate() { RescheduleCurrentCore(); } +void KScheduler::OnThreadStart() { + GetCurrentThread(kernel).EnableDispatch(); +} + u64 KScheduler::UpdateHighestPriorityThread(KThread* highest_thread) { if (KThread* prev_highest_thread = m_state.highest_priority_thread; prev_highest_thread != highest_thread) [[likely]] { @@ -372,37 +376,30 @@ void KScheduler::ScheduleImpl() { } // The highest priority thread is not the same as the current thread. - // Switch to the idle thread stack and continue executing from there. - m_idle_cur_thread = cur_thread; - m_idle_highest_priority_thread = highest_priority_thread; - Common::Fiber::YieldTo(cur_thread->host_context, *m_idle_stack); + // Jump to the switcher and continue executing from there. + m_switch_cur_thread = cur_thread; + m_switch_highest_priority_thread = highest_priority_thread; + m_switch_from_schedule = true; + Common::Fiber::YieldTo(cur_thread->host_context, *m_switch_fiber); // Returning from ScheduleImpl occurs after this thread has been scheduled again. } -void KScheduler::ScheduleImplOffStack() { - KThread* const cur_thread{m_idle_cur_thread}; - KThread* highest_priority_thread{m_idle_highest_priority_thread}; +void KScheduler::ScheduleImplFiber() { + KThread* const cur_thread{m_switch_cur_thread}; + KThread* highest_priority_thread{m_switch_highest_priority_thread}; - // Get a reference to the current thread's stack parameters. - auto& sp{cur_thread->GetStackParameters()}; + // If we're not coming from scheduling (i.e., we came from SC preemption), + // we should restart the scheduling loop directly. Not accurate to HOS. + if (!m_switch_from_schedule) { + goto retry; + } + + // Mark that we are not coming from scheduling anymore. + m_switch_from_schedule = false; // Save the original thread context. - { - auto& physical_core = kernel.System().CurrentPhysicalCore(); - auto& cpu_core = physical_core.ArmInterface(); - cpu_core.SaveContext(cur_thread->GetContext32()); - cpu_core.SaveContext(cur_thread->GetContext64()); - // Save the TPIDR_EL0 system register in case it was modified. - cur_thread->SetTPIDR_EL0(cpu_core.GetTPIDR_EL0()); - cpu_core.ClearExclusiveState(); - } - - // Check if the thread is terminated by checking the DPC flags. - if ((sp.dpc_flags & static_cast(DpcFlag::Terminated)) == 0) { - // The thread isn't terminated, so we want to unlock it. - sp.m_lock.store(false, std::memory_order_seq_cst); - } + Unload(cur_thread); // The current thread's context has been entirely taken care of. // Now we want to loop until we successfully switch the thread context. @@ -411,62 +408,39 @@ void KScheduler::ScheduleImplOffStack() { // Check if the highest priority thread is null. if (!highest_priority_thread) { // The next thread is nullptr! - // Switch to nullptr. This will actually switch to the idle thread. - SwitchThread(nullptr); - // We've switched to the idle thread, so we want to process interrupt tasks until we - // schedule a non-idle thread. - while (!m_state.interrupt_task_runnable) { - // Check if we need scheduling. - if (m_state.needs_scheduling.load(std::memory_order_seq_cst)) { - goto retry; - } + // Switch to the idle thread. Note: HOS treats idling as a special case for + // performance. This is not *required* for yuzu's purposes, and for singlecore + // compatibility, we can just move the logic that would go here into the execution + // of the idle thread. If we ever remove singlecore, we should implement this + // accurately to HOS. + highest_priority_thread = m_idle_thread; + } - // Clear the previous thread. - m_state.prev_thread = nullptr; - - // Wait for an interrupt before checking again. - kernel.System().GetCpuManager().WaitForAndHandleInterrupt(); + // We want to try to lock the highest priority thread's context. + // Try to take it. + while (!highest_priority_thread->context_guard.try_lock()) { + // The highest priority thread's context is already locked. + // Check if we need scheduling. If we don't, we can retry directly. + if (m_state.needs_scheduling.load(std::memory_order_seq_cst)) { + // If we do, another core is interfering, and we must start again. + goto retry; } + } - // Execute any pending interrupt tasks. - // m_state.interrupt_task_manager->DoTasks(); + // It's time to switch the thread. + // Switch to the highest priority thread. + SwitchThread(highest_priority_thread); - // Clear the interrupt task thread as runnable. - m_state.interrupt_task_runnable = false; - - // Retry the scheduling loop. + // Check if we need scheduling. If we do, then we can't complete the switch and should + // retry. + if (m_state.needs_scheduling.load(std::memory_order_seq_cst)) { + // Our switch failed. + // We should unlock the thread context, and then retry. + highest_priority_thread->context_guard.unlock(); goto retry; } else { - // We want to try to lock the highest priority thread's context. - // Try to take it. - bool expected{false}; - while (!highest_priority_thread->stack_parameters.m_lock.compare_exchange_strong( - expected, true, std::memory_order_seq_cst)) { - // The highest priority thread's context is already locked. - // Check if we need scheduling. If we don't, we can retry directly. - if (m_state.needs_scheduling.load(std::memory_order_seq_cst)) { - // If we do, another core is interfering, and we must start again. - goto retry; - } - expected = false; - } - - // It's time to switch the thread. - // Switch to the highest priority thread. - SwitchThread(highest_priority_thread); - - // Check if we need scheduling. If we do, then we can't complete the switch and should - // retry. - if (m_state.needs_scheduling.load(std::memory_order_seq_cst)) { - // Our switch failed. - // We should unlock the thread context, and then retry. - highest_priority_thread->stack_parameters.m_lock.store(false, - std::memory_order_seq_cst); - goto retry; - } else { - break; - } + break; } retry: @@ -480,18 +454,35 @@ void KScheduler::ScheduleImplOffStack() { } // Reload the guest thread context. - { - auto& cpu_core = kernel.System().CurrentArmInterface(); - cpu_core.LoadContext(highest_priority_thread->GetContext32()); - cpu_core.LoadContext(highest_priority_thread->GetContext64()); - cpu_core.SetTlsAddress(highest_priority_thread->GetTLSAddress()); - cpu_core.SetTPIDR_EL0(highest_priority_thread->GetTPIDR_EL0()); - cpu_core.LoadWatchpointArray(highest_priority_thread->GetOwnerProcess()->GetWatchpoints()); - cpu_core.ClearExclusiveState(); - } + Reload(highest_priority_thread); // Reload the host thread. - Common::Fiber::YieldTo(m_idle_stack, *highest_priority_thread->host_context); + Common::Fiber::YieldTo(m_switch_fiber, *highest_priority_thread->host_context); +} + +void KScheduler::Unload(KThread* thread) { + auto& cpu_core = kernel.System().ArmInterface(m_core_id); + cpu_core.SaveContext(thread->GetContext32()); + cpu_core.SaveContext(thread->GetContext64()); + // Save the TPIDR_EL0 system register in case it was modified. + thread->SetTPIDR_EL0(cpu_core.GetTPIDR_EL0()); + cpu_core.ClearExclusiveState(); + + // Check if the thread is terminated by checking the DPC flags. + if ((thread->GetStackParameters().dpc_flags & static_cast(DpcFlag::Terminated)) == 0) { + // The thread isn't terminated, so we want to unlock it. + thread->context_guard.unlock(); + } +} + +void KScheduler::Reload(KThread* thread) { + auto& cpu_core = kernel.System().ArmInterface(m_core_id); + cpu_core.LoadContext(thread->GetContext32()); + cpu_core.LoadContext(thread->GetContext64()); + cpu_core.SetTlsAddress(thread->GetTLSAddress()); + cpu_core.SetTPIDR_EL0(thread->GetTPIDR_EL0()); + cpu_core.LoadWatchpointArray(thread->GetOwnerProcess()->GetWatchpoints()); + cpu_core.ClearExclusiveState(); } void KScheduler::ClearPreviousThread(KernelCore& kernel, KThread* thread) { diff --git a/src/core/hle/kernel/k_scheduler.h b/src/core/hle/kernel/k_scheduler.h index 8f4eebf6ac..91e8709336 100644 --- a/src/core/hle/kernel/k_scheduler.h +++ b/src/core/hle/kernel/k_scheduler.h @@ -41,8 +41,11 @@ public: explicit KScheduler(KernelCore& kernel); ~KScheduler(); - void Initialize(KThread* idle_thread); + void Initialize(KThread* main_thread, KThread* idle_thread, s32 core_id); void Activate(); + void OnThreadStart(); + void Unload(KThread* thread); + void Reload(KThread* thread); void SetInterruptTaskRunnable(); void RequestScheduleOnInterrupt(); @@ -55,6 +58,14 @@ public: return m_idle_thread; } + bool IsIdle() const { + return m_current_thread.load() == m_idle_thread; + } + + std::shared_ptr GetSwitchFiber() { + return m_switch_fiber; + } + KThread* GetPreviousThread() const { return m_state.prev_thread; } @@ -69,7 +80,7 @@ public: // Static public API. static bool CanSchedule(KernelCore& kernel) { - return kernel.GetCurrentEmuThread()->GetDisableDispatchCount() == 0; + return GetCurrentThread(kernel).GetDisableDispatchCount() == 0; } static bool IsSchedulerLockedByCurrentThread(KernelCore& kernel) { return kernel.GlobalSchedulerContext().scheduler_lock.IsLockedByCurrentThread(); @@ -113,7 +124,7 @@ private: // Instanced private API. void ScheduleImpl(); - void ScheduleImplOffStack(); + void ScheduleImplFiber(); void SwitchThread(KThread* next_thread); void Schedule(); @@ -147,9 +158,10 @@ private: KThread* m_idle_thread{nullptr}; std::atomic m_current_thread{nullptr}; - std::shared_ptr m_idle_stack{}; - KThread* m_idle_cur_thread{}; - KThread* m_idle_highest_priority_thread{}; + std::shared_ptr m_switch_fiber{}; + KThread* m_switch_cur_thread{}; + KThread* m_switch_highest_priority_thread{}; + bool m_switch_from_schedule{}; }; class KScopedSchedulerLock : public KScopedLock { diff --git a/src/core/hle/kernel/k_thread.cpp b/src/core/hle/kernel/k_thread.cpp index 9daa589b5a..d5d390f046 100644 --- a/src/core/hle/kernel/k_thread.cpp +++ b/src/core/hle/kernel/k_thread.cpp @@ -268,7 +268,7 @@ Result KThread::InitializeMainThread(Core::System& system, KThread* thread, s32 Result KThread::InitializeIdleThread(Core::System& system, KThread* thread, s32 virt_core) { return InitializeThread(thread, {}, {}, {}, IdleThreadPriority, virt_core, {}, ThreadType::Main, - abort); + system.GetCpuManager().GetIdleThreadStartFunc()); } Result KThread::InitializeHighPriorityThread(Core::System& system, KThread* thread, @@ -1204,8 +1204,9 @@ KScopedDisableDispatch::~KScopedDisableDispatch() { return; } - // Skip the reschedule if single-core, as dispatch tracking is disabled here. + // Skip the reschedule if single-core. if (!Settings::values.use_multi_core.GetValue()) { + GetCurrentThread(kernel).EnableDispatch(); return; } diff --git a/src/core/hle/kernel/k_thread.h b/src/core/hle/kernel/k_thread.h index 416a861a94..1fc8f5f3e0 100644 --- a/src/core/hle/kernel/k_thread.h +++ b/src/core/hle/kernel/k_thread.h @@ -439,7 +439,6 @@ public: bool is_pinned; s32 disable_count; KThread* cur_thread; - std::atomic m_lock; }; [[nodiscard]] StackParameters& GetStackParameters() { @@ -485,39 +484,16 @@ public: return per_core_priority_queue_entry[core]; } - [[nodiscard]] bool IsKernelThread() const { - return GetActiveCore() == 3; - } - - [[nodiscard]] bool IsDispatchTrackingDisabled() const { - return is_single_core || IsKernelThread(); - } - [[nodiscard]] s32 GetDisableDispatchCount() const { - if (IsDispatchTrackingDisabled()) { - // TODO(bunnei): Until kernel threads are emulated, we cannot enable/disable dispatch. - return 1; - } - return this->GetStackParameters().disable_count; } void DisableDispatch() { - if (IsDispatchTrackingDisabled()) { - // TODO(bunnei): Until kernel threads are emulated, we cannot enable/disable dispatch. - return; - } - ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() >= 0); this->GetStackParameters().disable_count++; } void EnableDispatch() { - if (IsDispatchTrackingDisabled()) { - // TODO(bunnei): Until kernel threads are emulated, we cannot enable/disable dispatch. - return; - } - ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() > 0); this->GetStackParameters().disable_count--; } diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp index 10e1f47f65..926c6dc84c 100644 --- a/src/core/hle/kernel/kernel.cpp +++ b/src/core/hle/kernel/kernel.cpp @@ -64,8 +64,6 @@ struct KernelCore::Impl { is_phantom_mode_for_singlecore = false; - InitializePhysicalCores(); - // Derive the initial memory layout from the emulated board Init::InitializeSlabResourceCounts(kernel); DeriveInitialMemoryLayout(); @@ -77,6 +75,7 @@ struct KernelCore::Impl { Init::InitializeKPageBufferSlabHeap(system); InitializeShutdownThreads(); InitializePreemption(kernel); + InitializePhysicalCores(); RegisterHostThread(); } @@ -193,8 +192,21 @@ struct KernelCore::Impl { exclusive_monitor = Core::MakeExclusiveMonitor(system.Memory(), Core::Hardware::NUM_CPU_CORES); for (u32 i = 0; i < Core::Hardware::NUM_CPU_CORES; i++) { + const s32 core{static_cast(i)}; + schedulers[i] = std::make_unique(system.Kernel()); cores.emplace_back(i, system, *schedulers[i], interrupts); + + auto* main_thread{Kernel::KThread::Create(system.Kernel())}; + main_thread->SetName(fmt::format("MainThread:{}", core)); + main_thread->SetCurrentCore(core); + ASSERT(Kernel::KThread::InitializeMainThread(system, main_thread, core).IsSuccess()); + + auto* idle_thread{Kernel::KThread::Create(system.Kernel())}; + idle_thread->SetCurrentCore(core); + ASSERT(Kernel::KThread::InitializeIdleThread(system, idle_thread, core).IsSuccess()); + + schedulers[i]->Initialize(main_thread, idle_thread, core); } } @@ -1093,10 +1105,11 @@ void KernelCore::Suspend(bool suspended) { } void KernelCore::ShutdownCores() { + KScopedSchedulerLock lk{*this}; + for (auto* thread : impl->shutdown_threads) { void(thread->Run()); } - InterruptAllPhysicalCores(); } bool KernelCore::IsMulticore() const { diff --git a/src/core/hle/kernel/physical_core.cpp b/src/core/hle/kernel/physical_core.cpp index a5b16ae2eb..6e7dacf97a 100644 --- a/src/core/hle/kernel/physical_core.cpp +++ b/src/core/hle/kernel/physical_core.cpp @@ -43,6 +43,7 @@ void PhysicalCore::Initialize([[maybe_unused]] bool is_64_bit) { void PhysicalCore::Run() { arm_interface->Run(); + arm_interface->ClearExclusiveState(); } void PhysicalCore::Idle() { From da07e13e0798a4ebd423595830f04e2234a03942 Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 7 Jul 2022 12:34:46 -0400 Subject: [PATCH 10/50] kernel: fix single-core preemption points --- src/core/cpu_manager.cpp | 40 ++++++++++------------------- src/core/cpu_manager.h | 1 - src/core/hle/kernel/k_scheduler.cpp | 13 ++++++++++ src/core/hle/kernel/k_scheduler.h | 5 +--- src/core/hle/kernel/k_thread.cpp | 6 ----- src/core/hle/kernel/k_thread.h | 1 - 6 files changed, 27 insertions(+), 39 deletions(-) diff --git a/src/core/cpu_manager.cpp b/src/core/cpu_manager.cpp index 838d6be21c..9b1565ae1d 100644 --- a/src/core/cpu_manager.cpp +++ b/src/core/cpu_manager.cpp @@ -144,39 +144,25 @@ void CpuManager::SingleCoreRunIdleThread() { } void CpuManager::PreemptSingleCore(bool from_running_environment) { - { - auto& kernel = system.Kernel(); - auto& scheduler = kernel.Scheduler(current_core); + auto& kernel = system.Kernel(); - Kernel::KThread* current_thread = scheduler.GetSchedulerCurrentThread(); - if (idle_count >= 4 || from_running_environment) { - if (!from_running_environment) { - system.CoreTiming().Idle(); - idle_count = 0; - } - kernel.SetIsPhantomModeForSingleCore(true); - system.CoreTiming().Advance(); - kernel.SetIsPhantomModeForSingleCore(false); + if (idle_count >= 4 || from_running_environment) { + if (!from_running_environment) { + system.CoreTiming().Idle(); + idle_count = 0; } - current_core.store((current_core + 1) % Core::Hardware::NUM_CPU_CORES); - system.CoreTiming().ResetTicks(); - scheduler.Unload(scheduler.GetSchedulerCurrentThread()); - - auto& next_scheduler = kernel.Scheduler(current_core); - - // Disable dispatch. We're about to preempt this thread. - Kernel::KScopedDisableDispatch dd{kernel}; - Common::Fiber::YieldTo(current_thread->GetHostContext(), *next_scheduler.GetSwitchFiber()); + kernel.SetIsPhantomModeForSingleCore(true); + system.CoreTiming().Advance(); + kernel.SetIsPhantomModeForSingleCore(false); } + current_core.store((current_core + 1) % Core::Hardware::NUM_CPU_CORES); + system.CoreTiming().ResetTicks(); + kernel.Scheduler(current_core).PreemptSingleCore(); // We've now been scheduled again, and we may have exchanged schedulers. // Reload the scheduler in case it's different. - { - auto& scheduler = system.Kernel().Scheduler(current_core); - scheduler.Reload(scheduler.GetSchedulerCurrentThread()); - if (!scheduler.IsIdle()) { - idle_count = 0; - } + if (!kernel.Scheduler(current_core).IsIdle()) { + idle_count = 0; } } diff --git a/src/core/cpu_manager.h b/src/core/cpu_manager.h index 835505b922..95ea3ef390 100644 --- a/src/core/cpu_manager.h +++ b/src/core/cpu_manager.h @@ -5,7 +5,6 @@ #include #include -#include #include #include #include diff --git a/src/core/hle/kernel/k_scheduler.cpp b/src/core/hle/kernel/k_scheduler.cpp index cac96a7806..230ca3b953 100644 --- a/src/core/hle/kernel/k_scheduler.cpp +++ b/src/core/hle/kernel/k_scheduler.cpp @@ -103,7 +103,20 @@ void KScheduler::ScheduleOnInterrupt() { GetCurrentThread(kernel).EnableDispatch(); } +void KScheduler::PreemptSingleCore() { + GetCurrentThread(kernel).DisableDispatch(); + + auto* thread = GetCurrentThreadPointer(kernel); + auto& previous_scheduler = kernel.Scheduler(thread->GetCurrentCore()); + previous_scheduler.Unload(thread); + + Common::Fiber::YieldTo(thread->GetHostContext(), *m_switch_fiber); + + GetCurrentThread(kernel).EnableDispatch(); +} + void KScheduler::RescheduleCurrentCore() { + ASSERT(!kernel.IsPhantomModeForSingleCore()); ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() == 1); GetCurrentThread(kernel).EnableDispatch(); diff --git a/src/core/hle/kernel/k_scheduler.h b/src/core/hle/kernel/k_scheduler.h index 91e8709336..ac7421c9a9 100644 --- a/src/core/hle/kernel/k_scheduler.h +++ b/src/core/hle/kernel/k_scheduler.h @@ -49,6 +49,7 @@ public: void SetInterruptTaskRunnable(); void RequestScheduleOnInterrupt(); + void PreemptSingleCore(); u64 GetIdleCount() { return m_state.idle_count; @@ -62,10 +63,6 @@ public: return m_current_thread.load() == m_idle_thread; } - std::shared_ptr GetSwitchFiber() { - return m_switch_fiber; - } - KThread* GetPreviousThread() const { return m_state.prev_thread; } diff --git a/src/core/hle/kernel/k_thread.cpp b/src/core/hle/kernel/k_thread.cpp index d5d390f046..3640d1d13b 100644 --- a/src/core/hle/kernel/k_thread.cpp +++ b/src/core/hle/kernel/k_thread.cpp @@ -1204,12 +1204,6 @@ KScopedDisableDispatch::~KScopedDisableDispatch() { return; } - // Skip the reschedule if single-core. - if (!Settings::values.use_multi_core.GetValue()) { - GetCurrentThread(kernel).EnableDispatch(); - return; - } - if (GetCurrentThread(kernel).GetDisableDispatchCount() <= 1) { auto scheduler = kernel.CurrentScheduler(); diff --git a/src/core/hle/kernel/k_thread.h b/src/core/hle/kernel/k_thread.h index 1fc8f5f3e0..9ee20208eb 100644 --- a/src/core/hle/kernel/k_thread.h +++ b/src/core/hle/kernel/k_thread.h @@ -110,7 +110,6 @@ void SetCurrentThread(KernelCore& kernel, KThread* thread); [[nodiscard]] KThread* GetCurrentThreadPointer(KernelCore& kernel); [[nodiscard]] KThread& GetCurrentThread(KernelCore& kernel); [[nodiscard]] s32 GetCurrentCoreId(KernelCore& kernel); -size_t CaptureBacktrace(void** buffer, size_t max); class KThread final : public KAutoObjectWithSlabHeapAndContainer, public boost::intrusive::list_base_hook<> { From 77137583cd49526a492293d47477657c23ca164f Mon Sep 17 00:00:00 2001 From: Liam Date: Sat, 9 Jul 2022 18:47:32 -0400 Subject: [PATCH 11/50] kernel: be more careful about initialization path for HLE threads --- src/core/hle/kernel/k_scheduler.cpp | 1 + src/core/hle/kernel/k_thread.cpp | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/core/hle/kernel/k_scheduler.cpp b/src/core/hle/kernel/k_scheduler.cpp index 230ca3b953..d9ba8e4092 100644 --- a/src/core/hle/kernel/k_scheduler.cpp +++ b/src/core/hle/kernel/k_scheduler.cpp @@ -70,6 +70,7 @@ void KScheduler::EnableScheduling(KernelCore& kernel, u64 cores_needing_scheduli GetCurrentThread(kernel).IfDummyThreadTryWait(); } GetCurrentThread(kernel).EnableDispatch(); + ASSERT(GetCurrentThread(kernel).GetState() != ThreadState::Waiting); return; } diff --git a/src/core/hle/kernel/k_thread.cpp b/src/core/hle/kernel/k_thread.cpp index 3640d1d13b..985ce448e6 100644 --- a/src/core/hle/kernel/k_thread.cpp +++ b/src/core/hle/kernel/k_thread.cpp @@ -258,7 +258,13 @@ Result KThread::InitializeThread(KThread* thread, KThreadFunction func, uintptr_ } Result KThread::InitializeDummyThread(KThread* thread) { - return thread->Initialize({}, {}, {}, DummyThreadPriority, 3, {}, ThreadType::Dummy); + // Initialize the thread. + R_TRY(thread->Initialize({}, {}, {}, DummyThreadPriority, 3, {}, ThreadType::Dummy)); + + // Initialize emulation parameters. + thread->stack_parameters.disable_count = 0; + + return ResultSuccess; } Result KThread::InitializeMainThread(Core::System& system, KThread* thread, s32 virt_core) { From a9a83fa726b43a28f4e5a40516efd56fbf99009f Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 11 Jul 2022 10:13:13 -0400 Subject: [PATCH 12/50] kernel: Ensure all uses of disable_count are balanced --- src/core/hle/kernel/k_scheduler.cpp | 21 +++++++++++++-------- src/core/hle/kernel/k_scheduler.h | 2 ++ src/core/hle/kernel/k_thread.cpp | 8 ++++++-- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/core/hle/kernel/k_scheduler.cpp b/src/core/hle/kernel/k_scheduler.cpp index d9ba8e4092..c34ce7a178 100644 --- a/src/core/hle/kernel/k_scheduler.cpp +++ b/src/core/hle/kernel/k_scheduler.cpp @@ -63,14 +63,8 @@ void KScheduler::EnableScheduling(KernelCore& kernel, u64 cores_needing_scheduli auto* scheduler{kernel.CurrentScheduler()}; if (!scheduler || kernel.IsPhantomModeForSingleCore()) { - // HACK: we cannot schedule from this thread, it is not a core thread - RescheduleCores(kernel, cores_needing_scheduling); - if (GetCurrentThread(kernel).GetDisableDispatchCount() == 1) { - // Special case to ensure dummy threads that are waiting block - GetCurrentThread(kernel).IfDummyThreadTryWait(); - } - GetCurrentThread(kernel).EnableDispatch(); - ASSERT(GetCurrentThread(kernel).GetState() != ThreadState::Waiting); + KScheduler::RescheduleCores(kernel, cores_needing_scheduling); + KScheduler::RescheduleCurrentHLEThread(kernel); return; } @@ -83,6 +77,17 @@ void KScheduler::EnableScheduling(KernelCore& kernel, u64 cores_needing_scheduli } } +void KScheduler::RescheduleCurrentHLEThread(KernelCore& kernel) { + // HACK: we cannot schedule from this thread, it is not a core thread + ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() == 1); + + // Special case to ensure dummy threads that are waiting block + GetCurrentThread(kernel).IfDummyThreadTryWait(); + + ASSERT(GetCurrentThread(kernel).GetState() != ThreadState::Waiting); + GetCurrentThread(kernel).EnableDispatch(); +} + u64 KScheduler::UpdateHighestPriorityThreads(KernelCore& kernel) { if (IsSchedulerUpdateNeeded(kernel)) { return UpdateHighestPriorityThreadsImpl(kernel); diff --git a/src/core/hle/kernel/k_scheduler.h b/src/core/hle/kernel/k_scheduler.h index ac7421c9a9..534321d8d5 100644 --- a/src/core/hle/kernel/k_scheduler.h +++ b/src/core/hle/kernel/k_scheduler.h @@ -119,6 +119,8 @@ private: } static u64 UpdateHighestPriorityThreadsImpl(KernelCore& kernel); + static void RescheduleCurrentHLEThread(KernelCore& kernel); + // Instanced private API. void ScheduleImpl(); void ScheduleImplFiber(); diff --git a/src/core/hle/kernel/k_thread.cpp b/src/core/hle/kernel/k_thread.cpp index 985ce448e6..174afc80d7 100644 --- a/src/core/hle/kernel/k_thread.cpp +++ b/src/core/hle/kernel/k_thread.cpp @@ -1106,6 +1106,8 @@ void KThread::IfDummyThreadTryWait() { return; } + ASSERT(!kernel.IsPhantomModeForSingleCore()); + // Block until we are no longer waiting. std::unique_lock lk(dummy_wait_lock); dummy_wait_cv.wait( @@ -1211,10 +1213,12 @@ KScopedDisableDispatch::~KScopedDisableDispatch() { } if (GetCurrentThread(kernel).GetDisableDispatchCount() <= 1) { - auto scheduler = kernel.CurrentScheduler(); + auto* scheduler = kernel.CurrentScheduler(); - if (scheduler) { + if (scheduler && !kernel.IsPhantomModeForSingleCore()) { scheduler->RescheduleCurrentCore(); + } else { + KScheduler::RescheduleCurrentHLEThread(kernel); } } else { GetCurrentThread(kernel).EnableDispatch(); From e991525d630eb320e6f02c964d5c44a5abb9847f Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Fri, 15 Jul 2022 00:49:21 -0400 Subject: [PATCH 13/50] CMakeLists: Add QtConcurrent to required components We use QtConcurrent in various places in our Qt frontend, add it to the required components. --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 80a8d4ed81..8d572392f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -351,9 +351,9 @@ if(ENABLE_QT) set(YUZU_QT_NO_CMAKE_SYSTEM_PATH "NO_CMAKE_SYSTEM_PATH") endif() if ((${CMAKE_SYSTEM_NAME} STREQUAL "Linux") AND YUZU_USE_BUNDLED_QT) - find_package(Qt5 ${QT_VERSION} REQUIRED COMPONENTS Widgets DBus ${QT_PREFIX_HINT} ${YUZU_QT_NO_CMAKE_SYSTEM_PATH}) + find_package(Qt5 ${QT_VERSION} REQUIRED COMPONENTS Widgets Concurrent DBus ${QT_PREFIX_HINT} ${YUZU_QT_NO_CMAKE_SYSTEM_PATH}) else() - find_package(Qt5 ${QT_VERSION} REQUIRED COMPONENTS Widgets ${QT_PREFIX_HINT} ${YUZU_QT_NO_CMAKE_SYSTEM_PATH}) + find_package(Qt5 ${QT_VERSION} REQUIRED COMPONENTS Widgets Concurrent ${QT_PREFIX_HINT} ${YUZU_QT_NO_CMAKE_SYSTEM_PATH}) endif() if (YUZU_USE_QT_WEB_ENGINE) find_package(Qt5 COMPONENTS WebEngineCore WebEngineWidgets) From fc503c3445741206d7a845a2a9a1dca35f44f70f Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Fri, 15 Jul 2022 00:50:51 -0400 Subject: [PATCH 14/50] CMakeLists: Mark WebEngine(Core/Widgets) as required Mark these components as required when we are building with QtWebEngine enabled. --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d572392f1..c5ecb3ae74 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -356,7 +356,7 @@ if(ENABLE_QT) find_package(Qt5 ${QT_VERSION} REQUIRED COMPONENTS Widgets Concurrent ${QT_PREFIX_HINT} ${YUZU_QT_NO_CMAKE_SYSTEM_PATH}) endif() if (YUZU_USE_QT_WEB_ENGINE) - find_package(Qt5 COMPONENTS WebEngineCore WebEngineWidgets) + find_package(Qt5 REQUIRED COMPONENTS WebEngineCore WebEngineWidgets) endif() if (ENABLE_QT_TRANSLATION) From 1002563776157425ea8e1e4db86f2dd5a369e561 Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Fri, 15 Jul 2022 00:57:42 -0400 Subject: [PATCH 15/50] CopyYuzuQt5Deps: Remove unused dlls --- CMakeModules/CopyYuzuQt5Deps.cmake | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/CMakeModules/CopyYuzuQt5Deps.cmake b/CMakeModules/CopyYuzuQt5Deps.cmake index dd97f5b2b9..0c27d51a65 100644 --- a/CMakeModules/CopyYuzuQt5Deps.cmake +++ b/CMakeModules/CopyYuzuQt5Deps.cmake @@ -19,9 +19,6 @@ function(copy_yuzu_Qt5_deps target_dir) set(IMAGEFORMATS ${DLL_DEST}plugins/imageformats/) if (MSVC) windows_copy_files(${target_dir} ${Qt5_DLL_DIR} ${DLL_DEST} - icudt*.dll - icuin*.dll - icuuc*.dll Qt5Core$<$:d>.* Qt5Gui$<$:d>.* Qt5Widgets$<$:d>.* @@ -37,18 +34,17 @@ function(copy_yuzu_Qt5_deps target_dir) Qt5Quick$<$:d>.* Qt5QuickWidgets$<$:d>.* Qt5WebChannel$<$:d>.* - Qt5WebEngine$<$:d>.* Qt5WebEngineCore$<$:d>.* Qt5WebEngineWidgets$<$:d>.* QtWebEngineProcess$<$:d>.* ) windows_copy_files(${target_dir} ${Qt5_RESOURCES_DIR} ${DLL_DEST} - qtwebengine_resources.pak + icudtl.dat qtwebengine_devtools_resources.pak + qtwebengine_resources.pak qtwebengine_resources_100p.pak qtwebengine_resources_200p.pak - icudtl.dat ) endif () windows_copy_files(yuzu ${Qt5_PLATFORMS_DIR} ${PLATFORMS} qwindows$<$:d>.*) @@ -56,7 +52,7 @@ function(copy_yuzu_Qt5_deps target_dir) windows_copy_files(yuzu ${Qt5_IMAGEFORMATS_DIR} ${IMAGEFORMATS} qjpeg$<$:d>.* qgif$<$:d>.* - ) + ) else() set(Qt5_DLLS "${Qt5_DLL_DIR}libQt5Core.so.5" From 912cae21b070e6f5972ce34a08b25e4ae2182d5c Mon Sep 17 00:00:00 2001 From: Link4565 <49906383+Link4565@users.noreply.github.com> Date: Tue, 28 Jun 2022 20:29:41 +0100 Subject: [PATCH 16/50] Enable the use of MSG_DONTWAIT flag on RecvImpl --- src/core/hle/service/sockets/bsd.cpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/core/hle/service/sockets/bsd.cpp b/src/core/hle/service/sockets/bsd.cpp index 5114b8be2f..3e9dc4a139 100644 --- a/src/core/hle/service/sockets/bsd.cpp +++ b/src/core/hle/service/sockets/bsd.cpp @@ -720,7 +720,25 @@ std::pair BSD::RecvImpl(s32 fd, u32 flags, std::vector& message) if (!IsFileDescriptorValid(fd)) { return {-1, Errno::BADF}; } - return Translate(file_descriptors[fd]->socket->Recv(flags, message)); + + FileDescriptor& descriptor = *file_descriptors[fd]; + + // Apply flags + if ((flags & FLAG_MSG_DONTWAIT) != 0) { + flags &= ~FLAG_MSG_DONTWAIT; + if ((descriptor.flags & FLAG_O_NONBLOCK) == 0) { + descriptor.socket->SetNonBlock(true); + } + } + + const auto [ret, bsd_errno] = Translate(descriptor.socket->Recv(flags, message)); + + // Restore original state + if ((descriptor.flags & FLAG_O_NONBLOCK) == 0) { + descriptor.socket->SetNonBlock(false); + } + + return {ret, bsd_errno}; } std::pair BSD::RecvFromImpl(s32 fd, u32 flags, std::vector& message, From b4ee3fa39acf2a824145ccea555eaec29fa5c84e Mon Sep 17 00:00:00 2001 From: Merry Date: Sun, 17 Jul 2022 22:44:34 +0100 Subject: [PATCH 17/50] externals: Update dynarmic to 6.2.1 Fix issue with A64CallbackConfigPass --- externals/dynarmic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/externals/dynarmic b/externals/dynarmic index 1f0a43753e..91d1f944e3 160000 --- a/externals/dynarmic +++ b/externals/dynarmic @@ -1 +1 @@ -Subproject commit 1f0a43753e51e4855ee6c0936d30807f373245cc +Subproject commit 91d1f944e3870e0f3c505b48f5ec00ca9a82b95d From 742f67908c191a71b204519a353fb5e42a9dea56 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Thu, 14 Jul 2022 18:42:56 -0700 Subject: [PATCH 18/50] implement resume message --- src/core/hle/service/am/am.cpp | 4 ++++ src/core/hle/service/am/am.h | 1 + src/yuzu/main.cpp | 17 +++++++++++++++++ src/yuzu/main.h | 1 + 4 files changed, 23 insertions(+) diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index 9c62ebc60d..9116dd77c9 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -636,6 +636,10 @@ void AppletMessageQueue::RequestExit() { PushMessage(AppletMessage::Exit); } +void AppletMessageQueue::RequestResume() { + PushMessage(AppletMessage::Resume); +} + void AppletMessageQueue::FocusStateChanged() { PushMessage(AppletMessage::FocusStateChanged); } diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index 988ead2151..53144427b4 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -90,6 +90,7 @@ public: AppletMessage PopMessage(); std::size_t GetMessageCount() const; void RequestExit(); + void RequestResume(); void FocusStateChanged(); void OperationModeChanged(); diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index ed802d3290..e60d840548 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -1131,6 +1131,7 @@ void GMainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) { OnPauseGame(); } else if (!emu_thread->IsRunning() && auto_paused && state == Qt::ApplicationActive) { auto_paused = false; + RequestGameResume(); OnStartGame(); } } @@ -2570,6 +2571,7 @@ void GMainWindow::OnPauseContinueGame() { if (emu_thread->IsRunning()) { OnPauseGame(); } else { + RequestGameResume(); OnStartGame(); } } @@ -3749,6 +3751,21 @@ void GMainWindow::RequestGameExit() { } } +void GMainWindow::RequestGameResume() { + auto& sm{system->ServiceManager()}; + auto applet_oe = sm.GetService("appletOE"); + auto applet_ae = sm.GetService("appletAE"); + + if (applet_oe != nullptr) { + applet_oe->GetMessageQueue()->RequestResume(); + return; + } + + if (applet_ae != nullptr) { + applet_ae->GetMessageQueue()->RequestResume(); + } +} + void GMainWindow::filterBarSetChecked(bool state) { ui->action_Show_Filter_Bar->setChecked(state); emit(OnToggleFilterBar()); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 8cf224c9c2..09e37f1528 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -244,6 +244,7 @@ private: bool ConfirmChangeGame(); bool ConfirmForceLockedExit(); void RequestGameExit(); + void RequestGameResume(); void closeEvent(QCloseEvent* event) override; private slots: From f5964fe2a956673eb19df630c285b2237e9d682c Mon Sep 17 00:00:00 2001 From: lat9nq Date: Mon, 18 Jul 2022 21:43:29 -0400 Subject: [PATCH 19/50] externals: Revert SDL2 to release-2.0.20 prerelease-2.23.1 appears to have issues on the SteamDeck with external controllers. Revert to 2.0.20 for now (and as opposed to using prerelease-2.0.19 like before.) --- externals/SDL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/externals/SDL b/externals/SDL index 59fb7acbf7..b424665e08 160000 --- a/externals/SDL +++ b/externals/SDL @@ -1 +1 @@ -Subproject commit 59fb7acbf7af9d64a2d5432bb6677585a0ddd50a +Subproject commit b424665e0899769b200231ba943353a5fee1b6b6 From 6f9bfb10d38f8f03db452c2d745b9fc7ba503b04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Locatti?= <42481638+goldenx86@users.noreply.github.com> Date: Tue, 19 Jul 2022 16:20:16 -0300 Subject: [PATCH 20/50] Update configure_input.ui --- src/yuzu/configuration/configure_input.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yuzu/configuration/configure_input.ui b/src/yuzu/configuration/configure_input.ui index 2707025e7f..d517740285 100644 --- a/src/yuzu/configuration/configure_input.ui +++ b/src/yuzu/configuration/configure_input.ui @@ -166,7 +166,7 @@ - Undocked + Handheld From 382b41b18f304fcd7aca76b004f7e19f52706d27 Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 19 Jul 2022 17:46:26 -0400 Subject: [PATCH 21/50] video_core: use correct byte size for framebuffer --- src/video_core/renderer_vulkan/vk_blit_screen.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_blit_screen.cpp b/src/video_core/renderer_vulkan/vk_blit_screen.cpp index 1ec8392e14..4a1d963222 100644 --- a/src/video_core/renderer_vulkan/vk_blit_screen.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_screen.cpp @@ -87,8 +87,12 @@ u32 GetBytesPerPixel(const Tegra::FramebufferConfig& framebuffer) { } std::size_t GetSizeInBytes(const Tegra::FramebufferConfig& framebuffer) { - return static_cast(framebuffer.stride) * - static_cast(framebuffer.height) * GetBytesPerPixel(framebuffer); + // TODO(Rodrigo): Read this from HLE + constexpr u32 block_height_log2 = 4; + const u32 bytes_per_pixel = GetBytesPerPixel(framebuffer); + const u64 size_bytes{Tegra::Texture::CalculateSize( + true, bytes_per_pixel, framebuffer.stride, framebuffer.height, 1, block_height_log2, 0)}; + return size_bytes; } VkFormat GetFormat(const Tegra::FramebufferConfig& framebuffer) { @@ -169,9 +173,8 @@ VkSemaphore BlitScreen::Draw(const Tegra::FramebufferConfig& framebuffer, // TODO(Rodrigo): Read this from HLE constexpr u32 block_height_log2 = 4; const u32 bytes_per_pixel = GetBytesPerPixel(framebuffer); - const u64 size_bytes{Tegra::Texture::CalculateSize(true, bytes_per_pixel, - framebuffer.stride, framebuffer.height, - 1, block_height_log2, 0)}; + const u64 size_bytes{GetSizeInBytes(framebuffer)}; + Tegra::Texture::UnswizzleTexture( mapped_span.subspan(image_offset, size_bytes), std::span(host_ptr, size_bytes), bytes_per_pixel, framebuffer.width, framebuffer.height, 1, block_height_log2, 0); From 458da8a94877677f086f06cdeecf959ec4283a33 Mon Sep 17 00:00:00 2001 From: Kelebek1 Date: Sat, 16 Jul 2022 23:48:45 +0100 Subject: [PATCH 22/50] Project Andio --- .gitmodules | 2 +- src/audio_core/CMakeLists.txt | 253 +- src/audio_core/algorithm/filter.cpp | 79 - src/audio_core/algorithm/filter.h | 61 - src/audio_core/algorithm/interpolate.cpp | 232 -- src/audio_core/algorithm/interpolate.h | 43 - src/audio_core/audio_core.cpp | 68 + src/audio_core/audio_core.h | 100 + src/audio_core/audio_event.cpp | 61 + src/audio_core/audio_event.h | 92 + src/audio_core/audio_in_manager.cpp | 91 + src/audio_core/audio_in_manager.h | 92 + src/audio_core/audio_manager.cpp | 80 + src/audio_core/audio_manager.h | 101 + src/audio_core/audio_out.cpp | 62 - src/audio_core/audio_out.h | 49 - src/audio_core/audio_out_manager.cpp | 81 + src/audio_core/audio_out_manager.h | 89 + src/audio_core/audio_render_manager.cpp | 70 + src/audio_core/audio_render_manager.h | 103 + src/audio_core/audio_renderer.cpp | 343 -- src/audio_core/audio_renderer.h | 78 - src/audio_core/behavior_info.cpp | 104 - src/audio_core/behavior_info.h | 71 - src/audio_core/buffer.h | 44 - src/audio_core/codec.cpp | 77 - src/audio_core/codec.h | 43 - src/audio_core/command_generator.cpp | 1369 ------- src/audio_core/command_generator.h | 110 - src/audio_core/common.h | 132 - .../common/audio_renderer_parameter.h | 60 + src/audio_core/common/common.h | 138 + src/audio_core/common/feature_support.h | 105 + src/audio_core/common/wave_buffer.h | 35 + src/audio_core/common/workbuffer_allocator.h | 100 + src/audio_core/cubeb_sink.cpp | 249 -- src/audio_core/cubeb_sink.h | 35 - src/audio_core/delay_line.cpp | 107 - src/audio_core/delay_line.h | 49 - src/audio_core/device/audio_buffer.h | 21 + src/audio_core/device/audio_buffers.h | 304 ++ src/audio_core/device/device_session.cpp | 114 + src/audio_core/device/device_session.h | 126 + src/audio_core/effect_context.cpp | 320 -- src/audio_core/effect_context.h | 349 -- src/audio_core/in/audio_in.cpp | 100 + src/audio_core/in/audio_in.h | 147 + src/audio_core/in/audio_in_system.cpp | 213 + src/audio_core/in/audio_in_system.h | 275 ++ src/audio_core/info_updater.cpp | 511 --- src/audio_core/info_updater.h | 57 - src/audio_core/memory_pool.cpp | 60 - src/audio_core/memory_pool.h | 51 - src/audio_core/mix_context.cpp | 297 -- src/audio_core/mix_context.h | 113 - src/audio_core/null_sink.h | 32 - src/audio_core/out/audio_out.cpp | 100 + src/audio_core/out/audio_out.h | 147 + src/audio_core/out/audio_out_system.cpp | 207 + src/audio_core/out/audio_out_system.h | 257 ++ src/audio_core/renderer/adsp/adsp.cpp | 118 + src/audio_core/renderer/adsp/adsp.h | 173 + .../renderer/adsp/audio_renderer.cpp | 226 + src/audio_core/renderer/adsp/audio_renderer.h | 203 + src/audio_core/renderer/adsp/command_buffer.h | 21 + .../renderer/adsp/command_list_processor.cpp | 109 + .../renderer/adsp/command_list_processor.h | 118 + src/audio_core/renderer/audio_device.cpp | 52 + src/audio_core/renderer/audio_device.h | 88 + src/audio_core/renderer/audio_renderer.cpp | 67 + src/audio_core/renderer/audio_renderer.h | 97 + .../renderer/behavior/behavior_info.cpp | 191 + .../renderer/behavior/behavior_info.h | 376 ++ .../renderer/behavior/info_updater.cpp | 539 +++ .../renderer/behavior/info_updater.h | 205 + .../renderer/command/command_buffer.cpp | 714 ++++ .../renderer/command/command_buffer.h | 466 +++ .../renderer/command/command_generator.cpp | 796 ++++ .../renderer/command/command_generator.h | 349 ++ .../renderer/command/command_list_header.h | 22 + .../command_processing_time_estimator.cpp | 3620 +++++++++++++++++ .../command_processing_time_estimator.h | 254 ++ src/audio_core/renderer/command/commands.h | 32 + .../renderer/command/data_source/adpcm.cpp | 84 + .../renderer/command/data_source/adpcm.h | 119 + .../renderer/command/data_source/decode.cpp | 428 ++ .../renderer/command/data_source/decode.h | 59 + .../command/data_source/pcm_float.cpp | 86 + .../renderer/command/data_source/pcm_float.h | 113 + .../command/data_source/pcm_int16.cpp | 87 + .../renderer/command/data_source/pcm_int16.h | 110 + .../renderer/command/effect/aux_.cpp | 207 + src/audio_core/renderer/command/effect/aux_.h | 66 + .../renderer/command/effect/biquad_filter.cpp | 118 + .../renderer/command/effect/biquad_filter.h | 74 + .../renderer/command/effect/capture.cpp | 142 + .../renderer/command/effect/capture.h | 62 + .../renderer/command/effect/compressor.cpp | 156 + .../renderer/command/effect/compressor.h | 60 + .../renderer/command/effect/delay.cpp | 238 ++ .../renderer/command/effect/delay.h | 60 + .../renderer/command/effect/i3dl2_reverb.cpp | 437 ++ .../renderer/command/effect/i3dl2_reverb.h | 60 + .../renderer/command/effect/light_limiter.cpp | 222 + .../renderer/command/effect/light_limiter.h | 103 + .../effect/multi_tap_biquad_filter.cpp | 45 + .../command/effect/multi_tap_biquad_filter.h | 59 + .../renderer/command/effect/reverb.cpp | 440 ++ .../renderer/command/effect/reverb.h | 62 + src/audio_core/renderer/command/icommand.h | 93 + .../renderer/command/mix/clear_mix.cpp | 24 + .../renderer/command/mix/clear_mix.h | 45 + .../renderer/command/mix/copy_mix.cpp | 27 + .../renderer/command/mix/copy_mix.h | 49 + .../command/mix/depop_for_mix_buffers.cpp | 64 + .../command/mix/depop_for_mix_buffers.h | 55 + .../renderer/command/mix/depop_prepare.cpp | 36 + .../renderer/command/mix/depop_prepare.h | 54 + src/audio_core/renderer/command/mix/mix.cpp | 70 + src/audio_core/renderer/command/mix/mix.h | 54 + .../renderer/command/mix/mix_ramp.cpp | 94 + .../renderer/command/mix/mix_ramp.h | 73 + .../renderer/command/mix/mix_ramp_grouped.cpp | 65 + .../renderer/command/mix/mix_ramp_grouped.h | 61 + .../renderer/command/mix/volume.cpp | 72 + src/audio_core/renderer/command/mix/volume.h | 53 + .../renderer/command/mix/volume_ramp.cpp | 84 + .../renderer/command/mix/volume_ramp.h | 56 + .../command/performance/performance.cpp | 43 + .../command/performance/performance.h | 51 + .../command/resample/downmix_6ch_to_2ch.cpp | 74 + .../command/resample/downmix_6ch_to_2ch.h | 59 + .../renderer/command/resample/resample.cpp | 883 ++++ .../renderer/command/resample/resample.h | 29 + .../renderer/command/resample/upsample.cpp | 262 ++ .../renderer/command/resample/upsample.h | 60 + .../renderer/command/sink/circular_buffer.cpp | 48 + .../renderer/command/sink/circular_buffer.h | 55 + .../renderer/command/sink/device.cpp | 55 + src/audio_core/renderer/command/sink/device.h | 57 + src/audio_core/renderer/effect/aux_.cpp | 93 + src/audio_core/renderer/effect/aux_.h | 123 + .../renderer/effect/biquad_filter.cpp | 52 + .../renderer/effect/biquad_filter.h | 79 + .../renderer/effect/buffer_mixer.cpp | 49 + src/audio_core/renderer/effect/buffer_mixer.h | 75 + src/audio_core/renderer/effect/capture.cpp | 82 + src/audio_core/renderer/effect/capture.h | 65 + src/audio_core/renderer/effect/compressor.cpp | 40 + src/audio_core/renderer/effect/compressor.h | 106 + src/audio_core/renderer/effect/delay.cpp | 93 + src/audio_core/renderer/effect/delay.h | 135 + .../renderer/effect/effect_context.cpp | 41 + .../renderer/effect/effect_context.h | 75 + .../renderer/effect/effect_info_base.h | 435 ++ src/audio_core/renderer/effect/effect_reset.h | 71 + .../renderer/effect/effect_result_state.h | 16 + src/audio_core/renderer/effect/i3dl2.cpp | 94 + src/audio_core/renderer/effect/i3dl2.h | 200 + .../renderer/effect/light_limiter.cpp | 81 + .../renderer/effect/light_limiter.h | 138 + src/audio_core/renderer/effect/reverb.cpp | 93 + src/audio_core/renderer/effect/reverb.h | 190 + src/audio_core/renderer/memory/address_info.h | 125 + .../renderer/memory/memory_pool_info.cpp | 61 + .../renderer/memory/memory_pool_info.h | 170 + .../renderer/memory/pool_mapper.cpp | 243 ++ src/audio_core/renderer/memory/pool_mapper.h | 179 + src/audio_core/renderer/mix/mix_context.cpp | 141 + src/audio_core/renderer/mix/mix_context.h | 124 + src/audio_core/renderer/mix/mix_info.cpp | 120 + src/audio_core/renderer/mix/mix_info.h | 124 + src/audio_core/renderer/nodes/bit_array.h | 25 + src/audio_core/renderer/nodes/edge_matrix.cpp | 38 + src/audio_core/renderer/nodes/edge_matrix.h | 82 + src/audio_core/renderer/nodes/node_states.cpp | 141 + src/audio_core/renderer/nodes/node_states.h | 195 + .../renderer/performance/detail_aspect.cpp | 25 + .../renderer/performance/detail_aspect.h | 33 + .../renderer/performance/entry_aspect.cpp | 23 + .../renderer/performance/entry_aspect.h | 32 + .../renderer/performance/performance_detail.h | 50 + .../renderer/performance/performance_entry.h | 37 + .../performance/performance_entry_addresses.h | 17 + .../performance/performance_frame_header.h | 36 + .../performance/performance_manager.cpp | 645 +++ .../performance/performance_manager.h | 273 ++ .../sink/circular_buffer_sink_info.cpp | 76 + .../renderer/sink/circular_buffer_sink_info.h | 41 + .../renderer/sink/device_sink_info.cpp | 57 + .../renderer/sink/device_sink_info.h | 40 + src/audio_core/renderer/sink/sink_context.cpp | 21 + src/audio_core/renderer/sink/sink_context.h | 47 + .../renderer/sink/sink_info_base.cpp | 51 + src/audio_core/renderer/sink/sink_info_base.h | 177 + .../renderer/splitter/splitter_context.cpp | 217 + .../renderer/splitter/splitter_context.h | 189 + .../splitter/splitter_destinations_data.cpp | 87 + .../splitter/splitter_destinations_data.h | 135 + .../renderer/splitter/splitter_info.cpp | 79 + .../renderer/splitter/splitter_info.h | 107 + src/audio_core/renderer/system.cpp | 802 ++++ src/audio_core/renderer/system.h | 307 ++ src/audio_core/renderer/system_manager.cpp | 162 + src/audio_core/renderer/system_manager.h | 113 + .../renderer/upsampler/upsampler_info.cpp | 6 + .../renderer/upsampler/upsampler_info.h | 35 + .../renderer/upsampler/upsampler_manager.cpp | 44 + .../renderer/upsampler/upsampler_manager.h | 45 + .../renderer/upsampler/upsampler_state.h | 40 + .../renderer/voice/voice_channel_resource.h | 38 + .../renderer/voice/voice_context.cpp | 86 + src/audio_core/renderer/voice/voice_context.h | 126 + src/audio_core/renderer/voice/voice_info.cpp | 408 ++ src/audio_core/renderer/voice/voice_info.h | 378 ++ src/audio_core/renderer/voice/voice_state.h | 70 + src/audio_core/sdl2_sink.cpp | 160 - src/audio_core/sdl2_sink.h | 28 - src/audio_core/sink.h | 30 - src/audio_core/sink/cubeb_sink.cpp | 651 +++ src/audio_core/sink/cubeb_sink.h | 110 + src/audio_core/sink/null_sink.h | 52 + src/audio_core/sink/sdl2_sink.cpp | 556 +++ src/audio_core/sink/sdl2_sink.h | 101 + src/audio_core/sink/sink.h | 106 + src/audio_core/{ => sink}/sink_details.cpp | 27 +- src/audio_core/sink/sink_details.h | 43 + src/audio_core/sink/sink_stream.h | 224 + src/audio_core/sink_context.cpp | 47 - src/audio_core/sink_context.h | 95 - src/audio_core/sink_details.h | 23 - src/audio_core/sink_stream.h | 35 - src/audio_core/splitter_context.cpp | 616 --- src/audio_core/splitter_context.h | 218 - src/audio_core/stream.cpp | 175 - src/audio_core/stream.h | 130 - src/audio_core/voice_context.cpp | 579 --- src/audio_core/voice_context.h | 302 -- src/common/CMakeLists.txt | 3 + src/common/atomic_helpers.h | 772 ++++ src/common/fixed_point.h | 726 ++++ src/common/reader_writer_queue.h | 941 +++++ src/common/settings.cpp | 3 +- src/common/settings.h | 4 +- src/core/core.cpp | 51 +- src/core/core.h | 19 + src/core/hle/kernel/hle_ipc.cpp | 38 +- src/core/hle/kernel/hle_ipc.h | 8 + src/core/hle/kernel/kernel.cpp | 34 +- src/core/hle/kernel/kernel.h | 3 + src/core/hle/service/am/am.cpp | 10 +- src/core/hle/service/am/am.h | 1 + src/core/hle/service/audio/audin_u.cpp | 390 +- src/core/hle/service/audio/audin_u.h | 47 +- src/core/hle/service/audio/audout_u.cpp | 333 +- src/core/hle/service/audio/audout_u.h | 21 +- src/core/hle/service/audio/audren_u.cpp | 696 ++-- src/core/hle/service/audio/audren_u.h | 22 +- src/core/hle/service/audio/errors.h | 9 + src/core/hle/service/audio/hwopus.cpp | 2 +- src/core/memory.cpp | 6 +- src/core/memory.h | 13 + src/yuzu/configuration/config.cpp | 6 +- src/yuzu/configuration/configure_audio.cpp | 95 +- src/yuzu/configuration/configure_audio.h | 4 +- src/yuzu/configuration/configure_audio.ui | 28 +- src/yuzu/configuration/configure_debug.cpp | 2 + src/yuzu/configuration/configure_debug.ui | 11 + src/yuzu/main.cpp | 3 + src/yuzu_cmd/config.cpp | 2 +- 270 files changed, 33712 insertions(+), 8445 deletions(-) delete mode 100644 src/audio_core/algorithm/filter.cpp delete mode 100644 src/audio_core/algorithm/filter.h delete mode 100644 src/audio_core/algorithm/interpolate.cpp delete mode 100644 src/audio_core/algorithm/interpolate.h create mode 100644 src/audio_core/audio_core.cpp create mode 100644 src/audio_core/audio_core.h create mode 100644 src/audio_core/audio_event.cpp create mode 100644 src/audio_core/audio_event.h create mode 100644 src/audio_core/audio_in_manager.cpp create mode 100644 src/audio_core/audio_in_manager.h create mode 100644 src/audio_core/audio_manager.cpp create mode 100644 src/audio_core/audio_manager.h delete mode 100644 src/audio_core/audio_out.cpp delete mode 100644 src/audio_core/audio_out.h create mode 100644 src/audio_core/audio_out_manager.cpp create mode 100644 src/audio_core/audio_out_manager.h create mode 100644 src/audio_core/audio_render_manager.cpp create mode 100644 src/audio_core/audio_render_manager.h delete mode 100644 src/audio_core/audio_renderer.cpp delete mode 100644 src/audio_core/audio_renderer.h delete mode 100644 src/audio_core/behavior_info.cpp delete mode 100644 src/audio_core/behavior_info.h delete mode 100644 src/audio_core/buffer.h delete mode 100644 src/audio_core/codec.cpp delete mode 100644 src/audio_core/codec.h delete mode 100644 src/audio_core/command_generator.cpp delete mode 100644 src/audio_core/command_generator.h delete mode 100644 src/audio_core/common.h create mode 100644 src/audio_core/common/audio_renderer_parameter.h create mode 100644 src/audio_core/common/common.h create mode 100644 src/audio_core/common/feature_support.h create mode 100644 src/audio_core/common/wave_buffer.h create mode 100644 src/audio_core/common/workbuffer_allocator.h delete mode 100644 src/audio_core/cubeb_sink.cpp delete mode 100644 src/audio_core/cubeb_sink.h delete mode 100644 src/audio_core/delay_line.cpp delete mode 100644 src/audio_core/delay_line.h create mode 100644 src/audio_core/device/audio_buffer.h create mode 100644 src/audio_core/device/audio_buffers.h create mode 100644 src/audio_core/device/device_session.cpp create mode 100644 src/audio_core/device/device_session.h delete mode 100644 src/audio_core/effect_context.cpp delete mode 100644 src/audio_core/effect_context.h create mode 100644 src/audio_core/in/audio_in.cpp create mode 100644 src/audio_core/in/audio_in.h create mode 100644 src/audio_core/in/audio_in_system.cpp create mode 100644 src/audio_core/in/audio_in_system.h delete mode 100644 src/audio_core/info_updater.cpp delete mode 100644 src/audio_core/info_updater.h delete mode 100644 src/audio_core/memory_pool.cpp delete mode 100644 src/audio_core/memory_pool.h delete mode 100644 src/audio_core/mix_context.cpp delete mode 100644 src/audio_core/mix_context.h delete mode 100644 src/audio_core/null_sink.h create mode 100644 src/audio_core/out/audio_out.cpp create mode 100644 src/audio_core/out/audio_out.h create mode 100644 src/audio_core/out/audio_out_system.cpp create mode 100644 src/audio_core/out/audio_out_system.h create mode 100644 src/audio_core/renderer/adsp/adsp.cpp create mode 100644 src/audio_core/renderer/adsp/adsp.h create mode 100644 src/audio_core/renderer/adsp/audio_renderer.cpp create mode 100644 src/audio_core/renderer/adsp/audio_renderer.h create mode 100644 src/audio_core/renderer/adsp/command_buffer.h create mode 100644 src/audio_core/renderer/adsp/command_list_processor.cpp create mode 100644 src/audio_core/renderer/adsp/command_list_processor.h create mode 100644 src/audio_core/renderer/audio_device.cpp create mode 100644 src/audio_core/renderer/audio_device.h create mode 100644 src/audio_core/renderer/audio_renderer.cpp create mode 100644 src/audio_core/renderer/audio_renderer.h create mode 100644 src/audio_core/renderer/behavior/behavior_info.cpp create mode 100644 src/audio_core/renderer/behavior/behavior_info.h create mode 100644 src/audio_core/renderer/behavior/info_updater.cpp create mode 100644 src/audio_core/renderer/behavior/info_updater.h create mode 100644 src/audio_core/renderer/command/command_buffer.cpp create mode 100644 src/audio_core/renderer/command/command_buffer.h create mode 100644 src/audio_core/renderer/command/command_generator.cpp create mode 100644 src/audio_core/renderer/command/command_generator.h create mode 100644 src/audio_core/renderer/command/command_list_header.h create mode 100644 src/audio_core/renderer/command/command_processing_time_estimator.cpp create mode 100644 src/audio_core/renderer/command/command_processing_time_estimator.h create mode 100644 src/audio_core/renderer/command/commands.h create mode 100644 src/audio_core/renderer/command/data_source/adpcm.cpp create mode 100644 src/audio_core/renderer/command/data_source/adpcm.h create mode 100644 src/audio_core/renderer/command/data_source/decode.cpp create mode 100644 src/audio_core/renderer/command/data_source/decode.h create mode 100644 src/audio_core/renderer/command/data_source/pcm_float.cpp create mode 100644 src/audio_core/renderer/command/data_source/pcm_float.h create mode 100644 src/audio_core/renderer/command/data_source/pcm_int16.cpp create mode 100644 src/audio_core/renderer/command/data_source/pcm_int16.h create mode 100644 src/audio_core/renderer/command/effect/aux_.cpp create mode 100644 src/audio_core/renderer/command/effect/aux_.h create mode 100644 src/audio_core/renderer/command/effect/biquad_filter.cpp create mode 100644 src/audio_core/renderer/command/effect/biquad_filter.h create mode 100644 src/audio_core/renderer/command/effect/capture.cpp create mode 100644 src/audio_core/renderer/command/effect/capture.h create mode 100644 src/audio_core/renderer/command/effect/compressor.cpp create mode 100644 src/audio_core/renderer/command/effect/compressor.h create mode 100644 src/audio_core/renderer/command/effect/delay.cpp create mode 100644 src/audio_core/renderer/command/effect/delay.h create mode 100644 src/audio_core/renderer/command/effect/i3dl2_reverb.cpp create mode 100644 src/audio_core/renderer/command/effect/i3dl2_reverb.h create mode 100644 src/audio_core/renderer/command/effect/light_limiter.cpp create mode 100644 src/audio_core/renderer/command/effect/light_limiter.h create mode 100644 src/audio_core/renderer/command/effect/multi_tap_biquad_filter.cpp create mode 100644 src/audio_core/renderer/command/effect/multi_tap_biquad_filter.h create mode 100644 src/audio_core/renderer/command/effect/reverb.cpp create mode 100644 src/audio_core/renderer/command/effect/reverb.h create mode 100644 src/audio_core/renderer/command/icommand.h create mode 100644 src/audio_core/renderer/command/mix/clear_mix.cpp create mode 100644 src/audio_core/renderer/command/mix/clear_mix.h create mode 100644 src/audio_core/renderer/command/mix/copy_mix.cpp create mode 100644 src/audio_core/renderer/command/mix/copy_mix.h create mode 100644 src/audio_core/renderer/command/mix/depop_for_mix_buffers.cpp create mode 100644 src/audio_core/renderer/command/mix/depop_for_mix_buffers.h create mode 100644 src/audio_core/renderer/command/mix/depop_prepare.cpp create mode 100644 src/audio_core/renderer/command/mix/depop_prepare.h create mode 100644 src/audio_core/renderer/command/mix/mix.cpp create mode 100644 src/audio_core/renderer/command/mix/mix.h create mode 100644 src/audio_core/renderer/command/mix/mix_ramp.cpp create mode 100644 src/audio_core/renderer/command/mix/mix_ramp.h create mode 100644 src/audio_core/renderer/command/mix/mix_ramp_grouped.cpp create mode 100644 src/audio_core/renderer/command/mix/mix_ramp_grouped.h create mode 100644 src/audio_core/renderer/command/mix/volume.cpp create mode 100644 src/audio_core/renderer/command/mix/volume.h create mode 100644 src/audio_core/renderer/command/mix/volume_ramp.cpp create mode 100644 src/audio_core/renderer/command/mix/volume_ramp.h create mode 100644 src/audio_core/renderer/command/performance/performance.cpp create mode 100644 src/audio_core/renderer/command/performance/performance.h create mode 100644 src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.cpp create mode 100644 src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.h create mode 100644 src/audio_core/renderer/command/resample/resample.cpp create mode 100644 src/audio_core/renderer/command/resample/resample.h create mode 100644 src/audio_core/renderer/command/resample/upsample.cpp create mode 100644 src/audio_core/renderer/command/resample/upsample.h create mode 100644 src/audio_core/renderer/command/sink/circular_buffer.cpp create mode 100644 src/audio_core/renderer/command/sink/circular_buffer.h create mode 100644 src/audio_core/renderer/command/sink/device.cpp create mode 100644 src/audio_core/renderer/command/sink/device.h create mode 100644 src/audio_core/renderer/effect/aux_.cpp create mode 100644 src/audio_core/renderer/effect/aux_.h create mode 100644 src/audio_core/renderer/effect/biquad_filter.cpp create mode 100644 src/audio_core/renderer/effect/biquad_filter.h create mode 100644 src/audio_core/renderer/effect/buffer_mixer.cpp create mode 100644 src/audio_core/renderer/effect/buffer_mixer.h create mode 100644 src/audio_core/renderer/effect/capture.cpp create mode 100644 src/audio_core/renderer/effect/capture.h create mode 100644 src/audio_core/renderer/effect/compressor.cpp create mode 100644 src/audio_core/renderer/effect/compressor.h create mode 100644 src/audio_core/renderer/effect/delay.cpp create mode 100644 src/audio_core/renderer/effect/delay.h create mode 100644 src/audio_core/renderer/effect/effect_context.cpp create mode 100644 src/audio_core/renderer/effect/effect_context.h create mode 100644 src/audio_core/renderer/effect/effect_info_base.h create mode 100644 src/audio_core/renderer/effect/effect_reset.h create mode 100644 src/audio_core/renderer/effect/effect_result_state.h create mode 100644 src/audio_core/renderer/effect/i3dl2.cpp create mode 100644 src/audio_core/renderer/effect/i3dl2.h create mode 100644 src/audio_core/renderer/effect/light_limiter.cpp create mode 100644 src/audio_core/renderer/effect/light_limiter.h create mode 100644 src/audio_core/renderer/effect/reverb.cpp create mode 100644 src/audio_core/renderer/effect/reverb.h create mode 100644 src/audio_core/renderer/memory/address_info.h create mode 100644 src/audio_core/renderer/memory/memory_pool_info.cpp create mode 100644 src/audio_core/renderer/memory/memory_pool_info.h create mode 100644 src/audio_core/renderer/memory/pool_mapper.cpp create mode 100644 src/audio_core/renderer/memory/pool_mapper.h create mode 100644 src/audio_core/renderer/mix/mix_context.cpp create mode 100644 src/audio_core/renderer/mix/mix_context.h create mode 100644 src/audio_core/renderer/mix/mix_info.cpp create mode 100644 src/audio_core/renderer/mix/mix_info.h create mode 100644 src/audio_core/renderer/nodes/bit_array.h create mode 100644 src/audio_core/renderer/nodes/edge_matrix.cpp create mode 100644 src/audio_core/renderer/nodes/edge_matrix.h create mode 100644 src/audio_core/renderer/nodes/node_states.cpp create mode 100644 src/audio_core/renderer/nodes/node_states.h create mode 100644 src/audio_core/renderer/performance/detail_aspect.cpp create mode 100644 src/audio_core/renderer/performance/detail_aspect.h create mode 100644 src/audio_core/renderer/performance/entry_aspect.cpp create mode 100644 src/audio_core/renderer/performance/entry_aspect.h create mode 100644 src/audio_core/renderer/performance/performance_detail.h create mode 100644 src/audio_core/renderer/performance/performance_entry.h create mode 100644 src/audio_core/renderer/performance/performance_entry_addresses.h create mode 100644 src/audio_core/renderer/performance/performance_frame_header.h create mode 100644 src/audio_core/renderer/performance/performance_manager.cpp create mode 100644 src/audio_core/renderer/performance/performance_manager.h create mode 100644 src/audio_core/renderer/sink/circular_buffer_sink_info.cpp create mode 100644 src/audio_core/renderer/sink/circular_buffer_sink_info.h create mode 100644 src/audio_core/renderer/sink/device_sink_info.cpp create mode 100644 src/audio_core/renderer/sink/device_sink_info.h create mode 100644 src/audio_core/renderer/sink/sink_context.cpp create mode 100644 src/audio_core/renderer/sink/sink_context.h create mode 100644 src/audio_core/renderer/sink/sink_info_base.cpp create mode 100644 src/audio_core/renderer/sink/sink_info_base.h create mode 100644 src/audio_core/renderer/splitter/splitter_context.cpp create mode 100644 src/audio_core/renderer/splitter/splitter_context.h create mode 100644 src/audio_core/renderer/splitter/splitter_destinations_data.cpp create mode 100644 src/audio_core/renderer/splitter/splitter_destinations_data.h create mode 100644 src/audio_core/renderer/splitter/splitter_info.cpp create mode 100644 src/audio_core/renderer/splitter/splitter_info.h create mode 100644 src/audio_core/renderer/system.cpp create mode 100644 src/audio_core/renderer/system.h create mode 100644 src/audio_core/renderer/system_manager.cpp create mode 100644 src/audio_core/renderer/system_manager.h create mode 100644 src/audio_core/renderer/upsampler/upsampler_info.cpp create mode 100644 src/audio_core/renderer/upsampler/upsampler_info.h create mode 100644 src/audio_core/renderer/upsampler/upsampler_manager.cpp create mode 100644 src/audio_core/renderer/upsampler/upsampler_manager.h create mode 100644 src/audio_core/renderer/upsampler/upsampler_state.h create mode 100644 src/audio_core/renderer/voice/voice_channel_resource.h create mode 100644 src/audio_core/renderer/voice/voice_context.cpp create mode 100644 src/audio_core/renderer/voice/voice_context.h create mode 100644 src/audio_core/renderer/voice/voice_info.cpp create mode 100644 src/audio_core/renderer/voice/voice_info.h create mode 100644 src/audio_core/renderer/voice/voice_state.h delete mode 100644 src/audio_core/sdl2_sink.cpp delete mode 100644 src/audio_core/sdl2_sink.h delete mode 100644 src/audio_core/sink.h create mode 100644 src/audio_core/sink/cubeb_sink.cpp create mode 100644 src/audio_core/sink/cubeb_sink.h create mode 100644 src/audio_core/sink/null_sink.h create mode 100644 src/audio_core/sink/sdl2_sink.cpp create mode 100644 src/audio_core/sink/sdl2_sink.h create mode 100644 src/audio_core/sink/sink.h rename src/audio_core/{ => sink}/sink_details.cpp (76%) create mode 100644 src/audio_core/sink/sink_details.h create mode 100644 src/audio_core/sink/sink_stream.h delete mode 100644 src/audio_core/sink_context.cpp delete mode 100644 src/audio_core/sink_context.h delete mode 100644 src/audio_core/sink_details.h delete mode 100644 src/audio_core/sink_stream.h delete mode 100644 src/audio_core/splitter_context.cpp delete mode 100644 src/audio_core/splitter_context.h delete mode 100644 src/audio_core/stream.cpp delete mode 100644 src/audio_core/stream.h delete mode 100644 src/audio_core/voice_context.cpp delete mode 100644 src/audio_core/voice_context.h create mode 100644 src/common/atomic_helpers.h create mode 100644 src/common/fixed_point.h create mode 100644 src/common/reader_writer_queue.h diff --git a/.gitmodules b/.gitmodules index dc92d0a4b5..ed533f8d43 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,7 +3,7 @@ url = https://github.com/benhoyt/inih.git [submodule "cubeb"] path = externals/cubeb - url = https://github.com/kinetiknz/cubeb.git + url = https://github.com/mozilla/cubeb.git [submodule "dynarmic"] path = externals/dynarmic url = https://github.com/MerryMage/dynarmic.git diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index 89575a53ef..2971c42a2f 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -1,54 +1,218 @@ add_library(audio_core STATIC - algorithm/filter.cpp - algorithm/filter.h - algorithm/interpolate.cpp - algorithm/interpolate.h - audio_out.cpp - audio_out.h - audio_renderer.cpp - audio_renderer.h - behavior_info.cpp - behavior_info.h - buffer.h - codec.cpp - codec.h - command_generator.cpp - command_generator.h - common.h - delay_line.cpp - delay_line.h - effect_context.cpp - effect_context.h - info_updater.cpp - info_updater.h - memory_pool.cpp - memory_pool.h - mix_context.cpp - mix_context.h - null_sink.h - sink.h - sink_context.cpp - sink_context.h - sink_details.cpp - sink_details.h - sink_stream.h - splitter_context.cpp - splitter_context.h - stream.cpp - stream.h - voice_context.cpp - voice_context.h - - $<$:cubeb_sink.cpp cubeb_sink.h> - $<$:sdl2_sink.cpp sdl2_sink.h> + audio_core.cpp + audio_core.h + audio_event.h + audio_event.cpp + audio_render_manager.cpp + audio_render_manager.h + audio_in_manager.cpp + audio_in_manager.h + audio_out_manager.cpp + audio_out_manager.h + audio_manager.cpp + audio_manager.h + common/audio_renderer_parameter.h + common/common.h + common/feature_support.h + common/wave_buffer.h + common/workbuffer_allocator.h + device/audio_buffer.h + device/audio_buffers.h + device/device_session.cpp + device/device_session.h + in/audio_in.cpp + in/audio_in.h + in/audio_in_system.cpp + in/audio_in_system.h + out/audio_out.cpp + out/audio_out.h + out/audio_out_system.cpp + out/audio_out_system.h + renderer/adsp/adsp.cpp + renderer/adsp/adsp.h + renderer/adsp/audio_renderer.cpp + renderer/adsp/audio_renderer.h + renderer/adsp/command_buffer.h + renderer/adsp/command_list_processor.cpp + renderer/adsp/command_list_processor.h + renderer/audio_device.cpp + renderer/audio_device.h + renderer/audio_renderer.h + renderer/audio_renderer.cpp + renderer/behavior/behavior_info.cpp + renderer/behavior/behavior_info.h + renderer/behavior/info_updater.cpp + renderer/behavior/info_updater.h + renderer/command/data_source/adpcm.cpp + renderer/command/data_source/adpcm.h + renderer/command/data_source/decode.cpp + renderer/command/data_source/decode.h + renderer/command/data_source/pcm_float.cpp + renderer/command/data_source/pcm_float.h + renderer/command/data_source/pcm_int16.cpp + renderer/command/data_source/pcm_int16.h + renderer/command/effect/aux_.cpp + renderer/command/effect/aux_.h + renderer/command/effect/biquad_filter.cpp + renderer/command/effect/biquad_filter.h + renderer/command/effect/capture.cpp + renderer/command/effect/capture.h + renderer/command/effect/compressor.cpp + renderer/command/effect/compressor.h + renderer/command/effect/delay.cpp + renderer/command/effect/delay.h + renderer/command/effect/i3dl2_reverb.cpp + renderer/command/effect/i3dl2_reverb.h + renderer/command/effect/light_limiter.cpp + renderer/command/effect/light_limiter.h + renderer/command/effect/multi_tap_biquad_filter.cpp + renderer/command/effect/multi_tap_biquad_filter.h + renderer/command/effect/reverb.cpp + renderer/command/effect/reverb.h + renderer/command/mix/clear_mix.cpp + renderer/command/mix/clear_mix.h + renderer/command/mix/copy_mix.cpp + renderer/command/mix/copy_mix.h + renderer/command/mix/depop_for_mix_buffers.cpp + renderer/command/mix/depop_for_mix_buffers.h + renderer/command/mix/depop_prepare.cpp + renderer/command/mix/depop_prepare.h + renderer/command/mix/mix.cpp + renderer/command/mix/mix.h + renderer/command/mix/mix_ramp.cpp + renderer/command/mix/mix_ramp.h + renderer/command/mix/mix_ramp_grouped.cpp + renderer/command/mix/mix_ramp_grouped.h + renderer/command/mix/volume.cpp + renderer/command/mix/volume.h + renderer/command/mix/volume_ramp.cpp + renderer/command/mix/volume_ramp.h + renderer/command/performance/performance.cpp + renderer/command/performance/performance.h + renderer/command/resample/downmix_6ch_to_2ch.cpp + renderer/command/resample/downmix_6ch_to_2ch.h + renderer/command/resample/resample.h + renderer/command/resample/resample.cpp + renderer/command/resample/upsample.cpp + renderer/command/resample/upsample.h + renderer/command/sink/device.cpp + renderer/command/sink/device.h + renderer/command/sink/circular_buffer.cpp + renderer/command/sink/circular_buffer.h + renderer/command/command_buffer.cpp + renderer/command/command_buffer.h + renderer/command/command_generator.cpp + renderer/command/command_generator.h + renderer/command/command_list_header.h + renderer/command/command_processing_time_estimator.cpp + renderer/command/command_processing_time_estimator.h + renderer/command/commands.h + renderer/command/icommand.h + renderer/effect/aux_.cpp + renderer/effect/aux_.h + renderer/effect/biquad_filter.cpp + renderer/effect/biquad_filter.h + renderer/effect/buffer_mixer.cpp + renderer/effect/buffer_mixer.h + renderer/effect/capture.cpp + renderer/effect/capture.h + renderer/effect/compressor.cpp + renderer/effect/compressor.h + renderer/effect/delay.cpp + renderer/effect/delay.h + renderer/effect/effect_context.cpp + renderer/effect/effect_context.h + renderer/effect/effect_info_base.h + renderer/effect/effect_reset.h + renderer/effect/effect_result_state.h + renderer/effect/i3dl2.cpp + renderer/effect/i3dl2.h + renderer/effect/light_limiter.cpp + renderer/effect/light_limiter.h + renderer/effect/reverb.h + renderer/effect/reverb.cpp + renderer/mix/mix_context.cpp + renderer/mix/mix_context.h + renderer/mix/mix_info.cpp + renderer/mix/mix_info.h + renderer/memory/address_info.h + renderer/memory/memory_pool_info.cpp + renderer/memory/memory_pool_info.h + renderer/memory/pool_mapper.cpp + renderer/memory/pool_mapper.h + renderer/nodes/bit_array.h + renderer/nodes/edge_matrix.cpp + renderer/nodes/edge_matrix.h + renderer/nodes/node_states.cpp + renderer/nodes/node_states.h + renderer/performance/detail_aspect.cpp + renderer/performance/detail_aspect.h + renderer/performance/entry_aspect.cpp + renderer/performance/entry_aspect.h + renderer/performance/performance_detail.h + renderer/performance/performance_entry.h + renderer/performance/performance_entry_addresses.h + renderer/performance/performance_frame_header.h + renderer/performance/performance_manager.cpp + renderer/performance/performance_manager.h + renderer/sink/circular_buffer_sink_info.cpp + renderer/sink/circular_buffer_sink_info.h + renderer/sink/device_sink_info.cpp + renderer/sink/device_sink_info.h + renderer/sink/sink_context.cpp + renderer/sink/sink_context.h + renderer/sink/sink_info_base.cpp + renderer/sink/sink_info_base.h + renderer/splitter/splitter_context.cpp + renderer/splitter/splitter_context.h + renderer/splitter/splitter_destinations_data.cpp + renderer/splitter/splitter_destinations_data.h + renderer/splitter/splitter_info.cpp + renderer/splitter/splitter_info.h + renderer/system.cpp + renderer/system.h + renderer/system_manager.cpp + renderer/system_manager.h + renderer/upsampler/upsampler_info.h + renderer/upsampler/upsampler_manager.cpp + renderer/upsampler/upsampler_manager.h + renderer/upsampler/upsampler_state.h + renderer/voice/voice_channel_resource.h + renderer/voice/voice_context.cpp + renderer/voice/voice_context.h + renderer/voice/voice_info.cpp + renderer/voice/voice_info.h + renderer/voice/voice_state.h + sink/cubeb_sink.cpp + sink/cubeb_sink.h + sink/null_sink.h + sink/sdl2_sink.cpp + sink/sdl2_sink.h + sink/sink.h + sink/sink_details.cpp + sink/sink_details.h + sink/sink_stream.h ) create_target_directory_groups(audio_core) -if (NOT MSVC) +if (MSVC) + target_compile_options(audio_core PRIVATE + /we4242 # 'identifier': conversion from 'type1' to 'type2', possible loss of data + /we4244 # 'conversion': conversion from 'type1' to 'type2', possible loss of data + /we4245 # 'conversion': conversion from 'type1' to 'type2', signed/unsigned mismatch + /we4254 # 'operator': conversion from 'type1:field_bits' to 'type2:field_bits', possible loss of data + /we4456 # Declaration of 'identifier' hides previous local declaration + /we4457 # Declaration of 'identifier' hides function parameter + /we4458 # Declaration of 'identifier' hides class member + /we4459 # Declaration of 'identifier' hides global declaration + ) +else() target_compile_options(audio_core PRIVATE -Werror=conversion -Werror=ignored-qualifiers + -Werror=shadow + -Werror=unused-variable $<$:-Werror=unused-but-set-parameter> $<$:-Werror=unused-but-set-variable> @@ -58,6 +222,9 @@ if (NOT MSVC) endif() target_link_libraries(audio_core PUBLIC common core) +if (ARCHITECTURE_x86_64) + target_link_libraries(audio_core PRIVATE dynarmic) +endif() if(ENABLE_CUBEB) target_link_libraries(audio_core PRIVATE cubeb) diff --git a/src/audio_core/algorithm/filter.cpp b/src/audio_core/algorithm/filter.cpp deleted file mode 100644 index 96e37991f7..0000000000 --- a/src/audio_core/algorithm/filter.cpp +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#define _USE_MATH_DEFINES - -#include -#include -#include -#include -#include "audio_core/algorithm/filter.h" -#include "common/common_types.h" - -namespace AudioCore { - -Filter Filter::LowPass(double cutoff, double Q) { - const double w0 = 2.0 * M_PI * cutoff; - const double sin_w0 = std::sin(w0); - const double cos_w0 = std::cos(w0); - const double alpha = sin_w0 / (2 * Q); - - const double a0 = 1 + alpha; - const double a1 = -2.0 * cos_w0; - const double a2 = 1 - alpha; - const double b0 = 0.5 * (1 - cos_w0); - const double b1 = 1.0 * (1 - cos_w0); - const double b2 = 0.5 * (1 - cos_w0); - - return {a0, a1, a2, b0, b1, b2}; -} - -Filter::Filter() : Filter(1.0, 0.0, 0.0, 1.0, 0.0, 0.0) {} - -Filter::Filter(double a0_, double a1_, double a2_, double b0_, double b1_, double b2_) - : a1(a1_ / a0_), a2(a2_ / a0_), b0(b0_ / a0_), b1(b1_ / a0_), b2(b2_ / a0_) {} - -void Filter::Process(std::vector& signal) { - const std::size_t num_frames = signal.size() / 2; - for (std::size_t i = 0; i < num_frames; i++) { - std::rotate(in.begin(), in.end() - 1, in.end()); - std::rotate(out.begin(), out.end() - 1, out.end()); - - for (std::size_t ch = 0; ch < channel_count; ch++) { - in[0][ch] = signal[i * channel_count + ch]; - - out[0][ch] = b0 * in[0][ch] + b1 * in[1][ch] + b2 * in[2][ch] - a1 * out[1][ch] - - a2 * out[2][ch]; - - signal[i * 2 + ch] = static_cast(std::clamp(out[0][ch], -32768.0, 32767.0)); - } - } -} - -/// Calculates the appropriate Q for each biquad in a cascading filter. -/// @param total_count The total number of biquads to be cascaded. -/// @param index 0-index of the biquad to calculate the Q value for. -static double CascadingBiquadQ(std::size_t total_count, std::size_t index) { - const auto pole = - M_PI * static_cast(2 * index + 1) / (4.0 * static_cast(total_count)); - return 1.0 / (2.0 * std::cos(pole)); -} - -CascadingFilter CascadingFilter::LowPass(double cutoff, std::size_t cascade_size) { - std::vector cascade(cascade_size); - for (std::size_t i = 0; i < cascade_size; i++) { - cascade[i] = Filter::LowPass(cutoff, CascadingBiquadQ(cascade_size, i)); - } - return CascadingFilter{std::move(cascade)}; -} - -CascadingFilter::CascadingFilter() = default; -CascadingFilter::CascadingFilter(std::vector filters_) : filters(std::move(filters_)) {} - -void CascadingFilter::Process(std::vector& signal) { - for (auto& filter : filters) { - filter.Process(signal); - } -} - -} // namespace AudioCore diff --git a/src/audio_core/algorithm/filter.h b/src/audio_core/algorithm/filter.h deleted file mode 100644 index 2586f00799..0000000000 --- a/src/audio_core/algorithm/filter.h +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include "common/common_types.h" - -namespace AudioCore { - -/// Digital biquad filter: -/// -/// b0 + b1 z^-1 + b2 z^-2 -/// H(z) = ------------------------ -/// a0 + a1 z^-1 + b2 z^-2 -class Filter { -public: - /// Creates a low-pass filter. - /// @param cutoff Determines the cutoff frequency. A value from 0.0 to 1.0. - /// @param Q Determines the quality factor of this filter. - static Filter LowPass(double cutoff, double Q = 0.7071); - - /// Passthrough filter. - Filter(); - - Filter(double a0_, double a1_, double a2_, double b0_, double b1_, double b2_); - - void Process(std::vector& signal); - -private: - static constexpr std::size_t channel_count = 2; - - /// Coefficients are in normalized form (a0 = 1.0). - double a1, a2, b0, b1, b2; - /// Input History - std::array, 3> in; - /// Output History - std::array, 3> out; -}; - -/// Cascade filters to build up higher-order filters from lower-order ones. -class CascadingFilter { -public: - /// Creates a cascading low-pass filter. - /// @param cutoff Determines the cutoff frequency. A value from 0.0 to 1.0. - /// @param cascade_size Number of biquads in cascade. - static CascadingFilter LowPass(double cutoff, std::size_t cascade_size); - - /// Passthrough. - CascadingFilter(); - - explicit CascadingFilter(std::vector filters_); - - void Process(std::vector& signal); - -private: - std::vector filters; -}; - -} // namespace AudioCore diff --git a/src/audio_core/algorithm/interpolate.cpp b/src/audio_core/algorithm/interpolate.cpp deleted file mode 100644 index d2a4cd53f5..0000000000 --- a/src/audio_core/algorithm/interpolate.cpp +++ /dev/null @@ -1,232 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#define _USE_MATH_DEFINES - -#include -#include -#include -#include - -#include "audio_core/algorithm/interpolate.h" -#include "common/common_types.h" -#include "common/logging/log.h" - -namespace AudioCore { - -constexpr std::array curve_lut0{ - 6600, 19426, 6722, 3, 6479, 19424, 6845, 9, 6359, 19419, 6968, 15, 6239, - 19412, 7093, 22, 6121, 19403, 7219, 28, 6004, 19391, 7345, 34, 5888, 19377, - 7472, 41, 5773, 19361, 7600, 48, 5659, 19342, 7728, 55, 5546, 19321, 7857, - 62, 5434, 19298, 7987, 69, 5323, 19273, 8118, 77, 5213, 19245, 8249, 84, - 5104, 19215, 8381, 92, 4997, 19183, 8513, 101, 4890, 19148, 8646, 109, 4785, - 19112, 8780, 118, 4681, 19073, 8914, 127, 4579, 19031, 9048, 137, 4477, 18988, - 9183, 147, 4377, 18942, 9318, 157, 4277, 18895, 9454, 168, 4179, 18845, 9590, - 179, 4083, 18793, 9726, 190, 3987, 18738, 9863, 202, 3893, 18682, 10000, 215, - 3800, 18624, 10137, 228, 3709, 18563, 10274, 241, 3618, 18500, 10411, 255, 3529, - 18436, 10549, 270, 3441, 18369, 10687, 285, 3355, 18300, 10824, 300, 3269, 18230, - 10962, 317, 3186, 18157, 11100, 334, 3103, 18082, 11238, 351, 3022, 18006, 11375, - 369, 2942, 17927, 11513, 388, 2863, 17847, 11650, 408, 2785, 17765, 11788, 428, - 2709, 17681, 11925, 449, 2635, 17595, 12062, 471, 2561, 17507, 12198, 494, 2489, - 17418, 12334, 517, 2418, 17327, 12470, 541, 2348, 17234, 12606, 566, 2280, 17140, - 12741, 592, 2213, 17044, 12876, 619, 2147, 16946, 13010, 647, 2083, 16846, 13144, - 675, 2020, 16745, 13277, 704, 1958, 16643, 13409, 735, 1897, 16539, 13541, 766, - 1838, 16434, 13673, 798, 1780, 16327, 13803, 832, 1723, 16218, 13933, 866, 1667, - 16109, 14062, 901, 1613, 15998, 14191, 937, 1560, 15885, 14318, 975, 1508, 15772, - 14445, 1013, 1457, 15657, 14571, 1052, 1407, 15540, 14695, 1093, 1359, 15423, 14819, - 1134, 1312, 15304, 14942, 1177, 1266, 15185, 15064, 1221, 1221, 15064, 15185, 1266, - 1177, 14942, 15304, 1312, 1134, 14819, 15423, 1359, 1093, 14695, 15540, 1407, 1052, - 14571, 15657, 1457, 1013, 14445, 15772, 1508, 975, 14318, 15885, 1560, 937, 14191, - 15998, 1613, 901, 14062, 16109, 1667, 866, 13933, 16218, 1723, 832, 13803, 16327, - 1780, 798, 13673, 16434, 1838, 766, 13541, 16539, 1897, 735, 13409, 16643, 1958, - 704, 13277, 16745, 2020, 675, 13144, 16846, 2083, 647, 13010, 16946, 2147, 619, - 12876, 17044, 2213, 592, 12741, 17140, 2280, 566, 12606, 17234, 2348, 541, 12470, - 17327, 2418, 517, 12334, 17418, 2489, 494, 12198, 17507, 2561, 471, 12062, 17595, - 2635, 449, 11925, 17681, 2709, 428, 11788, 17765, 2785, 408, 11650, 17847, 2863, - 388, 11513, 17927, 2942, 369, 11375, 18006, 3022, 351, 11238, 18082, 3103, 334, - 11100, 18157, 3186, 317, 10962, 18230, 3269, 300, 10824, 18300, 3355, 285, 10687, - 18369, 3441, 270, 10549, 18436, 3529, 255, 10411, 18500, 3618, 241, 10274, 18563, - 3709, 228, 10137, 18624, 3800, 215, 10000, 18682, 3893, 202, 9863, 18738, 3987, - 190, 9726, 18793, 4083, 179, 9590, 18845, 4179, 168, 9454, 18895, 4277, 157, - 9318, 18942, 4377, 147, 9183, 18988, 4477, 137, 9048, 19031, 4579, 127, 8914, - 19073, 4681, 118, 8780, 19112, 4785, 109, 8646, 19148, 4890, 101, 8513, 19183, - 4997, 92, 8381, 19215, 5104, 84, 8249, 19245, 5213, 77, 8118, 19273, 5323, - 69, 7987, 19298, 5434, 62, 7857, 19321, 5546, 55, 7728, 19342, 5659, 48, - 7600, 19361, 5773, 41, 7472, 19377, 5888, 34, 7345, 19391, 6004, 28, 7219, - 19403, 6121, 22, 7093, 19412, 6239, 15, 6968, 19419, 6359, 9, 6845, 19424, - 6479, 3, 6722, 19426, 6600}; - -constexpr std::array curve_lut1{ - -68, 32639, 69, -5, -200, 32630, 212, -15, -328, 32613, 359, -26, -450, - 32586, 512, -36, -568, 32551, 669, -47, -680, 32507, 832, -58, -788, 32454, - 1000, -69, -891, 32393, 1174, -80, -990, 32323, 1352, -92, -1084, 32244, 1536, - -103, -1173, 32157, 1724, -115, -1258, 32061, 1919, -128, -1338, 31956, 2118, -140, - -1414, 31844, 2322, -153, -1486, 31723, 2532, -167, -1554, 31593, 2747, -180, -1617, - 31456, 2967, -194, -1676, 31310, 3192, -209, -1732, 31157, 3422, -224, -1783, 30995, - 3657, -240, -1830, 30826, 3897, -256, -1874, 30649, 4143, -272, -1914, 30464, 4393, - -289, -1951, 30272, 4648, -307, -1984, 30072, 4908, -325, -2014, 29866, 5172, -343, - -2040, 29652, 5442, -362, -2063, 29431, 5716, -382, -2083, 29203, 5994, -403, -2100, - 28968, 6277, -424, -2114, 28727, 6565, -445, -2125, 28480, 6857, -468, -2133, 28226, - 7153, -490, -2139, 27966, 7453, -514, -2142, 27700, 7758, -538, -2142, 27428, 8066, - -563, -2141, 27151, 8378, -588, -2136, 26867, 8694, -614, -2130, 26579, 9013, -641, - -2121, 26285, 9336, -668, -2111, 25987, 9663, -696, -2098, 25683, 9993, -724, -2084, - 25375, 10326, -753, -2067, 25063, 10662, -783, -2049, 24746, 11000, -813, -2030, 24425, - 11342, -844, -2009, 24100, 11686, -875, -1986, 23771, 12033, -907, -1962, 23438, 12382, - -939, -1937, 23103, 12733, -972, -1911, 22764, 13086, -1005, -1883, 22422, 13441, -1039, - -1855, 22077, 13798, -1072, -1825, 21729, 14156, -1107, -1795, 21380, 14516, -1141, -1764, - 21027, 14877, -1176, -1732, 20673, 15239, -1211, -1700, 20317, 15602, -1246, -1667, 19959, - 15965, -1282, -1633, 19600, 16329, -1317, -1599, 19239, 16694, -1353, -1564, 18878, 17058, - -1388, -1530, 18515, 17423, -1424, -1495, 18151, 17787, -1459, -1459, 17787, 18151, -1495, - -1424, 17423, 18515, -1530, -1388, 17058, 18878, -1564, -1353, 16694, 19239, -1599, -1317, - 16329, 19600, -1633, -1282, 15965, 19959, -1667, -1246, 15602, 20317, -1700, -1211, 15239, - 20673, -1732, -1176, 14877, 21027, -1764, -1141, 14516, 21380, -1795, -1107, 14156, 21729, - -1825, -1072, 13798, 22077, -1855, -1039, 13441, 22422, -1883, -1005, 13086, 22764, -1911, - -972, 12733, 23103, -1937, -939, 12382, 23438, -1962, -907, 12033, 23771, -1986, -875, - 11686, 24100, -2009, -844, 11342, 24425, -2030, -813, 11000, 24746, -2049, -783, 10662, - 25063, -2067, -753, 10326, 25375, -2084, -724, 9993, 25683, -2098, -696, 9663, 25987, - -2111, -668, 9336, 26285, -2121, -641, 9013, 26579, -2130, -614, 8694, 26867, -2136, - -588, 8378, 27151, -2141, -563, 8066, 27428, -2142, -538, 7758, 27700, -2142, -514, - 7453, 27966, -2139, -490, 7153, 28226, -2133, -468, 6857, 28480, -2125, -445, 6565, - 28727, -2114, -424, 6277, 28968, -2100, -403, 5994, 29203, -2083, -382, 5716, 29431, - -2063, -362, 5442, 29652, -2040, -343, 5172, 29866, -2014, -325, 4908, 30072, -1984, - -307, 4648, 30272, -1951, -289, 4393, 30464, -1914, -272, 4143, 30649, -1874, -256, - 3897, 30826, -1830, -240, 3657, 30995, -1783, -224, 3422, 31157, -1732, -209, 3192, - 31310, -1676, -194, 2967, 31456, -1617, -180, 2747, 31593, -1554, -167, 2532, 31723, - -1486, -153, 2322, 31844, -1414, -140, 2118, 31956, -1338, -128, 1919, 32061, -1258, - -115, 1724, 32157, -1173, -103, 1536, 32244, -1084, -92, 1352, 32323, -990, -80, - 1174, 32393, -891, -69, 1000, 32454, -788, -58, 832, 32507, -680, -47, 669, - 32551, -568, -36, 512, 32586, -450, -26, 359, 32613, -328, -15, 212, 32630, - -200, -5, 69, 32639, -68}; - -constexpr std::array curve_lut2{ - 3195, 26287, 3329, -32, 3064, 26281, 3467, -34, 2936, 26270, 3608, -38, 2811, - 26253, 3751, -42, 2688, 26230, 3897, -46, 2568, 26202, 4046, -50, 2451, 26169, - 4199, -54, 2338, 26130, 4354, -58, 2227, 26085, 4512, -63, 2120, 26035, 4673, - -67, 2015, 25980, 4837, -72, 1912, 25919, 5004, -76, 1813, 25852, 5174, -81, - 1716, 25780, 5347, -87, 1622, 25704, 5522, -92, 1531, 25621, 5701, -98, 1442, - 25533, 5882, -103, 1357, 25440, 6066, -109, 1274, 25342, 6253, -115, 1193, 25239, - 6442, -121, 1115, 25131, 6635, -127, 1040, 25018, 6830, -133, 967, 24899, 7027, - -140, 897, 24776, 7227, -146, 829, 24648, 7430, -153, 764, 24516, 7635, -159, - 701, 24379, 7842, -166, 641, 24237, 8052, -174, 583, 24091, 8264, -181, 526, - 23940, 8478, -187, 472, 23785, 8695, -194, 420, 23626, 8914, -202, 371, 23462, - 9135, -209, 324, 23295, 9358, -215, 279, 23123, 9583, -222, 236, 22948, 9809, - -230, 194, 22769, 10038, -237, 154, 22586, 10269, -243, 117, 22399, 10501, -250, - 81, 22208, 10735, -258, 47, 22015, 10970, -265, 15, 21818, 11206, -271, -16, - 21618, 11444, -277, -44, 21415, 11684, -283, -71, 21208, 11924, -290, -97, 20999, - 12166, -296, -121, 20786, 12409, -302, -143, 20571, 12653, -306, -163, 20354, 12898, - -311, -183, 20134, 13143, -316, -201, 19911, 13389, -321, -218, 19686, 13635, -325, - -234, 19459, 13882, -328, -248, 19230, 14130, -332, -261, 18998, 14377, -335, -273, - 18765, 14625, -337, -284, 18531, 14873, -339, -294, 18295, 15121, -341, -302, 18057, - 15369, -341, -310, 17817, 15617, -341, -317, 17577, 15864, -340, -323, 17335, 16111, - -340, -328, 17092, 16357, -338, -332, 16848, 16603, -336, -336, 16603, 16848, -332, - -338, 16357, 17092, -328, -340, 16111, 17335, -323, -340, 15864, 17577, -317, -341, - 15617, 17817, -310, -341, 15369, 18057, -302, -341, 15121, 18295, -294, -339, 14873, - 18531, -284, -337, 14625, 18765, -273, -335, 14377, 18998, -261, -332, 14130, 19230, - -248, -328, 13882, 19459, -234, -325, 13635, 19686, -218, -321, 13389, 19911, -201, - -316, 13143, 20134, -183, -311, 12898, 20354, -163, -306, 12653, 20571, -143, -302, - 12409, 20786, -121, -296, 12166, 20999, -97, -290, 11924, 21208, -71, -283, 11684, - 21415, -44, -277, 11444, 21618, -16, -271, 11206, 21818, 15, -265, 10970, 22015, - 47, -258, 10735, 22208, 81, -250, 10501, 22399, 117, -243, 10269, 22586, 154, - -237, 10038, 22769, 194, -230, 9809, 22948, 236, -222, 9583, 23123, 279, -215, - 9358, 23295, 324, -209, 9135, 23462, 371, -202, 8914, 23626, 420, -194, 8695, - 23785, 472, -187, 8478, 23940, 526, -181, 8264, 24091, 583, -174, 8052, 24237, - 641, -166, 7842, 24379, 701, -159, 7635, 24516, 764, -153, 7430, 24648, 829, - -146, 7227, 24776, 897, -140, 7027, 24899, 967, -133, 6830, 25018, 1040, -127, - 6635, 25131, 1115, -121, 6442, 25239, 1193, -115, 6253, 25342, 1274, -109, 6066, - 25440, 1357, -103, 5882, 25533, 1442, -98, 5701, 25621, 1531, -92, 5522, 25704, - 1622, -87, 5347, 25780, 1716, -81, 5174, 25852, 1813, -76, 5004, 25919, 1912, - -72, 4837, 25980, 2015, -67, 4673, 26035, 2120, -63, 4512, 26085, 2227, -58, - 4354, 26130, 2338, -54, 4199, 26169, 2451, -50, 4046, 26202, 2568, -46, 3897, - 26230, 2688, -42, 3751, 26253, 2811, -38, 3608, 26270, 2936, -34, 3467, 26281, - 3064, -32, 3329, 26287, 3195}; - -std::vector Interpolate(InterpolationState& state, std::vector input, double ratio) { - if (input.size() < 2) - return {}; - - if (ratio <= 0) { - LOG_ERROR(Audio, "Nonsensical interpolation ratio {}", ratio); - return input; - } - - const s32 step{static_cast(ratio * 0x8000)}; - const std::array& lut = [step] { - if (step > 0xaaaa) { - return curve_lut0; - } - if (step <= 0x8000) { - return curve_lut1; - } - return curve_lut2; - }(); - - const std::size_t num_frames{input.size() / 2}; - - std::vector output; - output.reserve(static_cast(static_cast(input.size()) / ratio + - InterpolationState::taps)); - - for (std::size_t frame{}; frame < num_frames; ++frame) { - const std::size_t lut_index{(state.fraction >> 8) * InterpolationState::taps}; - - std::rotate(state.history.begin(), state.history.end() - 1, state.history.end()); - state.history[0][0] = input[frame * 2 + 0]; - state.history[0][1] = input[frame * 2 + 1]; - - while (state.position <= 1.0) { - const s32 left{state.history[0][0] * lut[lut_index + 0] + - state.history[1][0] * lut[lut_index + 1] + - state.history[2][0] * lut[lut_index + 2] + - state.history[3][0] * lut[lut_index + 3]}; - const s32 right{state.history[0][1] * lut[lut_index + 0] + - state.history[1][1] * lut[lut_index + 1] + - state.history[2][1] * lut[lut_index + 2] + - state.history[3][1] * lut[lut_index + 3]}; - const s32 new_offset{state.fraction + step}; - - state.fraction = new_offset & 0x7fff; - - output.emplace_back(static_cast(std::clamp(left >> 15, SHRT_MIN, SHRT_MAX))); - output.emplace_back(static_cast(std::clamp(right >> 15, SHRT_MIN, SHRT_MAX))); - - state.position += ratio; - } - state.position -= 1.0; - } - - return output; -} - -void Resample(s32* output, const s32* input, s32 pitch, s32& fraction, std::size_t sample_count) { - const std::array& lut = [pitch] { - if (pitch > 0xaaaa) { - return curve_lut0; - } - if (pitch <= 0x8000) { - return curve_lut1; - } - return curve_lut2; - }(); - - std::size_t index{}; - - for (std::size_t i = 0; i < sample_count; i++) { - const std::size_t lut_index{(static_cast(fraction) >> 8) * 4}; - const auto l0 = lut[lut_index + 0]; - const auto l1 = lut[lut_index + 1]; - const auto l2 = lut[lut_index + 2]; - const auto l3 = lut[lut_index + 3]; - - const auto s0 = static_cast(input[index + 0]); - const auto s1 = static_cast(input[index + 1]); - const auto s2 = static_cast(input[index + 2]); - const auto s3 = static_cast(input[index + 3]); - - output[i] = (l0 * s0 + l1 * s1 + l2 * s2 + l3 * s3) >> 15; - fraction += pitch; - index += (fraction >> 15); - fraction &= 0x7fff; - } -} - -} // namespace AudioCore diff --git a/src/audio_core/algorithm/interpolate.h b/src/audio_core/algorithm/interpolate.h deleted file mode 100644 index 5e59f4d709..0000000000 --- a/src/audio_core/algorithm/interpolate.h +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include - -#include "common/common_types.h" - -namespace AudioCore { - -struct InterpolationState { - static constexpr std::size_t taps{4}; - static constexpr std::size_t history_size{taps * 2 - 1}; - std::array, history_size> history{}; - double position{}; - s32 fraction{}; -}; - -/// Interpolates input signal to produce output signal. -/// @param input The signal to interpolate. -/// @param ratio Interpolation ratio. -/// ratio > 1.0 results in fewer output samples. -/// ratio < 1.0 results in more output samples. -/// @returns Output signal. -std::vector Interpolate(InterpolationState& state, std::vector input, double ratio); - -/// Interpolates input signal to produce output signal. -/// @param input The signal to interpolate. -/// @param input_rate The sample rate of input. -/// @param output_rate The desired sample rate of the output. -/// @returns Output signal. -inline std::vector Interpolate(InterpolationState& state, std::vector input, - u32 input_rate, u32 output_rate) { - const double ratio = static_cast(input_rate) / static_cast(output_rate); - return Interpolate(state, std::move(input), ratio); -} - -/// Nintendo Switchs DSP resampling algorithm. Based on a single channel -void Resample(s32* output, const s32* input, s32 pitch, s32& fraction, std::size_t sample_count); - -} // namespace AudioCore diff --git a/src/audio_core/audio_core.cpp b/src/audio_core/audio_core.cpp new file mode 100644 index 0000000000..78e615a10e --- /dev/null +++ b/src/audio_core/audio_core.cpp @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_core.h" +#include "audio_core/sink/sink_details.h" +#include "common/settings.h" +#include "core/core.h" + +namespace AudioCore { + +AudioCore::AudioCore(Core::System& system) : audio_manager{std::make_unique(system)} { + CreateSinks(); + // Must be created after the sinks + adsp = std::make_unique(system, *output_sink); +} + +AudioCore ::~AudioCore() { + Shutdown(); +} + +void AudioCore::CreateSinks() { + const auto& sink_id{Settings::values.sink_id}; + const auto& audio_output_device_id{Settings::values.audio_output_device_id}; + const auto& audio_input_device_id{Settings::values.audio_input_device_id}; + + output_sink = Sink::CreateSinkFromID(sink_id.GetValue(), audio_output_device_id.GetValue()); + input_sink = Sink::CreateSinkFromID(sink_id.GetValue(), audio_input_device_id.GetValue()); +} + +void AudioCore::Shutdown() { + audio_manager->Shutdown(); +} + +AudioManager& AudioCore::GetAudioManager() { + return *audio_manager; +} + +Sink::Sink& AudioCore::GetOutputSink() { + return *output_sink; +} + +Sink::Sink& AudioCore::GetInputSink() { + return *input_sink; +} + +AudioRenderer::ADSP::ADSP& AudioCore::GetADSP() { + return *adsp; +} + +void AudioCore::PauseSinks(const bool pausing) const { + if (pausing) { + output_sink->PauseStreams(); + input_sink->PauseStreams(); + } else { + output_sink->UnpauseStreams(); + input_sink->UnpauseStreams(); + } +} + +u32 AudioCore::GetStreamQueue() const { + return estimated_queue.load(); +} + +void AudioCore::SetStreamQueue(u32 size) { + estimated_queue.store(size); +} + +} // namespace AudioCore diff --git a/src/audio_core/audio_core.h b/src/audio_core/audio_core.h new file mode 100644 index 0000000000..0f7d61ee4b --- /dev/null +++ b/src/audio_core/audio_core.h @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/audio_manager.h" +#include "audio_core/renderer/adsp/adsp.h" +#include "audio_core/sink/sink.h" + +namespace Core { +class System; +} + +namespace AudioCore { + +class AudioManager; +/** + * Main audio class, sotred inside the core, and holding the audio manager, all sinks, and the ADSP. + */ +class AudioCore { +public: + explicit AudioCore(Core::System& system); + ~AudioCore(); + + /** + * Shutdown the audio core. + */ + void Shutdown(); + + /** + * Get a reference to the audio manager. + * + * @return Ref to the audio manager. + */ + AudioManager& GetAudioManager(); + + /** + * Get the audio output sink currently in use. + * + * @return Ref to the sink. + */ + Sink::Sink& GetOutputSink(); + + /** + * Get the audio input sink currently in use. + * + * @return Ref to the sink. + */ + Sink::Sink& GetInputSink(); + + /** + * Get the ADSP. + * + * @return Ref to the ADSP. + */ + AudioRenderer::ADSP::ADSP& GetADSP(); + + /** + * Pause the sink. Called from the core. + * + * @param pausing - Is this pause due to an actual pause, or shutdown? + * Unfortunately, shutdown also pauses streams, which can cause issues. + */ + void PauseSinks(bool pausing) const; + + /** + * Get the size of the current stream queue. + * + * @return Current stream queue size. + */ + u32 GetStreamQueue() const; + + /** + * Get the size of the current stream queue. + * + * @param size - New stream size. + */ + void SetStreamQueue(u32 size); + +private: + /** + * Create the sinks on startup. + */ + void CreateSinks(); + + /// Main audio manager for audio in/out + std::unique_ptr audio_manager; + /// Sink used for audio renderer and audio out + std::unique_ptr output_sink; + /// Sink used for audio input + std::unique_ptr input_sink; + /// The ADSP in the sysmodule + std::unique_ptr adsp; + /// Current size of the stream queue + std::atomic estimated_queue{0}; +}; + +} // namespace AudioCore diff --git a/src/audio_core/audio_event.cpp b/src/audio_core/audio_event.cpp new file mode 100644 index 0000000000..424049c7a2 --- /dev/null +++ b/src/audio_core/audio_event.cpp @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_event.h" +#include "common/assert.h" + +namespace AudioCore { + +size_t Event::GetManagerIndex(const Type type) const { + switch (type) { + case Type::AudioInManager: + return 0; + case Type::AudioOutManager: + return 1; + case Type::FinalOutputRecorderManager: + return 2; + case Type::Max: + return 3; + default: + UNREACHABLE(); + } + return 3; +} + +void Event::SetAudioEvent(const Type type, const bool signalled) { + events_signalled[GetManagerIndex(type)] = signalled; + if (signalled) { + manager_event.notify_one(); + } +} + +bool Event::CheckAudioEventSet(const Type type) const { + return events_signalled[GetManagerIndex(type)]; +} + +std::mutex& Event::GetAudioEventLock() { + return event_lock; +} + +std::condition_variable_any& Event::GetAudioEvent() { + return manager_event; +} + +bool Event::Wait(std::unique_lock& l, const std::chrono::seconds timeout) { + bool timed_out{false}; + if (!manager_event.wait_for(l, timeout, [&]() { + return std::ranges::any_of(events_signalled, [](bool x) { return x; }); + })) { + timed_out = true; + } + return timed_out; +} + +void Event::ClearEvents() { + events_signalled[0] = false; + events_signalled[1] = false; + events_signalled[2] = false; + events_signalled[3] = false; +} + +} // namespace AudioCore diff --git a/src/audio_core/audio_event.h b/src/audio_core/audio_event.h new file mode 100644 index 0000000000..82dd32dca5 --- /dev/null +++ b/src/audio_core/audio_event.h @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +namespace AudioCore { +/** + * Responsible for the input/output events, set by the stream backend when buffers are consumed, and + * waited on by the audio manager. These callbacks signal the game's events to keep the audio buffer + * recycling going. + * In a real Switch this is not a seprate class, and exists entirely within the audio manager. + * On the Switch it's implemented more simply through a MultiWaitEventHolder, where it can + * wait on multiple events at once, and the events are not needed by the backend. + */ +class Event { +public: + enum class Type { + AudioInManager, + AudioOutManager, + FinalOutputRecorderManager, + Max, + }; + + /** + * Convert a manager type to an index. + * + * @param type - The manager type to convert + * @return The index of the type. + */ + size_t GetManagerIndex(Type type) const; + + /** + * Set an audio event to true or false. + * + * @param type - The manager type to signal. + * @param signalled - Its signal state. + */ + void SetAudioEvent(Type type, bool signalled); + + /** + * Check if the given manager type is signalled. + * + * @param type - The manager type to check. + * @return True if the event is signalled, otherwise false. + */ + bool CheckAudioEventSet(Type type) const; + + /** + * Get the lock for audio events. + * + * @return Reference to the lock. + */ + std::mutex& GetAudioEventLock(); + + /** + * Get the manager event, this signals the audio manager to release buffers and signal the game + * for more. + * + * @return Reference to the condition variable. + */ + std::condition_variable_any& GetAudioEvent(); + + /** + * Wait on the manager_event. + * + * @param l - Lock held by the wait. + * @param timeout - Timeout for the wait. This is 2 seconds by default. + * @return True if the wait timed out, otherwise false if signalled. + */ + bool Wait(std::unique_lock& l, std::chrono::seconds timeout); + + /** + * Reset all manager events. + */ + void ClearEvents(); + +private: + /// Lock, used bythe audio manager + std::mutex event_lock; + /// Array of events, one per system type (see Type), last event is used to terminate + std::array, 4> events_signalled; + /// Event to signal the audio manager + std::condition_variable_any manager_event; +}; + +} // namespace AudioCore diff --git a/src/audio_core/audio_in_manager.cpp b/src/audio_core/audio_in_manager.cpp new file mode 100644 index 0000000000..4aadb7fd61 --- /dev/null +++ b/src/audio_core/audio_in_manager.cpp @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_core.h" +#include "audio_core/audio_in_manager.h" +#include "audio_core/audio_manager.h" +#include "audio_core/in/audio_in.h" +#include "audio_core/sink/sink_details.h" +#include "common/settings.h" +#include "core/core.h" +#include "core/hle/service/audio/errors.h" + +namespace AudioCore::AudioIn { + +Manager::Manager(Core::System& system_) : system{system_} { + std::iota(session_ids.begin(), session_ids.end(), 0); + num_free_sessions = MaxInSessions; +} + +Result Manager::AcquireSessionId(size_t& session_id) { + if (num_free_sessions == 0) { + LOG_ERROR(Service_Audio, "All 4 AudioIn sessions are in use, cannot create any more"); + return Service::Audio::ERR_MAXIMUM_SESSIONS_REACHED; + } + session_id = session_ids[next_session_id]; + next_session_id = (next_session_id + 1) % MaxInSessions; + num_free_sessions--; + return ResultSuccess; +} + +void Manager::ReleaseSessionId(const size_t session_id) { + std::scoped_lock l{mutex}; + LOG_DEBUG(Service_Audio, "Freeing AudioIn session {}", session_id); + session_ids[free_session_id] = session_id; + num_free_sessions++; + free_session_id = (free_session_id + 1) % MaxInSessions; + sessions[session_id].reset(); + applet_resource_user_ids[session_id] = 0; +} + +Result Manager::LinkToManager() { + std::scoped_lock l{mutex}; + if (!linked_to_manager) { + AudioManager& manager{system.AudioCore().GetAudioManager()}; + manager.SetInManager(std::bind(&Manager::BufferReleaseAndRegister, this)); + linked_to_manager = true; + } + + return ResultSuccess; +} + +void Manager::Start() { + if (sessions_started) { + return; + } + + std::scoped_lock l{mutex}; + for (auto& session : sessions) { + if (session) { + session->StartSession(); + } + } + + sessions_started = true; +} + +void Manager::BufferReleaseAndRegister() { + std::scoped_lock l{mutex}; + for (auto& session : sessions) { + if (session != nullptr) { + session->ReleaseAndRegisterBuffers(); + } + } +} + +u32 Manager::GetDeviceNames(std::vector& names, + [[maybe_unused]] const u32 max_count, + [[maybe_unused]] const bool filter) { + std::scoped_lock l{mutex}; + + LinkToManager(); + + auto input_devices{Sink::GetDeviceListForSink(Settings::values.sink_id.GetValue(), true)}; + if (input_devices.size() > 1) { + names.push_back(AudioRenderer::AudioDevice::AudioDeviceName("Uac")); + return 1; + } + return 0; +} + +} // namespace AudioCore::AudioIn diff --git a/src/audio_core/audio_in_manager.h b/src/audio_core/audio_in_manager.h new file mode 100644 index 0000000000..75b73a0b6f --- /dev/null +++ b/src/audio_core/audio_in_manager.h @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "audio_core/renderer/audio_device.h" + +namespace Core { +class System; +} + +namespace AudioCore::AudioIn { +class In; + +constexpr size_t MaxInSessions = 4; +/** + * Manages all audio in sessions. + */ +class Manager { +public: + explicit Manager(Core::System& system); + + /** + * Acquire a free session id for opening a new audio in. + * + * @param session_id - Output session_id. + * @return Result code. + */ + Result AcquireSessionId(size_t& session_id); + + /** + * Release a session id on close. + * + * @param session_id - Session id to free. + */ + void ReleaseSessionId(size_t session_id); + + /** + * Link the audio in manager to the main audio manager. + * + * @return Result code. + */ + Result LinkToManager(); + + /** + * Start the audio in manager. + */ + void Start(); + + /** + * Callback function, called by the audio manager when the audio in event is signalled. + */ + void BufferReleaseAndRegister(); + + /** + * Get a list of audio in device names. + * + * @oaram names - Output container to write names to. + * @param max_count - Maximum numebr of deivce names to write. Unused + * @param filter - Should the list be filtered? Unused. + * @return Number of names written. + */ + u32 GetDeviceNames(std::vector& names, + u32 max_count, bool filter); + + /// Core system + Core::System& system; + /// Array of session ids + std::array session_ids{}; + /// Array of resource user ids + std::array applet_resource_user_ids{}; + /// Pointer to each open session + std::array, MaxInSessions> sessions{}; + /// The number of free sessions + size_t num_free_sessions{}; + /// The next session id to be taken + size_t next_session_id{}; + /// The next session id to be freed + size_t free_session_id{}; + /// Whether this is linked to the audio manager + bool linked_to_manager{}; + /// Whether the sessions have been started + bool sessions_started{}; + /// Protect state due to audio manager callback + std::recursive_mutex mutex{}; +}; + +} // namespace AudioCore::AudioIn diff --git a/src/audio_core/audio_manager.cpp b/src/audio_core/audio_manager.cpp new file mode 100644 index 0000000000..2f1bba9c3c --- /dev/null +++ b/src/audio_core/audio_manager.cpp @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_in_manager.h" +#include "audio_core/audio_manager.h" +#include "audio_core/audio_out_manager.h" +#include "core/core.h" + +namespace AudioCore { + +AudioManager::AudioManager(Core::System& system_) : system{system_} { + thread = std::jthread([this]() { ThreadFunc(); }); +} + +void AudioManager::Shutdown() { + running = false; + events.SetAudioEvent(Event::Type::Max, true); + thread.join(); +} + +Result AudioManager::SetOutManager(BufferEventFunc buffer_func) { + if (!running) { + return Service::Audio::ERR_OPERATION_FAILED; + } + + std::scoped_lock l{lock}; + + const auto index{events.GetManagerIndex(Event::Type::AudioOutManager)}; + if (buffer_events[index] == nullptr) { + buffer_events[index] = buffer_func; + needs_update = true; + events.SetAudioEvent(Event::Type::AudioOutManager, true); + } + return ResultSuccess; +} + +Result AudioManager::SetInManager(BufferEventFunc buffer_func) { + if (!running) { + return Service::Audio::ERR_OPERATION_FAILED; + } + + std::scoped_lock l{lock}; + + const auto index{events.GetManagerIndex(Event::Type::AudioInManager)}; + if (buffer_events[index] == nullptr) { + buffer_events[index] = buffer_func; + needs_update = true; + events.SetAudioEvent(Event::Type::AudioInManager, true); + } + return ResultSuccess; +} + +void AudioManager::SetEvent(const Event::Type type, const bool signalled) { + events.SetAudioEvent(type, signalled); +} + +void AudioManager::ThreadFunc() { + std::unique_lock l{events.GetAudioEventLock()}; + events.ClearEvents(); + running = true; + + while (running) { + auto timed_out{events.Wait(l, std::chrono::seconds(2))}; + + if (events.CheckAudioEventSet(Event::Type::Max)) { + break; + } + + for (size_t i = 0; i < buffer_events.size(); i++) { + if (events.CheckAudioEventSet(Event::Type(i)) || timed_out) { + if (buffer_events[i]) { + buffer_events[i](); + } + } + events.SetAudioEvent(Event::Type(i), false); + } + } +} + +} // namespace AudioCore diff --git a/src/audio_core/audio_manager.h b/src/audio_core/audio_manager.h new file mode 100644 index 0000000000..70316e9cbd --- /dev/null +++ b/src/audio_core/audio_manager.h @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#include "audio_core/audio_event.h" +#include "core/hle/service/audio/errors.h" + +namespace Core { +class System; +} + +namespace AudioCore { + +namespace AudioOut { +class Manager; +} + +namespace AudioIn { +class Manager; +} + +/** + * The AudioManager's main purpose is to wait for buffer events for the audio in and out managers, + * and call an associated callback to release buffers. + * + * Execution pattern is: + * Buffers appended -> + * Buffers queued and played by the backend stream -> + * When consumed, set the corresponding manager event and signal the audio manager -> + * Consumed buffers are released, game is signalled -> + * Game appends more buffers. + * + * This is only used by audio in and audio out. + */ +class AudioManager { + using BufferEventFunc = std::function; + +public: + explicit AudioManager(Core::System& system); + + /** + * Shutdown the audio manager. + */ + void Shutdown(); + + /** + * Register the out manager, keeping a function to be called when the out event is signalled. + * + * @param buffer_func - Function to be called on signal. + * @return Result code. + */ + Result SetOutManager(BufferEventFunc buffer_func); + + /** + * Register the in manager, keeping a function to be called when the in event is signalled. + * + * @param buffer_func - Function to be called on signal. + * @return Result code. + */ + Result SetInManager(BufferEventFunc buffer_func); + + /** + * Set an event to signalled, and signal the thread. + * + * @param type - Manager type to set. + * @param signalled - Set the event to true or false? + */ + void SetEvent(Event::Type type, bool signalled); + +private: + /** + * Main thread, waiting on a manager signal and calling the registered fucntion. + */ + void ThreadFunc(); + + /// Core system + Core::System& system; + /// Have sessions started palying? + bool sessions_started{}; + /// Is the main thread running? + std::atomic running{}; + /// Unused + bool needs_update{}; + /// Events to be set and signalled + Event events{}; + /// Callbacks for each manager + std::array buffer_events{}; + /// General lock + std::mutex lock{}; + /// Main thread for waiting and callbacks + std::jthread thread; +}; + +} // namespace AudioCore diff --git a/src/audio_core/audio_out.cpp b/src/audio_core/audio_out.cpp deleted file mode 100644 index 83ec0221ff..0000000000 --- a/src/audio_core/audio_out.cpp +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "audio_core/audio_out.h" -#include "audio_core/sink.h" -#include "audio_core/sink_details.h" -#include "common/assert.h" -#include "common/logging/log.h" -#include "common/settings.h" - -namespace AudioCore { - -/// Returns the stream format from the specified number of channels -static Stream::Format ChannelsToStreamFormat(u32 num_channels) { - switch (num_channels) { - case 1: - return Stream::Format::Mono16; - case 2: - return Stream::Format::Stereo16; - case 6: - return Stream::Format::Multi51Channel16; - } - - UNIMPLEMENTED_MSG("Unimplemented num_channels={}", num_channels); - return {}; -} - -StreamPtr AudioOut::OpenStream(Core::Timing::CoreTiming& core_timing, u32 sample_rate, - u32 num_channels, std::string&& name, - Stream::ReleaseCallback&& release_callback) { - if (!sink) { - sink = CreateSinkFromID(Settings::values.sink_id.GetValue(), - Settings::values.audio_device_id.GetValue()); - } - - return std::make_shared( - core_timing, sample_rate, ChannelsToStreamFormat(num_channels), std::move(release_callback), - sink->AcquireSinkStream(sample_rate, num_channels, name), std::move(name)); -} - -std::vector AudioOut::GetTagsAndReleaseBuffers(StreamPtr stream, - std::size_t max_count) { - return stream->GetTagsAndReleaseBuffers(max_count); -} - -std::vector AudioOut::GetTagsAndReleaseBuffers(StreamPtr stream) { - return stream->GetTagsAndReleaseBuffers(); -} - -void AudioOut::StartStream(StreamPtr stream) { - stream->Play(); -} - -void AudioOut::StopStream(StreamPtr stream) { - stream->Stop(); -} - -bool AudioOut::QueueBuffer(StreamPtr stream, Buffer::Tag tag, std::vector&& data) { - return stream->QueueBuffer(std::make_shared(tag, std::move(data))); -} - -} // namespace AudioCore diff --git a/src/audio_core/audio_out.h b/src/audio_core/audio_out.h deleted file mode 100644 index 6856373f10..0000000000 --- a/src/audio_core/audio_out.h +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include - -#include "audio_core/buffer.h" -#include "audio_core/sink.h" -#include "audio_core/stream.h" -#include "common/common_types.h" - -namespace Core::Timing { -class CoreTiming; -} - -namespace AudioCore { - -/** - * Represents an audio playback interface, used to open and play audio streams - */ -class AudioOut { -public: - /// Opens a new audio stream - StreamPtr OpenStream(Core::Timing::CoreTiming& core_timing, u32 sample_rate, u32 num_channels, - std::string&& name, Stream::ReleaseCallback&& release_callback); - - /// Returns a vector of recently released buffers specified by tag for the specified stream - std::vector GetTagsAndReleaseBuffers(StreamPtr stream, std::size_t max_count); - - /// Returns a vector of all recently released buffers specified by tag for the specified stream - std::vector GetTagsAndReleaseBuffers(StreamPtr stream); - - /// Starts an audio stream for playback - void StartStream(StreamPtr stream); - - /// Stops an audio stream that is currently playing - void StopStream(StreamPtr stream); - - /// Queues a buffer into the specified audio stream, returns true on success - bool QueueBuffer(StreamPtr stream, Buffer::Tag tag, std::vector&& data); - -private: - SinkPtr sink; -}; - -} // namespace AudioCore diff --git a/src/audio_core/audio_out_manager.cpp b/src/audio_core/audio_out_manager.cpp new file mode 100644 index 0000000000..71d67de647 --- /dev/null +++ b/src/audio_core/audio_out_manager.cpp @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_core.h" +#include "audio_core/audio_manager.h" +#include "audio_core/audio_out_manager.h" +#include "audio_core/out/audio_out.h" +#include "core/core.h" +#include "core/hle/kernel/k_event.h" +#include "core/hle/service/audio/errors.h" + +namespace AudioCore::AudioOut { + +Manager::Manager(Core::System& system_) : system{system_} { + std::iota(session_ids.begin(), session_ids.end(), 0); + num_free_sessions = MaxOutSessions; +} + +Result Manager::AcquireSessionId(size_t& session_id) { + if (num_free_sessions == 0) { + LOG_ERROR(Service_Audio, "All 12 Audio Out sessions are in use, cannot create any more"); + return Service::Audio::ERR_MAXIMUM_SESSIONS_REACHED; + } + session_id = session_ids[next_session_id]; + next_session_id = (next_session_id + 1) % MaxOutSessions; + num_free_sessions--; + return ResultSuccess; +} + +void Manager::ReleaseSessionId(const size_t session_id) { + std::scoped_lock l{mutex}; + LOG_DEBUG(Service_Audio, "Freeing AudioOut session {}", session_id); + session_ids[free_session_id] = session_id; + num_free_sessions++; + free_session_id = (free_session_id + 1) % MaxOutSessions; + sessions[session_id].reset(); + applet_resource_user_ids[session_id] = 0; +} + +Result Manager::LinkToManager() { + std::scoped_lock l{mutex}; + if (!linked_to_manager) { + AudioManager& manager{system.AudioCore().GetAudioManager()}; + manager.SetOutManager(std::bind(&Manager::BufferReleaseAndRegister, this)); + linked_to_manager = true; + } + + return ResultSuccess; +} + +void Manager::Start() { + if (sessions_started) { + return; + } + + std::scoped_lock l{mutex}; + for (auto& session : sessions) { + if (session) { + session->StartSession(); + } + } + + sessions_started = true; +} + +void Manager::BufferReleaseAndRegister() { + std::scoped_lock l{mutex}; + for (auto& session : sessions) { + if (session != nullptr) { + session->ReleaseAndRegisterBuffers(); + } + } +} + +u32 Manager::GetAudioOutDeviceNames( + std::vector& names) const { + names.push_back({"DeviceOut"}); + return 1; +} + +} // namespace AudioCore::AudioOut diff --git a/src/audio_core/audio_out_manager.h b/src/audio_core/audio_out_manager.h new file mode 100644 index 0000000000..24981e08f2 --- /dev/null +++ b/src/audio_core/audio_out_manager.h @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/audio_device.h" + +namespace Core { +class System; +} + +namespace AudioCore::AudioOut { +class Out; + +constexpr size_t MaxOutSessions = 12; +/** + * Manages all audio out sessions. + */ +class Manager { +public: + explicit Manager(Core::System& system); + + /** + * Acquire a free session id for opening a new audio out. + * + * @param session_id - Output session_id. + * @return Result code. + */ + Result AcquireSessionId(size_t& session_id); + + /** + * Release a session id on close. + * + * @param session_id - Session id to free. + */ + void ReleaseSessionId(size_t session_id); + + /** + * Link this manager to the main audio manager. + * + * @return Result code. + */ + Result LinkToManager(); + + /** + * Start the audio out manager. + */ + void Start(); + + /** + * Callback function, called by the audio manager when the audio out event is signalled. + */ + void BufferReleaseAndRegister(); + + /** + * Get a list of audio out device names. + * + * @oaram names - Output container to write names to. + * @return Number of names written. + */ + u32 GetAudioOutDeviceNames( + std::vector& names) const; + + /// Core system + Core::System& system; + /// Array of session ids + std::array session_ids{}; + /// Array of resource user ids + std::array applet_resource_user_ids{}; + /// Pointer to each open session + std::array, MaxOutSessions> sessions{}; + /// The number of free sessions + size_t num_free_sessions{}; + /// The next session id to be taken + size_t next_session_id{}; + /// The next session id to be freed + size_t free_session_id{}; + /// Whether this is linked to the audio manager + bool linked_to_manager{}; + /// Whether the sessions have been started + bool sessions_started{}; + /// Protect state due to audio manager callback + std::recursive_mutex mutex{}; +}; + +} // namespace AudioCore::AudioOut diff --git a/src/audio_core/audio_render_manager.cpp b/src/audio_core/audio_render_manager.cpp new file mode 100644 index 0000000000..7a846835bb --- /dev/null +++ b/src/audio_core/audio_render_manager.cpp @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_render_manager.h" +#include "audio_core/common/audio_renderer_parameter.h" +#include "audio_core/common/feature_support.h" +#include "core/core.h" + +namespace AudioCore::AudioRenderer { + +Manager::Manager(Core::System& system_) + : system{system_}, system_manager{std::make_unique(system)} { + std::iota(session_ids.begin(), session_ids.end(), 0); +} + +Manager::~Manager() { + Stop(); +} + +void Manager::Stop() { + system_manager->Stop(); +} + +SystemManager& Manager::GetSystemManager() { + return *system_manager; +} + +auto Manager::GetWorkBufferSize(const AudioRendererParameterInternal& params, u64& out_count) + -> Result { + if (!CheckValidRevision(params.revision)) { + return Service::Audio::ERR_INVALID_REVISION; + } + + out_count = System::GetWorkBufferSize(params); + + return ResultSuccess; +} + +s32 Manager::GetSessionId() { + std::scoped_lock l{session_lock}; + auto session_id{session_ids[session_count]}; + + if (session_id == -1) { + return -1; + } + + session_ids[session_count] = -1; + session_count++; + return session_id; +} + +void Manager::ReleaseSessionId(const s32 session_id) { + std::scoped_lock l{session_lock}; + session_ids[--session_count] = session_id; +} + +u32 Manager::GetSessionCount() { + std::scoped_lock l{session_lock}; + return session_count; +} + +bool Manager::AddSystem(System& system_) { + return system_manager->Add(system_); +} + +bool Manager::RemoveSystem(System& system_) { + return system_manager->Remove(system_); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/audio_render_manager.h b/src/audio_core/audio_render_manager.h new file mode 100644 index 0000000000..6a508ec560 --- /dev/null +++ b/src/audio_core/audio_render_manager.h @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/system_manager.h" +#include "core/hle/service/audio/errors.h" + +namespace Core { +class System; +} + +namespace AudioCore { +struct AudioRendererParameterInternal; + +namespace AudioRenderer { +/** + * Wrapper for the audio system manager, handles service calls. + */ +class Manager { +public: + explicit Manager(Core::System& system); + ~Manager(); + + /** + * Stop the manager. + */ + void Stop(); + + /** + * Get the system manager. + * + * @return The system manager. + */ + SystemManager& GetSystemManager(); + + /** + * Get required size for the audio renderer workbuffer. + * + * @param params - Input parameters with the numbers of voices/mixes/sinks etc. + * @param out_count - Output size of the required workbuffer. + * @return Result code. + */ + Result GetWorkBufferSize(const AudioRendererParameterInternal& params, u64& out_count); + + /** + * Get a new session id. + * + * @return The new session id. -1 if invalid, otherwise 0-MaxRendererSessions. + */ + s32 GetSessionId(); + + /** + * Get the number of currently active sessions. + * + * @return The number of active sessions. + */ + u32 GetSessionCount(); + + /** + * Add a renderer system to the manager. + * The system will be reguarly called to generate commands for the AudioRenderer. + * + * @param system - The system to add. + * @return True if the system was sucessfully added, otherwise false. + */ + bool AddSystem(System& system); + + /** + * Remove a renderer system from the manager. + * + * @param system - The system to remove. + * @return True if the system was sucessfully removed, otherwise false. + */ + bool RemoveSystem(System& system); + + /** + * Free a session id when the system wants to shut down. + * + * @param session_id - The session id to free. + */ + void ReleaseSessionId(s32 session_id); + +private: + /// Core system + Core::System& system; + /// Session ids, -1 when in use + std::array session_ids{}; + /// Number of active renderers + u32 session_count{}; + /// Lock for interacting with the sessions + std::mutex session_lock{}; + /// Regularly generates commands from the registered systems for the AudioRenderer + std::unique_ptr system_manager{}; +}; + +} // namespace AudioRenderer +} // namespace AudioCore diff --git a/src/audio_core/audio_renderer.cpp b/src/audio_core/audio_renderer.cpp deleted file mode 100644 index 9191ca0931..0000000000 --- a/src/audio_core/audio_renderer.cpp +++ /dev/null @@ -1,343 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include -#include -#include - -#include "audio_core/audio_out.h" -#include "audio_core/audio_renderer.h" -#include "audio_core/common.h" -#include "audio_core/info_updater.h" -#include "audio_core/voice_context.h" -#include "common/logging/log.h" -#include "common/settings.h" -#include "core/core_timing.h" -#include "core/memory.h" - -namespace { -[[nodiscard]] static constexpr s16 ClampToS16(s32 value) { - return static_cast(std::clamp(value, s32{std::numeric_limits::min()}, - s32{std::numeric_limits::max()})); -} - -[[nodiscard]] static constexpr s16 Mix2To1(s16 l_channel, s16 r_channel) { - // Mix 50% from left and 50% from right channel - constexpr float l_mix_amount = 50.0f / 100.0f; - constexpr float r_mix_amount = 50.0f / 100.0f; - return ClampToS16(static_cast((static_cast(l_channel) * l_mix_amount) + - (static_cast(r_channel) * r_mix_amount))); -} - -[[maybe_unused, nodiscard]] static constexpr std::tuple Mix6To2( - s16 fl_channel, s16 fr_channel, s16 fc_channel, [[maybe_unused]] s16 lf_channel, s16 bl_channel, - s16 br_channel) { - // Front channels are mixed 36.94%, Center channels are mixed to be 26.12% & the back channels - // are mixed to be 36.94% - - constexpr float front_mix_amount = 36.94f / 100.0f; - constexpr float center_mix_amount = 26.12f / 100.0f; - constexpr float back_mix_amount = 36.94f / 100.0f; - - // Mix 50% from left and 50% from right channel - const auto left = front_mix_amount * static_cast(fl_channel) + - center_mix_amount * static_cast(fc_channel) + - back_mix_amount * static_cast(bl_channel); - - const auto right = front_mix_amount * static_cast(fr_channel) + - center_mix_amount * static_cast(fc_channel) + - back_mix_amount * static_cast(br_channel); - - return {ClampToS16(static_cast(left)), ClampToS16(static_cast(right))}; -} - -[[nodiscard]] static constexpr std::tuple Mix6To2WithCoefficients( - s16 fl_channel, s16 fr_channel, s16 fc_channel, s16 lf_channel, s16 bl_channel, s16 br_channel, - const std::array& coeff) { - const auto left = - static_cast(fl_channel) * coeff[0] + static_cast(fc_channel) * coeff[1] + - static_cast(lf_channel) * coeff[2] + static_cast(bl_channel) * coeff[3]; - - const auto right = - static_cast(fr_channel) * coeff[0] + static_cast(fc_channel) * coeff[1] + - static_cast(lf_channel) * coeff[2] + static_cast(br_channel) * coeff[3]; - - return {ClampToS16(static_cast(left)), ClampToS16(static_cast(right))}; -} - -} // namespace - -namespace AudioCore { -constexpr s32 NUM_BUFFERS = 2; - -AudioRenderer::AudioRenderer(Core::Timing::CoreTiming& core_timing_, Core::Memory::Memory& memory_, - AudioCommon::AudioRendererParameter params, - Stream::ReleaseCallback&& release_callback, - std::size_t instance_number) - : worker_params{params}, memory_pool_info(params.effect_count + params.voice_count * 4), - voice_context(params.voice_count), effect_context(params.effect_count), mix_context(), - sink_context(params.sink_count), splitter_context(), - voices(params.voice_count), memory{memory_}, - command_generator(worker_params, voice_context, mix_context, splitter_context, effect_context, - memory), - core_timing{core_timing_} { - behavior_info.SetUserRevision(params.revision); - splitter_context.Initialize(behavior_info, params.splitter_count, - params.num_splitter_send_channels); - mix_context.Initialize(behavior_info, params.submix_count + 1, params.effect_count); - audio_out = std::make_unique(); - stream = audio_out->OpenStream( - core_timing, params.sample_rate, AudioCommon::STREAM_NUM_CHANNELS, - fmt::format("AudioRenderer-Instance{}", instance_number), std::move(release_callback)); - process_event = - Core::Timing::CreateEvent(fmt::format("AudioRenderer-Instance{}-Process", instance_number), - [this](std::uintptr_t, s64, std::chrono::nanoseconds) { - ReleaseAndQueueBuffers(); - return std::nullopt; - }); - for (s32 i = 0; i < NUM_BUFFERS; ++i) { - QueueMixedBuffer(i); - } -} - -AudioRenderer::~AudioRenderer() = default; - -Result AudioRenderer::Start() { - audio_out->StartStream(stream); - ReleaseAndQueueBuffers(); - return ResultSuccess; -} - -Result AudioRenderer::Stop() { - audio_out->StopStream(stream); - return ResultSuccess; -} - -u32 AudioRenderer::GetSampleRate() const { - return worker_params.sample_rate; -} - -u32 AudioRenderer::GetSampleCount() const { - return worker_params.sample_count; -} - -u32 AudioRenderer::GetMixBufferCount() const { - return worker_params.mix_buffer_count; -} - -Stream::State AudioRenderer::GetStreamState() const { - return stream->GetState(); -} - -Result AudioRenderer::UpdateAudioRenderer(const std::vector& input_params, - std::vector& output_params) { - std::scoped_lock lock{mutex}; - InfoUpdater info_updater{input_params, output_params, behavior_info}; - - if (!info_updater.UpdateBehaviorInfo(behavior_info)) { - LOG_ERROR(Audio, "Failed to update behavior info input parameters"); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - - if (!info_updater.UpdateMemoryPools(memory_pool_info)) { - LOG_ERROR(Audio, "Failed to update memory pool parameters"); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - - if (!info_updater.UpdateVoiceChannelResources(voice_context)) { - LOG_ERROR(Audio, "Failed to update voice channel resource parameters"); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - - if (!info_updater.UpdateVoices(voice_context, memory_pool_info, 0)) { - LOG_ERROR(Audio, "Failed to update voice parameters"); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - - // TODO(ogniK): Deal with stopped audio renderer but updates still taking place - if (!info_updater.UpdateEffects(effect_context, true)) { - LOG_ERROR(Audio, "Failed to update effect parameters"); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - - if (behavior_info.IsSplitterSupported()) { - if (!info_updater.UpdateSplitterInfo(splitter_context)) { - LOG_ERROR(Audio, "Failed to update splitter parameters"); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - } - - const auto mix_result = info_updater.UpdateMixes(mix_context, worker_params.mix_buffer_count, - splitter_context, effect_context); - - if (mix_result.IsError()) { - LOG_ERROR(Audio, "Failed to update mix parameters"); - return mix_result; - } - - // TODO(ogniK): Sinks - if (!info_updater.UpdateSinks(sink_context)) { - LOG_ERROR(Audio, "Failed to update sink parameters"); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - - // TODO(ogniK): Performance buffer - if (!info_updater.UpdatePerformanceBuffer()) { - LOG_ERROR(Audio, "Failed to update performance buffer parameters"); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - - if (!info_updater.UpdateErrorInfo(behavior_info)) { - LOG_ERROR(Audio, "Failed to update error info"); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - - if (behavior_info.IsElapsedFrameCountSupported()) { - if (!info_updater.UpdateRendererInfo(elapsed_frame_count)) { - LOG_ERROR(Audio, "Failed to update renderer info"); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - } - // TODO(ogniK): Statistics - - if (!info_updater.WriteOutputHeader()) { - LOG_ERROR(Audio, "Failed to write output header"); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - - // TODO(ogniK): Check when all sections are implemented - - if (!info_updater.CheckConsumedSize()) { - LOG_ERROR(Audio, "Audio buffers were not consumed!"); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - return ResultSuccess; -} - -void AudioRenderer::QueueMixedBuffer(Buffer::Tag tag) { - command_generator.PreCommand(); - // Clear mix buffers before our next operation - command_generator.ClearMixBuffers(); - - // If the splitter is not in use, sort our mixes - if (!splitter_context.UsingSplitter()) { - mix_context.SortInfo(); - } - // Sort our voices - voice_context.SortInfo(); - - // Handle samples - command_generator.GenerateVoiceCommands(); - command_generator.GenerateSubMixCommands(); - command_generator.GenerateFinalMixCommands(); - - command_generator.PostCommand(); - // Base sample size - std::size_t BUFFER_SIZE{worker_params.sample_count}; - // Samples, making sure to clear - std::vector buffer(BUFFER_SIZE * stream->GetNumChannels(), 0); - - if (sink_context.InUse()) { - const auto stream_channel_count = stream->GetNumChannels(); - const auto buffer_offsets = sink_context.OutputBuffers(); - const auto channel_count = buffer_offsets.size(); - const auto& final_mix = mix_context.GetFinalMixInfo(); - const auto& in_params = final_mix.GetInParams(); - std::vector> mix_buffers(channel_count); - for (std::size_t i = 0; i < channel_count; i++) { - mix_buffers[i] = - command_generator.GetMixBuffer(in_params.buffer_offset + buffer_offsets[i]); - } - - for (std::size_t i = 0; i < BUFFER_SIZE; i++) { - if (channel_count == 1) { - const auto sample = ClampToS16(mix_buffers[0][i]); - - // Place sample in all channels - for (u32 channel = 0; channel < stream_channel_count; channel++) { - buffer[i * stream_channel_count + channel] = sample; - } - - if (stream_channel_count == 6) { - // Output stream has a LF channel, mute it! - buffer[i * stream_channel_count + 3] = 0; - } - - } else if (channel_count == 2) { - const auto l_sample = ClampToS16(mix_buffers[0][i]); - const auto r_sample = ClampToS16(mix_buffers[1][i]); - if (stream_channel_count == 1) { - buffer[i * stream_channel_count + 0] = Mix2To1(l_sample, r_sample); - } else if (stream_channel_count == 2) { - buffer[i * stream_channel_count + 0] = l_sample; - buffer[i * stream_channel_count + 1] = r_sample; - } else if (stream_channel_count == 6) { - buffer[i * stream_channel_count + 0] = l_sample; - buffer[i * stream_channel_count + 1] = r_sample; - - // Combine both left and right channels to the center channel - buffer[i * stream_channel_count + 2] = Mix2To1(l_sample, r_sample); - - buffer[i * stream_channel_count + 4] = l_sample; - buffer[i * stream_channel_count + 5] = r_sample; - } - - } else if (channel_count == 6) { - const auto fl_sample = ClampToS16(mix_buffers[0][i]); - const auto fr_sample = ClampToS16(mix_buffers[1][i]); - const auto fc_sample = ClampToS16(mix_buffers[2][i]); - const auto lf_sample = ClampToS16(mix_buffers[3][i]); - const auto bl_sample = ClampToS16(mix_buffers[4][i]); - const auto br_sample = ClampToS16(mix_buffers[5][i]); - - if (stream_channel_count == 1) { - // Games seem to ignore the center channel half the time, we use the front left - // and right channel for mixing as that's where majority of the audio goes - buffer[i * stream_channel_count + 0] = Mix2To1(fl_sample, fr_sample); - } else if (stream_channel_count == 2) { - // Mix all channels into 2 channels - const auto [left, right] = Mix6To2WithCoefficients( - fl_sample, fr_sample, fc_sample, lf_sample, bl_sample, br_sample, - sink_context.GetDownmixCoefficients()); - buffer[i * stream_channel_count + 0] = left; - buffer[i * stream_channel_count + 1] = right; - } else if (stream_channel_count == 6) { - // Pass through - buffer[i * stream_channel_count + 0] = fl_sample; - buffer[i * stream_channel_count + 1] = fr_sample; - buffer[i * stream_channel_count + 2] = fc_sample; - buffer[i * stream_channel_count + 3] = lf_sample; - buffer[i * stream_channel_count + 4] = bl_sample; - buffer[i * stream_channel_count + 5] = br_sample; - } - } - } - } - - audio_out->QueueBuffer(stream, tag, std::move(buffer)); - elapsed_frame_count++; - voice_context.UpdateStateByDspShared(); -} - -void AudioRenderer::ReleaseAndQueueBuffers() { - if (!stream->IsPlaying()) { - return; - } - - { - std::scoped_lock lock{mutex}; - const auto released_buffers{audio_out->GetTagsAndReleaseBuffers(stream)}; - for (const auto& tag : released_buffers) { - QueueMixedBuffer(tag); - } - } - - const f32 sample_rate = static_cast(GetSampleRate()); - const f32 sample_count = static_cast(GetSampleCount()); - const f32 consume_rate = sample_rate / (sample_count * (sample_count / 240)); - const s32 ms = (1000 / static_cast(consume_rate)) - 1; - const std::chrono::milliseconds next_event_time(std::max(ms / NUM_BUFFERS, 1)); - core_timing.ScheduleEvent(next_event_time, process_event, {}); -} - -} // namespace AudioCore diff --git a/src/audio_core/audio_renderer.h b/src/audio_core/audio_renderer.h deleted file mode 100644 index a67ffd592e..0000000000 --- a/src/audio_core/audio_renderer.h +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include -#include - -#include "audio_core/behavior_info.h" -#include "audio_core/command_generator.h" -#include "audio_core/common.h" -#include "audio_core/effect_context.h" -#include "audio_core/memory_pool.h" -#include "audio_core/mix_context.h" -#include "audio_core/sink_context.h" -#include "audio_core/splitter_context.h" -#include "audio_core/stream.h" -#include "audio_core/voice_context.h" -#include "common/common_funcs.h" -#include "common/common_types.h" -#include "common/swap.h" -#include "core/hle/result.h" - -namespace Core::Timing { -class CoreTiming; -} - -namespace Core::Memory { -class Memory; -} - -namespace AudioCore { -using DSPStateHolder = std::array; - -class AudioOut; - -class AudioRenderer { -public: - AudioRenderer(Core::Timing::CoreTiming& core_timing, Core::Memory::Memory& memory_, - AudioCommon::AudioRendererParameter params, - Stream::ReleaseCallback&& release_callback, std::size_t instance_number); - ~AudioRenderer(); - - [[nodiscard]] Result UpdateAudioRenderer(const std::vector& input_params, - std::vector& output_params); - [[nodiscard]] Result Start(); - [[nodiscard]] Result Stop(); - void QueueMixedBuffer(Buffer::Tag tag); - void ReleaseAndQueueBuffers(); - [[nodiscard]] u32 GetSampleRate() const; - [[nodiscard]] u32 GetSampleCount() const; - [[nodiscard]] u32 GetMixBufferCount() const; - [[nodiscard]] Stream::State GetStreamState() const; - -private: - BehaviorInfo behavior_info{}; - - AudioCommon::AudioRendererParameter worker_params; - std::vector memory_pool_info; - VoiceContext voice_context; - EffectContext effect_context; - MixContext mix_context; - SinkContext sink_context; - SplitterContext splitter_context; - std::vector voices; - std::unique_ptr audio_out; - StreamPtr stream; - Core::Memory::Memory& memory; - CommandGenerator command_generator; - std::size_t elapsed_frame_count{}; - Core::Timing::CoreTiming& core_timing; - std::shared_ptr process_event; - std::mutex mutex; -}; - -} // namespace AudioCore diff --git a/src/audio_core/behavior_info.cpp b/src/audio_core/behavior_info.cpp deleted file mode 100644 index ea7e456179..0000000000 --- a/src/audio_core/behavior_info.cpp +++ /dev/null @@ -1,104 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include -#include "audio_core/behavior_info.h" -#include "audio_core/common.h" -#include "common/logging/log.h" - -namespace AudioCore { - -BehaviorInfo::BehaviorInfo() : process_revision(AudioCommon::CURRENT_PROCESS_REVISION) {} -BehaviorInfo::~BehaviorInfo() = default; - -bool BehaviorInfo::UpdateOutput(std::vector& buffer, std::size_t offset) { - if (!AudioCommon::CanConsumeBuffer(buffer.size(), offset, sizeof(OutParams))) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return false; - } - - OutParams params{}; - std::memcpy(params.errors.data(), errors.data(), sizeof(ErrorInfo) * errors.size()); - params.error_count = static_cast(error_count); - std::memcpy(buffer.data() + offset, ¶ms, sizeof(OutParams)); - return true; -} - -void BehaviorInfo::ClearError() { - error_count = 0; -} - -void BehaviorInfo::UpdateFlags(u64_le dest_flags) { - flags = dest_flags; -} - -void BehaviorInfo::SetUserRevision(u32_le revision) { - user_revision = revision; -} - -u32_le BehaviorInfo::GetUserRevision() const { - return user_revision; -} - -u32_le BehaviorInfo::GetProcessRevision() const { - return process_revision; -} - -bool BehaviorInfo::IsAdpcmLoopContextBugFixed() const { - return AudioCommon::IsRevisionSupported(2, user_revision); -} - -bool BehaviorInfo::IsSplitterSupported() const { - return AudioCommon::IsRevisionSupported(2, user_revision); -} - -bool BehaviorInfo::IsLongSizePreDelaySupported() const { - return AudioCommon::IsRevisionSupported(3, user_revision); -} - -bool BehaviorInfo::IsAudioRendererProcessingTimeLimit80PercentSupported() const { - return AudioCommon::IsRevisionSupported(5, user_revision); -} - -bool BehaviorInfo::IsAudioRendererProcessingTimeLimit75PercentSupported() const { - return AudioCommon::IsRevisionSupported(4, user_revision); -} - -bool BehaviorInfo::IsAudioRendererProcessingTimeLimit70PercentSupported() const { - return AudioCommon::IsRevisionSupported(1, user_revision); -} - -bool BehaviorInfo::IsElapsedFrameCountSupported() const { - return AudioCommon::IsRevisionSupported(5, user_revision); -} - -bool BehaviorInfo::IsMemoryPoolForceMappingEnabled() const { - return (flags & 1) != 0; -} - -bool BehaviorInfo::IsFlushVoiceWaveBuffersSupported() const { - return AudioCommon::IsRevisionSupported(5, user_revision); -} - -bool BehaviorInfo::IsVoicePlayedSampleCountResetAtLoopPointSupported() const { - return AudioCommon::IsRevisionSupported(5, user_revision); -} - -bool BehaviorInfo::IsVoicePitchAndSrcSkippedSupported() const { - return AudioCommon::IsRevisionSupported(5, user_revision); -} - -bool BehaviorInfo::IsMixInParameterDirtyOnlyUpdateSupported() const { - return AudioCommon::IsRevisionSupported(7, user_revision); -} - -bool BehaviorInfo::IsSplitterBugFixed() const { - return AudioCommon::IsRevisionSupported(5, user_revision); -} - -void BehaviorInfo::CopyErrorInfo(BehaviorInfo::OutParams& dst) { - dst.error_count = static_cast(error_count); - std::copy(errors.begin(), errors.begin() + error_count, dst.errors.begin()); -} - -} // namespace AudioCore diff --git a/src/audio_core/behavior_info.h b/src/audio_core/behavior_info.h deleted file mode 100644 index b8c3159b9e..0000000000 --- a/src/audio_core/behavior_info.h +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include - -#include -#include "common/common_funcs.h" -#include "common/common_types.h" -#include "common/swap.h" - -namespace AudioCore { -class BehaviorInfo { -public: - struct ErrorInfo { - u32_le result{}; - INSERT_PADDING_WORDS(1); - u64_le result_info{}; - }; - static_assert(sizeof(ErrorInfo) == 0x10, "ErrorInfo is an invalid size"); - - struct InParams { - u32_le revision{}; - u32_le padding{}; - u64_le flags{}; - }; - static_assert(sizeof(InParams) == 0x10, "InParams is an invalid size"); - - struct OutParams { - std::array errors{}; - u32_le error_count{}; - INSERT_PADDING_BYTES(12); - }; - static_assert(sizeof(OutParams) == 0xb0, "OutParams is an invalid size"); - - explicit BehaviorInfo(); - ~BehaviorInfo(); - - bool UpdateOutput(std::vector& buffer, std::size_t offset); - - void ClearError(); - void UpdateFlags(u64_le dest_flags); - void SetUserRevision(u32_le revision); - [[nodiscard]] u32_le GetUserRevision() const; - [[nodiscard]] u32_le GetProcessRevision() const; - - [[nodiscard]] bool IsAdpcmLoopContextBugFixed() const; - [[nodiscard]] bool IsSplitterSupported() const; - [[nodiscard]] bool IsLongSizePreDelaySupported() const; - [[nodiscard]] bool IsAudioRendererProcessingTimeLimit80PercentSupported() const; - [[nodiscard]] bool IsAudioRendererProcessingTimeLimit75PercentSupported() const; - [[nodiscard]] bool IsAudioRendererProcessingTimeLimit70PercentSupported() const; - [[nodiscard]] bool IsElapsedFrameCountSupported() const; - [[nodiscard]] bool IsMemoryPoolForceMappingEnabled() const; - [[nodiscard]] bool IsFlushVoiceWaveBuffersSupported() const; - [[nodiscard]] bool IsVoicePlayedSampleCountResetAtLoopPointSupported() const; - [[nodiscard]] bool IsVoicePitchAndSrcSkippedSupported() const; - [[nodiscard]] bool IsMixInParameterDirtyOnlyUpdateSupported() const; - [[nodiscard]] bool IsSplitterBugFixed() const; - void CopyErrorInfo(OutParams& dst); - -private: - u32_le process_revision{}; - u32_le user_revision{}; - u64_le flags{}; - std::array errors{}; - std::size_t error_count{}; -}; - -} // namespace AudioCore diff --git a/src/audio_core/buffer.h b/src/audio_core/buffer.h deleted file mode 100644 index ac001629f9..0000000000 --- a/src/audio_core/buffer.h +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include - -#include "common/common_types.h" - -namespace AudioCore { - -/** - * Represents a buffer of audio samples to be played in an audio stream - */ -class Buffer { -public: - using Tag = u64; - - Buffer(Tag tag_, std::vector&& samples_) : tag{tag_}, samples{std::move(samples_)} {} - - /// Returns the raw audio data for the buffer - std::vector& GetSamples() { - return samples; - } - - /// Returns the raw audio data for the buffer - const std::vector& GetSamples() const { - return samples; - } - - /// Returns the buffer tag, this is provided by the game to the audout service - Tag GetTag() const { - return tag; - } - -private: - Tag tag; - std::vector samples; -}; - -using BufferPtr = std::shared_ptr; - -} // namespace AudioCore diff --git a/src/audio_core/codec.cpp b/src/audio_core/codec.cpp deleted file mode 100644 index 868b7a173d..0000000000 --- a/src/audio_core/codec.cpp +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include - -#include "audio_core/codec.h" - -namespace AudioCore::Codec { - -std::vector DecodeADPCM(const u8* const data, std::size_t size, const ADPCM_Coeff& coeff, - ADPCMState& state) { - // GC-ADPCM with scale factor and variable coefficients. - // Frames are 8 bytes long containing 14 samples each. - // Samples are 4 bits (one nibble) long. - - constexpr std::size_t FRAME_LEN = 8; - constexpr std::size_t SAMPLES_PER_FRAME = 14; - static constexpr std::array SIGNED_NIBBLES{ - 0, 1, 2, 3, 4, 5, 6, 7, -8, -7, -6, -5, -4, -3, -2, -1, - }; - - const std::size_t sample_count = (size / FRAME_LEN) * SAMPLES_PER_FRAME; - const std::size_t ret_size = - sample_count % 2 == 0 ? sample_count : sample_count + 1; // Ensure multiple of two. - std::vector ret(ret_size); - - int yn1 = state.yn1, yn2 = state.yn2; - - const std::size_t NUM_FRAMES = - (sample_count + (SAMPLES_PER_FRAME - 1)) / SAMPLES_PER_FRAME; // Round up. - for (std::size_t framei = 0; framei < NUM_FRAMES; framei++) { - const int frame_header = data[framei * FRAME_LEN]; - const int scale = 1 << (frame_header & 0xF); - const int idx = (frame_header >> 4) & 0x7; - - // Coefficients are fixed point with 11 bits fractional part. - const int coef1 = coeff[idx * 2 + 0]; - const int coef2 = coeff[idx * 2 + 1]; - - // Decodes an audio sample. One nibble produces one sample. - const auto decode_sample = [&](const int nibble) -> s16 { - const int xn = nibble * scale; - // We first transform everything into 11 bit fixed point, perform the second order - // digital filter, then transform back. - // 0x400 == 0.5 in 11 bit fixed point. - // Filter: y[n] = x[n] + 0.5 + c1 * y[n-1] + c2 * y[n-2] - int val = ((xn << 11) + 0x400 + coef1 * yn1 + coef2 * yn2) >> 11; - // Clamp to output range. - val = std::clamp(val, -32768, 32767); - // Advance output feedback. - yn2 = yn1; - yn1 = val; - return static_cast(val); - }; - - std::size_t outputi = framei * SAMPLES_PER_FRAME; - std::size_t datai = framei * FRAME_LEN + 1; - for (std::size_t i = 0; i < SAMPLES_PER_FRAME && outputi < sample_count; i += 2) { - const s16 sample1 = decode_sample(SIGNED_NIBBLES[data[datai] >> 4]); - ret[outputi] = sample1; - outputi++; - - const s16 sample2 = decode_sample(SIGNED_NIBBLES[data[datai] & 0xF]); - ret[outputi] = sample2; - outputi++; - - datai++; - } - } - - state.yn1 = static_cast(yn1); - state.yn2 = static_cast(yn2); - - return ret; -} - -} // namespace AudioCore::Codec diff --git a/src/audio_core/codec.h b/src/audio_core/codec.h deleted file mode 100644 index 5a058b3684..0000000000 --- a/src/audio_core/codec.h +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include - -#include "common/common_types.h" - -namespace AudioCore::Codec { - -enum class PcmFormat : u32 { - Invalid = 0, - Int8 = 1, - Int16 = 2, - Int24 = 3, - Int32 = 4, - PcmFloat = 5, - Adpcm = 6, -}; - -/// See: Codec::DecodeADPCM -struct ADPCMState { - // Two historical samples from previous processed buffer, - // required for ADPCM decoding - s16 yn1; ///< y[n-1] - s16 yn2; ///< y[n-2] -}; - -using ADPCM_Coeff = std::array; - -/** - * @param data Pointer to buffer that contains ADPCM data to decode - * @param size Size of buffer in bytes - * @param coeff ADPCM coefficients - * @param state ADPCM state, this is updated with new state - * @return Decoded stereo signed PCM16 data, sample_count in length - */ -std::vector DecodeADPCM(const u8* data, std::size_t size, const ADPCM_Coeff& coeff, - ADPCMState& state); - -}; // namespace AudioCore::Codec diff --git a/src/audio_core/command_generator.cpp b/src/audio_core/command_generator.cpp deleted file mode 100644 index f975208206..0000000000 --- a/src/audio_core/command_generator.cpp +++ /dev/null @@ -1,1369 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include -#include -#include - -#include "audio_core/algorithm/interpolate.h" -#include "audio_core/command_generator.h" -#include "audio_core/effect_context.h" -#include "audio_core/mix_context.h" -#include "audio_core/voice_context.h" -#include "common/common_types.h" -#include "core/memory.h" - -namespace AudioCore { -namespace { -constexpr std::size_t MIX_BUFFER_SIZE = 0x3f00; -constexpr std::size_t SCALED_MIX_BUFFER_SIZE = MIX_BUFFER_SIZE << 15ULL; -using DelayLineTimes = std::array; - -constexpr DelayLineTimes FDN_MIN_DELAY_LINE_TIMES{5.0f, 6.0f, 13.0f, 14.0f}; -constexpr DelayLineTimes FDN_MAX_DELAY_LINE_TIMES{45.704f, 82.782f, 149.94f, 271.58f}; -constexpr DelayLineTimes DECAY0_MAX_DELAY_LINE_TIMES{17.0f, 13.0f, 9.0f, 7.0f}; -constexpr DelayLineTimes DECAY1_MAX_DELAY_LINE_TIMES{19.0f, 11.0f, 10.0f, 6.0f}; -constexpr std::array EARLY_TAP_TIMES{ - 0.017136f, 0.059154f, 0.161733f, 0.390186f, 0.425262f, 0.455411f, 0.689737f, - 0.745910f, 0.833844f, 0.859502f, 0.000000f, 0.075024f, 0.168788f, 0.299901f, - 0.337443f, 0.371903f, 0.599011f, 0.716741f, 0.817859f, 0.851664f}; -constexpr std::array EARLY_GAIN{ - 0.67096f, 0.61027f, 1.0f, 0.35680f, 0.68361f, 0.65978f, 0.51939f, - 0.24712f, 0.45945f, 0.45021f, 0.64196f, 0.54879f, 0.92925f, 0.38270f, - 0.72867f, 0.69794f, 0.5464f, 0.24563f, 0.45214f, 0.44042f}; - -template -void ApplyMix(std::span output, std::span input, s32 gain, s32 sample_count) { - for (std::size_t i = 0; i < static_cast(sample_count); i += N) { - for (std::size_t j = 0; j < N; j++) { - output[i + j] += - static_cast((static_cast(input[i + j]) * gain + 0x4000) >> 15); - } - } -} - -s32 ApplyMixRamp(std::span output, std::span input, float gain, float delta, - s32 sample_count) { - // XC2 passes in NaN mix volumes, causing further issues as we handle everything as s32 rather - // than float, so the NaN propogation is lost. As the samples get further modified for - // volume etc, they can get out of NaN range, so a later heuristic for catching this is - // more difficult. Handle it here by setting these samples to silence. - if (std::isnan(gain)) { - gain = 0.0f; - delta = 0.0f; - } - - s32 x = 0; - for (s32 i = 0; i < sample_count; i++) { - x = static_cast(static_cast(input[i]) * gain); - output[i] += x; - gain += delta; - } - return x; -} - -void ApplyGain(std::span output, std::span input, s32 gain, s32 delta, - s32 sample_count) { - for (s32 i = 0; i < sample_count; i++) { - output[i] = static_cast((static_cast(input[i]) * gain + 0x4000) >> 15); - gain += delta; - } -} - -void ApplyGainWithoutDelta(std::span output, std::span input, s32 gain, - s32 sample_count) { - for (s32 i = 0; i < sample_count; i++) { - output[i] = static_cast((static_cast(input[i]) * gain + 0x4000) >> 15); - } -} - -s32 ApplyMixDepop(std::span output, s32 first_sample, s32 delta, s32 sample_count) { - const bool positive = first_sample > 0; - auto final_sample = std::abs(first_sample); - for (s32 i = 0; i < sample_count; i++) { - final_sample = static_cast((static_cast(final_sample) * delta) >> 15); - if (positive) { - output[i] += final_sample; - } else { - output[i] -= final_sample; - } - } - if (positive) { - return final_sample; - } else { - return -final_sample; - } -} - -float Pow10(float x) { - if (x >= 0.0f) { - return 1.0f; - } else if (x <= -5.3f) { - return 0.0f; - } - return std::pow(10.0f, x); -} - -float SinD(float degrees) { - return std::sin(degrees * std::numbers::pi_v / 180.0f); -} - -float CosD(float degrees) { - return std::cos(degrees * std::numbers::pi_v / 180.0f); -} - -float ToFloat(s32 sample) { - return static_cast(sample) / 65536.f; -} - -s32 ToS32(float sample) { - constexpr auto min = -8388608.0f; - constexpr auto max = 8388607.f; - float rescaled_sample = sample * 65536.0f; - if (rescaled_sample < min) { - rescaled_sample = min; - } - if (rescaled_sample > max) { - rescaled_sample = max; - } - return static_cast(rescaled_sample); -} - -constexpr std::array REVERB_TAP_INDEX_1CH{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - -constexpr std::array REVERB_TAP_INDEX_2CH{0, 0, 0, 1, 1, 1, 1, 0, 0, 0, - 1, 1, 1, 0, 0, 0, 0, 1, 1, 1}; - -constexpr std::array REVERB_TAP_INDEX_4CH{0, 0, 0, 1, 1, 1, 1, 2, 2, 2, - 1, 1, 1, 0, 0, 0, 0, 3, 3, 3}; - -constexpr std::array REVERB_TAP_INDEX_6CH{4, 0, 0, 1, 1, 1, 1, 2, 2, 2, - 1, 1, 1, 0, 0, 0, 0, 3, 3, 3}; - -template -void ApplyReverbGeneric( - I3dl2ReverbState& state, - const std::array, AudioCommon::MAX_CHANNEL_COUNT>& input, - const std::array, AudioCommon::MAX_CHANNEL_COUNT>& output, s32 sample_count) { - - auto GetTapLookup = []() { - if constexpr (CHANNEL_COUNT == 1) { - return REVERB_TAP_INDEX_1CH; - } else if constexpr (CHANNEL_COUNT == 2) { - return REVERB_TAP_INDEX_2CH; - } else if constexpr (CHANNEL_COUNT == 4) { - return REVERB_TAP_INDEX_4CH; - } else if constexpr (CHANNEL_COUNT == 6) { - return REVERB_TAP_INDEX_6CH; - } - }; - - const auto& tap_index_lut = GetTapLookup(); - for (s32 sample = 0; sample < sample_count; sample++) { - std::array out_samples{}; - std::array fsamp{}; - std::array mixed{}; - std::array osamp{}; - - // Mix everything into a single sample - s32 temp_mixed_sample = 0; - for (std::size_t i = 0; i < CHANNEL_COUNT; i++) { - temp_mixed_sample += input[i][sample]; - } - const auto current_sample = ToFloat(temp_mixed_sample); - const auto early_tap = state.early_delay_line.TapOut(state.early_to_late_taps); - - for (std::size_t i = 0; i < AudioCommon::I3DL2REVERB_TAPS; i++) { - const auto tapped_samp = - state.early_delay_line.TapOut(state.early_tap_steps[i]) * EARLY_GAIN[i]; - out_samples[tap_index_lut[i]] += tapped_samp; - - if constexpr (CHANNEL_COUNT == 6) { - // handle lfe - out_samples[5] += tapped_samp; - } - } - - state.lowpass_0 = current_sample * state.lowpass_2 + state.lowpass_0 * state.lowpass_1; - state.early_delay_line.Tick(state.lowpass_0); - - for (std::size_t i = 0; i < CHANNEL_COUNT; i++) { - out_samples[i] *= state.early_gain; - } - - // Two channel seems to apply a latet gain, we require to save this - f32 filter{}; - for (std::size_t i = 0; i < AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT; i++) { - filter = state.fdn_delay_line[i].GetOutputSample(); - const auto computed = filter * state.lpf_coefficients[0][i] + state.shelf_filter[i]; - state.shelf_filter[i] = - filter * state.lpf_coefficients[1][i] + computed * state.lpf_coefficients[2][i]; - fsamp[i] = computed; - } - - // Mixing matrix - mixed[0] = fsamp[1] + fsamp[2]; - mixed[1] = -fsamp[0] - fsamp[3]; - mixed[2] = fsamp[0] - fsamp[3]; - mixed[3] = fsamp[1] - fsamp[2]; - - if constexpr (CHANNEL_COUNT == 2) { - for (auto& mix : mixed) { - mix *= (filter * state.late_gain); - } - } - - for (std::size_t i = 0; i < AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT; i++) { - const auto late = early_tap * state.late_gain; - osamp[i] = state.decay_delay_line0[i].Tick(late + mixed[i]); - osamp[i] = state.decay_delay_line1[i].Tick(osamp[i]); - state.fdn_delay_line[i].Tick(osamp[i]); - } - - if constexpr (CHANNEL_COUNT == 1) { - output[0][sample] = ToS32(state.dry_gain * ToFloat(input[0][sample]) + - (out_samples[0] + osamp[0] + osamp[1])); - } else if constexpr (CHANNEL_COUNT == 2 || CHANNEL_COUNT == 4) { - for (std::size_t i = 0; i < CHANNEL_COUNT; i++) { - output[i][sample] = - ToS32(state.dry_gain * ToFloat(input[i][sample]) + (out_samples[i] + osamp[i])); - } - } else if constexpr (CHANNEL_COUNT == 6) { - const auto temp_center = state.center_delay_line.Tick(0.5f * (osamp[2] - osamp[3])); - for (std::size_t i = 0; i < 4; i++) { - output[i][sample] = - ToS32(state.dry_gain * ToFloat(input[i][sample]) + (out_samples[i] + osamp[i])); - } - output[4][sample] = - ToS32(state.dry_gain * ToFloat(input[4][sample]) + (out_samples[4] + temp_center)); - output[5][sample] = - ToS32(state.dry_gain * ToFloat(input[5][sample]) + (out_samples[5] + osamp[3])); - } - } -} - -} // namespace - -CommandGenerator::CommandGenerator(AudioCommon::AudioRendererParameter& worker_params_, - VoiceContext& voice_context_, MixContext& mix_context_, - SplitterContext& splitter_context_, - EffectContext& effect_context_, Core::Memory::Memory& memory_) - : worker_params(worker_params_), voice_context(voice_context_), mix_context(mix_context_), - splitter_context(splitter_context_), effect_context(effect_context_), memory(memory_), - mix_buffer((worker_params.mix_buffer_count + AudioCommon::MAX_CHANNEL_COUNT) * - worker_params.sample_count), - sample_buffer(MIX_BUFFER_SIZE), - depop_buffer((worker_params.mix_buffer_count + AudioCommon::MAX_CHANNEL_COUNT) * - worker_params.sample_count) {} -CommandGenerator::~CommandGenerator() = default; - -void CommandGenerator::ClearMixBuffers() { - std::fill(mix_buffer.begin(), mix_buffer.end(), 0); - std::fill(sample_buffer.begin(), sample_buffer.end(), 0); - // std::fill(depop_buffer.begin(), depop_buffer.end(), 0); -} - -void CommandGenerator::GenerateVoiceCommands() { - if (dumping_frame) { - LOG_DEBUG(Audio, "(DSP_TRACE) GenerateVoiceCommands"); - } - // Grab all our voices - const auto voice_count = voice_context.GetVoiceCount(); - for (std::size_t i = 0; i < voice_count; i++) { - auto& voice_info = voice_context.GetSortedInfo(i); - // Update voices and check if we should queue them - if (voice_info.ShouldSkip() || !voice_info.UpdateForCommandGeneration(voice_context)) { - continue; - } - - // Queue our voice - GenerateVoiceCommand(voice_info); - } - // Update our splitters - splitter_context.UpdateInternalState(); -} - -void CommandGenerator::GenerateVoiceCommand(ServerVoiceInfo& voice_info) { - auto& in_params = voice_info.GetInParams(); - const auto channel_count = in_params.channel_count; - - for (s32 channel = 0; channel < channel_count; channel++) { - const auto resource_id = in_params.voice_channel_resource_id[channel]; - auto& dsp_state = voice_context.GetDspSharedState(resource_id); - auto& channel_resource = voice_context.GetChannelResource(resource_id); - - // Decode our samples for our channel - GenerateDataSourceCommand(voice_info, dsp_state, channel); - - if (in_params.should_depop) { - in_params.last_volume = 0.0f; - } else if (in_params.splitter_info_id != AudioCommon::NO_SPLITTER || - in_params.mix_id != AudioCommon::NO_MIX) { - // Apply a biquad filter if needed - GenerateBiquadFilterCommandForVoice(voice_info, dsp_state, - worker_params.mix_buffer_count, channel); - // Base voice volume ramping - GenerateVolumeRampCommand(in_params.last_volume, in_params.volume, channel, - in_params.node_id); - in_params.last_volume = in_params.volume; - - if (in_params.mix_id != AudioCommon::NO_MIX) { - // If we're using a mix id - auto& mix_info = mix_context.GetInfo(in_params.mix_id); - const auto& dest_mix_params = mix_info.GetInParams(); - - // Voice Mixing - GenerateVoiceMixCommand( - channel_resource.GetCurrentMixVolume(), channel_resource.GetLastMixVolume(), - dsp_state, dest_mix_params.buffer_offset, dest_mix_params.buffer_count, - worker_params.mix_buffer_count + channel, in_params.node_id); - - // Update last mix volumes - channel_resource.UpdateLastMixVolumes(); - } else if (in_params.splitter_info_id != AudioCommon::NO_SPLITTER) { - s32 base = channel; - while (auto* destination_data = - GetDestinationData(in_params.splitter_info_id, base)) { - base += channel_count; - - if (!destination_data->IsConfigured()) { - continue; - } - if (destination_data->GetMixId() >= static_cast(mix_context.GetCount())) { - continue; - } - - const auto& mix_info = mix_context.GetInfo(destination_data->GetMixId()); - const auto& dest_mix_params = mix_info.GetInParams(); - GenerateVoiceMixCommand( - destination_data->CurrentMixVolumes(), destination_data->LastMixVolumes(), - dsp_state, dest_mix_params.buffer_offset, dest_mix_params.buffer_count, - worker_params.mix_buffer_count + channel, in_params.node_id); - destination_data->MarkDirty(); - } - } - // Update biquad filter enabled states - for (std::size_t i = 0; i < AudioCommon::MAX_BIQUAD_FILTERS; i++) { - in_params.was_biquad_filter_enabled[i] = in_params.biquad_filter[i].enabled; - } - } - } -} - -void CommandGenerator::GenerateSubMixCommands() { - const auto mix_count = mix_context.GetCount(); - for (std::size_t i = 0; i < mix_count; i++) { - auto& mix_info = mix_context.GetSortedInfo(i); - const auto& in_params = mix_info.GetInParams(); - if (!in_params.in_use || in_params.mix_id == AudioCommon::FINAL_MIX) { - continue; - } - GenerateSubMixCommand(mix_info); - } -} - -void CommandGenerator::GenerateFinalMixCommands() { - GenerateFinalMixCommand(); -} - -void CommandGenerator::PreCommand() { - if (!dumping_frame) { - return; - } - for (std::size_t i = 0; i < splitter_context.GetInfoCount(); i++) { - const auto& base = splitter_context.GetInfo(i); - std::string graph = fmt::format("b[{}]", i); - const auto* head = base.GetHead(); - while (head != nullptr) { - graph += fmt::format("->{}", head->GetMixId()); - head = head->GetNextDestination(); - } - LOG_DEBUG(Audio, "(DSP_TRACE) SplitterGraph splitter_info={}, {}", i, graph); - } -} - -void CommandGenerator::PostCommand() { - if (!dumping_frame) { - return; - } - dumping_frame = false; -} - -void CommandGenerator::GenerateDataSourceCommand(ServerVoiceInfo& voice_info, VoiceState& dsp_state, - s32 channel) { - const auto& in_params = voice_info.GetInParams(); - const auto depop = in_params.should_depop; - - if (depop) { - if (in_params.mix_id != AudioCommon::NO_MIX) { - auto& mix_info = mix_context.GetInfo(in_params.mix_id); - const auto& mix_in = mix_info.GetInParams(); - GenerateDepopPrepareCommand(dsp_state, mix_in.buffer_count, mix_in.buffer_offset); - } else if (in_params.splitter_info_id != AudioCommon::NO_SPLITTER) { - s32 index{}; - while (const auto* destination = - GetDestinationData(in_params.splitter_info_id, index++)) { - if (!destination->IsConfigured()) { - continue; - } - auto& mix_info = mix_context.GetInfo(destination->GetMixId()); - const auto& mix_in = mix_info.GetInParams(); - GenerateDepopPrepareCommand(dsp_state, mix_in.buffer_count, mix_in.buffer_offset); - } - } - } else { - switch (in_params.sample_format) { - case SampleFormat::Pcm8: - case SampleFormat::Pcm16: - case SampleFormat::Pcm32: - case SampleFormat::PcmFloat: - DecodeFromWaveBuffers(voice_info, GetChannelMixBuffer(channel), dsp_state, channel, - worker_params.sample_rate, worker_params.sample_count, - in_params.node_id); - break; - case SampleFormat::Adpcm: - ASSERT(channel == 0 && in_params.channel_count == 1); - DecodeFromWaveBuffers(voice_info, GetChannelMixBuffer(0), dsp_state, 0, - worker_params.sample_rate, worker_params.sample_count, - in_params.node_id); - break; - default: - ASSERT_MSG(false, "Unimplemented sample format={}", in_params.sample_format); - } - } -} - -void CommandGenerator::GenerateBiquadFilterCommandForVoice(ServerVoiceInfo& voice_info, - VoiceState& dsp_state, - [[maybe_unused]] s32 mix_buffer_count, - [[maybe_unused]] s32 channel) { - for (std::size_t i = 0; i < AudioCommon::MAX_BIQUAD_FILTERS; i++) { - const auto& in_params = voice_info.GetInParams(); - auto& biquad_filter = in_params.biquad_filter[i]; - // Check if biquad filter is actually used - if (!biquad_filter.enabled) { - continue; - } - - // Reinitialize our biquad filter state if it was enabled previously - if (!in_params.was_biquad_filter_enabled[i]) { - dsp_state.biquad_filter_state.fill(0); - } - - // Generate biquad filter - // GenerateBiquadFilterCommand(mix_buffer_count, biquad_filter, - // dsp_state.biquad_filter_state, - // mix_buffer_count + channel, mix_buffer_count + channel, - // worker_params.sample_count, voice_info.GetInParams().node_id); - } -} - -void CommandGenerator::GenerateBiquadFilterCommand([[maybe_unused]] s32 mix_buffer_id, - const BiquadFilterParameter& params, - std::array& state, - std::size_t input_offset, - std::size_t output_offset, s32 sample_count, - s32 node_id) { - if (dumping_frame) { - LOG_DEBUG(Audio, - "(DSP_TRACE) GenerateBiquadFilterCommand node_id={}, " - "input_mix_buffer={}, output_mix_buffer={}", - node_id, input_offset, output_offset); - } - std::span input = GetMixBuffer(input_offset); - std::span output = GetMixBuffer(output_offset); - - // Biquad filter parameters - const auto [n0, n1, n2] = params.numerator; - const auto [d0, d1] = params.denominator; - - // Biquad filter states - auto [s0, s1] = state; - - constexpr s64 int32_min = std::numeric_limits::min(); - constexpr s64 int32_max = std::numeric_limits::max(); - - for (int i = 0; i < sample_count; ++i) { - const auto sample = static_cast(input[i]); - const auto f = (sample * n0 + s0 + 0x4000) >> 15; - const auto y = std::clamp(f, int32_min, int32_max); - s0 = sample * n1 + y * d0 + s1; - s1 = sample * n2 + y * d1; - output[i] = static_cast(y); - } - - state = {s0, s1}; -} - -void CommandGenerator::GenerateDepopPrepareCommand(VoiceState& dsp_state, - std::size_t mix_buffer_count, - std::size_t mix_buffer_offset) { - for (std::size_t i = 0; i < mix_buffer_count; i++) { - auto& sample = dsp_state.previous_samples[i]; - if (sample != 0) { - depop_buffer[mix_buffer_offset + i] += sample; - sample = 0; - } - } -} - -void CommandGenerator::GenerateDepopForMixBuffersCommand(std::size_t mix_buffer_count, - std::size_t mix_buffer_offset, - s32 sample_rate) { - const std::size_t end_offset = - std::min(mix_buffer_offset + mix_buffer_count, GetTotalMixBufferCount()); - const s32 delta = sample_rate == 48000 ? 0x7B29 : 0x78CB; - for (std::size_t i = mix_buffer_offset; i < end_offset; i++) { - if (depop_buffer[i] == 0) { - continue; - } - - depop_buffer[i] = - ApplyMixDepop(GetMixBuffer(i), depop_buffer[i], delta, worker_params.sample_count); - } -} - -void CommandGenerator::GenerateEffectCommand(ServerMixInfo& mix_info) { - const std::size_t effect_count = effect_context.GetCount(); - const auto buffer_offset = mix_info.GetInParams().buffer_offset; - for (std::size_t i = 0; i < effect_count; i++) { - const auto index = mix_info.GetEffectOrder(i); - if (index == AudioCommon::NO_EFFECT_ORDER) { - break; - } - auto* info = effect_context.GetInfo(index); - const auto type = info->GetType(); - - // TODO(ogniK): Finish remaining effects - switch (type) { - case EffectType::Aux: - GenerateAuxCommand(buffer_offset, info, info->IsEnabled()); - break; - case EffectType::I3dl2Reverb: - GenerateI3dl2ReverbEffectCommand(buffer_offset, info, info->IsEnabled()); - break; - case EffectType::BiquadFilter: - GenerateBiquadFilterEffectCommand(buffer_offset, info, info->IsEnabled()); - break; - default: - break; - } - - info->UpdateForCommandGeneration(); - } -} - -void CommandGenerator::GenerateI3dl2ReverbEffectCommand(s32 mix_buffer_offset, EffectBase* info, - bool enabled) { - auto* reverb = dynamic_cast(info); - const auto& params = reverb->GetParams(); - auto& state = reverb->GetState(); - const auto channel_count = params.channel_count; - - if (channel_count != 1 && channel_count != 2 && channel_count != 4 && channel_count != 6) { - return; - } - - std::array, AudioCommon::MAX_CHANNEL_COUNT> input{}; - std::array, AudioCommon::MAX_CHANNEL_COUNT> output{}; - - const auto status = params.status; - for (s32 i = 0; i < channel_count; i++) { - input[i] = GetMixBuffer(mix_buffer_offset + params.input[i]); - output[i] = GetMixBuffer(mix_buffer_offset + params.output[i]); - } - - if (enabled) { - if (status == ParameterStatus::Initialized) { - InitializeI3dl2Reverb(reverb->GetParams(), state, info->GetWorkBuffer()); - } else if (status == ParameterStatus::Updating) { - UpdateI3dl2Reverb(reverb->GetParams(), state, false); - } - } - - if (enabled) { - switch (channel_count) { - case 1: - ApplyReverbGeneric<1>(state, input, output, worker_params.sample_count); - break; - case 2: - ApplyReverbGeneric<2>(state, input, output, worker_params.sample_count); - break; - case 4: - ApplyReverbGeneric<4>(state, input, output, worker_params.sample_count); - break; - case 6: - ApplyReverbGeneric<6>(state, input, output, worker_params.sample_count); - break; - } - } else { - for (s32 i = 0; i < channel_count; i++) { - // Only copy if the buffer input and output do not match! - if ((mix_buffer_offset + params.input[i]) != (mix_buffer_offset + params.output[i])) { - std::memcpy(output[i].data(), input[i].data(), - worker_params.sample_count * sizeof(s32)); - } - } - } -} - -void CommandGenerator::GenerateBiquadFilterEffectCommand(s32 mix_buffer_offset, EffectBase* info, - bool enabled) { - if (!enabled) { - return; - } - const auto& params = dynamic_cast(info)->GetParams(); - const auto channel_count = params.channel_count; - for (s32 i = 0; i < channel_count; i++) { - // TODO(ogniK): Actually implement biquad filter - if (params.input[i] != params.output[i]) { - std::span input = GetMixBuffer(mix_buffer_offset + params.input[i]); - std::span output = GetMixBuffer(mix_buffer_offset + params.output[i]); - ApplyMix<1>(output, input, 32768, worker_params.sample_count); - } - } -} - -void CommandGenerator::GenerateAuxCommand(s32 mix_buffer_offset, EffectBase* info, bool enabled) { - auto* aux = dynamic_cast(info); - const auto& params = aux->GetParams(); - if (aux->GetSendBuffer() != 0 && aux->GetRecvBuffer() != 0) { - const auto max_channels = params.count; - u32 offset{}; - for (u32 channel = 0; channel < max_channels; channel++) { - u32 write_count = 0; - if (channel == (max_channels - 1)) { - write_count = offset + worker_params.sample_count; - } - - const auto input_index = params.input_mix_buffers[channel] + mix_buffer_offset; - const auto output_index = params.output_mix_buffers[channel] + mix_buffer_offset; - - if (enabled) { - AuxInfoDSP send_info{}; - AuxInfoDSP recv_info{}; - memory.ReadBlock(aux->GetSendInfo(), &send_info, sizeof(AuxInfoDSP)); - memory.ReadBlock(aux->GetRecvInfo(), &recv_info, sizeof(AuxInfoDSP)); - - WriteAuxBuffer(send_info, aux->GetSendBuffer(), params.sample_count, - GetMixBuffer(input_index), worker_params.sample_count, offset, - write_count); - memory.WriteBlock(aux->GetSendInfo(), &send_info, sizeof(AuxInfoDSP)); - - const auto samples_read = ReadAuxBuffer( - recv_info, aux->GetRecvBuffer(), params.sample_count, - GetMixBuffer(output_index), worker_params.sample_count, offset, write_count); - memory.WriteBlock(aux->GetRecvInfo(), &recv_info, sizeof(AuxInfoDSP)); - - if (samples_read != static_cast(worker_params.sample_count) && - samples_read <= params.sample_count) { - std::memset(GetMixBuffer(output_index).data(), 0, - params.sample_count - samples_read); - } - } else { - AuxInfoDSP empty{}; - memory.WriteBlock(aux->GetSendInfo(), &empty, sizeof(AuxInfoDSP)); - memory.WriteBlock(aux->GetRecvInfo(), &empty, sizeof(AuxInfoDSP)); - if (output_index != input_index) { - std::memcpy(GetMixBuffer(output_index).data(), GetMixBuffer(input_index).data(), - worker_params.sample_count * sizeof(s32)); - } - } - - offset += worker_params.sample_count; - } - } -} - -ServerSplitterDestinationData* CommandGenerator::GetDestinationData(s32 splitter_id, s32 index) { - if (splitter_id == AudioCommon::NO_SPLITTER) { - return nullptr; - } - return splitter_context.GetDestinationData(splitter_id, index); -} - -s32 CommandGenerator::WriteAuxBuffer(AuxInfoDSP& dsp_info, VAddr send_buffer, u32 max_samples, - std::span data, u32 sample_count, u32 write_offset, - u32 write_count) { - if (max_samples == 0) { - return 0; - } - u32 offset = dsp_info.write_offset + write_offset; - if (send_buffer == 0 || offset > max_samples) { - return 0; - } - - s32 data_offset{}; - u32 remaining = sample_count; - while (remaining > 0) { - // Get position in buffer - const auto base = send_buffer + (offset * sizeof(u32)); - const auto samples_to_grab = std::min(max_samples - offset, remaining); - // Write to output - memory.WriteBlock(base, (data.data() + data_offset), samples_to_grab * sizeof(u32)); - offset = (offset + samples_to_grab) % max_samples; - remaining -= samples_to_grab; - data_offset += samples_to_grab; - } - - if (write_count != 0) { - dsp_info.write_offset = (dsp_info.write_offset + write_count) % max_samples; - } - return sample_count; -} - -s32 CommandGenerator::ReadAuxBuffer(AuxInfoDSP& recv_info, VAddr recv_buffer, u32 max_samples, - std::span out_data, u32 sample_count, u32 read_offset, - u32 read_count) { - if (max_samples == 0) { - return 0; - } - - u32 offset = recv_info.read_offset + read_offset; - if (recv_buffer == 0 || offset > max_samples) { - return 0; - } - - u32 remaining = sample_count; - s32 data_offset{}; - while (remaining > 0) { - const auto base = recv_buffer + (offset * sizeof(u32)); - const auto samples_to_grab = std::min(max_samples - offset, remaining); - std::vector buffer(samples_to_grab); - memory.ReadBlock(base, buffer.data(), buffer.size() * sizeof(u32)); - std::memcpy(out_data.data() + data_offset, buffer.data(), buffer.size() * sizeof(u32)); - offset = (offset + samples_to_grab) % max_samples; - remaining -= samples_to_grab; - data_offset += samples_to_grab; - } - - if (read_count != 0) { - recv_info.read_offset = (recv_info.read_offset + read_count) % max_samples; - } - return sample_count; -} - -void CommandGenerator::InitializeI3dl2Reverb(I3dl2ReverbParams& info, I3dl2ReverbState& state, - std::vector& work_buffer) { - // Reset state - state.lowpass_0 = 0.0f; - state.lowpass_1 = 0.0f; - state.lowpass_2 = 0.0f; - - state.early_delay_line.Reset(); - state.early_tap_steps.fill(0); - state.early_gain = 0.0f; - state.late_gain = 0.0f; - state.early_to_late_taps = 0; - for (std::size_t i = 0; i < AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT; i++) { - state.fdn_delay_line[i].Reset(); - state.decay_delay_line0[i].Reset(); - state.decay_delay_line1[i].Reset(); - } - state.last_reverb_echo = 0.0f; - state.center_delay_line.Reset(); - for (auto& coef : state.lpf_coefficients) { - coef.fill(0.0f); - } - state.shelf_filter.fill(0.0f); - state.dry_gain = 0.0f; - - const auto sample_rate = info.sample_rate / 1000; - f32* work_buffer_ptr = reinterpret_cast(work_buffer.data()); - - s32 delay_samples{}; - for (std::size_t i = 0; i < AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT; i++) { - delay_samples = - AudioCommon::CalculateDelaySamples(sample_rate, FDN_MAX_DELAY_LINE_TIMES[i]); - state.fdn_delay_line[i].Initialize(delay_samples, work_buffer_ptr); - work_buffer_ptr += delay_samples + 1; - - delay_samples = - AudioCommon::CalculateDelaySamples(sample_rate, DECAY0_MAX_DELAY_LINE_TIMES[i]); - state.decay_delay_line0[i].Initialize(delay_samples, 0.0f, work_buffer_ptr); - work_buffer_ptr += delay_samples + 1; - - delay_samples = - AudioCommon::CalculateDelaySamples(sample_rate, DECAY1_MAX_DELAY_LINE_TIMES[i]); - state.decay_delay_line1[i].Initialize(delay_samples, 0.0f, work_buffer_ptr); - work_buffer_ptr += delay_samples + 1; - } - delay_samples = AudioCommon::CalculateDelaySamples(sample_rate, 5.0f); - state.center_delay_line.Initialize(delay_samples, work_buffer_ptr); - work_buffer_ptr += delay_samples + 1; - - delay_samples = AudioCommon::CalculateDelaySamples(sample_rate, 400.0f); - state.early_delay_line.Initialize(delay_samples, work_buffer_ptr); - - UpdateI3dl2Reverb(info, state, true); -} - -void CommandGenerator::UpdateI3dl2Reverb(I3dl2ReverbParams& info, I3dl2ReverbState& state, - bool should_clear) { - - state.dry_gain = info.dry_gain; - state.shelf_filter.fill(0.0f); - state.lowpass_0 = 0.0f; - state.early_gain = Pow10(std::min(info.room + info.reflection, 5000.0f) / 2000.0f); - state.late_gain = Pow10(std::min(info.room + info.reverb, 5000.0f) / 2000.0f); - - const auto sample_rate = info.sample_rate / 1000; - const f32 hf_gain = Pow10(info.room_hf / 2000.0f); - if (hf_gain >= 1.0f) { - state.lowpass_2 = 1.0f; - state.lowpass_1 = 0.0f; - } else { - const auto a = 1.0f - hf_gain; - const auto b = 2.0f * (2.0f - hf_gain * CosD(256.0f * info.hf_reference / - static_cast(info.sample_rate))); - const auto c = std::sqrt(b * b - 4.0f * a * a); - - state.lowpass_1 = (b - c) / (2.0f * a); - state.lowpass_2 = 1.0f - state.lowpass_1; - } - state.early_to_late_taps = AudioCommon::CalculateDelaySamples( - sample_rate, 1000.0f * (info.reflection_delay + info.reverb_delay)); - - state.last_reverb_echo = 0.6f * info.diffusion * 0.01f; - for (std::size_t i = 0; i < AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT; i++) { - const auto length = - FDN_MIN_DELAY_LINE_TIMES[i] + - (info.density / 100.0f) * (FDN_MAX_DELAY_LINE_TIMES[i] - FDN_MIN_DELAY_LINE_TIMES[i]); - state.fdn_delay_line[i].SetDelay(AudioCommon::CalculateDelaySamples(sample_rate, length)); - - const auto delay_sample_counts = state.fdn_delay_line[i].GetDelay() + - state.decay_delay_line0[i].GetDelay() + - state.decay_delay_line1[i].GetDelay(); - - float a = (-60.0f * static_cast(delay_sample_counts)) / - (info.decay_time * static_cast(info.sample_rate)); - float b = a / info.hf_decay_ratio; - float c = CosD(128.0f * 0.5f * info.hf_reference / static_cast(info.sample_rate)) / - SinD(128.0f * 0.5f * info.hf_reference / static_cast(info.sample_rate)); - float d = Pow10((b - a) / 40.0f); - float e = Pow10((b + a) / 40.0f) * 0.7071f; - - state.lpf_coefficients[0][i] = e * ((d * c) + 1.0f) / (c + d); - state.lpf_coefficients[1][i] = e * (1.0f - (d * c)) / (c + d); - state.lpf_coefficients[2][i] = (c - d) / (c + d); - - state.decay_delay_line0[i].SetCoefficient(state.last_reverb_echo); - state.decay_delay_line1[i].SetCoefficient(-0.9f * state.last_reverb_echo); - } - - if (should_clear) { - for (std::size_t i = 0; i < AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT; i++) { - state.fdn_delay_line[i].Clear(); - state.decay_delay_line0[i].Clear(); - state.decay_delay_line1[i].Clear(); - } - state.early_delay_line.Clear(); - state.center_delay_line.Clear(); - } - - const auto max_early_delay = state.early_delay_line.GetMaxDelay(); - const auto reflection_time = 1000.0f * (0.9998f * info.reverb_delay + 0.02f); - for (std::size_t tap = 0; tap < AudioCommon::I3DL2REVERB_TAPS; tap++) { - const auto length = AudioCommon::CalculateDelaySamples( - sample_rate, 1000.0f * info.reflection_delay + reflection_time * EARLY_TAP_TIMES[tap]); - state.early_tap_steps[tap] = std::min(length, max_early_delay); - } -} - -void CommandGenerator::GenerateVolumeRampCommand(float last_volume, float current_volume, - s32 channel, s32 node_id) { - const auto last = static_cast(last_volume * 32768.0f); - const auto current = static_cast(current_volume * 32768.0f); - const auto delta = static_cast((static_cast(current) - static_cast(last)) / - static_cast(worker_params.sample_count)); - - if (dumping_frame) { - LOG_DEBUG(Audio, - "(DSP_TRACE) GenerateVolumeRampCommand node_id={}, input={}, output={}, " - "last_volume={}, current_volume={}", - node_id, GetMixChannelBufferOffset(channel), GetMixChannelBufferOffset(channel), - last_volume, current_volume); - } - // Apply generic gain on samples - ApplyGain(GetChannelMixBuffer(channel), GetChannelMixBuffer(channel), last, delta, - worker_params.sample_count); -} - -void CommandGenerator::GenerateVoiceMixCommand(const MixVolumeBuffer& mix_volumes, - const MixVolumeBuffer& last_mix_volumes, - VoiceState& dsp_state, s32 mix_buffer_offset, - s32 mix_buffer_count, s32 voice_index, s32 node_id) { - // Loop all our mix buffers - for (s32 i = 0; i < mix_buffer_count; i++) { - if (last_mix_volumes[i] != 0.0f || mix_volumes[i] != 0.0f) { - const auto delta = static_cast((mix_volumes[i] - last_mix_volumes[i])) / - static_cast(worker_params.sample_count); - - if (dumping_frame) { - LOG_DEBUG(Audio, - "(DSP_TRACE) GenerateVoiceMixCommand node_id={}, input={}, " - "output={}, last_volume={}, current_volume={}", - node_id, voice_index, mix_buffer_offset + i, last_mix_volumes[i], - mix_volumes[i]); - } - - dsp_state.previous_samples[i] = - ApplyMixRamp(GetMixBuffer(mix_buffer_offset + i), GetMixBuffer(voice_index), - last_mix_volumes[i], delta, worker_params.sample_count); - } else { - dsp_state.previous_samples[i] = 0; - } - } -} - -void CommandGenerator::GenerateSubMixCommand(ServerMixInfo& mix_info) { - if (dumping_frame) { - LOG_DEBUG(Audio, "(DSP_TRACE) GenerateSubMixCommand"); - } - const auto& in_params = mix_info.GetInParams(); - GenerateDepopForMixBuffersCommand(in_params.buffer_count, in_params.buffer_offset, - in_params.sample_rate); - - GenerateEffectCommand(mix_info); - - GenerateMixCommands(mix_info); -} - -void CommandGenerator::GenerateMixCommands(ServerMixInfo& mix_info) { - if (!mix_info.HasAnyConnection()) { - return; - } - const auto& in_params = mix_info.GetInParams(); - if (in_params.dest_mix_id != AudioCommon::NO_MIX) { - const auto& dest_mix = mix_context.GetInfo(in_params.dest_mix_id); - const auto& dest_in_params = dest_mix.GetInParams(); - - const auto buffer_count = in_params.buffer_count; - - for (s32 i = 0; i < buffer_count; i++) { - for (s32 j = 0; j < dest_in_params.buffer_count; j++) { - const auto mixed_volume = in_params.volume * in_params.mix_volume[i][j]; - if (mixed_volume != 0.0f) { - GenerateMixCommand(dest_in_params.buffer_offset + j, - in_params.buffer_offset + i, mixed_volume, - in_params.node_id); - } - } - } - } else if (in_params.splitter_id != AudioCommon::NO_SPLITTER) { - s32 base{}; - while (const auto* destination_data = GetDestinationData(in_params.splitter_id, base++)) { - if (!destination_data->IsConfigured()) { - continue; - } - - const auto& dest_mix = mix_context.GetInfo(destination_data->GetMixId()); - const auto& dest_in_params = dest_mix.GetInParams(); - const auto mix_index = (base - 1) % in_params.buffer_count + in_params.buffer_offset; - for (std::size_t i = 0; i < static_cast(dest_in_params.buffer_count); - i++) { - const auto mixed_volume = in_params.volume * destination_data->GetMixVolume(i); - if (mixed_volume != 0.0f) { - GenerateMixCommand(dest_in_params.buffer_offset + i, mix_index, mixed_volume, - in_params.node_id); - } - } - } - } -} - -void CommandGenerator::GenerateMixCommand(std::size_t output_offset, std::size_t input_offset, - float volume, s32 node_id) { - - if (dumping_frame) { - LOG_DEBUG(Audio, - "(DSP_TRACE) GenerateMixCommand node_id={}, input={}, output={}, volume={}", - node_id, input_offset, output_offset, volume); - } - - std::span output = GetMixBuffer(output_offset); - std::span input = GetMixBuffer(input_offset); - - const s32 gain = static_cast(volume * 32768.0f); - // Mix with loop unrolling - if (worker_params.sample_count % 4 == 0) { - ApplyMix<4>(output, input, gain, worker_params.sample_count); - } else if (worker_params.sample_count % 2 == 0) { - ApplyMix<2>(output, input, gain, worker_params.sample_count); - } else { - ApplyMix<1>(output, input, gain, worker_params.sample_count); - } -} - -void CommandGenerator::GenerateFinalMixCommand() { - if (dumping_frame) { - LOG_DEBUG(Audio, "(DSP_TRACE) GenerateFinalMixCommand"); - } - auto& mix_info = mix_context.GetFinalMixInfo(); - const auto& in_params = mix_info.GetInParams(); - - GenerateDepopForMixBuffersCommand(in_params.buffer_count, in_params.buffer_offset, - in_params.sample_rate); - - GenerateEffectCommand(mix_info); - - for (s32 i = 0; i < in_params.buffer_count; i++) { - const s32 gain = static_cast(in_params.volume * 32768.0f); - if (dumping_frame) { - LOG_DEBUG( - Audio, - "(DSP_TRACE) ApplyGainWithoutDelta node_id={}, input={}, output={}, volume={}", - in_params.node_id, in_params.buffer_offset + i, in_params.buffer_offset + i, - in_params.volume); - } - ApplyGainWithoutDelta(GetMixBuffer(in_params.buffer_offset + i), - GetMixBuffer(in_params.buffer_offset + i), gain, - worker_params.sample_count); - } -} - -template -s32 CommandGenerator::DecodePcm(ServerVoiceInfo& voice_info, VoiceState& dsp_state, - s32 sample_start_offset, s32 sample_end_offset, s32 sample_count, - s32 channel, std::size_t mix_offset) { - const auto& in_params = voice_info.GetInParams(); - const auto& wave_buffer = in_params.wave_buffer[dsp_state.wave_buffer_index]; - if (wave_buffer.buffer_address == 0) { - return 0; - } - if (wave_buffer.buffer_size == 0) { - return 0; - } - if (sample_end_offset < sample_start_offset) { - return 0; - } - const auto samples_remaining = (sample_end_offset - sample_start_offset) - dsp_state.offset; - const auto start_offset = - ((dsp_state.offset + sample_start_offset) * in_params.channel_count) * sizeof(T); - const auto buffer_pos = wave_buffer.buffer_address + start_offset; - const auto samples_processed = std::min(sample_count, samples_remaining); - - const auto channel_count = in_params.channel_count; - std::vector buffer(samples_processed * channel_count); - memory.ReadBlock(buffer_pos, buffer.data(), buffer.size() * sizeof(T)); - - if constexpr (std::is_floating_point_v) { - for (std::size_t i = 0; i < static_cast(samples_processed); i++) { - sample_buffer[mix_offset + i] = static_cast(buffer[i * channel_count + channel] * - std::numeric_limits::max()); - } - } else if constexpr (sizeof(T) == 1) { - for (std::size_t i = 0; i < static_cast(samples_processed); i++) { - sample_buffer[mix_offset + i] = - static_cast(static_cast(buffer[i * channel_count + channel] / - std::numeric_limits::max()) * - std::numeric_limits::max()); - } - } else if constexpr (sizeof(T) == 2) { - for (std::size_t i = 0; i < static_cast(samples_processed); i++) { - sample_buffer[mix_offset + i] = buffer[i * channel_count + channel]; - } - } else { - for (std::size_t i = 0; i < static_cast(samples_processed); i++) { - sample_buffer[mix_offset + i] = - static_cast(static_cast(buffer[i * channel_count + channel] / - std::numeric_limits::max()) * - std::numeric_limits::max()); - } - } - - return samples_processed; -} - -s32 CommandGenerator::DecodeAdpcm(ServerVoiceInfo& voice_info, VoiceState& dsp_state, - s32 sample_start_offset, s32 sample_end_offset, s32 sample_count, - [[maybe_unused]] s32 channel, std::size_t mix_offset) { - const auto& in_params = voice_info.GetInParams(); - const auto& wave_buffer = in_params.wave_buffer[dsp_state.wave_buffer_index]; - if (wave_buffer.buffer_address == 0) { - return 0; - } - if (wave_buffer.buffer_size == 0) { - return 0; - } - if (sample_end_offset < sample_start_offset) { - return 0; - } - - static constexpr std::array SIGNED_NIBBLES{ - 0, 1, 2, 3, 4, 5, 6, 7, -8, -7, -6, -5, -4, -3, -2, -1, - }; - - constexpr std::size_t FRAME_LEN = 8; - constexpr std::size_t NIBBLES_PER_SAMPLE = 16; - constexpr std::size_t SAMPLES_PER_FRAME = 14; - - auto frame_header = dsp_state.context.header; - s32 idx = (frame_header >> 4) & 0xf; - s32 scale = frame_header & 0xf; - s16 yn1 = dsp_state.context.yn1; - s16 yn2 = dsp_state.context.yn2; - - Codec::ADPCM_Coeff coeffs; - memory.ReadBlock(in_params.additional_params_address, coeffs.data(), - sizeof(Codec::ADPCM_Coeff)); - - s32 coef1 = coeffs[idx * 2]; - s32 coef2 = coeffs[idx * 2 + 1]; - - const auto samples_remaining = (sample_end_offset - sample_start_offset) - dsp_state.offset; - const auto samples_processed = std::min(sample_count, samples_remaining); - const auto sample_pos = dsp_state.offset + sample_start_offset; - - const auto samples_remaining_in_frame = sample_pos % SAMPLES_PER_FRAME; - auto position_in_frame = ((sample_pos / SAMPLES_PER_FRAME) * NIBBLES_PER_SAMPLE) + - samples_remaining_in_frame + (samples_remaining_in_frame != 0 ? 2 : 0); - - const auto decode_sample = [&](const int nibble) -> s16 { - const int xn = nibble * (1 << scale); - // We first transform everything into 11 bit fixed point, perform the second order - // digital filter, then transform back. - // 0x400 == 0.5 in 11 bit fixed point. - // Filter: y[n] = x[n] + 0.5 + c1 * y[n-1] + c2 * y[n-2] - int val = ((xn << 11) + 0x400 + coef1 * yn1 + coef2 * yn2) >> 11; - // Clamp to output range. - val = std::clamp(val, -32768, 32767); - // Advance output feedback. - yn2 = yn1; - yn1 = static_cast(val); - return yn1; - }; - - std::size_t buffer_offset{}; - std::vector buffer( - std::max((samples_processed / FRAME_LEN) * SAMPLES_PER_FRAME, FRAME_LEN)); - memory.ReadBlock(wave_buffer.buffer_address + (position_in_frame / 2), buffer.data(), - buffer.size()); - std::size_t cur_mix_offset = mix_offset; - - auto remaining_samples = samples_processed; - while (remaining_samples > 0) { - if (position_in_frame % NIBBLES_PER_SAMPLE == 0) { - // Read header - frame_header = buffer[buffer_offset++]; - idx = (frame_header >> 4) & 0xf; - scale = frame_header & 0xf; - coef1 = coeffs[idx * 2]; - coef2 = coeffs[idx * 2 + 1]; - position_in_frame += 2; - - // Decode entire frame - if (remaining_samples >= static_cast(SAMPLES_PER_FRAME)) { - for (std::size_t i = 0; i < SAMPLES_PER_FRAME / 2; i++) { - // Sample 1 - const s32 s0 = SIGNED_NIBBLES[buffer[buffer_offset] >> 4]; - const s32 s1 = SIGNED_NIBBLES[buffer[buffer_offset++] & 0xf]; - const s16 sample_1 = decode_sample(s0); - const s16 sample_2 = decode_sample(s1); - sample_buffer[cur_mix_offset++] = sample_1; - sample_buffer[cur_mix_offset++] = sample_2; - } - remaining_samples -= static_cast(SAMPLES_PER_FRAME); - position_in_frame += SAMPLES_PER_FRAME; - continue; - } - } - // Decode mid frame - s32 current_nibble = buffer[buffer_offset]; - if (position_in_frame++ & 0x1) { - current_nibble &= 0xf; - buffer_offset++; - } else { - current_nibble >>= 4; - } - const s16 sample = decode_sample(SIGNED_NIBBLES[current_nibble]); - sample_buffer[cur_mix_offset++] = sample; - remaining_samples--; - } - - dsp_state.context.header = frame_header; - dsp_state.context.yn1 = yn1; - dsp_state.context.yn2 = yn2; - - return samples_processed; -} - -std::span CommandGenerator::GetMixBuffer(std::size_t index) { - return std::span(mix_buffer.data() + (index * worker_params.sample_count), - worker_params.sample_count); -} - -std::span CommandGenerator::GetMixBuffer(std::size_t index) const { - return std::span(mix_buffer.data() + (index * worker_params.sample_count), - worker_params.sample_count); -} - -std::size_t CommandGenerator::GetMixChannelBufferOffset(s32 channel) const { - return worker_params.mix_buffer_count + channel; -} - -std::size_t CommandGenerator::GetTotalMixBufferCount() const { - return worker_params.mix_buffer_count + AudioCommon::MAX_CHANNEL_COUNT; -} - -std::span CommandGenerator::GetChannelMixBuffer(s32 channel) { - return GetMixBuffer(worker_params.mix_buffer_count + channel); -} - -std::span CommandGenerator::GetChannelMixBuffer(s32 channel) const { - return GetMixBuffer(worker_params.mix_buffer_count + channel); -} - -void CommandGenerator::DecodeFromWaveBuffers(ServerVoiceInfo& voice_info, std::span output, - VoiceState& dsp_state, s32 channel, - s32 target_sample_rate, s32 sample_count, - s32 node_id) { - const auto& in_params = voice_info.GetInParams(); - if (dumping_frame) { - LOG_DEBUG(Audio, - "(DSP_TRACE) DecodeFromWaveBuffers, node_id={}, channel={}, " - "format={}, sample_count={}, sample_rate={}, mix_id={}, splitter_id={}", - node_id, channel, in_params.sample_format, sample_count, in_params.sample_rate, - in_params.mix_id, in_params.splitter_info_id); - } - ASSERT_OR_EXECUTE(output.data() != nullptr, { return; }); - - const auto resample_rate = static_cast( - static_cast(in_params.sample_rate) / static_cast(target_sample_rate) * - static_cast(static_cast(in_params.pitch * 32768.0f))); - if (dsp_state.fraction + sample_count * resample_rate > - static_cast(SCALED_MIX_BUFFER_SIZE - 4ULL)) { - return; - } - - auto min_required_samples = - std::min(static_cast(SCALED_MIX_BUFFER_SIZE) - dsp_state.fraction, resample_rate); - if (min_required_samples >= sample_count) { - min_required_samples = sample_count; - } - - std::size_t temp_mix_offset{}; - s32 samples_output{}; - auto samples_remaining = sample_count; - while (samples_remaining > 0) { - const auto samples_to_output = std::min(samples_remaining, min_required_samples); - const auto samples_to_read = (samples_to_output * resample_rate + dsp_state.fraction) >> 15; - - if (!in_params.behavior_flags.is_pitch_and_src_skipped) { - // Append sample histtory for resampler - for (std::size_t i = 0; i < AudioCommon::MAX_SAMPLE_HISTORY; i++) { - sample_buffer[temp_mix_offset + i] = dsp_state.sample_history[i]; - } - temp_mix_offset += 4; - } - - s32 samples_read{}; - while (samples_read < samples_to_read) { - const auto& wave_buffer = in_params.wave_buffer[dsp_state.wave_buffer_index]; - // No more data can be read - if (!dsp_state.is_wave_buffer_valid[dsp_state.wave_buffer_index]) { - break; - } - - if (in_params.sample_format == SampleFormat::Adpcm && dsp_state.offset == 0 && - wave_buffer.context_address != 0 && wave_buffer.context_size != 0) { - memory.ReadBlock(wave_buffer.context_address, &dsp_state.context, - sizeof(ADPCMContext)); - } - - s32 samples_offset_start; - s32 samples_offset_end; - if (dsp_state.loop_count > 0 && wave_buffer.loop_start_sample != 0 && - wave_buffer.loop_end_sample != 0 && - wave_buffer.loop_start_sample <= wave_buffer.loop_end_sample) { - samples_offset_start = wave_buffer.loop_start_sample; - samples_offset_end = wave_buffer.loop_end_sample; - } else { - samples_offset_start = wave_buffer.start_sample_offset; - samples_offset_end = wave_buffer.end_sample_offset; - } - - s32 samples_decoded{0}; - switch (in_params.sample_format) { - case SampleFormat::Pcm8: - samples_decoded = - DecodePcm(voice_info, dsp_state, samples_offset_start, samples_offset_end, - samples_to_read - samples_read, channel, temp_mix_offset); - break; - case SampleFormat::Pcm16: - samples_decoded = - DecodePcm(voice_info, dsp_state, samples_offset_start, samples_offset_end, - samples_to_read - samples_read, channel, temp_mix_offset); - break; - case SampleFormat::Pcm32: - samples_decoded = - DecodePcm(voice_info, dsp_state, samples_offset_start, samples_offset_end, - samples_to_read - samples_read, channel, temp_mix_offset); - break; - case SampleFormat::PcmFloat: - samples_decoded = - DecodePcm(voice_info, dsp_state, samples_offset_start, samples_offset_end, - samples_to_read - samples_read, channel, temp_mix_offset); - break; - case SampleFormat::Adpcm: - samples_decoded = - DecodeAdpcm(voice_info, dsp_state, samples_offset_start, samples_offset_end, - samples_to_read - samples_read, channel, temp_mix_offset); - break; - default: - ASSERT_MSG(false, "Unimplemented sample format={}", in_params.sample_format); - } - - temp_mix_offset += samples_decoded; - samples_read += samples_decoded; - dsp_state.offset += samples_decoded; - dsp_state.played_sample_count += samples_decoded; - - if (dsp_state.offset >= (samples_offset_end - samples_offset_start) || - samples_decoded == 0) { - // Reset our sample offset - dsp_state.offset = 0; - if (wave_buffer.is_looping) { - dsp_state.loop_count++; - if (wave_buffer.loop_count > 0 && - (dsp_state.loop_count > wave_buffer.loop_count || samples_decoded == 0)) { - // End of our buffer - voice_info.SetWaveBufferCompleted(dsp_state, wave_buffer); - } - - if (samples_decoded == 0) { - break; - } - - if (in_params.behavior_flags.is_played_samples_reset_at_loop_point.Value()) { - dsp_state.played_sample_count = 0; - } - } else { - // Update our wave buffer states - voice_info.SetWaveBufferCompleted(dsp_state, wave_buffer); - } - } - } - - if (in_params.behavior_flags.is_pitch_and_src_skipped.Value()) { - // No need to resample - std::memcpy(output.data() + samples_output, sample_buffer.data(), - samples_read * sizeof(s32)); - } else { - std::fill(sample_buffer.begin() + temp_mix_offset, - sample_buffer.begin() + temp_mix_offset + (samples_to_read - samples_read), - 0); - AudioCore::Resample(output.data() + samples_output, sample_buffer.data(), resample_rate, - dsp_state.fraction, samples_to_output); - // Resample - for (std::size_t i = 0; i < AudioCommon::MAX_SAMPLE_HISTORY; i++) { - dsp_state.sample_history[i] = sample_buffer[samples_to_read + i]; - } - } - samples_remaining -= samples_to_output; - samples_output += samples_to_output; - } -} - -} // namespace AudioCore diff --git a/src/audio_core/command_generator.h b/src/audio_core/command_generator.h deleted file mode 100644 index 8077e77682..0000000000 --- a/src/audio_core/command_generator.h +++ /dev/null @@ -1,110 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include "audio_core/common.h" -#include "audio_core/voice_context.h" -#include "common/common_types.h" - -namespace Core::Memory { -class Memory; -} - -namespace AudioCore { -class MixContext; -class SplitterContext; -class ServerSplitterDestinationData; -class ServerMixInfo; -class EffectContext; -class EffectBase; -struct AuxInfoDSP; -struct I3dl2ReverbParams; -struct I3dl2ReverbState; -using MixVolumeBuffer = std::array; - -class CommandGenerator { -public: - explicit CommandGenerator(AudioCommon::AudioRendererParameter& worker_params_, - VoiceContext& voice_context_, MixContext& mix_context_, - SplitterContext& splitter_context_, EffectContext& effect_context_, - Core::Memory::Memory& memory_); - ~CommandGenerator(); - - void ClearMixBuffers(); - void GenerateVoiceCommands(); - void GenerateVoiceCommand(ServerVoiceInfo& voice_info); - void GenerateSubMixCommands(); - void GenerateFinalMixCommands(); - void PreCommand(); - void PostCommand(); - - [[nodiscard]] std::span GetChannelMixBuffer(s32 channel); - [[nodiscard]] std::span GetChannelMixBuffer(s32 channel) const; - [[nodiscard]] std::span GetMixBuffer(std::size_t index); - [[nodiscard]] std::span GetMixBuffer(std::size_t index) const; - [[nodiscard]] std::size_t GetMixChannelBufferOffset(s32 channel) const; - - [[nodiscard]] std::size_t GetTotalMixBufferCount() const; - -private: - void GenerateDataSourceCommand(ServerVoiceInfo& voice_info, VoiceState& dsp_state, s32 channel); - void GenerateBiquadFilterCommandForVoice(ServerVoiceInfo& voice_info, VoiceState& dsp_state, - s32 mix_buffer_count, s32 channel); - void GenerateVolumeRampCommand(float last_volume, float current_volume, s32 channel, - s32 node_id); - void GenerateVoiceMixCommand(const MixVolumeBuffer& mix_volumes, - const MixVolumeBuffer& last_mix_volumes, VoiceState& dsp_state, - s32 mix_buffer_offset, s32 mix_buffer_count, s32 voice_index, - s32 node_id); - void GenerateSubMixCommand(ServerMixInfo& mix_info); - void GenerateMixCommands(ServerMixInfo& mix_info); - void GenerateMixCommand(std::size_t output_offset, std::size_t input_offset, float volume, - s32 node_id); - void GenerateFinalMixCommand(); - void GenerateBiquadFilterCommand(s32 mix_buffer, const BiquadFilterParameter& params, - std::array& state, std::size_t input_offset, - std::size_t output_offset, s32 sample_count, s32 node_id); - void GenerateDepopPrepareCommand(VoiceState& dsp_state, std::size_t mix_buffer_count, - std::size_t mix_buffer_offset); - void GenerateDepopForMixBuffersCommand(std::size_t mix_buffer_count, - std::size_t mix_buffer_offset, s32 sample_rate); - void GenerateEffectCommand(ServerMixInfo& mix_info); - void GenerateI3dl2ReverbEffectCommand(s32 mix_buffer_offset, EffectBase* info, bool enabled); - void GenerateBiquadFilterEffectCommand(s32 mix_buffer_offset, EffectBase* info, bool enabled); - void GenerateAuxCommand(s32 mix_buffer_offset, EffectBase* info, bool enabled); - [[nodiscard]] ServerSplitterDestinationData* GetDestinationData(s32 splitter_id, s32 index); - - s32 WriteAuxBuffer(AuxInfoDSP& dsp_info, VAddr send_buffer, u32 max_samples, - std::span data, u32 sample_count, u32 write_offset, - u32 write_count); - s32 ReadAuxBuffer(AuxInfoDSP& recv_info, VAddr recv_buffer, u32 max_samples, - std::span out_data, u32 sample_count, u32 read_offset, u32 read_count); - - void InitializeI3dl2Reverb(I3dl2ReverbParams& info, I3dl2ReverbState& state, - std::vector& work_buffer); - void UpdateI3dl2Reverb(I3dl2ReverbParams& info, I3dl2ReverbState& state, bool should_clear); - // DSP Code - template - s32 DecodePcm(ServerVoiceInfo& voice_info, VoiceState& dsp_state, s32 sample_start_offset, - s32 sample_end_offset, s32 sample_count, s32 channel, std::size_t mix_offset); - s32 DecodeAdpcm(ServerVoiceInfo& voice_info, VoiceState& dsp_state, s32 sample_start_offset, - s32 sample_end_offset, s32 sample_count, s32 channel, std::size_t mix_offset); - void DecodeFromWaveBuffers(ServerVoiceInfo& voice_info, std::span output, - VoiceState& dsp_state, s32 channel, s32 target_sample_rate, - s32 sample_count, s32 node_id); - - AudioCommon::AudioRendererParameter& worker_params; - VoiceContext& voice_context; - MixContext& mix_context; - SplitterContext& splitter_context; - EffectContext& effect_context; - Core::Memory::Memory& memory; - std::vector mix_buffer{}; - std::vector sample_buffer{}; - std::vector depop_buffer{}; - bool dumping_frame{false}; -}; -} // namespace AudioCore diff --git a/src/audio_core/common.h b/src/audio_core/common.h deleted file mode 100644 index 056a0ac707..0000000000 --- a/src/audio_core/common.h +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include "common/common_funcs.h" -#include "common/common_types.h" -#include "common/swap.h" -#include "core/hle/result.h" - -namespace AudioCommon { -namespace Audren { -constexpr Result ERR_INVALID_PARAMETERS{ErrorModule::Audio, 41}; -constexpr Result ERR_SPLITTER_SORT_FAILED{ErrorModule::Audio, 43}; -} // namespace Audren - -constexpr u8 BASE_REVISION = '0'; -constexpr u32_le CURRENT_PROCESS_REVISION = - Common::MakeMagic('R', 'E', 'V', static_cast(BASE_REVISION + 0xA)); -constexpr std::size_t MAX_MIX_BUFFERS = 24; -constexpr std::size_t MAX_BIQUAD_FILTERS = 2; -constexpr std::size_t MAX_CHANNEL_COUNT = 6; -constexpr std::size_t MAX_WAVE_BUFFERS = 4; -constexpr std::size_t MAX_SAMPLE_HISTORY = 4; -constexpr u32 STREAM_SAMPLE_RATE = 48000; -constexpr u32 STREAM_NUM_CHANNELS = 2; -constexpr s32 NO_SPLITTER = -1; -constexpr s32 NO_MIX = 0x7fffffff; -constexpr s32 NO_FINAL_MIX = std::numeric_limits::min(); -constexpr s32 FINAL_MIX = 0; -constexpr s32 NO_EFFECT_ORDER = -1; -constexpr std::size_t TEMP_MIX_BASE_SIZE = 0x3f00; // TODO(ogniK): Work out this constant -// Any size checks seem to take the sample history into account -// and our const ends up being 0x3f04, the 4 bytes are most -// likely the sample history -constexpr std::size_t TOTAL_TEMP_MIX_SIZE = TEMP_MIX_BASE_SIZE + AudioCommon::MAX_SAMPLE_HISTORY; -constexpr f32 I3DL2REVERB_MAX_LEVEL = 5000.0f; -constexpr f32 I3DL2REVERB_MIN_REFLECTION_DURATION = 0.02f; -constexpr std::size_t I3DL2REVERB_TAPS = 20; -constexpr std::size_t I3DL2REVERB_DELAY_LINE_COUNT = 4; -using Fractional = s32; - -template -constexpr Fractional ToFractional(T x) { - return static_cast(x * static_cast(0x4000)); -} - -constexpr Fractional MultiplyFractional(Fractional lhs, Fractional rhs) { - return static_cast(static_cast(lhs) * rhs >> 14); -} - -constexpr s32 FractionalToFixed(Fractional x) { - const auto s = x & (1 << 13); - return static_cast(x >> 14) + s; -} - -constexpr s32 CalculateDelaySamples(s32 sample_rate_khz, float time) { - return FractionalToFixed(MultiplyFractional(ToFractional(sample_rate_khz), ToFractional(time))); -} - -static constexpr u32 VersionFromRevision(u32_le rev) { - // "REV7" -> 7 - return ((rev >> 24) & 0xff) - 0x30; -} - -static constexpr bool IsRevisionSupported(u32 required, u32_le user_revision) { - const auto base = VersionFromRevision(user_revision); - return required <= base; -} - -static constexpr bool IsValidRevision(u32_le revision) { - const auto base = VersionFromRevision(revision); - constexpr auto max_rev = VersionFromRevision(CURRENT_PROCESS_REVISION); - return base <= max_rev; -} - -static constexpr bool CanConsumeBuffer(std::size_t size, std::size_t offset, std::size_t required) { - if (offset > size) { - return false; - } - if (size < required) { - return false; - } - if ((size - offset) < required) { - return false; - } - return true; -} - -struct UpdateDataSizes { - u32_le behavior{}; - u32_le memory_pool{}; - u32_le voice{}; - u32_le voice_channel_resource{}; - u32_le effect{}; - u32_le mixer{}; - u32_le sink{}; - u32_le performance{}; - u32_le splitter{}; - u32_le render_info{}; - INSERT_PADDING_WORDS(4); -}; -static_assert(sizeof(UpdateDataSizes) == 0x38, "UpdateDataSizes is an invalid size"); - -struct UpdateDataHeader { - u32_le revision{}; - UpdateDataSizes size{}; - u32_le total_size{}; -}; -static_assert(sizeof(UpdateDataHeader) == 0x40, "UpdateDataHeader is an invalid size"); - -struct AudioRendererParameter { - u32_le sample_rate; - u32_le sample_count; - u32_le mix_buffer_count; - u32_le submix_count; - u32_le voice_count; - u32_le sink_count; - u32_le effect_count; - u32_le performance_frame_count; - u8 is_voice_drop_enabled; - u8 unknown_21; - u8 unknown_22; - u8 execution_mode; - u32_le splitter_count; - u32_le num_splitter_send_channels; - u32_le unknown_30; - u32_le revision; -}; -static_assert(sizeof(AudioRendererParameter) == 52, "AudioRendererParameter is an invalid size"); - -} // namespace AudioCommon diff --git a/src/audio_core/common/audio_renderer_parameter.h b/src/audio_core/common/audio_renderer_parameter.h new file mode 100644 index 0000000000..2f62c383b8 --- /dev/null +++ b/src/audio_core/common/audio_renderer_parameter.h @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/memory/memory_pool_info.h" +#include "audio_core/renderer/upsampler/upsampler_manager.h" +#include "common/common_types.h" + +namespace AudioCore { +/** + * Execution mode of the audio renderer. + * Only Auto is currently supported. + */ +enum class ExecutionMode : u8 { + Auto, + Manual, +}; + +/** + * Parameters from the game, passed to the audio renderer for initialisation. + */ +struct AudioRendererParameterInternal { + /* 0x00 */ u32 sample_rate; + /* 0x04 */ u32 sample_count; + /* 0x08 */ u32 mixes; + /* 0x0C */ u32 sub_mixes; + /* 0x10 */ u32 voices; + /* 0x14 */ u32 sinks; + /* 0x18 */ u32 effects; + /* 0x1C */ u32 perf_frames; + /* 0x20 */ u16 voice_drop_enabled; + /* 0x22 */ u8 rendering_device; + /* 0x23 */ ExecutionMode execution_mode; + /* 0x24 */ u32 splitter_infos; + /* 0x28 */ s32 splitter_destinations; + /* 0x2C */ u32 external_context_size; + /* 0x30 */ u32 revision; + /* 0x34 */ char unk34[0x4]; +}; +static_assert(sizeof(AudioRendererParameterInternal) == 0x38, + "AudioRendererParameterInternal has the wrong size!"); + +/** + * Context for rendering, contains a bunch of useful fields for the command generator. + */ +struct AudioRendererSystemContext { + s32 session_id; + s8 channels; + s16 mix_buffer_count; + AudioRenderer::BehaviorInfo* behavior; + std::span depop_buffer; + AudioRenderer::UpsamplerManager* upsampler_manager; + AudioRenderer::MemoryPoolInfo* memory_pool_info; +}; + +} // namespace AudioCore diff --git a/src/audio_core/common/common.h b/src/audio_core/common/common.h new file mode 100644 index 0000000000..6abd9be45e --- /dev/null +++ b/src/audio_core/common/common.h @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "common/assert.h" +#include "common/common_funcs.h" +#include "common/common_types.h" + +namespace AudioCore { +using CpuAddr = std::uintptr_t; + +enum class PlayState : u8 { + Started, + Stopped, + Paused, +}; + +enum class SrcQuality : u8 { + Medium, + High, + Low, +}; + +enum class SampleFormat : u8 { + Invalid, + PcmInt8, + PcmInt16, + PcmInt24, + PcmInt32, + PcmFloat, + Adpcm, +}; + +enum class SessionTypes { + AudioIn, + AudioOut, + FinalOutputRecorder, +}; + +enum class Channels : u32 { + FrontLeft, + FrontRight, + Center, + LFE, + BackLeft, + BackRight, +}; + +// These are used by Delay, Reverb and I3dl2Reverb prior to Revision 11. +enum class OldChannels : u32 { + FrontLeft, + FrontRight, + BackLeft, + BackRight, + Center, + LFE, +}; + +constexpr u32 BufferCount = 32; + +constexpr u32 MaxRendererSessions = 2; +constexpr u32 TargetSampleCount = 240; +constexpr u32 TargetSampleRate = 48'000; +constexpr u32 MaxChannels = 6; +constexpr u32 MaxMixBuffers = 24; +constexpr u32 MaxWaveBuffers = 4; +constexpr s32 LowestVoicePriority = 0xFF; +constexpr s32 HighestVoicePriority = 0; +constexpr u32 BufferAlignment = 0x40; +constexpr u32 WorkbufferAlignment = 0x1000; +constexpr s32 FinalMixId = 0; +constexpr s32 InvalidDistanceFromFinalMix = std::numeric_limits::min(); +constexpr s32 UnusedSplitterId = -1; +constexpr s32 UnusedMixId = std::numeric_limits::max(); +constexpr u32 InvalidNodeId = 0xF0000000; +constexpr s32 InvalidProcessOrder = -1; +constexpr u32 MaxBiquadFilters = 2; +constexpr u32 MaxEffects = 256; + +constexpr bool IsChannelCountValid(u16 channel_count) { + return channel_count <= 6 && + (channel_count == 1 || channel_count == 2 || channel_count == 4 || channel_count == 6); +} + +constexpr void UseOldChannelMapping(std::span inputs, std::span outputs) { + constexpr auto old_center{static_cast(OldChannels::Center)}; + constexpr auto new_center{static_cast(Channels::Center)}; + constexpr auto old_lfe{static_cast(OldChannels::LFE)}; + constexpr auto new_lfe{static_cast(Channels::LFE)}; + + auto center{inputs[old_center]}; + auto lfe{inputs[old_lfe]}; + inputs[old_center] = inputs[new_center]; + inputs[old_lfe] = inputs[new_lfe]; + inputs[new_center] = center; + inputs[new_lfe] = lfe; + + center = outputs[old_center]; + lfe = outputs[old_lfe]; + outputs[old_center] = outputs[new_center]; + outputs[old_lfe] = outputs[new_lfe]; + outputs[new_center] = center; + outputs[new_lfe] = lfe; +} + +constexpr u32 GetSplitterInParamHeaderMagic() { + return Common::MakeMagic('S', 'N', 'D', 'H'); +} + +constexpr u32 GetSplitterInfoMagic() { + return Common::MakeMagic('S', 'N', 'D', 'I'); +} + +constexpr u32 GetSplitterSendDataMagic() { + return Common::MakeMagic('S', 'N', 'D', 'D'); +} + +constexpr size_t GetSampleFormatByteSize(SampleFormat format) { + switch (format) { + case SampleFormat::PcmInt8: + return 1; + case SampleFormat::PcmInt16: + return 2; + case SampleFormat::PcmInt24: + return 3; + case SampleFormat::PcmInt32: + case SampleFormat::PcmFloat: + return 4; + default: + return 2; + } +} + +} // namespace AudioCore diff --git a/src/audio_core/common/feature_support.h b/src/audio_core/common/feature_support.h new file mode 100644 index 0000000000..55c9e690df --- /dev/null +++ b/src/audio_core/common/feature_support.h @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "common/assert.h" +#include "common/common_funcs.h" +#include "common/common_types.h" + +namespace AudioCore { +constexpr u32 CurrentRevision = 11; + +enum class SupportTags { + CommandProcessingTimeEstimatorVersion4, + CommandProcessingTimeEstimatorVersion3, + CommandProcessingTimeEstimatorVersion2, + MultiTapBiquadFilterProcessing, + EffectInfoVer2, + WaveBufferVer2, + BiquadFilterFloatProcessing, + VolumeMixParameterPrecisionQ23, + MixInParameterDirtyOnlyUpdate, + BiquadFilterEffectStateClearBugFix, + VoicePlayedSampleCountResetAtLoopPoint, + VoicePitchAndSrcSkipped, + SplitterBugFix, + FlushVoiceWaveBuffers, + ElapsedFrameCount, + AudioRendererVariadicCommandBufferSize, + PerformanceMetricsDataFormatVersion2, + AudioRendererProcessingTimeLimit80Percent, + AudioRendererProcessingTimeLimit75Percent, + AudioRendererProcessingTimeLimit70Percent, + AdpcmLoopContextBugFix, + Splitter, + LongSizePreDelay, + AudioUsbDeviceOutput, + DeviceApiVersion2, + DelayChannelMappingChange, + ReverbChannelMappingChange, + I3dl2ReverbChannelMappingChange, + + // Not a real tag, just here to get the count. + Size +}; + +constexpr u32 GetRevisionNum(u32 user_revision) { + if (user_revision >= 0x100) { + user_revision -= Common::MakeMagic('R', 'E', 'V', '0'); + user_revision >>= 24; + } + return user_revision; +}; + +constexpr bool CheckFeatureSupported(SupportTags tag, u32 user_revision) { + constexpr std::array, static_cast(SupportTags::Size)> features{ + { + {SupportTags::AudioRendererProcessingTimeLimit70Percent, 1}, + {SupportTags::Splitter, 2}, + {SupportTags::AdpcmLoopContextBugFix, 2}, + {SupportTags::LongSizePreDelay, 3}, + {SupportTags::AudioUsbDeviceOutput, 4}, + {SupportTags::AudioRendererProcessingTimeLimit75Percent, 4}, + {SupportTags::VoicePlayedSampleCountResetAtLoopPoint, 5}, + {SupportTags::VoicePitchAndSrcSkipped, 5}, + {SupportTags::SplitterBugFix, 5}, + {SupportTags::FlushVoiceWaveBuffers, 5}, + {SupportTags::ElapsedFrameCount, 5}, + {SupportTags::AudioRendererProcessingTimeLimit80Percent, 5}, + {SupportTags::AudioRendererVariadicCommandBufferSize, 5}, + {SupportTags::PerformanceMetricsDataFormatVersion2, 5}, + {SupportTags::CommandProcessingTimeEstimatorVersion2, 5}, + {SupportTags::BiquadFilterEffectStateClearBugFix, 6}, + {SupportTags::BiquadFilterFloatProcessing, 7}, + {SupportTags::VolumeMixParameterPrecisionQ23, 7}, + {SupportTags::MixInParameterDirtyOnlyUpdate, 7}, + {SupportTags::WaveBufferVer2, 8}, + {SupportTags::CommandProcessingTimeEstimatorVersion3, 8}, + {SupportTags::EffectInfoVer2, 9}, + {SupportTags::CommandProcessingTimeEstimatorVersion4, 10}, + {SupportTags::MultiTapBiquadFilterProcessing, 10}, + {SupportTags::DelayChannelMappingChange, 11}, + {SupportTags::ReverbChannelMappingChange, 11}, + {SupportTags::I3dl2ReverbChannelMappingChange, 11}, + }}; + + const auto& feature = + std::ranges::find_if(features, [tag](const auto& entry) { return entry.first == tag; }); + if (feature == features.cend()) { + LOG_ERROR(Service_Audio, "Invalid SupportTag {}!", static_cast(tag)); + return false; + } + user_revision = GetRevisionNum(user_revision); + return (*feature).second <= user_revision; +} + +constexpr bool CheckValidRevision(u32 user_revision) { + return GetRevisionNum(user_revision) <= CurrentRevision; +}; + +} // namespace AudioCore diff --git a/src/audio_core/common/wave_buffer.h b/src/audio_core/common/wave_buffer.h new file mode 100644 index 0000000000..fc478ef798 --- /dev/null +++ b/src/audio_core/common/wave_buffer.h @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/common_types.h" + +namespace AudioCore { + +struct WaveBufferVersion1 { + CpuAddr buffer; + u64 buffer_size; + u32 start_offset; + u32 end_offset; + bool loop; + bool stream_ended; + CpuAddr context; + u64 context_size; +}; + +struct WaveBufferVersion2 { + CpuAddr buffer; + CpuAddr context; + u64 buffer_size; + u64 context_size; + u32 start_offset; + u32 end_offset; + u32 loop_start_offset; + u32 loop_end_offset; + s32 loop_count; + bool loop; + bool stream_ended; +}; + +} // namespace AudioCore diff --git a/src/audio_core/common/workbuffer_allocator.h b/src/audio_core/common/workbuffer_allocator.h new file mode 100644 index 0000000000..fb89f97fea --- /dev/null +++ b/src/audio_core/common/workbuffer_allocator.h @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/alignment.h" +#include "common/assert.h" +#include "common/common_types.h" + +namespace AudioCore { +/** + * Responsible for allocating up a workbuffer into multiple pieces. + * Takes in a buffer and size (it does not own them), and allocates up the buffer via Allocate. + */ +class WorkbufferAllocator { +public: + explicit WorkbufferAllocator(std::span buffer_, u64 size_) + : buffer{reinterpret_cast(buffer_.data())}, size{size_} {} + + /** + * Allocate the given count of T elements, aligned to alignment. + * + * @param count - The number of elements to allocate. + * @param alignment - The required starting alignment. + * @return Non-owning container of allocated elements. + */ + template + std::span Allocate(u64 count, u64 alignment) { + u64 out{0}; + u64 byte_size{count * sizeof(T)}; + + if (byte_size > 0) { + auto current{buffer + offset}; + auto aligned_buffer{Common::AlignUp(current, alignment)}; + if (aligned_buffer + byte_size <= buffer + size) { + out = aligned_buffer; + offset = byte_size - buffer + aligned_buffer; + } else { + LOG_ERROR( + Service_Audio, + "Allocated buffer was too small to hold new alloc.\nAllocator size={:08X}, " + "offset={:08X}.\nAttempting to allocate {:08X} with alignment={:02X}", + size, offset, byte_size, alignment); + count = 0; + } + } + + return std::span(reinterpret_cast(out), count); + } + + /** + * Align the current offset to the given alignment. + * + * @param alignment - The required starting alignment. + */ + void Align(u64 alignment) { + auto current{buffer + offset}; + auto aligned_buffer{Common::AlignUp(current, alignment)}; + offset = 0 - buffer + aligned_buffer; + } + + /** + * Get the current buffer offset. + * + * @return The current allocating offset. + */ + u64 GetCurrentOffset() const { + return offset; + } + + /** + * Get the current buffer size. + * + * @return The size of the current buffer. + */ + u64 GetSize() const { + return size; + } + + /** + * Get the remaining size that can be allocated. + * + * @return The remaining size left in the buffer. + */ + u64 GetRemainingSize() const { + return size - offset; + } + +private: + /// The buffer into which we are allocating. + u64 buffer; + /// Size of the buffer we're allocating to. + u64 size; + /// Current offset into the buffer, an error will be thrown if it exceeds size. + u64 offset{}; +}; + +} // namespace AudioCore diff --git a/src/audio_core/cubeb_sink.cpp b/src/audio_core/cubeb_sink.cpp deleted file mode 100644 index e48c1ee8eb..0000000000 --- a/src/audio_core/cubeb_sink.cpp +++ /dev/null @@ -1,249 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include -#include -#include -#include "audio_core/cubeb_sink.h" -#include "audio_core/stream.h" -#include "common/assert.h" -#include "common/logging/log.h" -#include "common/ring_buffer.h" -#include "common/settings.h" - -#ifdef _WIN32 -#include -#endif - -namespace AudioCore { - -class CubebSinkStream final : public SinkStream { -public: - CubebSinkStream(cubeb* ctx_, u32 sample_rate, u32 num_channels_, cubeb_devid output_device, - const std::string& name) - : ctx{ctx_}, num_channels{std::min(num_channels_, 6u)} { - - cubeb_stream_params params{}; - params.rate = sample_rate; - params.channels = num_channels; - params.format = CUBEB_SAMPLE_S16NE; - params.prefs = CUBEB_STREAM_PREF_PERSIST; - switch (num_channels) { - case 1: - params.layout = CUBEB_LAYOUT_MONO; - break; - case 2: - params.layout = CUBEB_LAYOUT_STEREO; - break; - case 6: - params.layout = CUBEB_LAYOUT_3F2_LFE; - break; - } - - u32 minimum_latency{}; - if (cubeb_get_min_latency(ctx, ¶ms, &minimum_latency) != CUBEB_OK) { - LOG_CRITICAL(Audio_Sink, "Error getting minimum latency"); - } - - if (cubeb_stream_init(ctx, &stream_backend, name.c_str(), nullptr, nullptr, output_device, - ¶ms, std::max(512u, minimum_latency), - &CubebSinkStream::DataCallback, &CubebSinkStream::StateCallback, - this) != CUBEB_OK) { - LOG_CRITICAL(Audio_Sink, "Error initializing cubeb stream"); - return; - } - - if (cubeb_stream_start(stream_backend) != CUBEB_OK) { - LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream"); - return; - } - } - - ~CubebSinkStream() override { - if (!ctx) { - return; - } - - if (cubeb_stream_stop(stream_backend) != CUBEB_OK) { - LOG_CRITICAL(Audio_Sink, "Error stopping cubeb stream"); - } - - cubeb_stream_destroy(stream_backend); - } - - void EnqueueSamples(u32 source_num_channels, const std::vector& samples) override { - if (source_num_channels > num_channels) { - // Downsample 6 channels to 2 - ASSERT_MSG(source_num_channels == 6, "Channel count must be 6"); - - std::vector buf; - buf.reserve(samples.size() * num_channels / source_num_channels); - for (std::size_t i = 0; i < samples.size(); i += source_num_channels) { - // Downmixing implementation taken from the ATSC standard - const s16 left{samples[i + 0]}; - const s16 right{samples[i + 1]}; - const s16 center{samples[i + 2]}; - const s16 surround_left{samples[i + 4]}; - const s16 surround_right{samples[i + 5]}; - // Not used in the ATSC reference implementation - [[maybe_unused]] const s16 low_frequency_effects{samples[i + 3]}; - - constexpr s32 clev{707}; // center mixing level coefficient - constexpr s32 slev{707}; // surround mixing level coefficient - - buf.push_back(static_cast(left + (clev * center / 1000) + - (slev * surround_left / 1000))); - buf.push_back(static_cast(right + (clev * center / 1000) + - (slev * surround_right / 1000))); - } - queue.Push(buf); - return; - } - - queue.Push(samples); - } - - std::size_t SamplesInQueue(u32 channel_count) const override { - if (!ctx) - return 0; - - return queue.Size() / channel_count; - } - - void Flush() override { - should_flush = true; - } - - u32 GetNumChannels() const { - return num_channels; - } - -private: - std::vector device_list; - - cubeb* ctx{}; - cubeb_stream* stream_backend{}; - u32 num_channels{}; - - Common::RingBuffer queue; - std::array last_frame{}; - std::atomic should_flush{}; - - static long DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, - void* output_buffer, long num_frames); - static void StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state); -}; - -CubebSink::CubebSink(std::string_view target_device_name) { - // Cubeb requires COM to be initialized on the thread calling cubeb_init on Windows -#ifdef _WIN32 - com_init_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED); -#endif - - if (cubeb_init(&ctx, "yuzu", nullptr) != CUBEB_OK) { - LOG_CRITICAL(Audio_Sink, "cubeb_init failed"); - return; - } - - if (target_device_name != auto_device_name && !target_device_name.empty()) { - cubeb_device_collection collection; - if (cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection) != CUBEB_OK) { - LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported"); - } else { - const auto collection_end{collection.device + collection.count}; - const auto device{ - std::find_if(collection.device, collection_end, [&](const cubeb_device_info& info) { - return info.friendly_name != nullptr && - target_device_name == info.friendly_name; - })}; - if (device != collection_end) { - output_device = device->devid; - } - cubeb_device_collection_destroy(ctx, &collection); - } - } -} - -CubebSink::~CubebSink() { - if (!ctx) { - return; - } - - for (auto& sink_stream : sink_streams) { - sink_stream.reset(); - } - - cubeb_destroy(ctx); - -#ifdef _WIN32 - if (SUCCEEDED(com_init_result)) { - CoUninitialize(); - } -#endif -} - -SinkStream& CubebSink::AcquireSinkStream(u32 sample_rate, u32 num_channels, - const std::string& name) { - sink_streams.push_back( - std::make_unique(ctx, sample_rate, num_channels, output_device, name)); - return *sink_streams.back(); -} - -long CubebSinkStream::DataCallback([[maybe_unused]] cubeb_stream* stream, void* user_data, - [[maybe_unused]] const void* input_buffer, void* output_buffer, - long num_frames) { - auto* impl = static_cast(user_data); - auto* buffer = static_cast(output_buffer); - - if (!impl) { - return {}; - } - - const std::size_t num_channels = impl->GetNumChannels(); - const std::size_t samples_to_write = num_channels * num_frames; - const std::size_t samples_written = impl->queue.Pop(buffer, samples_to_write); - - if (samples_written >= num_channels) { - std::memcpy(&impl->last_frame[0], buffer + (samples_written - num_channels) * sizeof(s16), - num_channels * sizeof(s16)); - } - - // Fill the rest of the frames with last_frame - for (std::size_t i = samples_written; i < samples_to_write; i += num_channels) { - std::memcpy(buffer + i * sizeof(s16), &impl->last_frame[0], num_channels * sizeof(s16)); - } - - return num_frames; -} - -void CubebSinkStream::StateCallback([[maybe_unused]] cubeb_stream* stream, - [[maybe_unused]] void* user_data, - [[maybe_unused]] cubeb_state state) {} - -std::vector ListCubebSinkDevices() { - std::vector device_list; - cubeb* ctx; - - if (cubeb_init(&ctx, "yuzu Device Enumerator", nullptr) != CUBEB_OK) { - LOG_CRITICAL(Audio_Sink, "cubeb_init failed"); - return {}; - } - - cubeb_device_collection collection; - if (cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection) != CUBEB_OK) { - LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported"); - } else { - for (std::size_t i = 0; i < collection.count; i++) { - const cubeb_device_info& device = collection.device[i]; - if (device.friendly_name) { - device_list.emplace_back(device.friendly_name); - } - } - cubeb_device_collection_destroy(ctx, &collection); - } - - cubeb_destroy(ctx); - return device_list; -} - -} // namespace AudioCore diff --git a/src/audio_core/cubeb_sink.h b/src/audio_core/cubeb_sink.h deleted file mode 100644 index c124b7ee8b..0000000000 --- a/src/audio_core/cubeb_sink.h +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include - -#include - -#include "audio_core/sink.h" - -namespace AudioCore { - -class CubebSink final : public Sink { -public: - explicit CubebSink(std::string_view device_id); - ~CubebSink() override; - - SinkStream& AcquireSinkStream(u32 sample_rate, u32 num_channels, - const std::string& name) override; - -private: - cubeb* ctx{}; - cubeb_devid output_device{}; - std::vector sink_streams; - -#ifdef _WIN32 - u32 com_init_result = 0; -#endif -}; - -std::vector ListCubebSinkDevices(); - -} // namespace AudioCore diff --git a/src/audio_core/delay_line.cpp b/src/audio_core/delay_line.cpp deleted file mode 100644 index b1626a71be..0000000000 --- a/src/audio_core/delay_line.cpp +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include -#include "audio_core/delay_line.h" - -namespace AudioCore { -DelayLineBase::DelayLineBase() = default; -DelayLineBase::~DelayLineBase() = default; - -void DelayLineBase::Initialize(s32 max_delay_, float* src_buffer) { - buffer = src_buffer; - buffer_end = buffer + max_delay_; - max_delay = max_delay_; - output = buffer; - SetDelay(max_delay_); - Clear(); -} - -void DelayLineBase::SetDelay(s32 new_delay) { - if (max_delay < new_delay) { - return; - } - delay = new_delay; - input = (buffer + ((output - buffer) + new_delay) % (max_delay + 1)); -} - -s32 DelayLineBase::GetDelay() const { - return delay; -} - -s32 DelayLineBase::GetMaxDelay() const { - return max_delay; -} - -f32 DelayLineBase::TapOut(s32 last_sample) { - const float* ptr = input - (last_sample + 1); - if (ptr < buffer) { - ptr += (max_delay + 1); - } - - return *ptr; -} - -f32 DelayLineBase::Tick(f32 sample) { - *(input++) = sample; - const auto out_sample = *(output++); - - if (buffer_end < input) { - input = buffer; - } - - if (buffer_end < output) { - output = buffer; - } - - return out_sample; -} - -float* DelayLineBase::GetInput() { - return input; -} - -const float* DelayLineBase::GetInput() const { - return input; -} - -f32 DelayLineBase::GetOutputSample() const { - return *output; -} - -void DelayLineBase::Clear() { - std::memset(buffer, 0, sizeof(float) * max_delay); -} - -void DelayLineBase::Reset() { - buffer = nullptr; - buffer_end = nullptr; - max_delay = 0; - input = nullptr; - output = nullptr; - delay = 0; -} - -DelayLineAllPass::DelayLineAllPass() = default; -DelayLineAllPass::~DelayLineAllPass() = default; - -void DelayLineAllPass::Initialize(u32 delay_, float coeffcient_, f32* src_buffer) { - DelayLineBase::Initialize(delay_, src_buffer); - SetCoefficient(coeffcient_); -} - -void DelayLineAllPass::SetCoefficient(float coeffcient_) { - coefficient = coeffcient_; -} - -f32 DelayLineAllPass::Tick(f32 sample) { - const auto temp = sample - coefficient * *output; - return coefficient * temp + DelayLineBase::Tick(temp); -} - -void DelayLineAllPass::Reset() { - coefficient = 0.0f; - DelayLineBase::Reset(); -} - -} // namespace AudioCore diff --git a/src/audio_core/delay_line.h b/src/audio_core/delay_line.h deleted file mode 100644 index 05fda536f9..0000000000 --- a/src/audio_core/delay_line.h +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include "common/common_types.h" - -namespace AudioCore { - -class DelayLineBase { -public: - DelayLineBase(); - ~DelayLineBase(); - - void Initialize(s32 max_delay_, float* src_buffer); - void SetDelay(s32 new_delay); - s32 GetDelay() const; - s32 GetMaxDelay() const; - f32 TapOut(s32 last_sample); - f32 Tick(f32 sample); - float* GetInput(); - const float* GetInput() const; - f32 GetOutputSample() const; - void Clear(); - void Reset(); - -protected: - float* buffer{nullptr}; - float* buffer_end{nullptr}; - s32 max_delay{}; - float* input{nullptr}; - float* output{nullptr}; - s32 delay{}; -}; - -class DelayLineAllPass final : public DelayLineBase { -public: - DelayLineAllPass(); - ~DelayLineAllPass(); - - void Initialize(u32 delay, float coeffcient_, f32* src_buffer); - void SetCoefficient(float coeffcient_); - f32 Tick(f32 sample); - void Reset(); - -private: - float coefficient{}; -}; -} // namespace AudioCore diff --git a/src/audio_core/device/audio_buffer.h b/src/audio_core/device/audio_buffer.h new file mode 100644 index 0000000000..cae7fa9704 --- /dev/null +++ b/src/audio_core/device/audio_buffer.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/common_types.h" + +namespace AudioCore { + +struct AudioBuffer { + /// Timestamp this buffer completed playing. + s64 played_timestamp; + /// Game memory address for these samples. + VAddr samples; + /// Unqiue identifier for this buffer. + u64 tag; + /// Size of the samples buffer. + u64 size; +}; + +} // namespace AudioCore diff --git a/src/audio_core/device/audio_buffers.h b/src/audio_core/device/audio_buffers.h new file mode 100644 index 0000000000..5d1979ea0c --- /dev/null +++ b/src/audio_core/device/audio_buffers.h @@ -0,0 +1,304 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "audio_buffer.h" +#include "audio_core/device/device_session.h" +#include "core/core_timing.h" + +namespace AudioCore { + +constexpr s32 BufferAppendLimit = 4; + +/** + * A ringbuffer of N audio buffers. + * The buffer contains 3 sections: + * Appended - Buffers added to the ring, but have yet to be sent to the audio backend. + * Registered - Buffers sent to the backend and queued for playback. + * Released - Buffers which have been played, and can now be recycled. + * Any others are free/untracked. + * + * @tparam N - Maximum number of buffers in the ring. + */ +template +class AudioBuffers { +public: + explicit AudioBuffers(size_t limit) : append_limit{static_cast(limit)} {} + + /** + * Append a new audio buffer to the ring. + * + * @param buffer - The new buffer. + */ + void AppendBuffer(AudioBuffer& buffer) { + std::scoped_lock l{lock}; + buffers[appended_index] = buffer; + appended_count++; + appended_index = (appended_index + 1) % append_limit; + } + + /** + * Register waiting buffers, up to a maximum of BufferAppendLimit. + * + * @param out_buffers - The buffers which were registered. + */ + void RegisterBuffers(std::vector& out_buffers) { + std::scoped_lock l{lock}; + const s32 to_register{std::min(std::min(appended_count, BufferAppendLimit), + BufferAppendLimit - registered_count)}; + + for (s32 i = 0; i < to_register; i++) { + s32 index{appended_index - appended_count}; + if (index < 0) { + index += N; + } + out_buffers.push_back(buffers[index]); + registered_count++; + registered_index = (registered_index + 1) % append_limit; + + appended_count--; + if (appended_count == 0) { + break; + } + } + } + + /** + * Release a single buffer. Must be already registered. + * + * @param index - The buffer index to release. + * @param timestamp - The released timestamp for this buffer. + */ + void ReleaseBuffer(s32 index, s64 timestamp) { + std::scoped_lock l{lock}; + buffers[index].played_timestamp = timestamp; + + registered_count--; + released_count++; + released_index = (released_index + 1) % append_limit; + } + + /** + * Release all registered buffers. + * + * @param timestamp - The released timestamp for this buffer. + * @return Is the buffer was released. + */ + bool ReleaseBuffers(Core::Timing::CoreTiming& core_timing, DeviceSession& session) { + std::scoped_lock l{lock}; + bool buffer_released{false}; + while (registered_count > 0) { + auto index{registered_index - registered_count}; + if (index < 0) { + index += N; + } + + // Check with the backend if this buffer can be released yet. + if (!session.IsBufferConsumed(buffers[index].tag)) { + break; + } + + ReleaseBuffer(index, core_timing.GetGlobalTimeNs().count()); + buffer_released = true; + } + + return buffer_released || registered_count == 0; + } + + /** + * Get all released buffers. + * + * @param tags - Container to be filled with the released buffers' tags. + * @return The number of buffers released. + */ + u32 GetReleasedBuffers(std::span tags) { + std::scoped_lock l{lock}; + u32 released{0}; + + while (released_count > 0) { + auto index{released_index - released_count}; + if (index < 0) { + index += N; + } + + auto& buffer{buffers[index]}; + released_count--; + + auto tag{buffer.tag}; + buffer.played_timestamp = 0; + buffer.samples = 0; + buffer.tag = 0; + buffer.size = 0; + + if (tag == 0) { + break; + } + + tags[released++] = tag; + + if (released >= tags.size()) { + break; + } + } + + return released; + } + + /** + * Get all appended and registered buffers. + * + * @param buffers_flushed - Output vector for the buffers which are released. + * @param max_buffers - Maximum number of buffers to released. + * @return The number of buffers released. + */ + u32 GetRegisteredAppendedBuffers(std::vector& buffers_flushed, u32 max_buffers) { + std::scoped_lock l{lock}; + if (registered_count + appended_count == 0) { + return 0; + } + + size_t buffers_to_flush{ + std::min(static_cast(registered_count + appended_count), max_buffers)}; + if (buffers_to_flush == 0) { + return 0; + } + + while (registered_count > 0) { + auto index{registered_index - registered_count}; + if (index < 0) { + index += N; + } + + buffers_flushed.push_back(buffers[index]); + + registered_count--; + released_count++; + released_index = (released_index + 1) % append_limit; + + if (buffers_flushed.size() >= buffers_to_flush) { + break; + } + } + + while (appended_count > 0) { + auto index{appended_index - appended_count}; + if (index < 0) { + index += N; + } + + buffers_flushed.push_back(buffers[index]); + + appended_count--; + released_count++; + released_index = (released_index + 1) % append_limit; + + if (buffers_flushed.size() >= buffers_to_flush) { + break; + } + } + + return static_cast(buffers_flushed.size()); + } + + /** + * Check if the given tag is in the buffers. + * + * @param tag - Unique tag of the buffer to search for. + * @return True if the buffer is still in the ring, otherwise false. + */ + bool ContainsBuffer(const u64 tag) const { + std::scoped_lock l{lock}; + const auto registered_buffers{appended_count + registered_count + released_count}; + + if (registered_buffers == 0) { + return false; + } + + auto index{released_index - released_count}; + if (index < 0) { + index += append_limit; + } + + for (s32 i = 0; i < registered_buffers; i++) { + if (buffers[index].tag == tag) { + return true; + } + index = (index + 1) % append_limit; + } + + return false; + } + + /** + * Get the number of active buffers in the ring. + * That is, appended, registered and released buffers. + * + * @return Number of active buffers. + */ + u32 GetAppendedRegisteredCount() const { + std::scoped_lock l{lock}; + return appended_count + registered_count; + } + + /** + * Get the total number of active buffers in the ring. + * That is, appended, registered and released buffers. + * + * @return Number of active buffers. + */ + u32 GetTotalBufferCount() const { + std::scoped_lock l{lock}; + return static_cast(appended_count + registered_count + released_count); + } + + /** + * Flush all of the currently appended and registered buffers + * + * @param buffers_released - Output count for the number of buffers released. + * @return True if buffers were successfully flushed, otherwise false. + */ + bool FlushBuffers(u32& buffers_released) { + std::scoped_lock l{lock}; + std::vector buffers_flushed{}; + + buffers_released = GetRegisteredAppendedBuffers(buffers_flushed, append_limit); + + if (registered_count > 0) { + return false; + } + + if (static_cast(released_count + appended_count) > append_limit) { + return false; + } + + return true; + } + +private: + /// Buffer lock + mutable std::recursive_mutex lock{}; + /// The audio buffers + std::array buffers{}; + /// Current released index + s32 released_index{}; + /// Number of released buffers + s32 released_count{}; + /// Current registered index + s32 registered_index{}; + /// Number of registered buffers + s32 registered_count{}; + /// Current appended index + s32 appended_index{}; + /// Number of appended buffers + s32 appended_count{}; + /// Maximum number of buffers (default 32) + u32 append_limit{}; +}; + +} // namespace AudioCore diff --git a/src/audio_core/device/device_session.cpp b/src/audio_core/device/device_session.cpp new file mode 100644 index 0000000000..095fc96ce1 --- /dev/null +++ b/src/audio_core/device/device_session.cpp @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_core.h" +#include "audio_core/audio_manager.h" +#include "audio_core/device/audio_buffer.h" +#include "audio_core/device/device_session.h" +#include "audio_core/sink/sink_stream.h" +#include "core/core.h" +#include "core/memory.h" + +namespace AudioCore { + +DeviceSession::DeviceSession(Core::System& system_) : system{system_} {} + +DeviceSession::~DeviceSession() { + Finalize(); +} + +Result DeviceSession::Initialize(std::string_view name_, SampleFormat sample_format_, + u16 channel_count_, size_t session_id_, u32 handle_, + u64 applet_resource_user_id_, Sink::StreamType type_) { + if (stream) { + Finalize(); + } + name = fmt::format("{}-{}", name_, session_id_); + type = type_; + sample_format = sample_format_; + channel_count = channel_count_; + session_id = session_id_; + handle = handle_; + applet_resource_user_id = applet_resource_user_id_; + + if (type == Sink::StreamType::In) { + sink = &system.AudioCore().GetInputSink(); + } else { + sink = &system.AudioCore().GetOutputSink(); + } + stream = sink->AcquireSinkStream(system, channel_count, name, type); + initialized = true; + return ResultSuccess; +} + +void DeviceSession::Finalize() { + if (initialized) { + Stop(); + sink->CloseStream(stream); + stream = nullptr; + } +} + +void DeviceSession::Start() { + stream->SetPlayedSampleCount(played_sample_count); + stream->Start(); +} + +void DeviceSession::Stop() { + if (stream) { + played_sample_count = stream->GetPlayedSampleCount(); + stream->Stop(); + } +} + +void DeviceSession::AppendBuffers(std::span buffers) const { + auto& memory{system.Memory()}; + + for (size_t i = 0; i < buffers.size(); i++) { + Sink::SinkBuffer new_buffer{ + .frames = buffers[i].size / (channel_count * sizeof(s16)), + .frames_played = 0, + .tag = buffers[i].tag, + .consumed = false, + }; + + if (type == Sink::StreamType::In) { + std::vector samples{}; + stream->AppendBuffer(new_buffer, samples); + } else { + std::vector samples(buffers[i].size / sizeof(s16)); + memory.ReadBlockUnsafe(buffers[i].samples, samples.data(), buffers[i].size); + stream->AppendBuffer(new_buffer, samples); + } + } +} + +void DeviceSession::ReleaseBuffer(AudioBuffer& buffer) const { + if (type == Sink::StreamType::In) { + auto& memory{system.Memory()}; + auto samples{stream->ReleaseBuffer(buffer.size / sizeof(s16))}; + memory.WriteBlockUnsafe(buffer.samples, samples.data(), buffer.size); + } +} + +bool DeviceSession::IsBufferConsumed(u64 tag) const { + if (stream) { + return stream->IsBufferConsumed(tag); + } + return true; +} + +void DeviceSession::SetVolume(f32 volume) const { + if (stream) { + stream->SetSystemVolume(volume); + } +} + +u64 DeviceSession::GetPlayedSampleCount() const { + if (stream) { + return stream->GetPlayedSampleCount(); + } + return 0; +} + +} // namespace AudioCore diff --git a/src/audio_core/device/device_session.h b/src/audio_core/device/device_session.h new file mode 100644 index 0000000000..4a031b7651 --- /dev/null +++ b/src/audio_core/device/device_session.h @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "audio_core/sink/sink.h" +#include "core/hle/service/audio/errors.h" + +namespace Core { +class System; +} + +namespace AudioCore { +namespace Sink { +class SinkStream; +struct SinkBuffer; +} // namespace Sink + +struct AudioBuffer; + +/** + * Represents an input or output device stream for audio in and audio out (not used for render). + **/ +class DeviceSession { +public: + explicit DeviceSession(Core::System& system); + ~DeviceSession(); + + /** + * Initialize this device session. + * + * @param name - Name of this device. + * @param sample_format - Sample format for this device's output. + * @param channel_count - Number of channels for this device (2 or 6). + * @param session_id - This session's id. + * @param handle - Handle for this device session (unused). + * @param applet_resource_user_id - Applet resource user id for this device session (unused). + * @param type - Type of this stream (Render, In, Out). + * @return Result code for this call. + */ + Result Initialize(std::string_view name, SampleFormat sample_format, u16 channel_count, + size_t session_id, u32 handle, u64 applet_resource_user_id, + Sink::StreamType type); + + /** + * Finalize this device session. + */ + void Finalize(); + + /** + * Append audio buffers to this device session to be played back. + * + * @param buffers - The buffers to play. + */ + void AppendBuffers(std::span buffers) const; + + /** + * (Audio In only) Pop samples from the backend, and write them back to this buffer's address. + * + * @param buffer - The buffer to write to. + */ + void ReleaseBuffer(AudioBuffer& buffer) const; + + /** + * Check if the buffer for the given tag has been consumed by the backend. + * + * @param tag - Unqiue tag of the buffer to check. + * @return true if the buffer has been consumed, otherwise false. + */ + bool IsBufferConsumed(u64 tag) const; + + /** + * Start this device session, starting the backend stream. + */ + void Start(); + + /** + * Stop this device session, stopping the backend stream. + */ + void Stop(); + + /** + * Set this device session's volume. + * + * @param volume - New volume for this session. + */ + void SetVolume(f32 volume) const; + + /** + * Get this device session's total played sample count. + * + * @return Samples played by this session. + */ + u64 GetPlayedSampleCount() const; + +private: + /// System + Core::System& system; + /// Output sink this device will use + Sink::Sink* sink{}; + /// The backend stream for this device session to send samples to + Sink::SinkStream* stream{}; + /// Name of this device session + std::string name{}; + /// Type of this device session (render/in/out) + Sink::StreamType type{}; + /// Sample format for this device. + SampleFormat sample_format{SampleFormat::PcmInt16}; + /// Channel count for this device session + u16 channel_count{}; + /// Session id of this device session + size_t session_id{}; + /// Handle of this device session + u32 handle{}; + /// Applet resource user id of this device session + u64 applet_resource_user_id{}; + /// Total number of samples played by this device session + u64 played_sample_count{}; + /// Is this session initialised? + bool initialized{}; +}; + +} // namespace AudioCore diff --git a/src/audio_core/effect_context.cpp b/src/audio_core/effect_context.cpp deleted file mode 100644 index 79bcd11923..0000000000 --- a/src/audio_core/effect_context.cpp +++ /dev/null @@ -1,320 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include -#include "audio_core/effect_context.h" - -namespace AudioCore { -namespace { -bool ValidChannelCountForEffect(s32 channel_count) { - return channel_count == 1 || channel_count == 2 || channel_count == 4 || channel_count == 6; -} -} // namespace - -EffectContext::EffectContext(std::size_t effect_count_) : effect_count(effect_count_) { - effects.reserve(effect_count); - std::generate_n(std::back_inserter(effects), effect_count, - [] { return std::make_unique(); }); -} -EffectContext::~EffectContext() = default; - -std::size_t EffectContext::GetCount() const { - return effect_count; -} - -EffectBase* EffectContext::GetInfo(std::size_t i) { - return effects.at(i).get(); -} - -EffectBase* EffectContext::RetargetEffect(std::size_t i, EffectType effect) { - switch (effect) { - case EffectType::Invalid: - effects[i] = std::make_unique(); - break; - case EffectType::BufferMixer: - effects[i] = std::make_unique(); - break; - case EffectType::Aux: - effects[i] = std::make_unique(); - break; - case EffectType::Delay: - effects[i] = std::make_unique(); - break; - case EffectType::Reverb: - effects[i] = std::make_unique(); - break; - case EffectType::I3dl2Reverb: - effects[i] = std::make_unique(); - break; - case EffectType::BiquadFilter: - effects[i] = std::make_unique(); - break; - default: - ASSERT_MSG(false, "Unimplemented effect {}", effect); - effects[i] = std::make_unique(); - } - return GetInfo(i); -} - -const EffectBase* EffectContext::GetInfo(std::size_t i) const { - return effects.at(i).get(); -} - -EffectStubbed::EffectStubbed() : EffectBase(EffectType::Invalid) {} -EffectStubbed::~EffectStubbed() = default; - -void EffectStubbed::Update([[maybe_unused]] EffectInfo::InParams& in_params) {} -void EffectStubbed::UpdateForCommandGeneration() {} - -EffectBase::EffectBase(EffectType effect_type_) : effect_type(effect_type_) {} -EffectBase::~EffectBase() = default; - -UsageState EffectBase::GetUsage() const { - return usage; -} - -EffectType EffectBase::GetType() const { - return effect_type; -} - -bool EffectBase::IsEnabled() const { - return enabled; -} - -s32 EffectBase::GetMixID() const { - return mix_id; -} - -s32 EffectBase::GetProcessingOrder() const { - return processing_order; -} - -std::vector& EffectBase::GetWorkBuffer() { - return work_buffer; -} - -const std::vector& EffectBase::GetWorkBuffer() const { - return work_buffer; -} - -EffectI3dl2Reverb::EffectI3dl2Reverb() : EffectGeneric(EffectType::I3dl2Reverb) {} -EffectI3dl2Reverb::~EffectI3dl2Reverb() = default; - -void EffectI3dl2Reverb::Update(EffectInfo::InParams& in_params) { - auto& params = GetParams(); - const auto* reverb_params = reinterpret_cast(in_params.raw.data()); - if (!ValidChannelCountForEffect(reverb_params->max_channels)) { - ASSERT_MSG(false, "Invalid reverb max channel count {}", reverb_params->max_channels); - return; - } - - const auto last_status = params.status; - mix_id = in_params.mix_id; - processing_order = in_params.processing_order; - params = *reverb_params; - if (!ValidChannelCountForEffect(reverb_params->channel_count)) { - params.channel_count = params.max_channels; - } - enabled = in_params.is_enabled; - if (last_status != ParameterStatus::Updated) { - params.status = last_status; - } - - if (in_params.is_new || skipped) { - usage = UsageState::Initialized; - params.status = ParameterStatus::Initialized; - skipped = in_params.buffer_address == 0 || in_params.buffer_size == 0; - if (!skipped) { - auto& cur_work_buffer = GetWorkBuffer(); - // Has two buffers internally - cur_work_buffer.resize(in_params.buffer_size * 2); - std::fill(cur_work_buffer.begin(), cur_work_buffer.end(), 0); - } - } -} - -void EffectI3dl2Reverb::UpdateForCommandGeneration() { - if (enabled) { - usage = UsageState::Running; - } else { - usage = UsageState::Stopped; - } - GetParams().status = ParameterStatus::Updated; -} - -I3dl2ReverbState& EffectI3dl2Reverb::GetState() { - return state; -} - -const I3dl2ReverbState& EffectI3dl2Reverb::GetState() const { - return state; -} - -EffectBiquadFilter::EffectBiquadFilter() : EffectGeneric(EffectType::BiquadFilter) {} -EffectBiquadFilter::~EffectBiquadFilter() = default; - -void EffectBiquadFilter::Update(EffectInfo::InParams& in_params) { - auto& params = GetParams(); - const auto* biquad_params = reinterpret_cast(in_params.raw.data()); - mix_id = in_params.mix_id; - processing_order = in_params.processing_order; - params = *biquad_params; - enabled = in_params.is_enabled; -} - -void EffectBiquadFilter::UpdateForCommandGeneration() { - if (enabled) { - usage = UsageState::Running; - } else { - usage = UsageState::Stopped; - } - GetParams().status = ParameterStatus::Updated; -} - -EffectAuxInfo::EffectAuxInfo() : EffectGeneric(EffectType::Aux) {} -EffectAuxInfo::~EffectAuxInfo() = default; - -void EffectAuxInfo::Update(EffectInfo::InParams& in_params) { - const auto* aux_params = reinterpret_cast(in_params.raw.data()); - mix_id = in_params.mix_id; - processing_order = in_params.processing_order; - GetParams() = *aux_params; - enabled = in_params.is_enabled; - - if (in_params.is_new || skipped) { - skipped = aux_params->send_buffer_info == 0 || aux_params->return_buffer_info == 0; - if (skipped) { - return; - } - - // There's two AuxInfos which are an identical size, the first one is managed by the cpu, - // the second is managed by the dsp. All we care about is managing the DSP one - send_info = aux_params->send_buffer_info + sizeof(AuxInfoDSP); - send_buffer = aux_params->send_buffer_info + (sizeof(AuxInfoDSP) * 2); - - recv_info = aux_params->return_buffer_info + sizeof(AuxInfoDSP); - recv_buffer = aux_params->return_buffer_info + (sizeof(AuxInfoDSP) * 2); - } -} - -void EffectAuxInfo::UpdateForCommandGeneration() { - if (enabled) { - usage = UsageState::Running; - } else { - usage = UsageState::Stopped; - } -} - -VAddr EffectAuxInfo::GetSendInfo() const { - return send_info; -} - -VAddr EffectAuxInfo::GetSendBuffer() const { - return send_buffer; -} - -VAddr EffectAuxInfo::GetRecvInfo() const { - return recv_info; -} - -VAddr EffectAuxInfo::GetRecvBuffer() const { - return recv_buffer; -} - -EffectDelay::EffectDelay() : EffectGeneric(EffectType::Delay) {} -EffectDelay::~EffectDelay() = default; - -void EffectDelay::Update(EffectInfo::InParams& in_params) { - const auto* delay_params = reinterpret_cast(in_params.raw.data()); - auto& params = GetParams(); - if (!ValidChannelCountForEffect(delay_params->max_channels)) { - return; - } - - const auto last_status = params.status; - mix_id = in_params.mix_id; - processing_order = in_params.processing_order; - params = *delay_params; - if (!ValidChannelCountForEffect(delay_params->channels)) { - params.channels = params.max_channels; - } - enabled = in_params.is_enabled; - - if (last_status != ParameterStatus::Updated) { - params.status = last_status; - } - - if (in_params.is_new || skipped) { - usage = UsageState::Initialized; - params.status = ParameterStatus::Initialized; - skipped = in_params.buffer_address == 0 || in_params.buffer_size == 0; - } -} - -void EffectDelay::UpdateForCommandGeneration() { - if (enabled) { - usage = UsageState::Running; - } else { - usage = UsageState::Stopped; - } - GetParams().status = ParameterStatus::Updated; -} - -EffectBufferMixer::EffectBufferMixer() : EffectGeneric(EffectType::BufferMixer) {} -EffectBufferMixer::~EffectBufferMixer() = default; - -void EffectBufferMixer::Update(EffectInfo::InParams& in_params) { - mix_id = in_params.mix_id; - processing_order = in_params.processing_order; - GetParams() = *reinterpret_cast(in_params.raw.data()); - enabled = in_params.is_enabled; -} - -void EffectBufferMixer::UpdateForCommandGeneration() { - if (enabled) { - usage = UsageState::Running; - } else { - usage = UsageState::Stopped; - } -} - -EffectReverb::EffectReverb() : EffectGeneric(EffectType::Reverb) {} -EffectReverb::~EffectReverb() = default; - -void EffectReverb::Update(EffectInfo::InParams& in_params) { - const auto* reverb_params = reinterpret_cast(in_params.raw.data()); - auto& params = GetParams(); - if (!ValidChannelCountForEffect(reverb_params->max_channels)) { - return; - } - - const auto last_status = params.status; - mix_id = in_params.mix_id; - processing_order = in_params.processing_order; - params = *reverb_params; - if (!ValidChannelCountForEffect(reverb_params->channels)) { - params.channels = params.max_channels; - } - enabled = in_params.is_enabled; - - if (last_status != ParameterStatus::Updated) { - params.status = last_status; - } - - if (in_params.is_new || skipped) { - usage = UsageState::Initialized; - params.status = ParameterStatus::Initialized; - skipped = in_params.buffer_address == 0 || in_params.buffer_size == 0; - } -} - -void EffectReverb::UpdateForCommandGeneration() { - if (enabled) { - usage = UsageState::Running; - } else { - usage = UsageState::Stopped; - } - GetParams().status = ParameterStatus::Updated; -} - -} // namespace AudioCore diff --git a/src/audio_core/effect_context.h b/src/audio_core/effect_context.h deleted file mode 100644 index cb47df4729..0000000000 --- a/src/audio_core/effect_context.h +++ /dev/null @@ -1,349 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include -#include "audio_core/common.h" -#include "audio_core/delay_line.h" -#include "common/common_funcs.h" -#include "common/common_types.h" -#include "common/swap.h" - -namespace AudioCore { -enum class EffectType : u8 { - Invalid = 0, - BufferMixer = 1, - Aux = 2, - Delay = 3, - Reverb = 4, - I3dl2Reverb = 5, - BiquadFilter = 6, -}; - -enum class UsageStatus : u8 { - Invalid = 0, - New = 1, - Initialized = 2, - Used = 3, - Removed = 4, -}; - -enum class UsageState { - Invalid = 0, - Initialized = 1, - Running = 2, - Stopped = 3, -}; - -enum class ParameterStatus : u8 { - Initialized = 0, - Updating = 1, - Updated = 2, -}; - -struct BufferMixerParams { - std::array input{}; - std::array output{}; - std::array volume{}; - s32_le count{}; -}; -static_assert(sizeof(BufferMixerParams) == 0x94, "BufferMixerParams is an invalid size"); - -struct AuxInfoDSP { - u32_le read_offset{}; - u32_le write_offset{}; - u32_le remaining{}; - INSERT_PADDING_WORDS(13); -}; -static_assert(sizeof(AuxInfoDSP) == 0x40, "AuxInfoDSP is an invalid size"); - -struct AuxInfo { - std::array input_mix_buffers{}; - std::array output_mix_buffers{}; - u32_le count{}; - s32_le sample_rate{}; - s32_le sample_count{}; - s32_le mix_buffer_count{}; - u64_le send_buffer_info{}; - u64_le send_buffer_base{}; - - u64_le return_buffer_info{}; - u64_le return_buffer_base{}; -}; -static_assert(sizeof(AuxInfo) == 0x60, "AuxInfo is an invalid size"); - -struct I3dl2ReverbParams { - std::array input{}; - std::array output{}; - u16_le max_channels{}; - u16_le channel_count{}; - INSERT_PADDING_BYTES(1); - u32_le sample_rate{}; - f32 room_hf{}; - f32 hf_reference{}; - f32 decay_time{}; - f32 hf_decay_ratio{}; - f32 room{}; - f32 reflection{}; - f32 reverb{}; - f32 diffusion{}; - f32 reflection_delay{}; - f32 reverb_delay{}; - f32 density{}; - f32 dry_gain{}; - ParameterStatus status{}; - INSERT_PADDING_BYTES(3); -}; -static_assert(sizeof(I3dl2ReverbParams) == 0x4c, "I3dl2ReverbParams is an invalid size"); - -struct BiquadFilterParams { - std::array input{}; - std::array output{}; - std::array numerator; - std::array denominator; - s8 channel_count{}; - ParameterStatus status{}; -}; -static_assert(sizeof(BiquadFilterParams) == 0x18, "BiquadFilterParams is an invalid size"); - -struct DelayParams { - std::array input{}; - std::array output{}; - u16_le max_channels{}; - u16_le channels{}; - s32_le max_delay{}; - s32_le delay{}; - s32_le sample_rate{}; - s32_le gain{}; - s32_le feedback_gain{}; - s32_le out_gain{}; - s32_le dry_gain{}; - s32_le channel_spread{}; - s32_le low_pass{}; - ParameterStatus status{}; - INSERT_PADDING_BYTES(3); -}; -static_assert(sizeof(DelayParams) == 0x38, "DelayParams is an invalid size"); - -struct ReverbParams { - std::array input{}; - std::array output{}; - u16_le max_channels{}; - u16_le channels{}; - s32_le sample_rate{}; - s32_le mode0{}; - s32_le mode0_gain{}; - s32_le pre_delay{}; - s32_le mode1{}; - s32_le mode1_gain{}; - s32_le decay{}; - s32_le hf_decay_ratio{}; - s32_le coloration{}; - s32_le reverb_gain{}; - s32_le out_gain{}; - s32_le dry_gain{}; - ParameterStatus status{}; - INSERT_PADDING_BYTES(3); -}; -static_assert(sizeof(ReverbParams) == 0x44, "ReverbParams is an invalid size"); - -class EffectInfo { -public: - struct InParams { - EffectType type{}; - u8 is_new{}; - u8 is_enabled{}; - INSERT_PADDING_BYTES(1); - s32_le mix_id{}; - u64_le buffer_address{}; - u64_le buffer_size{}; - s32_le processing_order{}; - INSERT_PADDING_BYTES(4); - union { - std::array raw; - }; - }; - static_assert(sizeof(InParams) == 0xc0, "InParams is an invalid size"); - - struct OutParams { - UsageStatus status{}; - INSERT_PADDING_BYTES(15); - }; - static_assert(sizeof(OutParams) == 0x10, "OutParams is an invalid size"); -}; - -struct AuxAddress { - VAddr send_dsp_info{}; - VAddr send_buffer_base{}; - VAddr return_dsp_info{}; - VAddr return_buffer_base{}; -}; - -class EffectBase { -public: - explicit EffectBase(EffectType effect_type_); - virtual ~EffectBase(); - - virtual void Update(EffectInfo::InParams& in_params) = 0; - virtual void UpdateForCommandGeneration() = 0; - [[nodiscard]] UsageState GetUsage() const; - [[nodiscard]] EffectType GetType() const; - [[nodiscard]] bool IsEnabled() const; - [[nodiscard]] s32 GetMixID() const; - [[nodiscard]] s32 GetProcessingOrder() const; - [[nodiscard]] std::vector& GetWorkBuffer(); - [[nodiscard]] const std::vector& GetWorkBuffer() const; - -protected: - UsageState usage{UsageState::Invalid}; - EffectType effect_type{}; - s32 mix_id{}; - s32 processing_order{}; - bool enabled = false; - std::vector work_buffer{}; -}; - -template -class EffectGeneric : public EffectBase { -public: - explicit EffectGeneric(EffectType effect_type_) : EffectBase(effect_type_) {} - - T& GetParams() { - return internal_params; - } - - const T& GetParams() const { - return internal_params; - } - -private: - T internal_params{}; -}; - -class EffectStubbed : public EffectBase { -public: - explicit EffectStubbed(); - ~EffectStubbed() override; - - void Update(EffectInfo::InParams& in_params) override; - void UpdateForCommandGeneration() override; -}; - -struct I3dl2ReverbState { - f32 lowpass_0{}; - f32 lowpass_1{}; - f32 lowpass_2{}; - - DelayLineBase early_delay_line{}; - std::array early_tap_steps{}; - f32 early_gain{}; - f32 late_gain{}; - - u32 early_to_late_taps{}; - std::array fdn_delay_line{}; - std::array decay_delay_line0{}; - std::array decay_delay_line1{}; - f32 last_reverb_echo{}; - DelayLineBase center_delay_line{}; - std::array, 3> lpf_coefficients{}; - std::array shelf_filter{}; - f32 dry_gain{}; -}; - -class EffectI3dl2Reverb : public EffectGeneric { -public: - explicit EffectI3dl2Reverb(); - ~EffectI3dl2Reverb() override; - - void Update(EffectInfo::InParams& in_params) override; - void UpdateForCommandGeneration() override; - - I3dl2ReverbState& GetState(); - const I3dl2ReverbState& GetState() const; - -private: - bool skipped = false; - I3dl2ReverbState state{}; -}; - -class EffectBiquadFilter : public EffectGeneric { -public: - explicit EffectBiquadFilter(); - ~EffectBiquadFilter() override; - - void Update(EffectInfo::InParams& in_params) override; - void UpdateForCommandGeneration() override; -}; - -class EffectAuxInfo : public EffectGeneric { -public: - explicit EffectAuxInfo(); - ~EffectAuxInfo() override; - - void Update(EffectInfo::InParams& in_params) override; - void UpdateForCommandGeneration() override; - [[nodiscard]] VAddr GetSendInfo() const; - [[nodiscard]] VAddr GetSendBuffer() const; - [[nodiscard]] VAddr GetRecvInfo() const; - [[nodiscard]] VAddr GetRecvBuffer() const; - -private: - VAddr send_info{}; - VAddr send_buffer{}; - VAddr recv_info{}; - VAddr recv_buffer{}; - bool skipped = false; - AuxAddress addresses{}; -}; - -class EffectDelay : public EffectGeneric { -public: - explicit EffectDelay(); - ~EffectDelay() override; - - void Update(EffectInfo::InParams& in_params) override; - void UpdateForCommandGeneration() override; - -private: - bool skipped = false; -}; - -class EffectBufferMixer : public EffectGeneric { -public: - explicit EffectBufferMixer(); - ~EffectBufferMixer() override; - - void Update(EffectInfo::InParams& in_params) override; - void UpdateForCommandGeneration() override; -}; - -class EffectReverb : public EffectGeneric { -public: - explicit EffectReverb(); - ~EffectReverb() override; - - void Update(EffectInfo::InParams& in_params) override; - void UpdateForCommandGeneration() override; - -private: - bool skipped = false; -}; - -class EffectContext { -public: - explicit EffectContext(std::size_t effect_count_); - ~EffectContext(); - - [[nodiscard]] std::size_t GetCount() const; - [[nodiscard]] EffectBase* GetInfo(std::size_t i); - [[nodiscard]] EffectBase* RetargetEffect(std::size_t i, EffectType effect); - [[nodiscard]] const EffectBase* GetInfo(std::size_t i) const; - -private: - std::size_t effect_count{}; - std::vector> effects; -}; -} // namespace AudioCore diff --git a/src/audio_core/in/audio_in.cpp b/src/audio_core/in/audio_in.cpp new file mode 100644 index 0000000000..c946895d64 --- /dev/null +++ b/src/audio_core/in/audio_in.cpp @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_in_manager.h" +#include "audio_core/in/audio_in.h" +#include "core/hle/kernel/k_event.h" + +namespace AudioCore::AudioIn { + +In::In(Core::System& system_, Manager& manager_, Kernel::KEvent* event_, size_t session_id_) + : manager{manager_}, parent_mutex{manager.mutex}, event{event_}, system{system_, event, + session_id_} {} + +void In::Free() { + std::scoped_lock l{parent_mutex}; + manager.ReleaseSessionId(system.GetSessionId()); +} + +System& In::GetSystem() { + return system; +} + +AudioIn::State In::GetState() { + std::scoped_lock l{parent_mutex}; + return system.GetState(); +} + +Result In::StartSystem() { + std::scoped_lock l{parent_mutex}; + return system.Start(); +} + +void In::StartSession() { + std::scoped_lock l{parent_mutex}; + system.StartSession(); +} + +Result In::StopSystem() { + std::scoped_lock l{parent_mutex}; + return system.Stop(); +} + +Result In::AppendBuffer(const AudioInBuffer& buffer, u64 tag) { + std::scoped_lock l{parent_mutex}; + + if (system.AppendBuffer(buffer, tag)) { + return ResultSuccess; + } + return Service::Audio::ERR_BUFFER_COUNT_EXCEEDED; +} + +void In::ReleaseAndRegisterBuffers() { + std::scoped_lock l{parent_mutex}; + if (system.GetState() == State::Started) { + system.ReleaseBuffers(); + system.RegisterBuffers(); + } +} + +bool In::FlushAudioInBuffers() { + std::scoped_lock l{parent_mutex}; + return system.FlushAudioInBuffers(); +} + +u32 In::GetReleasedBuffers(std::span tags) { + std::scoped_lock l{parent_mutex}; + return system.GetReleasedBuffers(tags); +} + +Kernel::KReadableEvent& In::GetBufferEvent() { + std::scoped_lock l{parent_mutex}; + return event->GetReadableEvent(); +} + +f32 In::GetVolume() { + std::scoped_lock l{parent_mutex}; + return system.GetVolume(); +} + +void In::SetVolume(f32 volume) { + std::scoped_lock l{parent_mutex}; + system.SetVolume(volume); +} + +bool In::ContainsAudioBuffer(u64 tag) { + std::scoped_lock l{parent_mutex}; + return system.ContainsAudioBuffer(tag); +} + +u32 In::GetBufferCount() { + std::scoped_lock l{parent_mutex}; + return system.GetBufferCount(); +} + +u64 In::GetPlayedSampleCount() { + std::scoped_lock l{parent_mutex}; + return system.GetPlayedSampleCount(); +} + +} // namespace AudioCore::AudioIn diff --git a/src/audio_core/in/audio_in.h b/src/audio_core/in/audio_in.h new file mode 100644 index 0000000000..6253891d5e --- /dev/null +++ b/src/audio_core/in/audio_in.h @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/in/audio_in_system.h" + +namespace Core { +class System; +} + +namespace Kernel { +class KEvent; +class KReadableEvent; +} // namespace Kernel + +namespace AudioCore::AudioIn { +class Manager; + +/** + * Interface between the service and audio in system. Mainly responsible for forwarding service + * calls to the system. + */ +class In { +public: + explicit In(Core::System& system, Manager& manager, Kernel::KEvent* event, size_t session_id); + + /** + * Free this audio in from the audio in manager. + */ + void Free(); + + /** + * Get this audio in's system. + */ + System& GetSystem(); + + /** + * Get the current state. + * + * @return Started or Stopped. + */ + AudioIn::State GetState(); + + /** + * Start the system + * + * @return Result code + */ + Result StartSystem(); + + /** + * Start the system's device session. + */ + void StartSession(); + + /** + * Stop the system. + * + * @return Result code + */ + Result StopSystem(); + + /** + * Append a new buffer to the system, the buffer event will be signalled when it is filled. + * + * @param buffer - The new buffer to append. + * @param tag - Unique tag for this buffer. + * @return Result code. + */ + Result AppendBuffer(const AudioInBuffer& buffer, u64 tag); + + /** + * Release all completed buffers, and register any appended. + */ + void ReleaseAndRegisterBuffers(); + + /** + * Flush all buffers. + */ + bool FlushAudioInBuffers(); + + /** + * Get all of the currently released buffers. + * + * @param tags - Output container for the buffer tags which were released. + * @return The number of buffers released. + */ + u32 GetReleasedBuffers(std::span tags); + + /** + * Get the buffer event for this audio in, this event will be signalled when a buffer is filled. + * + * @return The buffer event. + */ + Kernel::KReadableEvent& GetBufferEvent(); + + /** + * Get the current system volume. + * + * @return The current volume. + */ + f32 GetVolume(); + + /** + * Set the system volume. + * + * @param volume - The volume to set. + */ + void SetVolume(f32 volume); + + /** + * Check if a buffer is in the system. + * + * @param tag - The tag to search for. + * @return True if the buffer is in the system, otherwise false. + */ + bool ContainsAudioBuffer(u64 tag); + + /** + * Get the maximum number of buffers. + * + * @return The maximum number of buffers. + */ + u32 GetBufferCount(); + + /** + * Get the total played sample count for this audio in. + * + * @return The played sample count. + */ + u64 GetPlayedSampleCount(); + +private: + /// The AudioIn::Manager this audio in is registered with + Manager& manager; + /// Manager's mutex + std::recursive_mutex& parent_mutex; + /// Buffer event, signalled when buffers are ready to be released + Kernel::KEvent* event; + /// Main audio in system + System system; +}; + +} // namespace AudioCore::AudioIn diff --git a/src/audio_core/in/audio_in_system.cpp b/src/audio_core/in/audio_in_system.cpp new file mode 100644 index 0000000000..ec5d37ed44 --- /dev/null +++ b/src/audio_core/in/audio_in_system.cpp @@ -0,0 +1,213 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "audio_core/audio_event.h" +#include "audio_core/audio_manager.h" +#include "audio_core/in/audio_in_system.h" +#include "common/logging/log.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/hle/kernel/k_event.h" + +namespace AudioCore::AudioIn { + +System::System(Core::System& system_, Kernel::KEvent* event_, const size_t session_id_) + : system{system_}, buffer_event{event_}, + session_id{session_id_}, session{std::make_unique(system_)} {} + +System::~System() { + Finalize(); +} + +void System::Finalize() { + Stop(); + session->Finalize(); + buffer_event->GetWritableEvent().Signal(); +} + +void System::StartSession() { + session->Start(); +} + +size_t System::GetSessionId() const { + return session_id; +} + +std::string_view System::GetDefaultDeviceName() { + return "BuiltInHeadset"; +} + +std::string_view System::GetDefaultUacDeviceName() { + return "Uac"; +} + +Result System::IsConfigValid(const std::string_view device_name, + const AudioInParameter& in_params) { + if ((device_name.size() > 0) && + (device_name != GetDefaultDeviceName() && device_name != GetDefaultUacDeviceName())) { + return Service::Audio::ERR_INVALID_DEVICE_NAME; + } + + if (in_params.sample_rate != TargetSampleRate && in_params.sample_rate > 0) { + return Service::Audio::ERR_INVALID_SAMPLE_RATE; + } + + return ResultSuccess; +} + +Result System::Initialize(std::string& device_name, const AudioInParameter& in_params, + const u32 handle_, const u64 applet_resource_user_id_) { + auto result{IsConfigValid(device_name, in_params)}; + if (result.IsError()) { + return result; + } + + handle = handle_; + applet_resource_user_id = applet_resource_user_id_; + if (device_name.empty() || device_name[0] == '\0') { + name = std::string(GetDefaultDeviceName()); + } else { + name = std::move(device_name); + } + + sample_rate = TargetSampleRate; + sample_format = SampleFormat::PcmInt16; + channel_count = in_params.channel_count <= 2 ? 2 : 6; + volume = 1.0f; + is_uac = name == "Uac"; + return ResultSuccess; +} + +Result System::Start() { + if (state != State::Stopped) { + return Service::Audio::ERR_OPERATION_FAILED; + } + + session->Initialize(name, sample_format, channel_count, session_id, handle, + applet_resource_user_id, Sink::StreamType::In); + session->SetVolume(volume); + session->Start(); + state = State::Started; + + std::vector buffers_to_flush{}; + buffers.RegisterBuffers(buffers_to_flush); + session->AppendBuffers(buffers_to_flush); + + return ResultSuccess; +} + +Result System::Stop() { + if (state == State::Started) { + session->Stop(); + session->SetVolume(0.0f); + state = State::Stopped; + } + + return ResultSuccess; +} + +bool System::AppendBuffer(const AudioInBuffer& buffer, const u64 tag) { + if (buffers.GetTotalBufferCount() == BufferCount) { + return false; + } + + AudioBuffer new_buffer{ + .played_timestamp = 0, .samples = buffer.samples, .tag = tag, .size = buffer.size}; + + buffers.AppendBuffer(new_buffer); + RegisterBuffers(); + + return true; +} + +void System::RegisterBuffers() { + if (state == State::Started) { + std::vector registered_buffers{}; + buffers.RegisterBuffers(registered_buffers); + session->AppendBuffers(registered_buffers); + } +} + +void System::ReleaseBuffers() { + bool signal{buffers.ReleaseBuffers(system.CoreTiming(), *session)}; + + if (signal) { + // Signal if any buffer was released, or if none are registered, we need more. + buffer_event->GetWritableEvent().Signal(); + } +} + +u32 System::GetReleasedBuffers(std::span tags) { + return buffers.GetReleasedBuffers(tags); +} + +bool System::FlushAudioInBuffers() { + if (state != State::Started) { + return false; + } + + u32 buffers_released{}; + buffers.FlushBuffers(buffers_released); + + if (buffers_released > 0) { + buffer_event->GetWritableEvent().Signal(); + } + return true; +} + +u16 System::GetChannelCount() const { + return channel_count; +} + +u32 System::GetSampleRate() const { + return sample_rate; +} + +SampleFormat System::GetSampleFormat() const { + return sample_format; +} + +State System::GetState() { + switch (state) { + case State::Started: + case State::Stopped: + return state; + default: + LOG_ERROR(Service_Audio, "AudioIn invalid state!"); + state = State::Stopped; + break; + } + return state; +} + +std::string System::GetName() const { + return name; +} + +f32 System::GetVolume() const { + return volume; +} + +void System::SetVolume(const f32 volume_) { + volume = volume_; + session->SetVolume(volume_); +} + +bool System::ContainsAudioBuffer(const u64 tag) { + return buffers.ContainsBuffer(tag); +} + +u32 System::GetBufferCount() { + return buffers.GetAppendedRegisteredCount(); +} + +u64 System::GetPlayedSampleCount() const { + return session->GetPlayedSampleCount(); +} + +bool System::IsUac() const { + return is_uac; +} + +} // namespace AudioCore::AudioIn diff --git a/src/audio_core/in/audio_in_system.h b/src/audio_core/in/audio_in_system.h new file mode 100644 index 0000000000..165e35d83b --- /dev/null +++ b/src/audio_core/in/audio_in_system.h @@ -0,0 +1,275 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/device/audio_buffers.h" +#include "audio_core/device/device_session.h" +#include "core/hle/service/audio/errors.h" + +namespace Core { +class System; +} + +namespace Kernel { +class KEvent; +} + +namespace AudioCore::AudioIn { + +constexpr SessionTypes SessionType = SessionTypes::AudioIn; + +struct AudioInParameter { + /* 0x0 */ s32_le sample_rate; + /* 0x4 */ u16_le channel_count; + /* 0x6 */ u16_le reserved; +}; +static_assert(sizeof(AudioInParameter) == 0x8, "AudioInParameter is an invalid size"); + +struct AudioInParameterInternal { + /* 0x0 */ u32_le sample_rate; + /* 0x4 */ u32_le channel_count; + /* 0x8 */ u32_le sample_format; + /* 0xC */ u32_le state; +}; +static_assert(sizeof(AudioInParameterInternal) == 0x10, + "AudioInParameterInternal is an invalid size"); + +struct AudioInBuffer { + /* 0x00 */ AudioInBuffer* next; + /* 0x08 */ VAddr samples; + /* 0x10 */ u64 capacity; + /* 0x18 */ u64 size; + /* 0x20 */ u64 offset; +}; +static_assert(sizeof(AudioInBuffer) == 0x28, "AudioInBuffer is an invalid size"); + +enum class State { + Started, + Stopped, +}; + +/** + * Controls and drives audio input. + */ +class System { +public: + explicit System(Core::System& system, Kernel::KEvent* event, size_t session_id); + ~System(); + + /** + * Get the default audio input device name. + * + * @return The default audio input device name. + */ + std::string_view GetDefaultDeviceName(); + + /** + * Get the default USB audio input device name. + * This is preferred over non-USB as some games refuse to work with the BuiltInHeadset + * (e.g Let's Sing). + * + * @return The default USB audio input device name. + */ + std::string_view GetDefaultUacDeviceName(); + + /** + * Is the given initialize config valid? + * + * @param device_name - The name of the requested input device. + * @param in_params - Input parameters, see AudioInParameter. + * @return Result code. + */ + Result IsConfigValid(std::string_view device_name, const AudioInParameter& in_params); + + /** + * Initialize this system. + * + * @param device_name - The name of the requested input device. + * @param in_params - Input parameters, see AudioInParameter. + * @param handle - Unused. + * @param applet_resource_user_id - Unused. + * @return Result code. + */ + Result Initialize(std::string& device_name, const AudioInParameter& in_params, u32 handle, + u64 applet_resource_user_id); + + /** + * Start this system. + * + * @return Result code. + */ + Result Start(); + + /** + * Stop this system. + * + * @return Result code. + */ + Result Stop(); + + /** + * Finalize this system. + */ + void Finalize(); + + /** + * Start this system's device session. + */ + void StartSession(); + + /** + * Get this system's id. + */ + size_t GetSessionId() const; + + /** + * Append a new buffer to the device. + * + * @param buffer - New buffer to append. + * @param tag - Unique tag of the buffer. + * @return True if the buffer was appended, otherwise false. + */ + bool AppendBuffer(const AudioInBuffer& buffer, u64 tag); + + /** + * Register all appended buffers. + */ + void RegisterBuffers(); + + /** + * Release all registered buffers. + */ + void ReleaseBuffers(); + + /** + * Get all released buffers. + * + * @param tags - Container to be filled with the released buffers' tags. + * @return The number of buffers released. + */ + u32 GetReleasedBuffers(std::span tags); + + /** + * Flush all appended and registered buffers. + * + * @return True if buffers were successfully flushed, otherwise false. + */ + bool FlushAudioInBuffers(); + + /** + * Get this system's current channel count. + * + * @return The channel count. + */ + u16 GetChannelCount() const; + + /** + * Get this system's current sample rate. + * + * @return The sample rate. + */ + u32 GetSampleRate() const; + + /** + * Get this system's current sample format. + * + * @return The sample format. + */ + SampleFormat GetSampleFormat() const; + + /** + * Get this system's current state. + * + * @return The current state. + */ + State GetState(); + + /** + * Get this system's name. + * + * @return The system's name. + */ + std::string GetName() const; + + /** + * Get this system's current volume. + * + * @return The system's current volume. + */ + f32 GetVolume() const; + + /** + * Set this system's current volume. + * + * @param The new volume. + */ + void SetVolume(f32 volume); + + /** + * Does the system contain this buffer? + * + * @param tag - Unique tag to search for. + * @return True if the buffer is in the system, otherwise false. + */ + bool ContainsAudioBuffer(u64 tag); + + /** + * Get the maximum number of usable buffers (default 32). + * + * @return The number of buffers. + */ + u32 GetBufferCount(); + + /** + * Get the total number of samples played by this system. + * + * @return The number of samples. + */ + u64 GetPlayedSampleCount() const; + + /** + * Is this system using a USB device? + * + * @return True if using a USB device, otherwise false. + */ + bool IsUac() const; + +private: + /// Core system + Core::System& system; + /// (Unused) + u32 handle{}; + /// (Unused) + u64 applet_resource_user_id{}; + /// Buffer event, signalled when a buffer is ready + Kernel::KEvent* buffer_event; + /// Session id of this system + size_t session_id{}; + /// Device session for this system + std::unique_ptr session; + /// Audio buffers in use by this system + AudioBuffers buffers{BufferCount}; + /// Sample rate of this system + u32 sample_rate{}; + /// Sample format of this system + SampleFormat sample_format{SampleFormat::PcmInt16}; + /// Channel count of this system + u16 channel_count{}; + /// State of this system + std::atomic state{State::Stopped}; + /// Name of this system + std::string name{}; + /// Volume of this system + f32 volume{1.0f}; + /// Is this system's device USB? + bool is_uac{false}; +}; + +} // namespace AudioCore::AudioIn diff --git a/src/audio_core/info_updater.cpp b/src/audio_core/info_updater.cpp deleted file mode 100644 index 0065e6e536..0000000000 --- a/src/audio_core/info_updater.cpp +++ /dev/null @@ -1,511 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "audio_core/behavior_info.h" -#include "audio_core/effect_context.h" -#include "audio_core/info_updater.h" -#include "audio_core/memory_pool.h" -#include "audio_core/mix_context.h" -#include "audio_core/sink_context.h" -#include "audio_core/splitter_context.h" -#include "audio_core/voice_context.h" -#include "common/logging/log.h" - -namespace AudioCore { - -InfoUpdater::InfoUpdater(const std::vector& in_params_, std::vector& out_params_, - BehaviorInfo& behavior_info_) - : in_params(in_params_), out_params(out_params_), behavior_info(behavior_info_) { - ASSERT( - AudioCommon::CanConsumeBuffer(in_params.size(), 0, sizeof(AudioCommon::UpdateDataHeader))); - std::memcpy(&input_header, in_params.data(), sizeof(AudioCommon::UpdateDataHeader)); - output_header.total_size = sizeof(AudioCommon::UpdateDataHeader); -} - -InfoUpdater::~InfoUpdater() = default; - -bool InfoUpdater::UpdateBehaviorInfo(BehaviorInfo& in_behavior_info) { - if (input_header.size.behavior != sizeof(BehaviorInfo::InParams)) { - LOG_ERROR(Audio, "Behavior info is an invalid size, expecting 0x{:X} but got 0x{:X}", - sizeof(BehaviorInfo::InParams), input_header.size.behavior); - return false; - } - - if (!AudioCommon::CanConsumeBuffer(in_params.size(), input_offset, - sizeof(BehaviorInfo::InParams))) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return false; - } - - BehaviorInfo::InParams behavior_in{}; - std::memcpy(&behavior_in, in_params.data() + input_offset, sizeof(BehaviorInfo::InParams)); - input_offset += sizeof(BehaviorInfo::InParams); - - // Make sure it's an audio revision we can actually support - if (!AudioCommon::IsValidRevision(behavior_in.revision)) { - LOG_ERROR(Audio, "Invalid input revision, revision=0x{:08X}", behavior_in.revision); - return false; - } - - // Make sure that our behavior info revision matches the input - if (in_behavior_info.GetUserRevision() != behavior_in.revision) { - LOG_ERROR(Audio, - "User revision differs from input revision, expecting 0x{:08X} but got 0x{:08X}", - in_behavior_info.GetUserRevision(), behavior_in.revision); - return false; - } - - // Update behavior info flags - in_behavior_info.ClearError(); - in_behavior_info.UpdateFlags(behavior_in.flags); - - return true; -} - -bool InfoUpdater::UpdateMemoryPools(std::vector& memory_pool_info) { - const auto memory_pool_count = memory_pool_info.size(); - const auto total_memory_pool_in = sizeof(ServerMemoryPoolInfo::InParams) * memory_pool_count; - const auto total_memory_pool_out = sizeof(ServerMemoryPoolInfo::OutParams) * memory_pool_count; - - if (input_header.size.memory_pool != total_memory_pool_in) { - LOG_ERROR(Audio, "Memory pools are an invalid size, expecting 0x{:X} but got 0x{:X}", - total_memory_pool_in, input_header.size.memory_pool); - return false; - } - - if (!AudioCommon::CanConsumeBuffer(in_params.size(), input_offset, total_memory_pool_in)) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return false; - } - - std::vector mempool_in(memory_pool_count); - std::vector mempool_out(memory_pool_count); - - std::memcpy(mempool_in.data(), in_params.data() + input_offset, total_memory_pool_in); - input_offset += total_memory_pool_in; - - // Update our memory pools - for (std::size_t i = 0; i < memory_pool_count; i++) { - if (!memory_pool_info[i].Update(mempool_in[i], mempool_out[i])) { - LOG_ERROR(Audio, "Failed to update memory pool {}!", i); - return false; - } - } - - if (!AudioCommon::CanConsumeBuffer(out_params.size(), output_offset, - sizeof(BehaviorInfo::InParams))) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return false; - } - - std::memcpy(out_params.data() + output_offset, mempool_out.data(), total_memory_pool_out); - output_offset += total_memory_pool_out; - output_header.size.memory_pool = static_cast(total_memory_pool_out); - return true; -} - -bool InfoUpdater::UpdateVoiceChannelResources(VoiceContext& voice_context) { - const auto voice_count = voice_context.GetVoiceCount(); - const auto voice_size = voice_count * sizeof(VoiceChannelResource::InParams); - std::vector resources_in(voice_count); - - if (input_header.size.voice_channel_resource != voice_size) { - LOG_ERROR(Audio, "VoiceChannelResource is an invalid size, expecting 0x{:X} but got 0x{:X}", - voice_size, input_header.size.voice_channel_resource); - return false; - } - - if (!AudioCommon::CanConsumeBuffer(in_params.size(), input_offset, voice_size)) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return false; - } - - std::memcpy(resources_in.data(), in_params.data() + input_offset, voice_size); - input_offset += voice_size; - - // Update our channel resources - for (std::size_t i = 0; i < voice_count; i++) { - // Grab our channel resource - auto& resource = voice_context.GetChannelResource(i); - resource.Update(resources_in[i]); - } - - return true; -} - -bool InfoUpdater::UpdateVoices(VoiceContext& voice_context, - [[maybe_unused]] std::vector& memory_pool_info, - [[maybe_unused]] VAddr audio_codec_dsp_addr) { - const auto voice_count = voice_context.GetVoiceCount(); - std::vector voice_in(voice_count); - std::vector voice_out(voice_count); - - const auto voice_in_size = voice_count * sizeof(VoiceInfo::InParams); - const auto voice_out_size = voice_count * sizeof(VoiceInfo::OutParams); - - if (input_header.size.voice != voice_in_size) { - LOG_ERROR(Audio, "Voices are an invalid size, expecting 0x{:X} but got 0x{:X}", - voice_in_size, input_header.size.voice); - return false; - } - - if (!AudioCommon::CanConsumeBuffer(in_params.size(), input_offset, voice_in_size)) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return false; - } - - std::memcpy(voice_in.data(), in_params.data() + input_offset, voice_in_size); - input_offset += voice_in_size; - - // Set all voices to not be in use - for (std::size_t i = 0; i < voice_count; i++) { - voice_context.GetInfo(i).GetInParams().in_use = false; - } - - // Update our voices - for (std::size_t i = 0; i < voice_count; i++) { - auto& voice_in_params = voice_in[i]; - const auto channel_count = static_cast(voice_in_params.channel_count); - // Skip if it's not currently in use - if (!voice_in_params.is_in_use) { - continue; - } - // Voice states for each channel - std::array voice_states{}; - ASSERT(static_cast(voice_in_params.id) < voice_count); - - // Grab our current voice info - auto& voice_info = voice_context.GetInfo(static_cast(voice_in_params.id)); - - ASSERT(channel_count <= AudioCommon::MAX_CHANNEL_COUNT); - - // Get all our channel voice states - for (std::size_t channel = 0; channel < channel_count; channel++) { - voice_states[channel] = - &voice_context.GetState(voice_in_params.voice_channel_resource_ids[channel]); - } - - if (voice_in_params.is_new) { - // Default our values for our voice - voice_info.Initialize(); - - // Zero out our voice states - for (std::size_t channel = 0; channel < channel_count; channel++) { - std::memset(voice_states[channel], 0, sizeof(VoiceState)); - } - } - - // Update our voice - voice_info.UpdateParameters(voice_in_params, behavior_info); - // TODO(ogniK): Handle mapping errors with behavior info based on in params response - - // Update our wave buffers - voice_info.UpdateWaveBuffers(voice_in_params, voice_states, behavior_info); - voice_info.WriteOutStatus(voice_out[i], voice_in_params, voice_states); - } - - if (!AudioCommon::CanConsumeBuffer(out_params.size(), output_offset, voice_out_size)) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return false; - } - std::memcpy(out_params.data() + output_offset, voice_out.data(), voice_out_size); - output_offset += voice_out_size; - output_header.size.voice = static_cast(voice_out_size); - return true; -} - -bool InfoUpdater::UpdateEffects(EffectContext& effect_context, bool is_active) { - const auto effect_count = effect_context.GetCount(); - std::vector effect_in(effect_count); - std::vector effect_out(effect_count); - - const auto total_effect_in = effect_count * sizeof(EffectInfo::InParams); - const auto total_effect_out = effect_count * sizeof(EffectInfo::OutParams); - - if (input_header.size.effect != total_effect_in) { - LOG_ERROR(Audio, "Effects are an invalid size, expecting 0x{:X} but got 0x{:X}", - total_effect_in, input_header.size.effect); - return false; - } - - if (!AudioCommon::CanConsumeBuffer(in_params.size(), input_offset, total_effect_in)) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return false; - } - - std::memcpy(effect_in.data(), in_params.data() + input_offset, total_effect_in); - input_offset += total_effect_in; - - // Update effects - for (std::size_t i = 0; i < effect_count; i++) { - auto* info = effect_context.GetInfo(i); - if (effect_in[i].type != info->GetType()) { - info = effect_context.RetargetEffect(i, effect_in[i].type); - } - - info->Update(effect_in[i]); - - if ((!is_active && info->GetUsage() != UsageState::Initialized) || - info->GetUsage() == UsageState::Stopped) { - effect_out[i].status = UsageStatus::Removed; - } else { - effect_out[i].status = UsageStatus::Used; - } - } - - if (!AudioCommon::CanConsumeBuffer(out_params.size(), output_offset, total_effect_out)) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return false; - } - - std::memcpy(out_params.data() + output_offset, effect_out.data(), total_effect_out); - output_offset += total_effect_out; - output_header.size.effect = static_cast(total_effect_out); - - return true; -} - -bool InfoUpdater::UpdateSplitterInfo(SplitterContext& splitter_context) { - std::size_t start_offset = input_offset; - std::size_t bytes_read{}; - // Update splitter context - if (!splitter_context.Update(in_params, input_offset, bytes_read)) { - LOG_ERROR(Audio, "Failed to update splitter context!"); - return false; - } - - const auto consumed = input_offset - start_offset; - - if (input_header.size.splitter != consumed) { - LOG_ERROR(Audio, "Splitters is an invalid size, expecting 0x{:X} but got 0x{:X}", - bytes_read, input_header.size.splitter); - return false; - } - - return true; -} - -Result InfoUpdater::UpdateMixes(MixContext& mix_context, std::size_t mix_buffer_count, - SplitterContext& splitter_context, EffectContext& effect_context) { - std::vector mix_in_params; - - if (!behavior_info.IsMixInParameterDirtyOnlyUpdateSupported()) { - // If we're not dirty, get ALL mix in parameters - const auto context_mix_count = mix_context.GetCount(); - const auto total_mix_in = context_mix_count * sizeof(MixInfo::InParams); - if (input_header.size.mixer != total_mix_in) { - LOG_ERROR(Audio, "Mixer is an invalid size, expecting 0x{:X} but got 0x{:X}", - total_mix_in, input_header.size.mixer); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - - if (!AudioCommon::CanConsumeBuffer(in_params.size(), input_offset, total_mix_in)) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - - mix_in_params.resize(context_mix_count); - std::memcpy(mix_in_params.data(), in_params.data() + input_offset, total_mix_in); - - input_offset += total_mix_in; - } else { - // Only update the "dirty" mixes - MixInfo::DirtyHeader dirty_header{}; - if (!AudioCommon::CanConsumeBuffer(in_params.size(), input_offset, - sizeof(MixInfo::DirtyHeader))) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - - std::memcpy(&dirty_header, in_params.data() + input_offset, sizeof(MixInfo::DirtyHeader)); - input_offset += sizeof(MixInfo::DirtyHeader); - - const auto total_mix_in = - dirty_header.mixer_count * sizeof(MixInfo::InParams) + sizeof(MixInfo::DirtyHeader); - - if (input_header.size.mixer != total_mix_in) { - LOG_ERROR(Audio, "Mixer is an invalid size, expecting 0x{:X} but got 0x{:X}", - total_mix_in, input_header.size.mixer); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - - if (dirty_header.mixer_count != 0) { - mix_in_params.resize(dirty_header.mixer_count); - std::memcpy(mix_in_params.data(), in_params.data() + input_offset, - mix_in_params.size() * sizeof(MixInfo::InParams)); - input_offset += mix_in_params.size() * sizeof(MixInfo::InParams); - } - } - - // Get our total input count - const auto mix_count = mix_in_params.size(); - - if (!behavior_info.IsMixInParameterDirtyOnlyUpdateSupported()) { - // Only verify our buffer count if we're not dirty - std::size_t total_buffer_count{}; - for (std::size_t i = 0; i < mix_count; i++) { - const auto& in = mix_in_params[i]; - total_buffer_count += in.buffer_count; - if (static_cast(in.dest_mix_id) > mix_count && - in.dest_mix_id != AudioCommon::NO_MIX && in.mix_id != AudioCommon::FINAL_MIX) { - LOG_ERROR( - Audio, - "Invalid mix destination, mix_id={:X}, dest_mix_id={:X}, mix_buffer_count={:X}", - in.mix_id, in.dest_mix_id, mix_buffer_count); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - } - - if (total_buffer_count > mix_buffer_count) { - LOG_ERROR(Audio, - "Too many mix buffers used! mix_buffer_count={:X}, requesting_buffers={:X}", - mix_buffer_count, total_buffer_count); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - } - - if (mix_buffer_count == 0) { - LOG_ERROR(Audio, "No mix buffers!"); - return AudioCommon::Audren::ERR_INVALID_PARAMETERS; - } - - bool should_sort = false; - for (std::size_t i = 0; i < mix_count; i++) { - const auto& mix_in = mix_in_params[i]; - std::size_t target_mix{}; - if (behavior_info.IsMixInParameterDirtyOnlyUpdateSupported()) { - target_mix = mix_in.mix_id; - } else { - // Non dirty supported games just use i instead of the actual mix_id - target_mix = i; - } - auto& mix_info = mix_context.GetInfo(target_mix); - auto& mix_info_params = mix_info.GetInParams(); - if (mix_info_params.in_use != mix_in.in_use) { - mix_info_params.in_use = mix_in.in_use; - mix_info.ResetEffectProcessingOrder(); - should_sort = true; - } - - if (mix_in.in_use) { - should_sort |= mix_info.Update(mix_context.GetEdgeMatrix(), mix_in, behavior_info, - splitter_context, effect_context); - } - } - - if (should_sort && behavior_info.IsSplitterSupported()) { - // Sort our splitter data - if (!mix_context.TsortInfo(splitter_context)) { - return AudioCommon::Audren::ERR_SPLITTER_SORT_FAILED; - } - } - - // TODO(ogniK): Sort when splitter is suppoorted - - return ResultSuccess; -} - -bool InfoUpdater::UpdateSinks(SinkContext& sink_context) { - const auto sink_count = sink_context.GetCount(); - std::vector sink_in_params(sink_count); - const auto total_sink_in = sink_count * sizeof(SinkInfo::InParams); - - if (input_header.size.sink != total_sink_in) { - LOG_ERROR(Audio, "Sinks are an invalid size, expecting 0x{:X} but got 0x{:X}", - total_sink_in, input_header.size.effect); - return false; - } - - if (!AudioCommon::CanConsumeBuffer(in_params.size(), input_offset, total_sink_in)) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return false; - } - - std::memcpy(sink_in_params.data(), in_params.data() + input_offset, total_sink_in); - input_offset += total_sink_in; - - // TODO(ogniK): Properly update sinks - if (!sink_in_params.empty()) { - sink_context.UpdateMainSink(sink_in_params[0]); - } - - output_header.size.sink = static_cast(0x20 * sink_count); - output_offset += 0x20 * sink_count; - return true; -} - -bool InfoUpdater::UpdatePerformanceBuffer() { - output_header.size.performance = 0x10; - output_offset += 0x10; - return true; -} - -bool InfoUpdater::UpdateErrorInfo([[maybe_unused]] BehaviorInfo& in_behavior_info) { - const auto total_beahvior_info_out = sizeof(BehaviorInfo::OutParams); - - if (!AudioCommon::CanConsumeBuffer(out_params.size(), output_offset, total_beahvior_info_out)) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return false; - } - - BehaviorInfo::OutParams behavior_info_out{}; - behavior_info.CopyErrorInfo(behavior_info_out); - - std::memcpy(out_params.data() + output_offset, &behavior_info_out, total_beahvior_info_out); - output_offset += total_beahvior_info_out; - output_header.size.behavior = total_beahvior_info_out; - - return true; -} - -struct RendererInfo { - u64_le elasped_frame_count{}; - INSERT_PADDING_WORDS(2); -}; -static_assert(sizeof(RendererInfo) == 0x10, "RendererInfo is an invalid size"); - -bool InfoUpdater::UpdateRendererInfo(std::size_t elapsed_frame_count) { - const auto total_renderer_info_out = sizeof(RendererInfo); - if (!AudioCommon::CanConsumeBuffer(out_params.size(), output_offset, total_renderer_info_out)) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return false; - } - RendererInfo out{}; - out.elasped_frame_count = elapsed_frame_count; - std::memcpy(out_params.data() + output_offset, &out, total_renderer_info_out); - output_offset += total_renderer_info_out; - output_header.size.render_info = total_renderer_info_out; - - return true; -} - -bool InfoUpdater::CheckConsumedSize() const { - if (output_offset != out_params.size()) { - LOG_ERROR(Audio, "Output is not consumed! Consumed {}, but requires {}. {} bytes remaining", - output_offset, out_params.size(), out_params.size() - output_offset); - return false; - } - /*if (input_offset != in_params.size()) { - LOG_ERROR(Audio, "Input is not consumed!"); - return false; - }*/ - return true; -} - -bool InfoUpdater::WriteOutputHeader() { - if (!AudioCommon::CanConsumeBuffer(out_params.size(), 0, - sizeof(AudioCommon::UpdateDataHeader))) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return false; - } - output_header.revision = AudioCommon::CURRENT_PROCESS_REVISION; - const auto& sz = output_header.size; - output_header.total_size += sz.behavior + sz.memory_pool + sz.voice + - sz.voice_channel_resource + sz.effect + sz.mixer + sz.sink + - sz.performance + sz.splitter + sz.render_info; - - std::memcpy(out_params.data(), &output_header, sizeof(AudioCommon::UpdateDataHeader)); - return true; -} - -} // namespace AudioCore diff --git a/src/audio_core/info_updater.h b/src/audio_core/info_updater.h deleted file mode 100644 index 17e66b0363..0000000000 --- a/src/audio_core/info_updater.h +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include "audio_core/common.h" -#include "common/common_types.h" - -namespace AudioCore { - -class BehaviorInfo; -class ServerMemoryPoolInfo; -class VoiceContext; -class EffectContext; -class MixContext; -class SinkContext; -class SplitterContext; - -class InfoUpdater { -public: - // TODO(ogniK): Pass process handle when we support it - InfoUpdater(const std::vector& in_params_, std::vector& out_params_, - BehaviorInfo& behavior_info_); - ~InfoUpdater(); - - bool UpdateBehaviorInfo(BehaviorInfo& in_behavior_info); - bool UpdateMemoryPools(std::vector& memory_pool_info); - bool UpdateVoiceChannelResources(VoiceContext& voice_context); - bool UpdateVoices(VoiceContext& voice_context, - std::vector& memory_pool_info, - VAddr audio_codec_dsp_addr); - bool UpdateEffects(EffectContext& effect_context, bool is_active); - bool UpdateSplitterInfo(SplitterContext& splitter_context); - Result UpdateMixes(MixContext& mix_context, std::size_t mix_buffer_count, - SplitterContext& splitter_context, EffectContext& effect_context); - bool UpdateSinks(SinkContext& sink_context); - bool UpdatePerformanceBuffer(); - bool UpdateErrorInfo(BehaviorInfo& in_behavior_info); - bool UpdateRendererInfo(std::size_t elapsed_frame_count); - bool CheckConsumedSize() const; - - bool WriteOutputHeader(); - -private: - const std::vector& in_params; - std::vector& out_params; - BehaviorInfo& behavior_info; - - AudioCommon::UpdateDataHeader input_header{}; - AudioCommon::UpdateDataHeader output_header{}; - - std::size_t input_offset{sizeof(AudioCommon::UpdateDataHeader)}; - std::size_t output_offset{sizeof(AudioCommon::UpdateDataHeader)}; -}; - -} // namespace AudioCore diff --git a/src/audio_core/memory_pool.cpp b/src/audio_core/memory_pool.cpp deleted file mode 100644 index 627e5f15e6..0000000000 --- a/src/audio_core/memory_pool.cpp +++ /dev/null @@ -1,60 +0,0 @@ - -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "audio_core/memory_pool.h" -#include "common/logging/log.h" - -namespace AudioCore { - -ServerMemoryPoolInfo::ServerMemoryPoolInfo() = default; -ServerMemoryPoolInfo::~ServerMemoryPoolInfo() = default; - -bool ServerMemoryPoolInfo::Update(const InParams& in_params, OutParams& out_params) { - // Our state does not need to be changed - if (in_params.state != State::RequestAttach && in_params.state != State::RequestDetach) { - return true; - } - - // Address or size is null - if (in_params.address == 0 || in_params.size == 0) { - LOG_ERROR(Audio, "Memory pool address or size is zero! address={:X}, size={:X}", - in_params.address, in_params.size); - return false; - } - - // Address or size is not aligned - if ((in_params.address % 0x1000) != 0 || (in_params.size % 0x1000) != 0) { - LOG_ERROR(Audio, "Memory pool address or size is not aligned! address={:X}, size={:X}", - in_params.address, in_params.size); - return false; - } - - if (in_params.state == State::RequestAttach) { - cpu_address = in_params.address; - size = in_params.size; - used = true; - out_params.state = State::Attached; - } else { - // Unexpected address - if (cpu_address != in_params.address) { - LOG_ERROR(Audio, "Memory pool address differs! Expecting {:X} but address is {:X}", - cpu_address, in_params.address); - return false; - } - - if (size != in_params.size) { - LOG_ERROR(Audio, "Memory pool size differs! Expecting {:X} but size is {:X}", size, - in_params.size); - return false; - } - - cpu_address = 0; - size = 0; - used = false; - out_params.state = State::Detached; - } - return true; -} - -} // namespace AudioCore diff --git a/src/audio_core/memory_pool.h b/src/audio_core/memory_pool.h deleted file mode 100644 index e71bc025bc..0000000000 --- a/src/audio_core/memory_pool.h +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include "common/common_funcs.h" -#include "common/common_types.h" -#include "common/swap.h" - -namespace AudioCore { - -class ServerMemoryPoolInfo { -public: - ServerMemoryPoolInfo(); - ~ServerMemoryPoolInfo(); - - enum class State : u32_le { - Invalid = 0x0, - Aquired = 0x1, - RequestDetach = 0x2, - Detached = 0x3, - RequestAttach = 0x4, - Attached = 0x5, - Released = 0x6, - }; - - struct InParams { - u64_le address{}; - u64_le size{}; - State state{}; - INSERT_PADDING_WORDS(3); - }; - static_assert(sizeof(InParams) == 0x20, "InParams are an invalid size"); - - struct OutParams { - State state{}; - INSERT_PADDING_WORDS(3); - }; - static_assert(sizeof(OutParams) == 0x10, "OutParams are an invalid size"); - - bool Update(const InParams& in_params, OutParams& out_params); - -private: - // There's another entry here which is the DSP address, however since we're not talking to the - // DSP we can just use the same address provided by the guest without needing to remap - u64_le cpu_address{}; - u64_le size{}; - bool used{}; -}; - -} // namespace AudioCore diff --git a/src/audio_core/mix_context.cpp b/src/audio_core/mix_context.cpp deleted file mode 100644 index bcaa7afabd..0000000000 --- a/src/audio_core/mix_context.cpp +++ /dev/null @@ -1,297 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include - -#include "audio_core/behavior_info.h" -#include "audio_core/common.h" -#include "audio_core/effect_context.h" -#include "audio_core/mix_context.h" -#include "audio_core/splitter_context.h" - -namespace AudioCore { -MixContext::MixContext() = default; -MixContext::~MixContext() = default; - -void MixContext::Initialize(const BehaviorInfo& behavior_info, std::size_t mix_count, - std::size_t effect_count) { - info_count = mix_count; - infos.resize(info_count); - auto& final_mix = GetInfo(AudioCommon::FINAL_MIX); - final_mix.GetInParams().mix_id = AudioCommon::FINAL_MIX; - sorted_info.reserve(infos.size()); - for (auto& info : infos) { - sorted_info.push_back(&info); - } - - for (auto& info : infos) { - info.SetEffectCount(effect_count); - } - - // Only initialize our edge matrix and node states if splitters are supported - if (behavior_info.IsSplitterSupported()) { - node_states.Initialize(mix_count); - edge_matrix.Initialize(mix_count); - } -} - -void MixContext::UpdateDistancesFromFinalMix() { - // Set all distances to be invalid - for (std::size_t i = 0; i < info_count; i++) { - GetInfo(i).GetInParams().final_mix_distance = AudioCommon::NO_FINAL_MIX; - } - - for (std::size_t i = 0; i < info_count; i++) { - auto& info = GetInfo(i); - auto& in_params = info.GetInParams(); - // Populate our sorted info - sorted_info[i] = &info; - - if (!in_params.in_use) { - continue; - } - - auto mix_id = in_params.mix_id; - // Needs to be referenced out of scope - s32 distance_to_final_mix{AudioCommon::FINAL_MIX}; - for (; distance_to_final_mix < static_cast(info_count); distance_to_final_mix++) { - if (mix_id == AudioCommon::FINAL_MIX) { - // If we're at the final mix, we're done - break; - } else if (mix_id == AudioCommon::NO_MIX) { - // If we have no more mix ids, we're done - distance_to_final_mix = AudioCommon::NO_FINAL_MIX; - break; - } else { - const auto& dest_mix = GetInfo(mix_id); - const auto dest_mix_distance = dest_mix.GetInParams().final_mix_distance; - - if (dest_mix_distance == AudioCommon::NO_FINAL_MIX) { - // If our current mix isn't pointing to a final mix, follow through - mix_id = dest_mix.GetInParams().dest_mix_id; - } else { - // Our current mix + 1 = final distance - distance_to_final_mix = dest_mix_distance + 1; - break; - } - } - } - - // If we're out of range for our distance, mark it as no final mix - if (distance_to_final_mix >= static_cast(info_count)) { - distance_to_final_mix = AudioCommon::NO_FINAL_MIX; - } - - in_params.final_mix_distance = distance_to_final_mix; - } -} - -void MixContext::CalcMixBufferOffset() { - s32 offset{}; - for (std::size_t i = 0; i < info_count; i++) { - auto& info = GetSortedInfo(i); - auto& in_params = info.GetInParams(); - if (in_params.in_use) { - // Only update if in use - in_params.buffer_offset = offset; - offset += in_params.buffer_count; - } - } -} - -void MixContext::SortInfo() { - // Get the distance to the final mix - UpdateDistancesFromFinalMix(); - - // Sort based on the distance to the final mix - std::sort(sorted_info.begin(), sorted_info.end(), - [](const ServerMixInfo* lhs, const ServerMixInfo* rhs) { - return lhs->GetInParams().final_mix_distance > - rhs->GetInParams().final_mix_distance; - }); - - // Calculate the mix buffer offset - CalcMixBufferOffset(); -} - -bool MixContext::TsortInfo(SplitterContext& splitter_context) { - // If we're not using mixes, just calculate the mix buffer offset - if (!splitter_context.UsingSplitter()) { - CalcMixBufferOffset(); - return true; - } - // Sort our node states - if (!node_states.Tsort(edge_matrix)) { - return false; - } - - // Get our sorted list - const auto sorted_list = node_states.GetIndexList(); - std::size_t info_id{}; - for (auto itr = sorted_list.rbegin(); itr != sorted_list.rend(); ++itr) { - // Set our sorted info - sorted_info[info_id++] = &GetInfo(*itr); - } - - // Calculate the mix buffer offset - CalcMixBufferOffset(); - return true; -} - -std::size_t MixContext::GetCount() const { - return info_count; -} - -ServerMixInfo& MixContext::GetInfo(std::size_t i) { - ASSERT(i < info_count); - return infos.at(i); -} - -const ServerMixInfo& MixContext::GetInfo(std::size_t i) const { - ASSERT(i < info_count); - return infos.at(i); -} - -ServerMixInfo& MixContext::GetSortedInfo(std::size_t i) { - ASSERT(i < info_count); - return *sorted_info.at(i); -} - -const ServerMixInfo& MixContext::GetSortedInfo(std::size_t i) const { - ASSERT(i < info_count); - return *sorted_info.at(i); -} - -ServerMixInfo& MixContext::GetFinalMixInfo() { - return infos.at(AudioCommon::FINAL_MIX); -} - -const ServerMixInfo& MixContext::GetFinalMixInfo() const { - return infos.at(AudioCommon::FINAL_MIX); -} - -EdgeMatrix& MixContext::GetEdgeMatrix() { - return edge_matrix; -} - -const EdgeMatrix& MixContext::GetEdgeMatrix() const { - return edge_matrix; -} - -ServerMixInfo::ServerMixInfo() { - Cleanup(); -} -ServerMixInfo::~ServerMixInfo() = default; - -const ServerMixInfo::InParams& ServerMixInfo::GetInParams() const { - return in_params; -} - -ServerMixInfo::InParams& ServerMixInfo::GetInParams() { - return in_params; -} - -bool ServerMixInfo::Update(EdgeMatrix& edge_matrix, const MixInfo::InParams& mix_in, - BehaviorInfo& behavior_info, SplitterContext& splitter_context, - EffectContext& effect_context) { - in_params.volume = mix_in.volume; - in_params.sample_rate = mix_in.sample_rate; - in_params.buffer_count = mix_in.buffer_count; - in_params.in_use = mix_in.in_use; - in_params.mix_id = mix_in.mix_id; - in_params.node_id = mix_in.node_id; - for (std::size_t i = 0; i < mix_in.mix_volume.size(); i++) { - std::copy(mix_in.mix_volume[i].begin(), mix_in.mix_volume[i].end(), - in_params.mix_volume[i].begin()); - } - - bool require_sort = false; - - if (behavior_info.IsSplitterSupported()) { - require_sort = UpdateConnection(edge_matrix, mix_in, splitter_context); - } else { - in_params.dest_mix_id = mix_in.dest_mix_id; - in_params.splitter_id = AudioCommon::NO_SPLITTER; - } - - ResetEffectProcessingOrder(); - const auto effect_count = effect_context.GetCount(); - for (std::size_t i = 0; i < effect_count; i++) { - auto* effect_info = effect_context.GetInfo(i); - if (effect_info->GetMixID() == in_params.mix_id) { - effect_processing_order[effect_info->GetProcessingOrder()] = static_cast(i); - } - } - - // TODO(ogniK): Update effect processing order - return require_sort; -} - -bool ServerMixInfo::HasAnyConnection() const { - return in_params.splitter_id != AudioCommon::NO_SPLITTER || - in_params.mix_id != AudioCommon::NO_MIX; -} - -void ServerMixInfo::Cleanup() { - in_params.volume = 0.0f; - in_params.sample_rate = 0; - in_params.buffer_count = 0; - in_params.in_use = false; - in_params.mix_id = AudioCommon::NO_MIX; - in_params.node_id = 0; - in_params.buffer_offset = 0; - in_params.dest_mix_id = AudioCommon::NO_MIX; - in_params.splitter_id = AudioCommon::NO_SPLITTER; - std::memset(in_params.mix_volume.data(), 0, sizeof(float) * in_params.mix_volume.size()); -} - -void ServerMixInfo::SetEffectCount(std::size_t count) { - effect_processing_order.resize(count); - ResetEffectProcessingOrder(); -} - -void ServerMixInfo::ResetEffectProcessingOrder() { - for (auto& order : effect_processing_order) { - order = AudioCommon::NO_EFFECT_ORDER; - } -} - -s32 ServerMixInfo::GetEffectOrder(std::size_t i) const { - return effect_processing_order.at(i); -} - -bool ServerMixInfo::UpdateConnection(EdgeMatrix& edge_matrix, const MixInfo::InParams& mix_in, - SplitterContext& splitter_context) { - // Mixes are identical - if (in_params.dest_mix_id == mix_in.dest_mix_id && - in_params.splitter_id == mix_in.splitter_id && - ((in_params.splitter_id == AudioCommon::NO_SPLITTER) || - !splitter_context.GetInfo(in_params.splitter_id).HasNewConnection())) { - return false; - } - // Remove current edges for mix id - edge_matrix.RemoveEdges(in_params.mix_id); - if (mix_in.dest_mix_id != AudioCommon::NO_MIX) { - // If we have a valid destination mix id, set our edge matrix - edge_matrix.Connect(in_params.mix_id, mix_in.dest_mix_id); - } else if (mix_in.splitter_id != AudioCommon::NO_SPLITTER) { - // Recurse our splitter linked and set our edges - auto& splitter_info = splitter_context.GetInfo(mix_in.splitter_id); - const auto length = splitter_info.GetLength(); - for (s32 i = 0; i < length; i++) { - const auto* splitter_destination = - splitter_context.GetDestinationData(mix_in.splitter_id, i); - if (splitter_destination == nullptr) { - continue; - } - if (splitter_destination->ValidMixId()) { - edge_matrix.Connect(in_params.mix_id, splitter_destination->GetMixId()); - } - } - } - in_params.dest_mix_id = mix_in.dest_mix_id; - in_params.splitter_id = mix_in.splitter_id; - return true; -} - -} // namespace AudioCore diff --git a/src/audio_core/mix_context.h b/src/audio_core/mix_context.h deleted file mode 100644 index 3939c77e93..0000000000 --- a/src/audio_core/mix_context.h +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include "audio_core/common.h" -#include "audio_core/splitter_context.h" -#include "common/common_funcs.h" -#include "common/common_types.h" - -namespace AudioCore { -class BehaviorInfo; -class EffectContext; - -class MixInfo { -public: - struct DirtyHeader { - u32_le magic{}; - u32_le mixer_count{}; - INSERT_PADDING_BYTES(0x18); - }; - static_assert(sizeof(DirtyHeader) == 0x20, "MixInfo::DirtyHeader is an invalid size"); - - struct InParams { - float_le volume{}; - s32_le sample_rate{}; - s32_le buffer_count{}; - bool in_use{}; - INSERT_PADDING_BYTES(3); - s32_le mix_id{}; - s32_le effect_count{}; - u32_le node_id{}; - INSERT_PADDING_WORDS(2); - std::array, AudioCommon::MAX_MIX_BUFFERS> - mix_volume{}; - s32_le dest_mix_id{}; - s32_le splitter_id{}; - INSERT_PADDING_WORDS(1); - }; - static_assert(sizeof(MixInfo::InParams) == 0x930, "MixInfo::InParams is an invalid size"); -}; - -class ServerMixInfo { -public: - struct InParams { - float volume{}; - s32 sample_rate{}; - s32 buffer_count{}; - bool in_use{}; - s32 mix_id{}; - u32 node_id{}; - std::array, AudioCommon::MAX_MIX_BUFFERS> - mix_volume{}; - s32 dest_mix_id{}; - s32 splitter_id{}; - s32 buffer_offset{}; - s32 final_mix_distance{}; - }; - ServerMixInfo(); - ~ServerMixInfo(); - - [[nodiscard]] const ServerMixInfo::InParams& GetInParams() const; - [[nodiscard]] ServerMixInfo::InParams& GetInParams(); - - bool Update(EdgeMatrix& edge_matrix, const MixInfo::InParams& mix_in, - BehaviorInfo& behavior_info, SplitterContext& splitter_context, - EffectContext& effect_context); - [[nodiscard]] bool HasAnyConnection() const; - void Cleanup(); - void SetEffectCount(std::size_t count); - void ResetEffectProcessingOrder(); - [[nodiscard]] s32 GetEffectOrder(std::size_t i) const; - -private: - std::vector effect_processing_order; - InParams in_params{}; - bool UpdateConnection(EdgeMatrix& edge_matrix, const MixInfo::InParams& mix_in, - SplitterContext& splitter_context); -}; - -class MixContext { -public: - MixContext(); - ~MixContext(); - - void Initialize(const BehaviorInfo& behavior_info, std::size_t mix_count, - std::size_t effect_count); - void SortInfo(); - bool TsortInfo(SplitterContext& splitter_context); - - [[nodiscard]] std::size_t GetCount() const; - [[nodiscard]] ServerMixInfo& GetInfo(std::size_t i); - [[nodiscard]] const ServerMixInfo& GetInfo(std::size_t i) const; - [[nodiscard]] ServerMixInfo& GetSortedInfo(std::size_t i); - [[nodiscard]] const ServerMixInfo& GetSortedInfo(std::size_t i) const; - [[nodiscard]] ServerMixInfo& GetFinalMixInfo(); - [[nodiscard]] const ServerMixInfo& GetFinalMixInfo() const; - [[nodiscard]] EdgeMatrix& GetEdgeMatrix(); - [[nodiscard]] const EdgeMatrix& GetEdgeMatrix() const; - -private: - void CalcMixBufferOffset(); - void UpdateDistancesFromFinalMix(); - - NodeStates node_states{}; - EdgeMatrix edge_matrix{}; - std::size_t info_count{}; - std::vector infos{}; - std::vector sorted_info{}; -}; -} // namespace AudioCore diff --git a/src/audio_core/null_sink.h b/src/audio_core/null_sink.h deleted file mode 100644 index 37b2f7eff2..0000000000 --- a/src/audio_core/null_sink.h +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include "audio_core/sink.h" - -namespace AudioCore { - -class NullSink final : public Sink { -public: - explicit NullSink(std::string_view) {} - ~NullSink() override = default; - - SinkStream& AcquireSinkStream(u32 /*sample_rate*/, u32 /*num_channels*/, - const std::string& /*name*/) override { - return null_sink_stream; - } - -private: - struct NullSinkStreamImpl final : SinkStream { - void EnqueueSamples(u32 /*num_channels*/, const std::vector& /*samples*/) override {} - - std::size_t SamplesInQueue(u32 /*num_channels*/) const override { - return 0; - } - - void Flush() override {} - } null_sink_stream; -}; - -} // namespace AudioCore diff --git a/src/audio_core/out/audio_out.cpp b/src/audio_core/out/audio_out.cpp new file mode 100644 index 0000000000..9a8d8a7421 --- /dev/null +++ b/src/audio_core/out/audio_out.cpp @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_out_manager.h" +#include "audio_core/out/audio_out.h" +#include "core/hle/kernel/k_event.h" + +namespace AudioCore::AudioOut { + +Out::Out(Core::System& system_, Manager& manager_, Kernel::KEvent* event_, size_t session_id_) + : manager{manager_}, parent_mutex{manager.mutex}, event{event_}, system{system_, event, + session_id_} {} + +void Out::Free() { + std::scoped_lock l{parent_mutex}; + manager.ReleaseSessionId(system.GetSessionId()); +} + +System& Out::GetSystem() { + return system; +} + +AudioOut::State Out::GetState() { + std::scoped_lock l{parent_mutex}; + return system.GetState(); +} + +Result Out::StartSystem() { + std::scoped_lock l{parent_mutex}; + return system.Start(); +} + +void Out::StartSession() { + std::scoped_lock l{parent_mutex}; + system.StartSession(); +} + +Result Out::StopSystem() { + std::scoped_lock l{parent_mutex}; + return system.Stop(); +} + +Result Out::AppendBuffer(const AudioOutBuffer& buffer, const u64 tag) { + std::scoped_lock l{parent_mutex}; + + if (system.AppendBuffer(buffer, tag)) { + return ResultSuccess; + } + return Service::Audio::ERR_BUFFER_COUNT_EXCEEDED; +} + +void Out::ReleaseAndRegisterBuffers() { + std::scoped_lock l{parent_mutex}; + if (system.GetState() == State::Started) { + system.ReleaseBuffers(); + system.RegisterBuffers(); + } +} + +bool Out::FlushAudioOutBuffers() { + std::scoped_lock l{parent_mutex}; + return system.FlushAudioOutBuffers(); +} + +u32 Out::GetReleasedBuffers(std::span tags) { + std::scoped_lock l{parent_mutex}; + return system.GetReleasedBuffers(tags); +} + +Kernel::KReadableEvent& Out::GetBufferEvent() { + std::scoped_lock l{parent_mutex}; + return event->GetReadableEvent(); +} + +f32 Out::GetVolume() { + std::scoped_lock l{parent_mutex}; + return system.GetVolume(); +} + +void Out::SetVolume(const f32 volume) { + std::scoped_lock l{parent_mutex}; + system.SetVolume(volume); +} + +bool Out::ContainsAudioBuffer(const u64 tag) { + std::scoped_lock l{parent_mutex}; + return system.ContainsAudioBuffer(tag); +} + +u32 Out::GetBufferCount() { + std::scoped_lock l{parent_mutex}; + return system.GetBufferCount(); +} + +u64 Out::GetPlayedSampleCount() { + std::scoped_lock l{parent_mutex}; + return system.GetPlayedSampleCount(); +} + +} // namespace AudioCore::AudioOut diff --git a/src/audio_core/out/audio_out.h b/src/audio_core/out/audio_out.h new file mode 100644 index 0000000000..f6b9216451 --- /dev/null +++ b/src/audio_core/out/audio_out.h @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/out/audio_out_system.h" + +namespace Core { +class System; +} + +namespace Kernel { +class KEvent; +class KReadableEvent; +} // namespace Kernel + +namespace AudioCore::AudioOut { +class Manager; + +/** + * Interface between the service and audio out system. Mainly responsible for forwarding service + * calls to the system. + */ +class Out { +public: + explicit Out(Core::System& system, Manager& manager, Kernel::KEvent* event, size_t session_id); + + /** + * Free this audio out from the audio out manager. + */ + void Free(); + + /** + * Get this audio out's system. + */ + System& GetSystem(); + + /** + * Get the current state. + * + * @return Started or Stopped. + */ + AudioOut::State GetState(); + + /** + * Start the system + * + * @return Result code + */ + Result StartSystem(); + + /** + * Start the system's device session. + */ + void StartSession(); + + /** + * Stop the system. + * + * @return Result code + */ + Result StopSystem(); + + /** + * Append a new buffer to the system, the buffer event will be signalled when it is filled. + * + * @param buffer - The new buffer to append. + * @param tag - Unique tag for this buffer. + * @return Result code. + */ + Result AppendBuffer(const AudioOutBuffer& buffer, u64 tag); + + /** + * Release all completed buffers, and register any appended. + */ + void ReleaseAndRegisterBuffers(); + + /** + * Flush all buffers. + */ + bool FlushAudioOutBuffers(); + + /** + * Get all of the currently released buffers. + * + * @param tags - Output container for the buffer tags which were released. + * @return The number of buffers released. + */ + u32 GetReleasedBuffers(std::span tags); + + /** + * Get the buffer event for this audio out, this event will be signalled when a buffer is + * filled. + * @return The buffer event. + */ + Kernel::KReadableEvent& GetBufferEvent(); + + /** + * Get the current system volume. + * + * @return The current volume. + */ + f32 GetVolume(); + + /** + * Set the system volume. + * + * @param volume - The volume to set. + */ + void SetVolume(f32 volume); + + /** + * Check if a buffer is in the system. + * + * @param tag - The tag to search for. + * @return True if the buffer is in the system, otherwise false. + */ + bool ContainsAudioBuffer(u64 tag); + + /** + * Get the maximum number of buffers. + * + * @return The maximum number of buffers. + */ + u32 GetBufferCount(); + + /** + * Get the total played sample count for this audio out. + * + * @return The played sample count. + */ + u64 GetPlayedSampleCount(); + +private: + /// The AudioOut::Manager this audio out is registered with + Manager& manager; + /// Manager's mutex + std::recursive_mutex& parent_mutex; + /// Buffer event, signalled when buffers are ready to be released + Kernel::KEvent* event; + /// Main audio out system + System system; +}; + +} // namespace AudioCore::AudioOut diff --git a/src/audio_core/out/audio_out_system.cpp b/src/audio_core/out/audio_out_system.cpp new file mode 100644 index 0000000000..35afddf06d --- /dev/null +++ b/src/audio_core/out/audio_out_system.cpp @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/audio_event.h" +#include "audio_core/audio_manager.h" +#include "audio_core/out/audio_out_system.h" +#include "common/logging/log.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/hle/kernel/k_event.h" + +namespace AudioCore::AudioOut { + +System::System(Core::System& system_, Kernel::KEvent* event_, size_t session_id_) + : system{system_}, buffer_event{event_}, + session_id{session_id_}, session{std::make_unique(system_)} {} + +System::~System() { + Finalize(); +} + +void System::Finalize() { + Stop(); + session->Finalize(); + buffer_event->GetWritableEvent().Signal(); +} + +std::string_view System::GetDefaultOutputDeviceName() { + return "DeviceOut"; +} + +Result System::IsConfigValid(std::string_view device_name, const AudioOutParameter& in_params) { + if ((device_name.size() > 0) && (device_name != GetDefaultOutputDeviceName())) { + return Service::Audio::ERR_INVALID_DEVICE_NAME; + } + + if (in_params.sample_rate != TargetSampleRate && in_params.sample_rate > 0) { + return Service::Audio::ERR_INVALID_SAMPLE_RATE; + } + + if (in_params.channel_count == 0 || in_params.channel_count == 2 || + in_params.channel_count == 6) { + return ResultSuccess; + } + + return Service::Audio::ERR_INVALID_CHANNEL_COUNT; +} + +Result System::Initialize(std::string& device_name, const AudioOutParameter& in_params, u32 handle_, + u64& applet_resource_user_id_) { + auto result = IsConfigValid(device_name, in_params); + if (result.IsError()) { + return result; + } + + handle = handle_; + applet_resource_user_id = applet_resource_user_id_; + if (device_name.empty() || device_name[0] == '\0') { + name = std::string(GetDefaultOutputDeviceName()); + } else { + name = std::move(device_name); + } + + sample_rate = TargetSampleRate; + sample_format = SampleFormat::PcmInt16; + channel_count = in_params.channel_count <= 2 ? 2 : 6; + volume = 1.0f; + return ResultSuccess; +} + +void System::StartSession() { + session->Start(); +} + +size_t System::GetSessionId() const { + return session_id; +} + +Result System::Start() { + if (state != State::Stopped) { + return Service::Audio::ERR_OPERATION_FAILED; + } + + session->Initialize(name, sample_format, channel_count, session_id, handle, + applet_resource_user_id, Sink::StreamType::Out); + session->SetVolume(volume); + session->Start(); + state = State::Started; + + std::vector buffers_to_flush{}; + buffers.RegisterBuffers(buffers_to_flush); + session->AppendBuffers(buffers_to_flush); + + return ResultSuccess; +} + +Result System::Stop() { + if (state == State::Started) { + session->Stop(); + session->SetVolume(0.0f); + state = State::Stopped; + } + + return ResultSuccess; +} + +bool System::AppendBuffer(const AudioOutBuffer& buffer, u64 tag) { + if (buffers.GetTotalBufferCount() == BufferCount) { + return false; + } + + AudioBuffer new_buffer{ + .played_timestamp = 0, .samples = buffer.samples, .tag = tag, .size = buffer.size}; + + buffers.AppendBuffer(new_buffer); + RegisterBuffers(); + + return true; +} + +void System::RegisterBuffers() { + if (state == State::Started) { + std::vector registered_buffers{}; + buffers.RegisterBuffers(registered_buffers); + session->AppendBuffers(registered_buffers); + } +} + +void System::ReleaseBuffers() { + bool signal{buffers.ReleaseBuffers(system.CoreTiming(), *session)}; + if (signal) { + // Signal if any buffer was released, or if none are registered, we need more. + buffer_event->GetWritableEvent().Signal(); + } +} + +u32 System::GetReleasedBuffers(std::span tags) { + return buffers.GetReleasedBuffers(tags); +} + +bool System::FlushAudioOutBuffers() { + if (state != State::Started) { + return false; + } + + u32 buffers_released{}; + buffers.FlushBuffers(buffers_released); + + if (buffers_released > 0) { + buffer_event->GetWritableEvent().Signal(); + } + return true; +} + +u16 System::GetChannelCount() const { + return channel_count; +} + +u32 System::GetSampleRate() const { + return sample_rate; +} + +SampleFormat System::GetSampleFormat() const { + return sample_format; +} + +State System::GetState() { + switch (state) { + case State::Started: + case State::Stopped: + return state; + default: + LOG_ERROR(Service_Audio, "AudioOut invalid state!"); + state = State::Stopped; + break; + } + return state; +} + +std::string System::GetName() const { + return name; +} + +f32 System::GetVolume() const { + return volume; +} + +void System::SetVolume(const f32 volume_) { + volume = volume_; + session->SetVolume(volume_); +} + +bool System::ContainsAudioBuffer(const u64 tag) { + return buffers.ContainsBuffer(tag); +} + +u32 System::GetBufferCount() { + return buffers.GetAppendedRegisteredCount(); +} + +u64 System::GetPlayedSampleCount() const { + return session->GetPlayedSampleCount(); +} + +} // namespace AudioCore::AudioOut diff --git a/src/audio_core/out/audio_out_system.h b/src/audio_core/out/audio_out_system.h new file mode 100644 index 0000000000..4ca2f3417f --- /dev/null +++ b/src/audio_core/out/audio_out_system.h @@ -0,0 +1,257 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/device/audio_buffers.h" +#include "audio_core/device/device_session.h" +#include "core/hle/service/audio/errors.h" + +namespace Core { +class System; +} + +namespace Kernel { +class KEvent; +} + +namespace AudioCore::AudioOut { + +constexpr SessionTypes SessionType = SessionTypes::AudioOut; + +struct AudioOutParameter { + /* 0x0 */ s32_le sample_rate; + /* 0x4 */ u16_le channel_count; + /* 0x6 */ u16_le reserved; +}; +static_assert(sizeof(AudioOutParameter) == 0x8, "AudioOutParameter is an invalid size"); + +struct AudioOutParameterInternal { + /* 0x0 */ u32_le sample_rate; + /* 0x4 */ u32_le channel_count; + /* 0x8 */ u32_le sample_format; + /* 0xC */ u32_le state; +}; +static_assert(sizeof(AudioOutParameterInternal) == 0x10, + "AudioOutParameterInternal is an invalid size"); + +struct AudioOutBuffer { + /* 0x00 */ AudioOutBuffer* next; + /* 0x08 */ VAddr samples; + /* 0x10 */ u64 capacity; + /* 0x18 */ u64 size; + /* 0x20 */ u64 offset; +}; +static_assert(sizeof(AudioOutBuffer) == 0x28, "AudioOutBuffer is an invalid size"); + +enum class State { + Started, + Stopped, +}; + +/** + * Controls and drives audio output. + */ +class System { +public: + explicit System(Core::System& system, Kernel::KEvent* event, size_t session_id); + ~System(); + + /** + * Get the default audio output device name. + * + * @return The default audio output device name. + */ + std::string_view GetDefaultOutputDeviceName(); + + /** + * Is the given initialize config valid? + * + * @param device_name - The name of the requested output device. + * @param in_params - Input parameters, see AudioOutParameter. + * @return Result code. + */ + Result IsConfigValid(std::string_view device_name, const AudioOutParameter& in_params); + + /** + * Initialize this system. + * + * @param device_name - The name of the requested output device. + * @param in_params - Input parameters, see AudioOutParameter. + * @param handle - Unused. + * @param applet_resource_user_id - Unused. + * @return Result code. + */ + Result Initialize(std::string& device_name, const AudioOutParameter& in_params, u32 handle, + u64& applet_resource_user_id); + + /** + * Start this system. + * + * @return Result code. + */ + Result Start(); + + /** + * Stop this system. + * + * @return Result code. + */ + Result Stop(); + + /** + * Finalize this system. + */ + void Finalize(); + + /** + * Start this system's device session. + */ + void StartSession(); + + /** + * Get this system's id. + */ + size_t GetSessionId() const; + + /** + * Append a new buffer to the device. + * + * @param buffer - New buffer to append. + * @param tag - Unique tag of the buffer. + * @return True if the buffer was appended, otherwise false. + */ + bool AppendBuffer(const AudioOutBuffer& buffer, u64 tag); + + /** + * Register all appended buffers. + */ + void RegisterBuffers(); + + /** + * Release all registered buffers. + */ + void ReleaseBuffers(); + + /** + * Get all released buffers. + * + * @param tags - Container to be filled with the released buffers' tags. + * @return The number of buffers released. + */ + u32 GetReleasedBuffers(std::span tags); + + /** + * Flush all appended and registered buffers. + * + * @return True if buffers were successfully flushed, otherwise false. + */ + bool FlushAudioOutBuffers(); + + /** + * Get this system's current channel count. + * + * @return The channel count. + */ + u16 GetChannelCount() const; + + /** + * Get this system's current sample rate. + * + * @return The sample rate. + */ + u32 GetSampleRate() const; + + /** + * Get this system's current sample format. + * + * @return The sample format. + */ + SampleFormat GetSampleFormat() const; + + /** + * Get this system's current state. + * + * @return The current state. + */ + State GetState(); + + /** + * Get this system's name. + * + * @return The system's name. + */ + std::string GetName() const; + + /** + * Get this system's current volume. + * + * @return The system's current volume. + */ + f32 GetVolume() const; + + /** + * Set this system's current volume. + * + * @param The new volume. + */ + void SetVolume(f32 volume); + + /** + * Does the system contain this buffer? + * + * @param tag - Unique tag to search for. + * @return True if the buffer is in the system, otherwise false. + */ + bool ContainsAudioBuffer(u64 tag); + + /** + * Get the maximum number of usable buffers (default 32). + * + * @return The number of buffers. + */ + u32 GetBufferCount(); + + /** + * Get the total number of samples played by this system. + * + * @return The number of samples. + */ + u64 GetPlayedSampleCount() const; + +private: + /// Core system + Core::System& system; + /// (Unused) + u32 handle{}; + /// (Unused) + u64 applet_resource_user_id{}; + /// Buffer event, signalled when a buffer is ready + Kernel::KEvent* buffer_event; + /// Session id of this system + size_t session_id{}; + /// Device session for this system + std::unique_ptr session; + /// Audio buffers in use by this system + AudioBuffers buffers{BufferCount}; + /// Sample rate of this system + u32 sample_rate{}; + /// Sample format of this system + SampleFormat sample_format{SampleFormat::PcmInt16}; + /// Channel count of this system + u16 channel_count{}; + /// State of this system + std::atomic state{State::Stopped}; + /// Name of this system + std::string name{}; + /// Volume of this system + f32 volume{1.0f}; +}; + +} // namespace AudioCore::AudioOut diff --git a/src/audio_core/renderer/adsp/adsp.cpp b/src/audio_core/renderer/adsp/adsp.cpp new file mode 100644 index 0000000000..e05a22d863 --- /dev/null +++ b/src/audio_core/renderer/adsp/adsp.cpp @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/adsp.h" +#include "audio_core/renderer/adsp/command_buffer.h" +#include "audio_core/sink/sink.h" +#include "common/logging/log.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/core_timing_util.h" +#include "core/memory.h" + +namespace AudioCore::AudioRenderer::ADSP { + +ADSP::ADSP(Core::System& system_, Sink::Sink& sink_) + : system{system_}, memory{system.Memory()}, sink{sink_} {} + +ADSP::~ADSP() { + ClearCommandBuffers(); +} + +State ADSP::GetState() const { + if (running) { + return State::Started; + } + return State::Stopped; +} + +AudioRenderer_Mailbox* ADSP::GetRenderMailbox() { + return &render_mailbox; +} + +void ADSP::ClearRemainCount(const u32 session_id) { + render_mailbox.ClearRemainCount(session_id); +} + +u64 ADSP::GetSignalledTick() const { + return render_mailbox.GetSignalledTick(); +} + +u64 ADSP::GetTimeTaken() const { + return render_mailbox.GetRenderTimeTaken(); +} + +u64 ADSP::GetRenderTimeTaken(const u32 session_id) { + return render_mailbox.GetCommandBuffer(session_id).render_time_taken; +} + +u32 ADSP::GetRemainCommandCount(const u32 session_id) const { + return render_mailbox.GetRemainCommandCount(session_id); +} + +void ADSP::SendCommandBuffer(const u32 session_id, CommandBuffer& command_buffer) { + render_mailbox.SetCommandBuffer(session_id, command_buffer); +} + +u64 ADSP::GetRenderingStartTick(const u32 session_id) { + return render_mailbox.GetSignalledTick() + + render_mailbox.GetCommandBuffer(session_id).render_time_taken; +} + +bool ADSP::Start() { + if (running) { + return running; + } + + running = true; + systems_active++; + audio_renderer = std::make_unique(system); + audio_renderer->Start(&render_mailbox); + render_mailbox.HostSendMessage(RenderMessage::AudioRenderer_InitializeOK); + if (render_mailbox.HostWaitMessage() != RenderMessage::AudioRenderer_InitializeOK) { + LOG_ERROR( + Service_Audio, + "Host Audio Renderer -- Failed to receive initialize message response from ADSP!"); + } + return running; +} + +void ADSP::Stop() { + systems_active--; + if (running && systems_active == 0) { + { + std::scoped_lock l{mailbox_lock}; + render_mailbox.HostSendMessage(RenderMessage::AudioRenderer_Shutdown); + if (render_mailbox.HostWaitMessage() != RenderMessage::AudioRenderer_Shutdown) { + LOG_ERROR(Service_Audio, "Host Audio Renderer -- Failed to receive shutdown " + "message response from ADSP!"); + } + } + audio_renderer->Stop(); + running = false; + } +} + +void ADSP::Signal() { + const auto signalled_tick{system.CoreTiming().GetClockTicks()}; + render_mailbox.SetSignalledTick(signalled_tick); + render_mailbox.HostSendMessage(RenderMessage::AudioRenderer_Render); +} + +void ADSP::Wait() { + std::scoped_lock l{mailbox_lock}; + auto response{render_mailbox.HostWaitMessage()}; + if (response != RenderMessage::AudioRenderer_RenderResponse) { + LOG_ERROR(Service_Audio, "Invalid ADSP response message, expected 0x{:02X}, got 0x{:02X}", + static_cast(RenderMessage::AudioRenderer_RenderResponse), + static_cast(response)); + } + + ClearCommandBuffers(); +} + +void ADSP::ClearCommandBuffers() { + render_mailbox.ClearCommandBuffers(); +} + +} // namespace AudioCore::AudioRenderer::ADSP diff --git a/src/audio_core/renderer/adsp/adsp.h b/src/audio_core/renderer/adsp/adsp.h new file mode 100644 index 0000000000..4dfcef4a51 --- /dev/null +++ b/src/audio_core/renderer/adsp/adsp.h @@ -0,0 +1,173 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/adsp/audio_renderer.h" +#include "common/common_types.h" + +namespace Core { +namespace Memory { +class Memory; +} +class System; +} // namespace Core + +namespace AudioCore { +namespace Sink { +class Sink; +} + +namespace AudioRenderer::ADSP { +struct CommandBuffer; + +enum class State { + Started, + Stopped, +}; + +/** + * Represents the ADSP embedded within the audio sysmodule. + * This is a 32-bit Linux4Tegra kernel from nVidia, which is launched with the sysmodule on boot. + * + * The kernel will run apps you program for it, Nintendo have the following: + * + * Gmix - Responsible for mixing final audio and sending it out to hardware. This is last place all + * audio samples end up, and we skip it entirely, since we have very different backends and + * mixing is implicitly handled by the OS (but also due to lack of research/simplicity). + * + * AudioRenderer - Receives command lists generated by the audio render + * system, processes them, and sends the samples to Gmix. + * + * OpusDecoder - Contains libopus, and controls processing Opus audio and sends it to Gmix. + * Not much research done here, TODO if needed. + * + * We only implement the AudioRenderer for now. + * + * Communication for the apps is done through mailboxes, and some shared memory. + */ +class ADSP { +public: + explicit ADSP(Core::System& system, Sink::Sink& sink); + ~ADSP(); + + /** + * Start the ADSP. + * + * @return True if started or already running, otherwise false. + */ + bool Start(); + + /** + * Stop the ADSP. + * + * @return True if started or already running, otherwise false. + */ + void Stop(); + + /** + * Get the ADSP's state. + * + * @return Started or Stopped. + */ + State GetState() const; + + /** + * Get the AudioRenderer mailbox to communicate with it. + * + * @return The AudioRenderer mailbox. + */ + AudioRenderer_Mailbox* GetRenderMailbox(); + + /** + * Get the tick the ADSP was signalled. + * + * @return The tick the ADSP was signalled. + */ + u64 GetSignalledTick() const; + + /** + * Get the total time it took for the ADSP to run the last command lists (both command lists). + * + * @return The tick the ADSP was signalled. + */ + u64 GetTimeTaken() const; + + /** + * Get the last time a given command list took to run. + * + * @param session_id - The session id to check (0 or 1). + * @return The time it took. + */ + u64 GetRenderTimeTaken(u32 session_id); + + /** + * Clear the remaining command count for a given session. + * + * @param session_id - The session id to check (0 or 1). + */ + void ClearRemainCount(u32 session_id); + + /** + * Get the remaining number of commands left to process for a command list. + * + * @param session_id - The session id to check (0 or 1). + * @return The number of commands remaining. + */ + u32 GetRemainCommandCount(u32 session_id) const; + + /** + * Get the last tick a command list started processing. + * + * @param session_id - The session id to check (0 or 1). + * @return The last tick the given command list started. + */ + u64 GetRenderingStartTick(u32 session_id); + + /** + * Set a command buffer to be processed. + * + * @param session_id - The session id to check (0 or 1). + * @param command_buffer - The command buffer to process. + */ + void SendCommandBuffer(u32 session_id, CommandBuffer& command_buffer); + + /** + * Clear the command buffers (does not clear the time taken or the remaining command count) + */ + void ClearCommandBuffers(); + + /** + * Signal the AudioRenderer to begin processing. + */ + void Signal(); + + /** + * Wait for the AudioRenderer to finish processing. + */ + void Wait(); + +private: + /// Core system + Core::System& system; + /// Core memory + Core::Memory::Memory& memory; + /// Number of systems active, used to prevent accidental shutdowns + u8 systems_active{0}; + /// ADSP running state + std::atomic running{false}; + /// Output sink used by the ADSP + Sink::Sink& sink; + /// AudioRenderer app + std::unique_ptr audio_renderer{}; + /// Communication for the AudioRenderer + AudioRenderer_Mailbox render_mailbox{}; + /// Mailbox lock ffor the render mailbox + std::mutex mailbox_lock; +}; + +} // namespace AudioRenderer::ADSP +} // namespace AudioCore diff --git a/src/audio_core/renderer/adsp/audio_renderer.cpp b/src/audio_core/renderer/adsp/audio_renderer.cpp new file mode 100644 index 0000000000..3967ccfe69 --- /dev/null +++ b/src/audio_core/renderer/adsp/audio_renderer.cpp @@ -0,0 +1,226 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "audio_core/audio_core.h" +#include "audio_core/common/common.h" +#include "audio_core/renderer/adsp/audio_renderer.h" +#include "audio_core/sink/sink.h" +#include "common/logging/log.h" +#include "common/microprofile.h" +#include "common/thread.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/core_timing_util.h" + +MICROPROFILE_DEFINE(Audio_Renderer, "Audio", "DSP", MP_RGB(60, 19, 97)); + +namespace AudioCore::AudioRenderer::ADSP { + +void AudioRenderer_Mailbox::HostSendMessage(RenderMessage message_) { + adsp_messages.enqueue(message_); + adsp_event.Set(); +} + +RenderMessage AudioRenderer_Mailbox::HostWaitMessage() { + host_event.Wait(); + RenderMessage msg{RenderMessage::Invalid}; + if (!host_messages.try_dequeue(msg)) { + LOG_ERROR(Service_Audio, "Failed to dequeue host message!"); + } + return msg; +} + +void AudioRenderer_Mailbox::ADSPSendMessage(const RenderMessage message_) { + host_messages.enqueue(message_); + host_event.Set(); +} + +RenderMessage AudioRenderer_Mailbox::ADSPWaitMessage() { + adsp_event.Wait(); + RenderMessage msg{RenderMessage::Invalid}; + if (!adsp_messages.try_dequeue(msg)) { + LOG_ERROR(Service_Audio, "Failed to dequeue ADSP message!"); + } + return msg; +} + +CommandBuffer& AudioRenderer_Mailbox::GetCommandBuffer(const s32 session_id) { + return command_buffers[session_id]; +} + +void AudioRenderer_Mailbox::SetCommandBuffer(const u32 session_id, CommandBuffer& buffer) { + command_buffers[session_id] = buffer; +} + +u64 AudioRenderer_Mailbox::GetRenderTimeTaken() const { + return command_buffers[0].render_time_taken + command_buffers[1].render_time_taken; +} + +u64 AudioRenderer_Mailbox::GetSignalledTick() const { + return signalled_tick; +} + +void AudioRenderer_Mailbox::SetSignalledTick(const u64 tick) { + signalled_tick = tick; +} + +void AudioRenderer_Mailbox::ClearRemainCount(const u32 session_id) { + command_buffers[session_id].remaining_command_count = 0; +} + +u32 AudioRenderer_Mailbox::GetRemainCommandCount(const u32 session_id) const { + return command_buffers[session_id].remaining_command_count; +} + +void AudioRenderer_Mailbox::ClearCommandBuffers() { + command_buffers[0].buffer = 0; + command_buffers[0].size = 0; + command_buffers[0].reset_buffers = false; + command_buffers[1].buffer = 0; + command_buffers[1].size = 0; + command_buffers[1].reset_buffers = false; +} + +AudioRenderer::AudioRenderer(Core::System& system_) + : system{system_}, sink{system.AudioCore().GetOutputSink()} { + CreateSinkStreams(); +} + +AudioRenderer::~AudioRenderer() { + Stop(); + for (auto& stream : streams) { + if (stream) { + sink.CloseStream(stream); + } + stream = nullptr; + } +} + +void AudioRenderer::Start(AudioRenderer_Mailbox* mailbox_) { + if (running) { + return; + } + + mailbox = mailbox_; + thread = std::thread(&AudioRenderer::ThreadFunc, this); + for (auto& stream : streams) { + stream->Start(); + } + running = true; +} + +void AudioRenderer::Stop() { + if (!running) { + return; + } + + for (auto& stream : streams) { + stream->Stop(); + } + thread.join(); + running = false; +} + +void AudioRenderer::CreateSinkStreams() { + u32 channels{sink.GetDeviceChannels()}; + for (u32 i = 0; i < MaxRendererSessions; i++) { + std::string name{fmt::format("ADSP_RenderStream-{}", i)}; + streams[i] = + sink.AcquireSinkStream(system, channels, name, ::AudioCore::Sink::StreamType::Render); + } +} + +void AudioRenderer::ThreadFunc() { + constexpr char name[]{"yuzu:AudioRenderer"}; + MicroProfileOnThreadCreate(name); + Common::SetCurrentThreadName(name); + Common::SetCurrentThreadPriority(Common::ThreadPriority::Critical); + if (mailbox->ADSPWaitMessage() != RenderMessage::AudioRenderer_InitializeOK) { + LOG_ERROR(Service_Audio, + "ADSP Audio Renderer -- Failed to receive initialize message from host!"); + return; + } + + mailbox->ADSPSendMessage(RenderMessage::AudioRenderer_InitializeOK); + + constexpr u64 max_process_time{2'304'000ULL}; + + while (true) { + auto message{mailbox->ADSPWaitMessage()}; + switch (message) { + case RenderMessage::AudioRenderer_Shutdown: + mailbox->ADSPSendMessage(RenderMessage::AudioRenderer_Shutdown); + return; + + case RenderMessage::AudioRenderer_Render: { + std::array buffers_reset{}; + std::array render_times_taken{}; + const auto start_time{system.CoreTiming().GetClockTicks()}; + + for (u32 index = 0; index < 2; index++) { + auto& command_buffer{mailbox->GetCommandBuffer(index)}; + auto& command_list_processor{command_list_processors[index]}; + + // Check this buffer is valid, as it may not be used. + if (command_buffer.buffer != 0) { + // If there are no remaining commands (from the previous list), + // this is a new command list, initalize it. + if (command_buffer.remaining_command_count == 0) { + command_list_processor.Initialize(system, command_buffer.buffer, + command_buffer.size, streams[index]); + } + + if (command_buffer.reset_buffers && !buffers_reset[index]) { + streams[index]->ClearQueue(); + buffers_reset[index] = true; + } + + u64 max_time{max_process_time}; + if (index == 1 && command_buffer.applet_resource_user_id == + mailbox->GetCommandBuffer(0).applet_resource_user_id) { + max_time = max_process_time - + Core::Timing::CyclesToNs(render_times_taken[0]).count(); + if (render_times_taken[0] > max_process_time) { + max_time = 0; + } + } + + max_time = std::min(command_buffer.time_limit, max_time); + command_list_processor.SetProcessTimeMax(max_time); + + // Process the command list + { + MICROPROFILE_SCOPE(Audio_Renderer); + render_times_taken[index] = + command_list_processor.Process(index) - start_time; + } + + if (index == 0) { + auto stream{command_list_processor.GetOutputSinkStream()}; + system.AudioCore().SetStreamQueue(stream->GetQueueSize()); + } + + const auto end_time{system.CoreTiming().GetClockTicks()}; + + command_buffer.remaining_command_count = + command_list_processor.GetRemainingCommandCount(); + command_buffer.render_time_taken = end_time - start_time; + } + } + + mailbox->ADSPSendMessage(RenderMessage::AudioRenderer_RenderResponse); + } break; + + default: + LOG_WARNING(Service_Audio, + "ADSP AudioRenderer received an invalid message, msg={:02X}!", + static_cast(message)); + break; + } + } +} + +} // namespace AudioCore::AudioRenderer::ADSP diff --git a/src/audio_core/renderer/adsp/audio_renderer.h b/src/audio_core/renderer/adsp/audio_renderer.h new file mode 100644 index 0000000000..b6ced9d2b0 --- /dev/null +++ b/src/audio_core/renderer/adsp/audio_renderer.h @@ -0,0 +1,203 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "audio_core/renderer/adsp/command_buffer.h" +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "common/common_types.h" +#include "common/reader_writer_queue.h" +#include "common/thread.h" + +namespace Core { +namespace Timing { +struct EventType; +} +class System; +} // namespace Core + +namespace AudioCore { +namespace Sink { +class Sink; +} + +namespace AudioRenderer::ADSP { + +enum class RenderMessage { + /* 0x00 */ Invalid, + /* 0x01 */ AudioRenderer_MapUnmap_Map, + /* 0x02 */ AudioRenderer_MapUnmap_MapResponse, + /* 0x03 */ AudioRenderer_MapUnmap_Unmap, + /* 0x04 */ AudioRenderer_MapUnmap_UnmapResponse, + /* 0x05 */ AudioRenderer_MapUnmap_InvalidateCache, + /* 0x06 */ AudioRenderer_MapUnmap_InvalidateCacheResponse, + /* 0x07 */ AudioRenderer_MapUnmap_Shutdown, + /* 0x08 */ AudioRenderer_MapUnmap_ShutdownResponse, + /* 0x16 */ AudioRenderer_InitializeOK = 0x16, + /* 0x20 */ AudioRenderer_RenderResponse = 0x20, + /* 0x2A */ AudioRenderer_Render = 0x2A, + /* 0x34 */ AudioRenderer_Shutdown = 0x34, +}; + +/** + * A mailbox for the AudioRenderer, allowing communication between the host and the AudioRenderer + * running on the ADSP. + */ +class AudioRenderer_Mailbox { +public: + /** + * Send a message from the host to the AudioRenderer. + * + * @param message_ - The message to send to the AudioRenderer. + */ + void HostSendMessage(RenderMessage message); + + /** + * Host wait for a message from the AudioRenderer. + * + * @return The message returned from the AudioRenderer. + */ + RenderMessage HostWaitMessage(); + + /** + * Send a message from the AudioRenderer to the host. + * + * @param message_ - The message to send to the host. + */ + void ADSPSendMessage(RenderMessage message); + + /** + * AudioRenderer wait for a message from the host. + * + * @return The message returned from the AudioRenderer. + */ + RenderMessage ADSPWaitMessage(); + + /** + * Get the command buffer with the given session id (0 or 1). + * + * @param session_id - The session id to get (0 or 1). + * @return The command buffer. + */ + CommandBuffer& GetCommandBuffer(s32 session_id); + + /** + * Set the command buffer with the given session id (0 or 1). + * + * @param session_id - The session id to get (0 or 1). + * @param buffer - The command buffer to set. + */ + void SetCommandBuffer(u32 session_id, CommandBuffer& buffer); + + /** + * Get the total render time taken for the last command lists sent. + * + * @return Total render time taken for the last command lists. + */ + u64 GetRenderTimeTaken() const; + + /** + * Get the tick the AudioRenderer was signalled. + * + * @return The tick the AudioRenderer was signalled. + */ + u64 GetSignalledTick() const; + + /** + * Set the tick the AudioRenderer was signalled. + * + * @param tick - The tick the AudioRenderer was signalled. + */ + void SetSignalledTick(u64 tick); + + /** + * Clear the remaining command count. + * + * @param session_id - Index for which command list to clear (0 or 1). + */ + void ClearRemainCount(u32 session_id); + + /** + * Get the remaining command count for a given command list. + * + * @param session_id - Index for which command list to clear (0 or 1). + * @return The remaining command count. + */ + u32 GetRemainCommandCount(u32 session_id) const; + + /** + * Clear the command buffers (does not clear the time taken or the remaining command count). + */ + void ClearCommandBuffers(); + +private: + /// Host signalling event + Common::Event host_event{}; + /// AudioRenderer signalling event + Common::Event adsp_event{}; + /// Host message queue + + Common::ReaderWriterQueue host_messages{}; + /// AudioRenderer message queue + + Common::ReaderWriterQueue adsp_messages{}; + /// Command buffers + + std::array command_buffers{}; + /// Tick the AudioRnederer was signalled + u64 signalled_tick{}; +}; + +/** + * The AudioRenderer application running on the ADSP. + */ +class AudioRenderer { +public: + explicit AudioRenderer(Core::System& system); + ~AudioRenderer(); + + /** + * Start the AudioRenderer. + * + * @param The mailbox to use for this session. + */ + void Start(AudioRenderer_Mailbox* mailbox); + + /** + * Stop the AudioRenderer. + */ + void Stop(); + +private: + /** + * Main AudioRenderer thread, responsible for processing the command lists. + */ + void ThreadFunc(); + + /** + * Creates the streams which will receive the processed samples. + */ + void CreateSinkStreams(); + + /// Core system + Core::System& system; + /// Main thread + std::thread thread{}; + /// The current state + std::atomic running{}; + /// The active mailbox + AudioRenderer_Mailbox* mailbox{}; + /// The command lists to process + std::array command_list_processors{}; + /// The output sink the AudioRenderer will use + Sink::Sink& sink; + /// The streams which will receive the processed samples + std::array streams; +}; + +} // namespace AudioRenderer::ADSP +} // namespace AudioCore diff --git a/src/audio_core/renderer/adsp/command_buffer.h b/src/audio_core/renderer/adsp/command_buffer.h new file mode 100644 index 0000000000..880b279d8f --- /dev/null +++ b/src/audio_core/renderer/adsp/command_buffer.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer::ADSP { + +struct CommandBuffer { + CpuAddr buffer; + u64 size; + u64 time_limit; + u32 remaining_command_count; + bool reset_buffers; + u64 applet_resource_user_id; + u64 render_time_taken; +}; + +} // namespace AudioCore::AudioRenderer::ADSP diff --git a/src/audio_core/renderer/adsp/command_list_processor.cpp b/src/audio_core/renderer/adsp/command_list_processor.cpp new file mode 100644 index 0000000000..e3bf2d7ecd --- /dev/null +++ b/src/audio_core/renderer/adsp/command_list_processor.cpp @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/command_list_header.h" +#include "audio_core/renderer/command/commands.h" +#include "common/settings.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/core_timing_util.h" +#include "core/memory.h" + +namespace AudioCore::AudioRenderer::ADSP { + +void CommandListProcessor::Initialize(Core::System& system_, CpuAddr buffer, u64 size, + Sink::SinkStream* stream_) { + system = &system_; + memory = &system->Memory(); + stream = stream_; + header = reinterpret_cast(buffer); + commands = reinterpret_cast(buffer + sizeof(CommandListHeader)); + commands_buffer_size = size; + command_count = header->command_count; + sample_count = header->sample_count; + target_sample_rate = header->sample_rate; + mix_buffers = header->samples_buffer; + buffer_count = header->buffer_count; + processed_command_count = 0; +} + +void CommandListProcessor::SetProcessTimeMax(const u64 time) { + max_process_time = time; +} + +u32 CommandListProcessor::GetRemainingCommandCount() const { + return command_count - processed_command_count; +} + +void CommandListProcessor::SetBuffer(const CpuAddr buffer, const u64 size) { + commands = reinterpret_cast(buffer + sizeof(CommandListHeader)); + commands_buffer_size = size; +} + +Sink::SinkStream* CommandListProcessor::GetOutputSinkStream() const { + return stream; +} + +u64 CommandListProcessor::Process(u32 session_id) { + const auto start_time_{system->CoreTiming().GetClockTicks()}; + const auto command_base{CpuAddr(commands)}; + + if (processed_command_count > 0) { + current_processing_time += start_time_ - end_time; + } else { + start_time = start_time_; + current_processing_time = 0; + } + + std::string dump{fmt::format("\nSession {}\n", session_id)}; + + for (u32 index = 0; index < command_count; index++) { + auto& command{*reinterpret_cast(commands)}; + + if (command.magic != 0xCAFEBABE) { + LOG_ERROR(Service_Audio, "Command has invalid magic! Expected 0xCAFEBABE, got {:08X}", + command.magic); + return system->CoreTiming().GetClockTicks() - start_time_; + } + + auto current_offset{CpuAddr(commands) - command_base}; + + if (current_offset + command.size > commands_buffer_size) { + LOG_ERROR(Service_Audio, + "Command exceeded command buffer, buffer size {:08X}, command ends at {:08X}", + commands_buffer_size, + CpuAddr(commands) + command.size - sizeof(CommandListHeader)); + return system->CoreTiming().GetClockTicks() - start_time_; + } + + if (Settings::values.dump_audio_commands) { + command.Dump(*this, dump); + } + + if (!command.Verify(*this)) { + break; + } + + if (command.enabled) { + command.Process(*this); + } else { + dump += fmt::format("\tDisabled!\n"); + } + + processed_command_count++; + commands += command.size; + } + + if (Settings::values.dump_audio_commands && dump != last_dump) { + LOG_WARNING(Service_Audio, "{}", dump); + last_dump = dump; + } + + end_time = system->CoreTiming().GetClockTicks(); + return end_time - start_time_; +} + +} // namespace AudioCore::AudioRenderer::ADSP diff --git a/src/audio_core/renderer/adsp/command_list_processor.h b/src/audio_core/renderer/adsp/command_list_processor.h new file mode 100644 index 0000000000..3f99173e3e --- /dev/null +++ b/src/audio_core/renderer/adsp/command_list_processor.h @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace Core { +namespace Memory { +class Memory; +} +class System; +} // namespace Core + +namespace AudioCore { +namespace Sink { +class SinkStream; +} + +namespace AudioRenderer { +struct CommandListHeader; + +namespace ADSP { + +/** + * A processor for command lists given to the AudioRenderer. + */ +class CommandListProcessor { +public: + /** + * Initialize the processor. + * + * @param system_ - The core system. + * @param buffer - The command buffer to process. + * @param size - The size of the buffer. + * @param stream_ - The stream to be used for sending the samples. + */ + void Initialize(Core::System& system, CpuAddr buffer, u64 size, Sink::SinkStream* stream); + + /** + * Set the maximum processing time for this command list. + * + * @param time - The maximum process time. + */ + void SetProcessTimeMax(u64 time); + + /** + * Get the remaining command count for this list. + * + * @return The remaining command count. + */ + u32 GetRemainingCommandCount() const; + + /** + * Set the command buffer. + * + * @param buffer - The buffer to use. + * @param size - The size of the buffer. + */ + void SetBuffer(CpuAddr buffer, u64 size); + + /** + * Get the stream for this command list. + * + * @return The stream associated with this command list. + */ + Sink::SinkStream* GetOutputSinkStream() const; + + /** + * Process the command list. + * + * @param index - Index of the current command list. + * @return The time taken to process. + */ + u64 Process(u32 session_id); + + /// Core system + Core::System* system{}; + /// Core memory + Core::Memory::Memory* memory{}; + /// Stream for the processed samples + Sink::SinkStream* stream{}; + /// Header info for this command list + CommandListHeader* header{}; + /// The command buffer + u8* commands{}; + /// The command buffer size + u64 commands_buffer_size{}; + /// The maximum processing time alloted + u64 max_process_time{}; + /// The number of commands in the buffer + u32 command_count{}; + /// The target sample count for output + u32 sample_count{}; + /// The target sample rate for output + u32 target_sample_rate{}; + /// The mixing buffers used by the commands + std::span mix_buffers{}; + /// The number of mix buffers + u32 buffer_count{}; + /// The number of processed commands so far + u32 processed_command_count{}; + /// The processing start time of this list + u64 start_time{}; + /// The current processing time for this list + u64 current_processing_time{}; + /// The end processing time for this list + u64 end_time{}; + /// Last command list string generated, used for dumping audio commands to console + std::string last_dump{}; +}; + +} // namespace ADSP +} // namespace AudioRenderer +} // namespace AudioCore diff --git a/src/audio_core/renderer/audio_device.cpp b/src/audio_core/renderer/audio_device.cpp new file mode 100644 index 0000000000..d5886e55e4 --- /dev/null +++ b/src/audio_core/renderer/audio_device.cpp @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_core.h" +#include "audio_core/common/feature_support.h" +#include "audio_core/renderer/audio_device.h" +#include "audio_core/sink/sink.h" +#include "core/core.h" + +namespace AudioCore::AudioRenderer { + +AudioDevice::AudioDevice(Core::System& system, const u64 applet_resource_user_id_, + const u32 revision) + : output_sink{system.AudioCore().GetOutputSink()}, + applet_resource_user_id{applet_resource_user_id_}, user_revision{revision} {} + +u32 AudioDevice::ListAudioDeviceName(std::vector& out_buffer, + const size_t max_count) { + std::span names{}; + + if (CheckFeatureSupported(SupportTags::AudioUsbDeviceOutput, user_revision)) { + names = usb_device_names; + } else { + names = device_names; + } + + u32 out_count{static_cast(std::min(max_count, names.size()))}; + for (u32 i = 0; i < out_count; i++) { + out_buffer.push_back(names[i]); + } + return out_count; +} + +u32 AudioDevice::ListAudioOutputDeviceName(std::vector& out_buffer, + const size_t max_count) { + u32 out_count{static_cast(std::min(max_count, output_device_names.size()))}; + + for (u32 i = 0; i < out_count; i++) { + out_buffer.push_back(output_device_names[i]); + } + return out_count; +} + +void AudioDevice::SetDeviceVolumes(const f32 volume) { + output_sink.SetDeviceVolume(volume); +} + +f32 AudioDevice::GetDeviceVolume([[maybe_unused]] std::string_view name) { + return output_sink.GetDeviceVolume(); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/audio_device.h b/src/audio_core/renderer/audio_device.h new file mode 100644 index 0000000000..1f449f2612 --- /dev/null +++ b/src/audio_core/renderer/audio_device.h @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/audio_render_manager.h" + +namespace Core { +class System; +} + +namespace AudioCore { +namespace Sink { +class Sink; +} + +namespace AudioRenderer { +/** + * An interface to an output audio device available to the Switch. + */ +class AudioDevice { +public: + struct AudioDeviceName { + std::array name; + + AudioDeviceName(const char* name_) { + std::strncpy(name.data(), name_, name.size()); + } + }; + + std::array usb_device_names{"AudioStereoJackOutput", + "AudioBuiltInSpeakerOutput", "AudioTvOutput", + "AudioUsbDeviceOutput"}; + std::array device_names{"AudioStereoJackOutput", + "AudioBuiltInSpeakerOutput", "AudioTvOutput"}; + std::array output_device_names{"AudioBuiltInSpeakerOutput", "AudioTvOutput", + "AudioExternalOutput"}; + + explicit AudioDevice(Core::System& system, u64 applet_resource_user_id, u32 revision); + + /** + * Get a list of the available output devices. + * + * @param out_buffer - Output buffer to write the available device names. + * @param max_count - Maximum number of devices to write (count of out_buffer). + * @return Number of device names written. + */ + u32 ListAudioDeviceName(std::vector& out_buffer, size_t max_count); + + /** + * Get a list of the available output devices. + * Different to above somehow... + * + * @param out_buffer - Output buffer to write the available device names. + * @param max_count - Maximum number of devices to write (count of out_buffer). + * @return Number of device names written. + */ + u32 ListAudioOutputDeviceName(std::vector& out_buffer, size_t max_count); + + /** + * Set the volume of all streams in the backend sink. + * + * @param volume - Volume to set. + */ + void SetDeviceVolumes(f32 volume); + + /** + * Get the volume for a given device name. + * Note: This is not fully implemented, we only assume 1 device for all streams. + * + * @param name - Name of the device to check. Unused. + * @return Volume of the device. + */ + f32 GetDeviceVolume(std::string_view name); + +private: + /// Backend output sink for the device + Sink::Sink& output_sink; + /// Resource id this device is used for + const u64 applet_resource_user_id; + /// User audio renderer revision + const u32 user_revision; +}; + +} // namespace AudioRenderer +} // namespace AudioCore diff --git a/src/audio_core/renderer/audio_renderer.cpp b/src/audio_core/renderer/audio_renderer.cpp new file mode 100644 index 0000000000..51aa17599e --- /dev/null +++ b/src/audio_core/renderer/audio_renderer.cpp @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/audio_render_manager.h" +#include "audio_core/common/audio_renderer_parameter.h" +#include "audio_core/renderer/audio_renderer.h" +#include "audio_core/renderer/system_manager.h" +#include "core/core.h" +#include "core/hle/kernel/k_transfer_memory.h" +#include "core/hle/service/audio/errors.h" + +namespace AudioCore::AudioRenderer { + +Renderer::Renderer(Core::System& system_, Manager& manager_, Kernel::KEvent* rendered_event) + : core{system_}, manager{manager_}, system{system_, rendered_event} {} + +Result Renderer::Initialize(const AudioRendererParameterInternal& params, + Kernel::KTransferMemory* transfer_memory, + const u64 transfer_memory_size, const u32 process_handle, + const u64 applet_resource_user_id, const s32 session_id) { + if (params.execution_mode == ExecutionMode::Auto) { + if (!manager.AddSystem(system)) { + LOG_ERROR(Service_Audio, + "Both Audio Render sessions are in use, cannot create any more"); + return Service::Audio::ERR_MAXIMUM_SESSIONS_REACHED; + } + system_registered = true; + } + + initialized = true; + system.Initialize(params, transfer_memory, transfer_memory_size, process_handle, + applet_resource_user_id, session_id); + + return ResultSuccess; +} + +void Renderer::Finalize() { + auto session_id{system.GetSessionId()}; + + system.Finalize(); + + if (system_registered) { + manager.RemoveSystem(system); + system_registered = false; + } + + manager.ReleaseSessionId(session_id); +} + +System& Renderer::GetSystem() { + return system; +} + +void Renderer::Start() { + system.Start(); +} + +void Renderer::Stop() { + system.Stop(); +} + +Result Renderer::RequestUpdate(std::span input, std::span performance, + std::span output) { + return system.Update(input, performance, output); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/audio_renderer.h b/src/audio_core/renderer/audio_renderer.h new file mode 100644 index 0000000000..90c6f97279 --- /dev/null +++ b/src/audio_core/renderer/audio_renderer.h @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/system.h" +#include "core/hle/service/audio/errors.h" + +namespace Core { +class System; +} + +namespace Kernel { +class KTransferMemory; +} + +namespace AudioCore { +struct AudioRendererParameterInternal; + +namespace AudioRenderer { +class Manager; + +/** + * Audio Renderer, wraps the main audio system and is mainly responsible for handling service calls. + */ +class Renderer { +public: + explicit Renderer(Core::System& system, Manager& manager, Kernel::KEvent* rendered_event); + + /** + * Initialize the renderer. + * Registers the system with the AudioRenderer::Manager, allocates workbuffers and initializes + * everything to a default state. + * + * @param params - Input parameters to initialize the system with. + * @param transfer_memory - Game-supplied memory for all workbuffers. Unused. + * @param transfer_memory_size - Size of the transfer memory. Unused. + * @param process_handle - Process handle, also used for memory. Unused. + * @param applet_resource_user_id - Applet id for this renderer. Unused. + * @param session_id - Session id of this renderer. + * @return Result code. + */ + Result Initialize(const AudioRendererParameterInternal& params, + Kernel::KTransferMemory* transfer_memory, u64 transfer_memory_size, + u32 process_handle, u64 applet_resource_user_id, s32 session_id); + + /** + * Finalize the renderer for shutdown. + */ + void Finalize(); + + /** + * Get the renderer's system. + * + * @return Reference to the system. + */ + System& GetSystem(); + + /** + * Start the renderer. + */ + void Start(); + + /** + * Stop the renderer. + */ + void Stop(); + + /** + * Update the audio renderer with new information. + * Called via RequestUpdate from the AudRen:U service. + * + * @param input - Input buffer containing the new data. + * @param performance - Optional performance buffer for outputting performance metrics. + * @param output - Output data from the renderer. + * @return Result code. + */ + Result RequestUpdate(std::span input, std::span performance, + std::span output); + +private: + /// System core + Core::System& core; + /// Manager this renderer is registered with + Manager& manager; + /// Is the audio renderer initialized? + bool initialized{}; + /// Is the system registered with the manager? + bool system_registered{}; + /// Audio render system, main driver of audio rendering + System system; +}; + +} // namespace AudioRenderer +} // namespace AudioCore diff --git a/src/audio_core/renderer/behavior/behavior_info.cpp b/src/audio_core/renderer/behavior/behavior_info.cpp new file mode 100644 index 0000000000..c5d4d66d8f --- /dev/null +++ b/src/audio_core/renderer/behavior/behavior_info.cpp @@ -0,0 +1,191 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/common/feature_support.h" +#include "audio_core/renderer/behavior/behavior_info.h" + +namespace AudioCore::AudioRenderer { + +BehaviorInfo::BehaviorInfo() : process_revision{CurrentRevision} {} + +u32 BehaviorInfo::GetProcessRevisionNum() const { + return process_revision; +} + +u32 BehaviorInfo::GetProcessRevision() const { + return Common::MakeMagic('R', 'E', 'V', + static_cast(static_cast('0') + process_revision)); +} + +u32 BehaviorInfo::GetUserRevisionNum() const { + return user_revision; +} + +u32 BehaviorInfo::GetUserRevision() const { + return Common::MakeMagic('R', 'E', 'V', + static_cast(static_cast('0') + user_revision)); +} + +void BehaviorInfo::SetUserLibRevision(const u32 user_revision_) { + user_revision = GetRevisionNum(user_revision_); +} + +void BehaviorInfo::ClearError() { + error_count = 0; +} + +void BehaviorInfo::AppendError(ErrorInfo& error) { + LOG_ERROR(Service_Audio, "Error during RequestUpdate, reporting code {:04X} address {:08X}", + error.error_code.raw, error.address); + if (error_count < MaxErrors) { + errors[error_count++] = error; + } +} + +void BehaviorInfo::CopyErrorInfo(std::span out_errors, u32& out_count) { + auto error_count_{std::min(error_count, MaxErrors)}; + std::memset(out_errors.data(), 0, MaxErrors * sizeof(ErrorInfo)); + + for (size_t i = 0; i < error_count_; i++) { + out_errors[i] = errors[i]; + } + out_count = error_count_; +} + +void BehaviorInfo::UpdateFlags(const Flags flags_) { + flags = flags_; +} + +bool BehaviorInfo::IsMemoryForceMappingEnabled() const { + return flags.IsMemoryForceMappingEnabled; +} + +bool BehaviorInfo::IsAdpcmLoopContextBugFixed() const { + return CheckFeatureSupported(SupportTags::AdpcmLoopContextBugFix, user_revision); +} + +bool BehaviorInfo::IsSplitterSupported() const { + return CheckFeatureSupported(SupportTags::Splitter, user_revision); +} + +bool BehaviorInfo::IsSplitterBugFixed() const { + return CheckFeatureSupported(SupportTags::SplitterBugFix, user_revision); +} + +bool BehaviorInfo::IsEffectInfoVersion2Supported() const { + return CheckFeatureSupported(SupportTags::EffectInfoVer2, user_revision); +} + +bool BehaviorInfo::IsVariadicCommandBufferSizeSupported() const { + return CheckFeatureSupported(SupportTags::AudioRendererVariadicCommandBufferSize, + user_revision); +} + +bool BehaviorInfo::IsWaveBufferVer2Supported() const { + return CheckFeatureSupported(SupportTags::WaveBufferVer2, user_revision); +} + +bool BehaviorInfo::IsLongSizePreDelaySupported() const { + return CheckFeatureSupported(SupportTags::LongSizePreDelay, user_revision); +} + +bool BehaviorInfo::IsCommandProcessingTimeEstimatorVersion2Supported() const { + return CheckFeatureSupported(SupportTags::CommandProcessingTimeEstimatorVersion2, + user_revision); +} + +bool BehaviorInfo::IsCommandProcessingTimeEstimatorVersion3Supported() const { + return CheckFeatureSupported(SupportTags::CommandProcessingTimeEstimatorVersion3, + user_revision); +} + +bool BehaviorInfo::IsCommandProcessingTimeEstimatorVersion4Supported() const { + return CheckFeatureSupported(SupportTags::CommandProcessingTimeEstimatorVersion4, + user_revision); +} + +bool BehaviorInfo::IsCommandProcessingTimeEstimatorVersion5Supported() const { + return CheckFeatureSupported(SupportTags::CommandProcessingTimeEstimatorVersion4, + user_revision); +} + +bool BehaviorInfo::IsAudioRendererProcessingTimeLimit70PercentSupported() const { + return CheckFeatureSupported(SupportTags::AudioRendererProcessingTimeLimit70Percent, + user_revision); +} + +bool BehaviorInfo::IsAudioRendererProcessingTimeLimit75PercentSupported() const { + return CheckFeatureSupported(SupportTags::AudioRendererProcessingTimeLimit75Percent, + user_revision); +} + +bool BehaviorInfo::IsAudioRendererProcessingTimeLimit80PercentSupported() const { + return CheckFeatureSupported(SupportTags::AudioRendererProcessingTimeLimit80Percent, + user_revision); +} + +bool BehaviorInfo::IsFlushVoiceWaveBuffersSupported() const { + return CheckFeatureSupported(SupportTags::FlushVoiceWaveBuffers, user_revision); +} + +bool BehaviorInfo::IsElapsedFrameCountSupported() const { + return CheckFeatureSupported(SupportTags::ElapsedFrameCount, user_revision); +} + +bool BehaviorInfo::IsPerformanceMetricsDataFormatVersion2Supported() const { + return CheckFeatureSupported(SupportTags::PerformanceMetricsDataFormatVersion2, user_revision); +} + +size_t BehaviorInfo::GetPerformanceMetricsDataFormat() const { + if (CheckFeatureSupported(SupportTags::PerformanceMetricsDataFormatVersion2, user_revision)) { + return 2; + } + return 1; +} + +bool BehaviorInfo::IsVoicePitchAndSrcSkippedSupported() const { + return CheckFeatureSupported(SupportTags::VoicePitchAndSrcSkipped, user_revision); +} + +bool BehaviorInfo::IsVoicePlayedSampleCountResetAtLoopPointSupported() const { + return CheckFeatureSupported(SupportTags::VoicePlayedSampleCountResetAtLoopPoint, + user_revision); +} + +bool BehaviorInfo::IsBiquadFilterEffectStateClearBugFixed() const { + return CheckFeatureSupported(SupportTags::BiquadFilterEffectStateClearBugFix, user_revision); +} + +bool BehaviorInfo::IsVolumeMixParameterPrecisionQ23Supported() const { + return CheckFeatureSupported(SupportTags::VolumeMixParameterPrecisionQ23, user_revision); +} + +bool BehaviorInfo::UseBiquadFilterFloatProcessing() const { + return CheckFeatureSupported(SupportTags::BiquadFilterFloatProcessing, user_revision); +} + +bool BehaviorInfo::IsMixInParameterDirtyOnlyUpdateSupported() const { + return CheckFeatureSupported(SupportTags::MixInParameterDirtyOnlyUpdate, user_revision); +} + +bool BehaviorInfo::UseMultiTapBiquadFilterProcessing() const { + return CheckFeatureSupported(SupportTags::MultiTapBiquadFilterProcessing, user_revision); +} + +bool BehaviorInfo::IsDeviceApiVersion2Supported() const { + return CheckFeatureSupported(SupportTags::DeviceApiVersion2, user_revision); +} + +bool BehaviorInfo::IsDelayChannelMappingChanged() const { + return CheckFeatureSupported(SupportTags::DelayChannelMappingChange, user_revision); +} + +bool BehaviorInfo::IsReverbChannelMappingChanged() const { + return CheckFeatureSupported(SupportTags::ReverbChannelMappingChange, user_revision); +} + +bool BehaviorInfo::IsI3dl2ReverbChannelMappingChanged() const { + return CheckFeatureSupported(SupportTags::I3dl2ReverbChannelMappingChange, user_revision); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/behavior/behavior_info.h b/src/audio_core/renderer/behavior/behavior_info.h new file mode 100644 index 0000000000..7333c297f3 --- /dev/null +++ b/src/audio_core/renderer/behavior/behavior_info.h @@ -0,0 +1,376 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" +#include "core/hle/service/audio/errors.h" + +namespace AudioCore::AudioRenderer { +/** + * Holds host and user revisions, checks whether render features can be enabled, and reports errors. + */ +class BehaviorInfo { + static constexpr u32 MaxErrors = 10; + +public: + struct ErrorInfo { + /* 0x00 */ Result error_code{0}; + /* 0x04 */ u32 unk_04; + /* 0x08 */ CpuAddr address; + }; + static_assert(sizeof(ErrorInfo) == 0x10, "BehaviorInfo::ErrorInfo has the wrong size!"); + + struct Flags { + u64 IsMemoryForceMappingEnabled : 1; + }; + + struct InParameter { + /* 0x00 */ u32 revision; + /* 0x08 */ Flags flags; + }; + static_assert(sizeof(InParameter) == 0x10, "BehaviorInfo::InParameter has the wrong size!"); + + struct OutStatus { + /* 0x00 */ std::array errors; + /* 0xA0 */ u32 error_count; + /* 0xA4 */ char unkA4[0xC]; + }; + static_assert(sizeof(OutStatus) == 0xB0, "BehaviorInfo::OutStatus has the wrong size!"); + + BehaviorInfo(); + + /** + * Get the host revision as a number. + * + * @return The host revision. + */ + u32 GetProcessRevisionNum() const; + + /** + * Get the host revision in chars, e.g REV8. + * Rev 10 and higher use the ascii characters above 9. + * E.g: + * Rev 10 = REV: + * Rev 11 = REV; + * + * @return The host revision. + */ + u32 GetProcessRevision() const; + + /** + * Get the user revision as a number. + * + * @return The user revision. + */ + u32 GetUserRevisionNum() const; + + /** + * Get the user revision in chars, e.g REV8. + * Rev 10 and higher use the ascii characters above 9. REV: REV; etc. + * + * @return The user revision. + */ + u32 GetUserRevision() const; + + /** + * Set the user revision. + * + * @param user_revision - The user's revision. + */ + void SetUserLibRevision(u32 user_revision); + + /** + * Clear the current error count. + */ + void ClearError(); + + /** + * Append an error to the error list. + * + * @param error - The new error. + */ + void AppendError(ErrorInfo& error); + + /** + * Copy errors to the given output container. + * + * @param out_errors - Output container to receive the errors. + * @param out_count - The number of errors written. + */ + void CopyErrorInfo(std::span out_errors, u32& out_count); + + /** + * Update the behaviour flags. + * + * @param flags - New flags to use. + */ + void UpdateFlags(Flags flags); + + /** + * Check if memory pools can be forcibly mapped. + * + * @return True if enabled, otherwise false. + */ + bool IsMemoryForceMappingEnabled() const; + + /** + * Check if the ADPCM context bug is fixed. + * The ADPCM context was not being sent to the AudioRenderer, leading to incorrect scaling being + * used. + * + * @return True if fixed, otherwise false. + */ + bool IsAdpcmLoopContextBugFixed() const; + + /** + * Check if the splitter is supported. + * + * @return True if supported, otherwise false. + */ + bool IsSplitterSupported() const; + + /** + * Check if the splitter bug is fixed. + * Update is given the wrong number of splitter destinations, leading to invalid data + * being processed. + * + * @return True if supported, otherwise false. + */ + bool IsSplitterBugFixed() const; + + /** + * Check if effects version 2 are supported. + * This gives support for returning effect states from the AudioRenderer, currently only used + * for Limiter statistics. + * + * @return True if supported, otherwise false. + */ + bool IsEffectInfoVersion2Supported() const; + + /** + * Check if a variadic command buffer is supported. + * As of Rev 5 with the added optional performance metric logging, the command + * buffer can be a variable size, so take that into account for calcualting its size. + * + * @return True if supported, otherwise false. + */ + bool IsVariadicCommandBufferSizeSupported() const; + + /** + * Check if wave buffers version 2 are supported. + * See WaveBufferVersion1 and WaveBufferVersion2. + * + * @return True if supported, otherwise false. + */ + bool IsWaveBufferVer2Supported() const; + + /** + * Check if long size pre delay is supported. + * This allows a longer initial delay time for the Reverb command. + * + * @return True if supported, otherwise false. + */ + bool IsLongSizePreDelaySupported() const; + + /** + * Check if the command time estimator version 2 is supported. + * + * @return True if supported, otherwise false. + */ + bool IsCommandProcessingTimeEstimatorVersion2Supported() const; + + /** + * Check if the command time estimator version 3 is supported. + * + * @return True if supported, otherwise false. + */ + bool IsCommandProcessingTimeEstimatorVersion3Supported() const; + + /** + * Check if the command time estimator version 4 is supported. + * + * @return True if supported, otherwise false. + */ + bool IsCommandProcessingTimeEstimatorVersion4Supported() const; + + /** + * Check if the command time estimator version 5 is supported. + * + * @return True if supported, otherwise false. + */ + bool IsCommandProcessingTimeEstimatorVersion5Supported() const; + + /** + * Check if the AudioRenderer can use up to 70% of the allocated processing timeslice. + * + * @return True if supported, otherwise false. + */ + bool IsAudioRendererProcessingTimeLimit70PercentSupported() const; + + /** + * Check if the AudioRenderer can use up to 75% of the allocated processing timeslice. + * + * @return True if supported, otherwise false. + */ + bool IsAudioRendererProcessingTimeLimit75PercentSupported() const; + + /** + * Check if the AudioRenderer can use up to 80% of the allocated processing timeslice. + * + * @return True if supported, otherwise false. + */ + bool IsAudioRendererProcessingTimeLimit80PercentSupported() const; + + /** + * Check if voice flushing is supported + * This allowws low-priority voices to be dropped if the AudioRenderer is running behind. + * + * @return True if supported, otherwise false. + */ + bool IsFlushVoiceWaveBuffersSupported() const; + + /** + * Check if counting the number of elapsed frames is supported. + * This adds extra output to RequestUpdate, returning the number of times the AudioRenderer + * processed a command list. + * + * @return True if supported, otherwise false. + */ + bool IsElapsedFrameCountSupported() const; + + /** + * Check if performance metrics version 2 are supported. + * This adds extra output to RequestUpdate, returning the number of times the AudioRenderer + * (Unused?). + * + * @return True if supported, otherwise false. + */ + bool IsPerformanceMetricsDataFormatVersion2Supported() const; + + /** + * Get the supported performance metrics version. + * Version 2 logs some extra fields in output, such as number of voices dropped, + * processing start time, if the AudioRenderer exceeded its time, etc. + * + * @return Version supported, either 1 or 2. + */ + size_t GetPerformanceMetricsDataFormat() const; + + /** + * Check if skipping voice pitch and sample rate conversion is supported. + * This speeds up the data source commands by skipping resampling if unwanted. + * See AudioCore::AudioRenderer::DecodeFromWaveBuffers + * + * @return True if supported, otherwise false. + */ + bool IsVoicePitchAndSrcSkippedSupported() const; + + /** + * Check if resetting played sample count at loop points is supported. + * This resets the number of samples played in a voice state when a loop point is reached. + * See AudioCore::AudioRenderer::DecodeFromWaveBuffers + * + * @return True if supported, otherwise false. + */ + bool IsVoicePlayedSampleCountResetAtLoopPointSupported() const; + + /** + * Check if the clear state bug for biquad filters is fixed. + * The biquad state was not marked as needing re-initialisation when the effect was updated, it + * was only initialized once with a new effect. + * + * @return True if fixed, otherwise false. + */ + bool IsBiquadFilterEffectStateClearBugFixed() const; + + /** + * Check if Q23 precision is supported for fixed point. + * + * @return True if supported, otherwise false. + */ + bool IsVolumeMixParameterPrecisionQ23Supported() const; + + /** + * Check if float processing for biuad filters is supported. + * + * @return True if supported, otherwise false. + */ + bool UseBiquadFilterFloatProcessing() const; + + /** + * Check if dirty-only mix updates are supported. + * This saves a lot of buffer size as mixes can be large and not change much. + * + * @return True if supported, otherwise false. + */ + bool IsMixInParameterDirtyOnlyUpdateSupported() const; + + /** + * Check if multi-tap biquad filters are supported. + * + * @return True if supported, otherwise false. + */ + bool UseMultiTapBiquadFilterProcessing() const; + + /** + * Check if device api version 2 is supported. + * In the SDK but not in any sysmodule? Not sure, left here for completeness anyway. + * + * @return True if supported, otherwise false. + */ + bool IsDeviceApiVersion2Supported() const; + + /** + * Check if new channel mappings are used for Delay commands. + * Older commands used: + * front left/front right/back left/back right/center/lfe + * Whereas everywhere else in the code uses: + * front left/front right/center/lfe/back left/back right + * This corrects that and makes everything standardised. + * + * @return True if supported, otherwise false. + */ + bool IsDelayChannelMappingChanged() const; + + /** + * Check if new channel mappings are used for Reverb commands. + * Older commands used: + * front left/front right/back left/back right/center/lfe + * Whereas everywhere else in the code uses: + * front left/front right/center/lfe/back left/back right + * This corrects that and makes everything standardised. + * + * @return True if supported, otherwise false. + */ + bool IsReverbChannelMappingChanged() const; + + /** + * Check if new channel mappings are used for I3dl2Reverb commands. + * Older commands used: + * front left/front right/back left/back right/center/lfe + * Whereas everywhere else in the code uses: + * front left/front right/center/lfe/back left/back right + * This corrects that and makes everything standardised. + * + * @return True if supported, otherwise false. + */ + bool IsI3dl2ReverbChannelMappingChanged() const; + + /// Host version + u32 process_revision; + /// User version + u32 user_revision{}; + /// Behaviour flags + Flags flags{}; + /// Errors generated and reported during Update + std::array errors{}; + /// Error count + u32 error_count{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/behavior/info_updater.cpp b/src/audio_core/renderer/behavior/info_updater.cpp new file mode 100644 index 0000000000..06a37e1a66 --- /dev/null +++ b/src/audio_core/renderer/behavior/info_updater.cpp @@ -0,0 +1,539 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/common/feature_support.h" +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/behavior/info_updater.h" +#include "audio_core/renderer/effect/effect_context.h" +#include "audio_core/renderer/effect/effect_reset.h" +#include "audio_core/renderer/memory/memory_pool_info.h" +#include "audio_core/renderer/mix/mix_context.h" +#include "audio_core/renderer/performance/performance_manager.h" +#include "audio_core/renderer/sink/circular_buffer_sink_info.h" +#include "audio_core/renderer/sink/device_sink_info.h" +#include "audio_core/renderer/sink/sink_context.h" +#include "audio_core/renderer/splitter/splitter_context.h" +#include "audio_core/renderer/voice/voice_context.h" + +namespace AudioCore::AudioRenderer { + +InfoUpdater::InfoUpdater(std::span input_, std::span output_, + const u32 process_handle_, BehaviorInfo& behaviour_) + : input{input_.data() + sizeof(UpdateDataHeader)}, + input_origin{input_}, output{output_.data() + sizeof(UpdateDataHeader)}, + output_origin{output_}, in_header{reinterpret_cast( + input_origin.data())}, + out_header{reinterpret_cast(output_origin.data())}, + expected_input_size{input_.size()}, expected_output_size{output_.size()}, + process_handle{process_handle_}, behaviour{behaviour_} { + std::construct_at(out_header, behaviour.GetProcessRevision()); +} + +Result InfoUpdater::UpdateVoiceChannelResources(VoiceContext& voice_context) { + const auto voice_count{voice_context.GetCount()}; + std::span in_params{ + reinterpret_cast(input), voice_count}; + + for (u32 i = 0; i < voice_count; i++) { + auto& resource{voice_context.GetChannelResource(i)}; + resource.in_use = in_params[i].in_use; + if (in_params[i].in_use) { + resource.mix_volumes = in_params[i].mix_volumes; + } + } + + const auto consumed_input_size{voice_count * + static_cast(sizeof(VoiceChannelResource::InParameter))}; + if (consumed_input_size != in_header->voice_resources_size) { + LOG_ERROR(Service_Audio, + "Consumed an incorrect voice resource size, header size={}, consumed={}", + in_header->voice_resources_size, consumed_input_size); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + input += consumed_input_size; + return ResultSuccess; +} + +Result InfoUpdater::UpdateVoices(VoiceContext& voice_context, + std::span memory_pools, + const u32 memory_pool_count) { + const PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count, + behaviour.IsMemoryForceMappingEnabled()); + const auto voice_count{voice_context.GetCount()}; + std::span in_params{ + reinterpret_cast(input), voice_count}; + std::span out_params{reinterpret_cast(output), + voice_count}; + + for (u32 i = 0; i < voice_count; i++) { + auto& voice_info{voice_context.GetInfo(i)}; + voice_info.in_use = false; + } + + u32 new_voice_count{0}; + + for (u32 i = 0; i < voice_count; i++) { + const auto& in_param{in_params[i]}; + std::array voice_states{}; + + if (!in_param.in_use) { + continue; + } + + auto& voice_info{voice_context.GetInfo(in_param.id)}; + + for (u32 channel = 0; channel < in_param.channel_count; channel++) { + voice_states[channel] = &voice_context.GetState(in_param.channel_resource_ids[channel]); + } + + if (in_param.is_new) { + voice_info.Initialize(); + + for (u32 channel = 0; channel < in_param.channel_count; channel++) { + std::memset(voice_states[channel], 0, sizeof(VoiceState)); + } + } + + BehaviorInfo::ErrorInfo update_error{}; + voice_info.UpdateParameters(update_error, in_param, pool_mapper, behaviour); + + if (!update_error.error_code.IsSuccess()) { + behaviour.AppendError(update_error); + } + + std::array, MaxWaveBuffers> wavebuffer_errors{}; + voice_info.UpdateWaveBuffers(wavebuffer_errors, MaxWaveBuffers * 2, in_param, voice_states, + pool_mapper, behaviour); + + for (auto& wavebuffer_error : wavebuffer_errors) { + for (auto& error : wavebuffer_error) { + if (error.error_code.IsError()) { + behaviour.AppendError(error); + } + } + } + + voice_info.WriteOutStatus(out_params[i], in_param, voice_states); + new_voice_count += in_param.channel_count; + } + + auto consumed_input_size{voice_count * static_cast(sizeof(VoiceInfo::InParameter))}; + auto consumed_output_size{voice_count * static_cast(sizeof(VoiceInfo::OutStatus))}; + if (consumed_input_size != in_header->voices_size) { + LOG_ERROR(Service_Audio, "Consumed an incorrect voices size, header size={}, consumed={}", + in_header->voices_size, consumed_input_size); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + out_header->voices_size = consumed_output_size; + out_header->size += consumed_output_size; + input += consumed_input_size; + output += consumed_output_size; + + voice_context.SetActiveCount(new_voice_count); + + return ResultSuccess; +} + +Result InfoUpdater::UpdateEffects(EffectContext& effect_context, const bool renderer_active, + std::span memory_pools, + const u32 memory_pool_count) { + if (behaviour.IsEffectInfoVersion2Supported()) { + return UpdateEffectsVersion2(effect_context, renderer_active, memory_pools, + memory_pool_count); + } else { + return UpdateEffectsVersion1(effect_context, renderer_active, memory_pools, + memory_pool_count); + } +} + +Result InfoUpdater::UpdateEffectsVersion1(EffectContext& effect_context, const bool renderer_active, + std::span memory_pools, + const u32 memory_pool_count) { + PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count, + behaviour.IsMemoryForceMappingEnabled()); + + const auto effect_count{effect_context.GetCount()}; + + std::span in_params{ + reinterpret_cast(input), effect_count}; + std::span out_params{ + reinterpret_cast(output), effect_count}; + + for (u32 i = 0; i < effect_count; i++) { + auto effect_info{&effect_context.GetInfo(i)}; + if (effect_info->GetType() != in_params[i].type) { + effect_info->ForceUnmapBuffers(pool_mapper); + ResetEffect(effect_info, in_params[i].type); + } + + BehaviorInfo::ErrorInfo error_info{}; + effect_info->Update(error_info, in_params[i], pool_mapper); + if (error_info.error_code.IsError()) { + behaviour.AppendError(error_info); + } + + effect_info->StoreStatus(out_params[i], renderer_active); + } + + auto consumed_input_size{effect_count * + static_cast(sizeof(EffectInfoBase::InParameterVersion1))}; + auto consumed_output_size{effect_count * + static_cast(sizeof(EffectInfoBase::OutStatusVersion1))}; + if (consumed_input_size != in_header->effects_size) { + LOG_ERROR(Service_Audio, "Consumed an incorrect effects size, header size={}, consumed={}", + in_header->effects_size, consumed_input_size); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + out_header->effects_size = consumed_output_size; + out_header->size += consumed_output_size; + input += consumed_input_size; + output += consumed_output_size; + + return ResultSuccess; +} + +Result InfoUpdater::UpdateEffectsVersion2(EffectContext& effect_context, const bool renderer_active, + std::span memory_pools, + const u32 memory_pool_count) { + PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count, + behaviour.IsMemoryForceMappingEnabled()); + + const auto effect_count{effect_context.GetCount()}; + + std::span in_params{ + reinterpret_cast(input), effect_count}; + std::span out_params{ + reinterpret_cast(output), effect_count}; + + for (u32 i = 0; i < effect_count; i++) { + auto effect_info{&effect_context.GetInfo(i)}; + if (effect_info->GetType() != in_params[i].type) { + effect_info->ForceUnmapBuffers(pool_mapper); + ResetEffect(effect_info, in_params[i].type); + } + + BehaviorInfo::ErrorInfo error_info{}; + effect_info->Update(error_info, in_params[i], pool_mapper); + + if (error_info.error_code.IsError()) { + behaviour.AppendError(error_info); + } + + effect_info->StoreStatus(out_params[i], renderer_active); + + if (in_params[i].is_new) { + effect_info->InitializeResultState(effect_context.GetDspSharedResultState(i)); + effect_info->InitializeResultState(effect_context.GetResultState(i)); + } + effect_info->UpdateResultState(out_params[i].result_state, + effect_context.GetResultState(i)); + } + + auto consumed_input_size{effect_count * + static_cast(sizeof(EffectInfoBase::InParameterVersion2))}; + auto consumed_output_size{effect_count * + static_cast(sizeof(EffectInfoBase::OutStatusVersion2))}; + if (consumed_input_size != in_header->effects_size) { + LOG_ERROR(Service_Audio, "Consumed an incorrect effects size, header size={}, consumed={}", + in_header->effects_size, consumed_input_size); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + out_header->effects_size = consumed_output_size; + out_header->size += consumed_output_size; + input += consumed_input_size; + output += consumed_output_size; + + return ResultSuccess; +} + +Result InfoUpdater::UpdateMixes(MixContext& mix_context, const u32 mix_buffer_count, + EffectContext& effect_context, SplitterContext& splitter_context) { + s32 mix_count{0}; + u32 consumed_input_size{0}; + + if (behaviour.IsMixInParameterDirtyOnlyUpdateSupported()) { + auto in_dirty_params{reinterpret_cast(input)}; + mix_count = in_dirty_params->count; + input += sizeof(MixInfo::InDirtyParameter); + consumed_input_size = static_cast(sizeof(MixInfo::InDirtyParameter) + + mix_count * sizeof(MixInfo::InParameter)); + } else { + mix_count = mix_context.GetCount(); + consumed_input_size = static_cast(mix_count * sizeof(MixInfo::InParameter)); + } + + if (mix_buffer_count == 0) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + std::span in_params{ + reinterpret_cast(input), static_cast(mix_count)}; + + u32 total_buffer_count{0}; + for (s32 i = 0; i < mix_count; i++) { + const auto& params{in_params[i]}; + + if (params.in_use) { + total_buffer_count += params.buffer_count; + if (params.dest_mix_id > static_cast(mix_context.GetCount()) && + params.dest_mix_id != UnusedMixId && params.mix_id != FinalMixId) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + } + } + + if (total_buffer_count > mix_buffer_count) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + bool mix_dirty{false}; + for (s32 i = 0; i < mix_count; i++) { + const auto& params{in_params[i]}; + + s32 mix_id{i}; + if (behaviour.IsMixInParameterDirtyOnlyUpdateSupported()) { + mix_id = params.mix_id; + } + + auto mix_info{mix_context.GetInfo(mix_id)}; + if (mix_info->in_use != params.in_use) { + mix_info->in_use = params.in_use; + if (!params.in_use) { + mix_info->ClearEffectProcessingOrder(); + } + mix_dirty = true; + } + + if (params.in_use) { + mix_dirty |= mix_info->Update(mix_context.GetEdgeMatrix(), params, effect_context, + splitter_context, behaviour); + } + } + + if (mix_dirty) { + if (behaviour.IsSplitterSupported() && splitter_context.UsingSplitter()) { + if (!mix_context.TSortInfo(splitter_context)) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + } else { + mix_context.SortInfo(); + } + } + + if (consumed_input_size != in_header->mix_size) { + LOG_ERROR(Service_Audio, "Consumed an incorrect mixes size, header size={}, consumed={}", + in_header->mix_size, consumed_input_size); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + input += mix_count * sizeof(MixInfo::InParameter); + + return ResultSuccess; +} + +Result InfoUpdater::UpdateSinks(SinkContext& sink_context, std::span memory_pools, + const u32 memory_pool_count) { + PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count, + behaviour.IsMemoryForceMappingEnabled()); + + std::span in_params{ + reinterpret_cast(input), memory_pool_count}; + std::span out_params{ + reinterpret_cast(output), memory_pool_count}; + + const auto sink_count{sink_context.GetCount()}; + + for (u32 i = 0; i < sink_count; i++) { + const auto& params{in_params[i]}; + auto sink_info{sink_context.GetInfo(i)}; + + if (sink_info->GetType() != params.type) { + sink_info->CleanUp(); + switch (params.type) { + case SinkInfoBase::Type::Invalid: + std::construct_at(reinterpret_cast(sink_info)); + break; + case SinkInfoBase::Type::DeviceSink: + std::construct_at(reinterpret_cast(sink_info)); + break; + case SinkInfoBase::Type::CircularBufferSink: + std::construct_at( + reinterpret_cast(sink_info)); + break; + default: + LOG_ERROR(Service_Audio, "Invalid sink type {}", static_cast(params.type)); + break; + } + } + + BehaviorInfo::ErrorInfo error_info{}; + sink_info->Update(error_info, out_params[i], params, pool_mapper); + + if (error_info.error_code.IsError()) { + behaviour.AppendError(error_info); + } + } + + const auto consumed_input_size{sink_count * + static_cast(sizeof(SinkInfoBase::InParameter))}; + const auto consumed_output_size{sink_count * static_cast(sizeof(SinkInfoBase::OutStatus))}; + if (consumed_input_size != in_header->sinks_size) { + LOG_ERROR(Service_Audio, "Consumed an incorrect sinks size, header size={}, consumed={}", + in_header->sinks_size, consumed_input_size); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + input += consumed_input_size; + output += consumed_output_size; + out_header->sinks_size = consumed_output_size; + out_header->size += consumed_output_size; + + return ResultSuccess; +} + +Result InfoUpdater::UpdateMemoryPools(std::span memory_pools, + const u32 memory_pool_count) { + PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count, + behaviour.IsMemoryForceMappingEnabled()); + std::span in_params{ + reinterpret_cast(input), memory_pool_count}; + std::span out_params{ + reinterpret_cast(output), memory_pool_count}; + + for (size_t i = 0; i < memory_pool_count; i++) { + auto state{pool_mapper.Update(memory_pools[i], in_params[i], out_params[i])}; + if (state != MemoryPoolInfo::ResultState::Success && + state != MemoryPoolInfo::ResultState::BadParam && + state != MemoryPoolInfo::ResultState::MapFailed && + state != MemoryPoolInfo::ResultState::InUse) { + LOG_WARNING(Service_Audio, "Invalid ResultState from updating memory pools"); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + } + + const auto consumed_input_size{memory_pool_count * + static_cast(sizeof(MemoryPoolInfo::InParameter))}; + const auto consumed_output_size{memory_pool_count * + static_cast(sizeof(MemoryPoolInfo::OutStatus))}; + if (consumed_input_size != in_header->memory_pool_size) { + LOG_ERROR(Service_Audio, + "Consumed an incorrect memory pool size, header size={}, consumed={}", + in_header->memory_pool_size, consumed_input_size); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + input += consumed_input_size; + output += consumed_output_size; + out_header->memory_pool_size = consumed_output_size; + out_header->size += consumed_output_size; + return ResultSuccess; +} + +Result InfoUpdater::UpdatePerformanceBuffer(std::span performance_output, + const u64 performance_output_size, + PerformanceManager* performance_manager) { + auto in_params{reinterpret_cast(input)}; + auto out_params{reinterpret_cast(output)}; + + if (performance_manager != nullptr) { + out_params->history_size = + performance_manager->CopyHistories(performance_output.data(), performance_output_size); + performance_manager->SetDetailTarget(in_params->target_node_id); + } else { + out_params->history_size = 0; + } + + const auto consumed_input_size{static_cast(sizeof(PerformanceManager::InParameter))}; + const auto consumed_output_size{static_cast(sizeof(PerformanceManager::OutStatus))}; + if (consumed_input_size != in_header->performance_buffer_size) { + LOG_ERROR(Service_Audio, + "Consumed an incorrect performance size, header size={}, consumed={}", + in_header->performance_buffer_size, consumed_input_size); + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + input += consumed_input_size; + output += consumed_output_size; + out_header->performance_buffer_size = consumed_output_size; + out_header->size += consumed_output_size; + return ResultSuccess; +} + +Result InfoUpdater::UpdateBehaviorInfo(BehaviorInfo& behaviour_) { + const auto in_params{reinterpret_cast(input)}; + + if (!CheckValidRevision(in_params->revision)) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + if (in_params->revision != behaviour_.GetUserRevision()) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + behaviour_.ClearError(); + behaviour_.UpdateFlags(in_params->flags); + + if (in_header->behaviour_size != sizeof(BehaviorInfo::InParameter)) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + input += sizeof(BehaviorInfo::InParameter); + return ResultSuccess; +} + +Result InfoUpdater::UpdateErrorInfo(BehaviorInfo& behaviour_) { + auto out_params{reinterpret_cast(output)}; + behaviour_.CopyErrorInfo(out_params->errors, out_params->error_count); + + const auto consumed_output_size{static_cast(sizeof(BehaviorInfo::OutStatus))}; + + output += consumed_output_size; + out_header->behaviour_size = consumed_output_size; + out_header->size += consumed_output_size; + return ResultSuccess; +} + +Result InfoUpdater::UpdateSplitterInfo(SplitterContext& splitter_context) { + u32 consumed_size{0}; + if (!splitter_context.Update(input, consumed_size)) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + + input += consumed_size; + + return ResultSuccess; +} + +Result InfoUpdater::UpdateRendererInfo(const u64 elapsed_frames) { + struct RenderInfo { + /* 0x00 */ u64 frames_elapsed; + /* 0x08 */ char unk08[0x8]; + }; + static_assert(sizeof(RenderInfo) == 0x10, "RenderInfo has the wrong size!"); + + auto out_params{reinterpret_cast(output)}; + out_params->frames_elapsed = elapsed_frames; + + const auto consumed_output_size{static_cast(sizeof(RenderInfo))}; + + output += consumed_output_size; + out_header->render_info_size = consumed_output_size; + out_header->size += consumed_output_size; + + return ResultSuccess; +} + +Result InfoUpdater::CheckConsumedSize() { + if (CpuAddr(input) - CpuAddr(input_origin.data()) != expected_input_size) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } else if (CpuAddr(output) - CpuAddr(output_origin.data()) != expected_output_size) { + return Service::Audio::ERR_INVALID_UPDATE_DATA; + } + return ResultSuccess; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/behavior/info_updater.h b/src/audio_core/renderer/behavior/info_updater.h new file mode 100644 index 0000000000..f0b445d9c0 --- /dev/null +++ b/src/audio_core/renderer/behavior/info_updater.h @@ -0,0 +1,205 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" +#include "core/hle/service/audio/errors.h" + +namespace AudioCore::AudioRenderer { +class BehaviorInfo; +class VoiceContext; +class MixContext; +class SinkContext; +class SplitterContext; +class EffectContext; +class MemoryPoolInfo; +class PerformanceManager; + +class InfoUpdater { + struct UpdateDataHeader { + explicit UpdateDataHeader(u32 revision_) : revision{revision_} {} + + /* 0x00 */ u32 revision; + /* 0x04 */ u32 behaviour_size{}; + /* 0x08 */ u32 memory_pool_size{}; + /* 0x0C */ u32 voices_size{}; + /* 0x10 */ u32 voice_resources_size{}; + /* 0x14 */ u32 effects_size{}; + /* 0x18 */ u32 mix_size{}; + /* 0x1C */ u32 sinks_size{}; + /* 0x20 */ u32 performance_buffer_size{}; + /* 0x24 */ char unk24[4]; + /* 0x28 */ u32 render_info_size{}; + /* 0x2C */ char unk2C[0x10]; + /* 0x3C */ u32 size{sizeof(UpdateDataHeader)}; + }; + static_assert(sizeof(UpdateDataHeader) == 0x40, "UpdateDataHeader has the wrong size!"); + +public: + explicit InfoUpdater(std::span input, std::span output, u32 process_handle, + BehaviorInfo& behaviour); + + /** + * Update the voice channel resources. + * + * @param voice_context - Voice context to update. + * @return Result code. + */ + Result UpdateVoiceChannelResources(VoiceContext& voice_context); + + /** + * Update voices. + * + * @param voice_context - Voice context to update. + * @param memory_pools - Memory pools to use for these voices. + * @param memory_pool_count - Number of memory pools. + * @return Result code. + */ + Result UpdateVoices(VoiceContext& voice_context, std::span memory_pools, + u32 memory_pool_count); + + /** + * Update effects. + * + * @param effect_context - Effect context to update. + * @param renderer_active - Whether the AudioRenderer is active. + * @param memory_pools - Memory pools to use for these voices. + * @param memory_pool_count - Number of memory pools. + * @return Result code. + */ + Result UpdateEffects(EffectContext& effect_context, bool renderer_active, + std::span memory_pools, u32 memory_pool_count); + + /** + * Update mixes. + * + * @param mix_context - Mix context to update. + * @param mix_buffer_count - Number of mix buffers. + * @param effect_context - Effect context to update effort order. + * @param splitter_context - Splitter context for the mixes. + * @return Result code. + */ + Result UpdateMixes(MixContext& mix_context, u32 mix_buffer_count, EffectContext& effect_context, + SplitterContext& splitter_context); + + /** + * Update sinks. + * + * @param sink_context - Sink context to update. + * @param memory_pools - Memory pools to use for these voices. + * @param memory_pool_count - Number of memory pools. + * @return Result code. + */ + Result UpdateSinks(SinkContext& sink_context, std::span memory_pools, + u32 memory_pool_count); + + /** + * Update memory pools. + * + * @param memory_pools - Memory pools to use for these voices. + * @param memory_pool_count - Number of memory pools. + * @return Result code. + */ + Result UpdateMemoryPools(std::span memory_pools, u32 memory_pool_count); + + /** + * Update the performance buffer. + * + * @param output - Output buffer for performance metrics. + * @param output_size - Output buffer size. + * @param performance_manager - Performance manager.. + * @return Result code. + */ + Result UpdatePerformanceBuffer(std::span output, u64 output_size, + PerformanceManager* performance_manager); + + /** + * Update behaviour. + * + * @param behaviour - Behaviour to update. + * @return Result code. + */ + Result UpdateBehaviorInfo(BehaviorInfo& behaviour); + + /** + * Update errors. + * + * @param behaviour - Behaviour to update. + * @return Result code. + */ + Result UpdateErrorInfo(BehaviorInfo& behaviour); + + /** + * Update splitter. + * + * @param splitter_context - Splitter context to update. + * @return Result code. + */ + Result UpdateSplitterInfo(SplitterContext& splitter_context); + + /** + * Update renderer info. + * + * @param elapsed_frames - Number of elapsed frames. + * @return Result code. + */ + Result UpdateRendererInfo(u64 elapsed_frames); + + /** + * Check that the input.output sizes match their expected values. + * + * @return Result code. + */ + Result CheckConsumedSize(); + +private: + /** + * Update effects version 1. + * + * @param effect_context - Effect context to update. + * @param renderer_active - Is the AudioRenderer active? + * @param memory_pools - Memory pools to use for these voices. + * @param memory_pool_count - Number of memory pools. + * @return Result code. + */ + Result UpdateEffectsVersion1(EffectContext& effect_context, bool renderer_active, + std::span memory_pools, u32 memory_pool_count); + + /** + * Update effects version 2. + * + * @param effect_context - Effect context to update. + * @param renderer_active - Is the AudioRenderer active? + * @param memory_pools - Memory pools to use for these voices. + * @param memory_pool_count - Number of memory pools. + * @return Result code. + */ + Result UpdateEffectsVersion2(EffectContext& effect_context, bool renderer_active, + std::span memory_pools, u32 memory_pool_count); + + /// Input buffer + u8 const* input; + /// Input buffer start + std::span input_origin; + /// Output buffer start + u8* output; + /// Output buffer start + std::span output_origin; + /// Input header + const UpdateDataHeader* in_header; + /// Output header + UpdateDataHeader* out_header; + /// Expected input size, see CheckConsumedSize + u64 expected_input_size; + /// Expected output size, see CheckConsumedSize + u64 expected_output_size; + /// Unused + u32 process_handle; + /// Behaviour + BehaviorInfo& behaviour; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/command_buffer.cpp b/src/audio_core/renderer/command/command_buffer.cpp new file mode 100644 index 0000000000..40074cf14a --- /dev/null +++ b/src/audio_core/renderer/command/command_buffer.cpp @@ -0,0 +1,714 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/command/command_buffer.h" +#include "audio_core/renderer/command/command_list_header.h" +#include "audio_core/renderer/command/command_processing_time_estimator.h" +#include "audio_core/renderer/effect/biquad_filter.h" +#include "audio_core/renderer/effect/delay.h" +#include "audio_core/renderer/effect/reverb.h" +#include "audio_core/renderer/memory/memory_pool_info.h" +#include "audio_core/renderer/mix/mix_info.h" +#include "audio_core/renderer/sink/circular_buffer_sink_info.h" +#include "audio_core/renderer/sink/device_sink_info.h" +#include "audio_core/renderer/sink/sink_info_base.h" +#include "audio_core/renderer/voice/voice_info.h" +#include "audio_core/renderer/voice/voice_state.h" + +namespace AudioCore::AudioRenderer { + +template +T& CommandBuffer::GenerateStart(const s32 node_id) { + if (size + sizeof(T) >= command_list.size_bytes()) { + LOG_ERROR( + Service_Audio, + "Attempting to write commands beyond the end of allocated command buffer memory!"); + UNREACHABLE(); + } + + auto& cmd{*std::construct_at(reinterpret_cast(&command_list[size]))}; + + cmd.magic = CommandMagic; + cmd.enabled = true; + cmd.type = Id; + cmd.size = sizeof(T); + cmd.node_id = node_id; + + return cmd; +} + +template +void CommandBuffer::GenerateEnd(T& cmd) { + cmd.estimated_process_time = time_estimator->Estimate(cmd); + estimated_process_time += cmd.estimated_process_time; + size += sizeof(T); + count++; +} + +void CommandBuffer::GeneratePcmInt16Version1Command(const s32 node_id, + const MemoryPoolInfo& memory_pool_, + VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel) { + auto& cmd{ + GenerateStart( + node_id)}; + + cmd.src_quality = voice_info.src_quality; + cmd.output_index = buffer_count + channel; + cmd.flags = voice_info.flags & 3; + cmd.sample_rate = voice_info.sample_rate; + cmd.pitch = voice_info.pitch; + cmd.channel_index = channel; + cmd.channel_count = voice_info.channel_count; + + for (u32 i = 0; i < MaxWaveBuffers; i++) { + voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]); + } + + cmd.voice_state = memory_pool_.Translate(CpuAddr(&voice_state), sizeof(VoiceState)); + + GenerateEnd(cmd); +} + +void CommandBuffer::GeneratePcmInt16Version2Command(const s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel) { + auto& cmd{ + GenerateStart( + node_id)}; + + cmd.src_quality = voice_info.src_quality; + cmd.output_index = buffer_count + channel; + cmd.flags = voice_info.flags & 3; + cmd.sample_rate = voice_info.sample_rate; + cmd.pitch = voice_info.pitch; + cmd.channel_index = channel; + cmd.channel_count = voice_info.channel_count; + + for (u32 i = 0; i < MaxWaveBuffers; i++) { + voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]); + } + + cmd.voice_state = memory_pool->Translate(CpuAddr(&voice_state), sizeof(VoiceState)); + + GenerateEnd(cmd); +} + +void CommandBuffer::GeneratePcmFloatVersion1Command(const s32 node_id, + const MemoryPoolInfo& memory_pool_, + VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel) { + auto& cmd{ + GenerateStart( + node_id)}; + + cmd.src_quality = voice_info.src_quality; + cmd.output_index = buffer_count + channel; + cmd.flags = voice_info.flags & 3; + cmd.sample_rate = voice_info.sample_rate; + cmd.pitch = voice_info.pitch; + cmd.channel_index = channel; + cmd.channel_count = voice_info.channel_count; + + for (u32 i = 0; i < MaxWaveBuffers; i++) { + voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]); + } + + cmd.voice_state = memory_pool_.Translate(CpuAddr(&voice_state), sizeof(VoiceState)); + + GenerateEnd(cmd); +} + +void CommandBuffer::GeneratePcmFloatVersion2Command(const s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel) { + auto& cmd{ + GenerateStart( + node_id)}; + + cmd.src_quality = voice_info.src_quality; + cmd.output_index = buffer_count + channel; + cmd.flags = voice_info.flags & 3; + cmd.sample_rate = voice_info.sample_rate; + cmd.pitch = voice_info.pitch; + cmd.channel_index = channel; + cmd.channel_count = voice_info.channel_count; + + for (u32 i = 0; i < MaxWaveBuffers; i++) { + voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]); + } + + cmd.voice_state = memory_pool->Translate(CpuAddr(&voice_state), sizeof(VoiceState)); + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateAdpcmVersion1Command(const s32 node_id, + const MemoryPoolInfo& memory_pool_, + VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel) { + auto& cmd{ + GenerateStart(node_id)}; + + cmd.src_quality = voice_info.src_quality; + cmd.output_index = buffer_count + channel; + cmd.flags = voice_info.flags & 3; + cmd.sample_rate = voice_info.sample_rate; + cmd.pitch = voice_info.pitch; + + for (u32 i = 0; i < MaxWaveBuffers; i++) { + voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]); + } + + cmd.voice_state = memory_pool_.Translate(CpuAddr(&voice_state), sizeof(VoiceState)); + cmd.data_address = voice_info.data_address.GetReference(true); + cmd.data_size = voice_info.data_address.GetSize(); + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateAdpcmVersion2Command(const s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel) { + auto& cmd{ + GenerateStart(node_id)}; + + cmd.src_quality = voice_info.src_quality; + cmd.output_index = buffer_count + channel; + cmd.flags = voice_info.flags & 3; + cmd.sample_rate = voice_info.sample_rate; + cmd.pitch = voice_info.pitch; + cmd.channel_index = channel; + cmd.channel_count = voice_info.channel_count; + + for (u32 i = 0; i < MaxWaveBuffers; i++) { + voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]); + } + + cmd.voice_state = memory_pool->Translate(CpuAddr(&voice_state), sizeof(VoiceState)); + cmd.data_address = voice_info.data_address.GetReference(true); + cmd.data_size = voice_info.data_address.GetSize(); + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateVolumeCommand(const s32 node_id, const s16 buffer_offset, + const s16 input_index, const f32 volume, + const u8 precision) { + auto& cmd{GenerateStart(node_id)}; + + cmd.precision = precision; + cmd.input_index = buffer_offset + input_index; + cmd.output_index = buffer_offset + input_index; + cmd.volume = volume; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateVolumeRampCommand(const s32 node_id, VoiceInfo& voice_info, + const s16 buffer_count, const u8 precision) { + auto& cmd{GenerateStart(node_id)}; + + cmd.input_index = buffer_count; + cmd.output_index = buffer_count; + cmd.prev_volume = voice_info.prev_volume; + cmd.volume = voice_info.volume; + cmd.precision = precision; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel, + const u32 biquad_index, + const bool use_float_processing) { + auto& cmd{GenerateStart(node_id)}; + + cmd.input = buffer_count + channel; + cmd.output = buffer_count + channel; + + cmd.biquad = voice_info.biquads[biquad_index]; + + cmd.state = memory_pool->Translate(CpuAddr(voice_state.biquad_states[biquad_index].data()), + MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState)); + + cmd.needs_init = !voice_info.biquad_initialized[biquad_index]; + cmd.use_float_processing = use_float_processing; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, EffectInfoBase& effect_info, + const s16 buffer_offset, const s8 channel, + const bool needs_init, + const bool use_float_processing) { + auto& cmd{GenerateStart(node_id)}; + + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + const auto state{ + reinterpret_cast(effect_info.GetStateBuffer())}; + + cmd.input = buffer_offset + parameter.inputs[channel]; + cmd.output = buffer_offset + parameter.outputs[channel]; + + cmd.biquad.b = parameter.b; + cmd.biquad.a = parameter.a; + + cmd.state = memory_pool->Translate(CpuAddr(state), + MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState)); + + cmd.needs_init = needs_init; + cmd.use_float_processing = use_float_processing; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateMixCommand(const s32 node_id, const s16 input_index, + const s16 output_index, const s16 buffer_offset, + const f32 volume, const u8 precision) { + auto& cmd{GenerateStart(node_id)}; + + cmd.input_index = input_index; + cmd.output_index = output_index; + cmd.volume = volume; + cmd.precision = precision; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateMixRampCommand(const s32 node_id, + [[maybe_unused]] const s16 buffer_count, + const s16 input_index, const s16 output_index, + const f32 volume, const f32 prev_volume, + const CpuAddr prev_samples, const u8 precision) { + if (volume == 0.0f && prev_volume == 0.0f) { + return; + } + + auto& cmd{GenerateStart(node_id)}; + + cmd.input_index = input_index; + cmd.output_index = output_index; + cmd.prev_volume = prev_volume; + cmd.volume = volume; + cmd.previous_sample = prev_samples; + cmd.precision = precision; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateMixRampGroupedCommand(const s32 node_id, const s16 buffer_count, + const s16 input_index, s16 output_index, + std::span volumes, + std::span prev_volumes, + const CpuAddr prev_samples, const u8 precision) { + auto& cmd{GenerateStart(node_id)}; + + cmd.buffer_count = buffer_count; + + for (s32 i = 0; i < buffer_count; i++) { + cmd.inputs[i] = input_index; + cmd.outputs[i] = output_index++; + cmd.prev_volumes[i] = prev_volumes[i]; + cmd.volumes[i] = volumes[i]; + } + + cmd.previous_samples = prev_samples; + cmd.precision = precision; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateDepopPrepareCommand(const s32 node_id, const VoiceState& voice_state, + std::span buffer, const s16 buffer_count, + s16 buffer_offset, const bool was_playing) { + auto& cmd{GenerateStart(node_id)}; + + cmd.enabled = was_playing; + + for (u32 i = 0; i < MaxMixBuffers; i++) { + cmd.inputs[i] = buffer_offset++; + } + + cmd.previous_samples = memory_pool->Translate(CpuAddr(voice_state.previous_samples.data()), + MaxMixBuffers * sizeof(s32)); + cmd.buffer_count = buffer_count; + cmd.depop_buffer = memory_pool->Translate(CpuAddr(buffer.data()), buffer_count * sizeof(s32)); + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateDepopForMixBuffersCommand(const s32 node_id, const MixInfo& mix_info, + std::span depop_buffer) { + auto& cmd{GenerateStart(node_id)}; + + cmd.input = mix_info.buffer_offset; + cmd.count = mix_info.buffer_count; + cmd.decay = mix_info.sample_rate == TargetSampleRate ? 0.96218872f : 0.94369507f; + cmd.depop_buffer = + memory_pool->Translate(CpuAddr(depop_buffer.data()), mix_info.buffer_count * sizeof(s32)); + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateDelayCommand(const s32 node_id, EffectInfoBase& effect_info, + const s16 buffer_offset) { + auto& cmd{GenerateStart(node_id)}; + + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + const auto state{effect_info.GetStateBuffer()}; + + if (IsChannelCountValid(parameter.channel_count)) { + const auto state_buffer{memory_pool->Translate(CpuAddr(state), sizeof(DelayInfo::State))}; + if (state_buffer) { + for (s16 channel = 0; channel < parameter.channel_count; channel++) { + cmd.inputs[channel] = buffer_offset + parameter.inputs[channel]; + cmd.outputs[channel] = buffer_offset + parameter.outputs[channel]; + } + + if (!behavior->IsDelayChannelMappingChanged() && parameter.channel_count == 6) { + UseOldChannelMapping(cmd.inputs, cmd.outputs); + } + + cmd.parameter = parameter; + cmd.effect_enabled = effect_info.IsEnabled(); + cmd.state = state_buffer; + cmd.workbuffer = effect_info.GetWorkbuffer(-1); + } + } + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateUpsampleCommand(const s32 node_id, const s16 buffer_offset, + UpsamplerInfo& upsampler_info, const u32 input_count, + std::span inputs, const s16 buffer_count, + const u32 sample_count_, const u32 sample_rate_) { + auto& cmd{GenerateStart(node_id)}; + + cmd.samples_buffer = memory_pool->Translate(upsampler_info.samples_pos, + upsampler_info.sample_count * sizeof(s32)); + cmd.inputs = memory_pool->Translate(CpuAddr(upsampler_info.inputs.data()), MaxChannels); + cmd.buffer_count = buffer_count; + cmd.unk_20 = 0; + cmd.source_sample_count = sample_count_; + cmd.source_sample_rate = sample_rate_; + + upsampler_info.input_count = input_count; + for (u32 i = 0; i < input_count; i++) { + upsampler_info.inputs[i] = buffer_offset + inputs[i]; + } + + cmd.upsampler_info = memory_pool->Translate(CpuAddr(&upsampler_info), sizeof(UpsamplerInfo)); + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateDownMix6chTo2chCommand(const s32 node_id, std::span inputs, + const s16 buffer_offset, + std::span downmix_coeff) { + auto& cmd{GenerateStart(node_id)}; + + for (u32 i = 0; i < MaxChannels; i++) { + cmd.inputs[i] = buffer_offset + inputs[i]; + cmd.outputs[i] = buffer_offset + inputs[i]; + } + + for (u32 i = 0; i < 4; i++) { + cmd.down_mix_coeff[i] = downmix_coeff[i]; + } + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateAuxCommand(const s32 node_id, EffectInfoBase& effect_info, + const s16 input_index, const s16 output_index, + const s16 buffer_offset, const u32 update_count, + const u32 count_max, const u32 write_offset) { + auto& cmd{GenerateStart(node_id)}; + + if (effect_info.GetSendBuffer() != 0 && effect_info.GetReturnBuffer() != 0) { + cmd.input = buffer_offset + input_index; + cmd.output = buffer_offset + output_index; + cmd.send_buffer_info = effect_info.GetSendBufferInfo(); + cmd.send_buffer = effect_info.GetSendBuffer(); + cmd.return_buffer_info = effect_info.GetReturnBufferInfo(); + cmd.return_buffer = effect_info.GetReturnBuffer(); + cmd.count_max = count_max; + cmd.write_offset = write_offset; + cmd.update_count = update_count; + cmd.effect_enabled = effect_info.IsEnabled(); + } + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateDeviceSinkCommand(const s32 node_id, const s16 buffer_offset, + SinkInfoBase& sink_info, const u32 session_id, + std::span samples_buffer) { + auto& cmd{GenerateStart(node_id)}; + const auto& parameter{ + *reinterpret_cast(sink_info.GetParameter())}; + auto state{*reinterpret_cast(sink_info.GetState())}; + + cmd.session_id = session_id; + + if (state.upsampler_info != nullptr) { + const auto size_{state.upsampler_info->sample_count * parameter.input_count}; + const auto size_bytes{size_ * sizeof(s32)}; + const auto addr{memory_pool->Translate(state.upsampler_info->samples_pos, size_bytes)}; + cmd.sample_buffer = {reinterpret_cast(addr), + parameter.input_count * state.upsampler_info->sample_count}; + } else { + cmd.sample_buffer = samples_buffer; + } + + cmd.input_count = parameter.input_count; + for (u32 i = 0; i < parameter.input_count; i++) { + cmd.inputs[i] = buffer_offset + parameter.inputs[i]; + } + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateCircularBufferSinkCommand(const s32 node_id, SinkInfoBase& sink_info, + const s16 buffer_offset) { + auto& cmd{GenerateStart(node_id)}; + const auto& parameter{*reinterpret_cast( + sink_info.GetParameter())}; + auto state{ + *reinterpret_cast(sink_info.GetState())}; + + cmd.input_count = parameter.input_count; + for (u32 i = 0; i < parameter.input_count; i++) { + cmd.inputs[i] = buffer_offset + parameter.inputs[i]; + } + + cmd.address = state.address_info.GetReference(true); + cmd.size = parameter.size; + cmd.pos = state.current_pos; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateReverbCommand(const s32 node_id, EffectInfoBase& effect_info, + const s16 buffer_offset, + const bool long_size_pre_delay_supported) { + auto& cmd{GenerateStart(node_id)}; + + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + const auto state{effect_info.GetStateBuffer()}; + + if (IsChannelCountValid(parameter.channel_count)) { + const auto state_buffer{memory_pool->Translate(CpuAddr(state), sizeof(ReverbInfo::State))}; + if (state_buffer) { + for (s16 channel = 0; channel < parameter.channel_count; channel++) { + cmd.inputs[channel] = buffer_offset + parameter.inputs[channel]; + cmd.outputs[channel] = buffer_offset + parameter.outputs[channel]; + } + + if (!behavior->IsReverbChannelMappingChanged() && parameter.channel_count == 6) { + UseOldChannelMapping(cmd.inputs, cmd.outputs); + } + + cmd.parameter = parameter; + cmd.effect_enabled = effect_info.IsEnabled(); + cmd.state = state_buffer; + cmd.workbuffer = effect_info.GetWorkbuffer(-1); + cmd.long_size_pre_delay_supported = long_size_pre_delay_supported; + } + } + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateI3dl2ReverbCommand(const s32 node_id, EffectInfoBase& effect_info, + const s16 buffer_offset) { + auto& cmd{GenerateStart(node_id)}; + + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + const auto state{effect_info.GetStateBuffer()}; + + if (IsChannelCountValid(parameter.channel_count)) { + const auto state_buffer{ + memory_pool->Translate(CpuAddr(state), sizeof(I3dl2ReverbInfo::State))}; + if (state_buffer) { + for (s16 channel = 0; channel < parameter.channel_count; channel++) { + cmd.inputs[channel] = buffer_offset + parameter.inputs[channel]; + cmd.outputs[channel] = buffer_offset + parameter.outputs[channel]; + } + + if (!behavior->IsI3dl2ReverbChannelMappingChanged() && parameter.channel_count == 6) { + UseOldChannelMapping(cmd.inputs, cmd.outputs); + } + + cmd.parameter = parameter; + cmd.effect_enabled = effect_info.IsEnabled(); + cmd.state = state_buffer; + cmd.workbuffer = effect_info.GetWorkbuffer(-1); + } + } + + GenerateEnd(cmd); +} + +void CommandBuffer::GeneratePerformanceCommand(const s32 node_id, const PerformanceState state, + const PerformanceEntryAddresses& entry_addresses) { + auto& cmd{GenerateStart(node_id)}; + + cmd.state = state; + cmd.entry_address = entry_addresses; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateClearMixCommand(const s32 node_id) { + auto& cmd{GenerateStart(node_id)}; + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateCopyMixBufferCommand(const s32 node_id, EffectInfoBase& effect_info, + const s16 buffer_offset, const s8 channel) { + auto& cmd{GenerateStart(node_id)}; + + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + cmd.input_index = buffer_offset + parameter.inputs[channel]; + cmd.output_index = buffer_offset + parameter.outputs[channel]; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateLightLimiterCommand( + const s32 node_id, const s16 buffer_offset, + const LightLimiterInfo::ParameterVersion1& parameter, const LightLimiterInfo::State& state, + const bool enabled, const CpuAddr workbuffer) { + auto& cmd{GenerateStart(node_id)}; + + if (IsChannelCountValid(parameter.channel_count)) { + const auto state_buffer{ + memory_pool->Translate(CpuAddr(&state), sizeof(LightLimiterInfo::State))}; + if (state_buffer) { + for (s8 channel = 0; channel < parameter.channel_count; channel++) { + cmd.inputs[channel] = buffer_offset + parameter.inputs[channel]; + cmd.outputs[channel] = buffer_offset + parameter.outputs[channel]; + } + + std::memcpy(&cmd.parameter, ¶meter, sizeof(LightLimiterInfo::ParameterVersion1)); + cmd.effect_enabled = enabled; + cmd.state = state_buffer; + cmd.workbuffer = workbuffer; + } + } + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateLightLimiterCommand( + const s32 node_id, const s16 buffer_offset, + const LightLimiterInfo::ParameterVersion2& parameter, + const LightLimiterInfo::StatisticsInternal& statistics, const LightLimiterInfo::State& state, + const bool enabled, const CpuAddr workbuffer) { + auto& cmd{GenerateStart(node_id)}; + if (IsChannelCountValid(parameter.channel_count)) { + const auto state_buffer{ + memory_pool->Translate(CpuAddr(&state), sizeof(LightLimiterInfo::State))}; + if (state_buffer) { + for (s8 channel = 0; channel < parameter.channel_count; channel++) { + cmd.inputs[channel] = buffer_offset + parameter.inputs[channel]; + cmd.outputs[channel] = buffer_offset + parameter.outputs[channel]; + } + + cmd.parameter = parameter; + cmd.effect_enabled = enabled; + cmd.state = state_buffer; + if (cmd.parameter.statistics_enabled) { + cmd.result_state = memory_pool->Translate( + CpuAddr(&statistics), sizeof(LightLimiterInfo::StatisticsInternal)); + } else { + cmd.result_state = 0; + } + cmd.workbuffer = workbuffer; + } + } + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateMultitapBiquadFilterCommand(const s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel) { + auto& cmd{GenerateStart(node_id)}; + + cmd.input = buffer_count + channel; + cmd.output = buffer_count + channel; + cmd.biquads = voice_info.biquads; + + cmd.states[0] = + memory_pool->Translate(CpuAddr(voice_state.biquad_states[0].data()), + MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState)); + cmd.states[1] = + memory_pool->Translate(CpuAddr(voice_state.biquad_states[1].data()), + MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState)); + + cmd.needs_init[0] = !voice_info.biquad_initialized[0]; + cmd.needs_init[1] = !voice_info.biquad_initialized[1]; + cmd.filter_tap_count = MaxBiquadFilters; + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateCaptureCommand(const s32 node_id, EffectInfoBase& effect_info, + const s16 input_index, const s16 output_index, + const s16 buffer_offset, const u32 update_count, + const u32 count_max, const u32 write_offset) { + auto& cmd{GenerateStart(node_id)}; + + if (effect_info.GetSendBuffer()) { + cmd.input = buffer_offset + input_index; + cmd.output = buffer_offset + output_index; + cmd.send_buffer_info = effect_info.GetSendBufferInfo(); + cmd.send_buffer = effect_info.GetSendBuffer(); + cmd.count_max = count_max; + cmd.write_offset = write_offset; + cmd.update_count = update_count; + cmd.effect_enabled = effect_info.IsEnabled(); + } + + GenerateEnd(cmd); +} + +void CommandBuffer::GenerateCompressorCommand(s16 buffer_offset, EffectInfoBase& effect_info, + s32 node_id) { + auto& cmd{GenerateStart(node_id)}; + + auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + auto state{reinterpret_cast(effect_info.GetStateBuffer())}; + + if (IsChannelCountValid(parameter.channel_count)) { + auto state_buffer{memory_pool->Translate(CpuAddr(state), sizeof(CompressorInfo::State))}; + if (state_buffer) { + for (u16 channel = 0; channel < parameter.channel_count; channel++) { + cmd.inputs[channel] = buffer_offset + parameter.inputs[channel]; + cmd.outputs[channel] = buffer_offset + parameter.outputs[channel]; + } + cmd.parameter = parameter; + cmd.workbuffer = state_buffer; + cmd.enabled = effect_info.IsEnabled(); + } + } + + GenerateEnd(cmd); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/command_buffer.h b/src/audio_core/renderer/command/command_buffer.h new file mode 100644 index 0000000000..496b0e50a3 --- /dev/null +++ b/src/audio_core/renderer/command/command_buffer.h @@ -0,0 +1,466 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/commands.h" +#include "audio_core/renderer/effect/light_limiter.h" +#include "audio_core/renderer/performance/performance_manager.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +struct UpsamplerInfo; +struct VoiceState; +class EffectInfoBase; +class ICommandProcessingTimeEstimator; +class MixInfo; +class MemoryPoolInfo; +class SinkInfoBase; +class VoiceInfo; + +/** + * Utility functions to generate and add commands into the current command list. + */ +class CommandBuffer { +public: + /** + * Generate a PCM s16 version 1 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param memory_pool - Memory pool for translating buffer addresses to the DSP. + * @param voice_info - The voice info this command is generated from. + * @param voice_state - The voice state the DSP will use for this command. + * @param buffer_count - Number of mix buffers in use, + * data will be read into this index + channel. + * @param channel - Channel index for this command. + */ + void GeneratePcmInt16Version1Command(s32 node_id, const MemoryPoolInfo& memory_pool, + VoiceInfo& voice_info, const VoiceState& voice_state, + s16 buffer_count, s8 channel); + + /** + * Generate a PCM s16 version 2 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param voice_info - The voice info this command is generated from. + * @param voice_state - The voice state the DSP will use for this command. + * @param buffer_count - Number of mix buffers in use, + * data will be read into this index + channel. + * @param channel - Channel index for this command. + */ + void GeneratePcmInt16Version2Command(s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, s16 buffer_count, + s8 channel); + + /** + * Generate a PCM f32 version 1 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param memory_pool - Memory pool for translating buffer addresses to the DSP. + * @param voice_info - The voice info this command is generated from. + * @param voice_state - The voice state the DSP will use for this command. + * @param buffer_count - Number of mix buffers in use, + * data will be read into this index + channel. + * @param channel - Channel index for this command. + */ + void GeneratePcmFloatVersion1Command(s32 node_id, const MemoryPoolInfo& memory_pool, + VoiceInfo& voice_info, const VoiceState& voice_state, + s16 buffer_count, s8 channel); + + /** + * Generate a PCM f32 version 2 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param voice_info - The voice info this command is generated from. + * @param voice_state - The voice state the DSP will use for this command. + * @param buffer_count - Number of mix buffers in use, + * data will be read into this index + channel. + * @param channel - Channel index for this command. + */ + void GeneratePcmFloatVersion2Command(s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, s16 buffer_count, + s8 channel); + + /** + * Generate an ADPCM version 1 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param memory_pool - Memory pool for translating buffer addresses to the DSP. + * @param voice_info - The voice info this command is generated from. + * @param voice_state - The voice state the DSP will use for this command. + * @param buffer_count - Number of mix buffers in use, + * data will be read into this index + channel. + * @param channel - Channel index for this command. + */ + void GenerateAdpcmVersion1Command(s32 node_id, const MemoryPoolInfo& memory_pool, + VoiceInfo& voice_info, const VoiceState& voice_state, + s16 buffer_count, s8 channel); + + /** + * Generate an ADPCM version 2 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param voice_info - The voice info this command is generated from. + * @param voice_state - The voice state the DSP will use for this command. + * @param buffer_count - Number of mix buffers in use, + * data will be read into this index + channel. + * @param channel - Channel index for this command. + */ + void GenerateAdpcmVersion2Command(s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, s16 buffer_count, s8 channel); + + /** + * Generate a volume command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param buffer_offset - Base mix buffer index to generate this command at. + * @param input_index - Channel index and mix buffer offset for this command. + * @param volume - Mix volume added to the input samples. + * @param precision - Number of decimal bits for fixed point operations. + */ + void GenerateVolumeCommand(s32 node_id, s16 buffer_offset, s16 input_index, f32 volume, + u8 precision); + + /** + * Generate a volume ramp command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param voice_info - The voice info this command takes its volumes from. + * @param buffer_count - Number of active mix buffers, command will generate at this index. + * @param precision - Number of decimal bits for fixed point operations. + */ + void GenerateVolumeRampCommand(s32 node_id, VoiceInfo& voice_info, s16 buffer_count, + u8 precision); + + /** + * Generate a biquad filter command from a voice, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param voice_info - The voice info this command takes biquad parameters from. + * @param voice_state - Used by the AudioRenderer to track previous samples. + * @param buffer_count - Number of active mix buffers, + * command will generate at this index + channel. + * @param channel - Channel index for this filter to work on. + * @param biquad_index - Which biquad filter to use for this command (0-1). + * @param use_float_processing - Should int or float processing be used? + */ + void GenerateBiquadFilterCommand(s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, s16 buffer_count, s8 channel, + u32 biquad_index, bool use_float_processing); + + /** + * Generate a biquad filter effect command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param effect_info - The effect info this command takes biquad parameters from. + * @param buffer_offset - Mix buffer offset this command will use, + * command will generate at this index + channel. + * @param channel - Channel index for this filter to work on. + * @param needs_init - True if the biquad state needs initialisation. + * @param use_float_processing - Should int or float processing be used? + */ + void GenerateBiquadFilterCommand(s32 node_id, EffectInfoBase& effect_info, s16 buffer_offset, + s8 channel, bool needs_init, bool use_float_processing); + + /** + * Generate a mix command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param input_index - Input mix buffer index for this command. + * Added to the buffer offset. + * @param output_index - Output mix buffer index for this command. + * Added to the buffer offset. + * @param buffer_offset - Mix buffer offset this command will use. + * @param volume - Volume to be applied to the input. + * @param precision - Number of decimal bits for fixed point operations. + */ + void GenerateMixCommand(s32 node_id, s16 input_index, s16 output_index, s16 buffer_offset, + f32 volume, u8 precision); + + /** + * Generate a mix ramp command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param buffer_count - Number of active mix buffers. + * @param input_index - Input mix buffer index for this command. + * Added to buffer_count. + * @param output_index - Output mix buffer index for this command. + * Added to buffer_count. + * @param volume - Current mix volume used for calculating the ramp. + * @param prev_volume - Previous mix volume, used for calculating the ramp, + * also applied to the input. + * @param precision - Number of decimal bits for fixed point operations. + */ + void GenerateMixRampCommand(s32 node_id, s16 buffer_count, s16 input_index, s16 output_index, + f32 volume, f32 prev_volume, CpuAddr prev_samples, u8 precision); + + /** + * Generate a mix ramp grouped command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param buffer_count - Number of active mix buffers. + * @param input_index - Input mix buffer index for this command. + * Added to buffer_count. + * @param output_index - Output mix buffer index for this command. + * Added to buffer_count. + * @param volumes - Current mix volumes used for calculating the ramp. + * @param prev_volumes - Previous mix volumes, used for calculating the ramp, + * also applied to the input. + * @param precision - Number of decimal bits for fixed point operations. + */ + void GenerateMixRampGroupedCommand(s32 node_id, s16 buffer_count, s16 input_index, + s16 output_index, std::span volumes, + std::span prev_volumes, CpuAddr prev_samples, + u8 precision); + + /** + * Generate a depop prepare command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param voice_state - State to track the previous depop samples for each mix buffer. + * @param buffer - State to track the current depop samples for each mix buffer. + * @param buffer_count - Number of active mix buffers. + * @param buffer_offset - Base mix buffer index to generate the channel depops at. + * @param was_playing - Command only needs to work if the voice was previously playing. + */ + void GenerateDepopPrepareCommand(s32 node_id, const VoiceState& voice_state, + std::span buffer, s16 buffer_count, + s16 buffer_offset, bool was_playing); + + /** + * Generate a depop command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param mix_info - Mix info to get the buffer count and base offsets from. + * @param depop_buffer - Buffer of current depop sample values to be added to the input + * channels. + */ + void GenerateDepopForMixBuffersCommand(s32 node_id, const MixInfo& mix_info, + std::span depop_buffer); + + /** + * Generate a delay command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param effect_info - Delay effect info to generate this command from. + * @param buffer_offset - Base mix buffer offset to apply the apply the delay. + */ + void GenerateDelayCommand(s32 node_id, EffectInfoBase& effect_info, s16 buffer_offset); + + /** + * Generate an upsample command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param buffer_offset - Base mix buffer offset to upsample. + * @param upsampler_info - Upsampler info to control the upsampling. + * @param input_count - Number of input channels to upsample. + * @param inputs - Input mix buffer indexes. + * @param buffer_count - Number of active mix buffers. + * @param sample_count - Source sample count of the input. + * @param sample_rate - Source sample rate of the input. + */ + void GenerateUpsampleCommand(s32 node_id, s16 buffer_offset, UpsamplerInfo& upsampler_info, + u32 input_count, std::span inputs, s16 buffer_count, + u32 sample_count, u32 sample_rate); + + /** + * Generate a downmix 6 -> 2 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param inputs - Input mix buffer indexes. + * @param buffer_offset - Base mix buffer offset of the channels to downmix. + * @param downmix_coeff - Downmixing coefficients. + */ + void GenerateDownMix6chTo2chCommand(s32 node_id, std::span inputs, s16 buffer_offset, + std::span downmix_coeff); + + /** + * Generate an aux buffer command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param effect_info - Aux effect info to generate this command from. + * @param input_index - Input mix buffer index for this command. + * Added to buffer_offset. + * @param output_index - Output mix buffer index for this command. + * Added to buffer_offset. + * @param buffer_offset - Base mix buffer offset to use. + * @param update_count - Number of samples to write back to the game as updated, can be 0. + * @param count_max - Maximum number of samples to read or write. + * @param write_offset - Current read or write offset within the buffer. + */ + void GenerateAuxCommand(s32 node_id, EffectInfoBase& effect_info, s16 input_index, + s16 output_index, s16 buffer_offset, u32 update_count, u32 count_max, + u32 write_offset); + + /** + * Generate a device sink command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param buffer_offset - Base mix buffer offset to use. + * @param sink_info - The sink_info to generate this command from. + * @session_id - System session id this command is generated from. + * @samples_buffer - The buffer to be sent to the sink if upsampling is not used. + */ + void GenerateDeviceSinkCommand(s32 node_id, s16 buffer_offset, SinkInfoBase& sink_info, + u32 session_id, std::span samples_buffer); + + /** + * Generate a circular buffer sink command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param sink_info - The sink_info to generate this command from. + * @param buffer_offset - Base mix buffer offset to use. + */ + void GenerateCircularBufferSinkCommand(s32 node_id, SinkInfoBase& sink_info, s16 buffer_offset); + + /** + * Generate a reverb command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param effect_info - Reverb effect info to generate this command from. + * @param buffer_offset - Base mix buffer offset to use. + * @param long_size_pre_delay_supported - Should a longer pre-delay time be used before reverb + * begins? + */ + void GenerateReverbCommand(s32 node_id, EffectInfoBase& effect_info, s16 buffer_offset, + bool long_size_pre_delay_supported); + + /** + * Generate an I3DL2 reverb command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param effect_info - I3DL2Reverb effect info to generate this command from. + * @param buffer_offset - Base mix buffer offset to use. + */ + void GenerateI3dl2ReverbCommand(s32 node_id, EffectInfoBase& effect_info, s16 buffer_offset); + + /** + * Generate a performance command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param state - State of the performance. + * @param entry_addresses - The addresses to be filled in by the AudioRenderer. + */ + void GeneratePerformanceCommand(s32 node_id, PerformanceState state, + const PerformanceEntryAddresses& entry_addresses); + + /** + * Generate a clear mix command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + */ + void GenerateClearMixCommand(s32 node_id); + + /** + * Generate a copy mix command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param effect_info - BiquadFilter effect info to generate this command from. + * @param buffer_offset - Base mix buffer offset to use. + * @param channel - Index to the effect's parameters input indexes for this command. + */ + void GenerateCopyMixBufferCommand(s32 node_id, EffectInfoBase& effect_info, s16 buffer_offset, + s8 channel); + + /** + * Generate a light limiter version 1 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param buffer_offset - Base mix buffer offset to use. + * @param parameter - Effect parameter to generate from. + * @param state - State used by the AudioRenderer between commands. + * @param enabled - Is this command enabled? + * @param workbuffer - Game-supplied memory for the state. + */ + void GenerateLightLimiterCommand(s32 node_id, s16 buffer_offset, + const LightLimiterInfo::ParameterVersion1& parameter, + const LightLimiterInfo::State& state, bool enabled, + CpuAddr workbuffer); + + /** + * Generate a light limiter version 2 command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param buffer_offset - Base mix buffer offset to use. + * @param parameter - Effect parameter to generate from. + * @param statistics - Statistics reported by the AudioRenderer on the limiter's state. + * @param state - State used by the AudioRenderer between commands. + * @param enabled - Is this command enabled? + * @param workbuffer - Game-supplied memory for the state. + */ + void GenerateLightLimiterCommand(s32 node_id, s16 buffer_offset, + const LightLimiterInfo::ParameterVersion2& parameter, + const LightLimiterInfo::StatisticsInternal& statistics, + const LightLimiterInfo::State& state, bool enabled, + CpuAddr workbuffer); + + /** + * Generate a multitap biquad filter command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param voice_info - The voice info this command takes biquad parameters from. + * @param voice_state - Used by the AudioRenderer to track previous samples. + * @param buffer_count - Number of active mix buffers, + * command will generate at this index + channel. + * @param channel - Channel index for this filter to work on. + */ + void GenerateMultitapBiquadFilterCommand(s32 node_id, VoiceInfo& voice_info, + const VoiceState& voice_state, s16 buffer_count, + s8 channel); + + /** + * Generate a capture command, adding it to the command list. + * + * @param node_id - Node id of the voice this command is generated for. + * @param effect_info - Capture effect info to generate this command from. + * @param input_index - Input mix buffer index for this command. + * Added to buffer_offset. + * @param output_index - Output mix buffer index for this command (unused). + * Added to buffer_offset. + * @param buffer_offset - Base mix buffer offset to use. + * @param update_count - Number of samples to write back to the game as updated, can be 0. + * @param count_max - Maximum number of samples to read or write. + * @param write_offset - Current read or write offset within the buffer. + */ + void GenerateCaptureCommand(s32 node_id, EffectInfoBase& effect_info, s16 input_index, + s16 output_index, s16 buffer_offset, u32 update_count, + u32 count_max, u32 write_offset); + + /** + * Generate a compressor command, adding it to the command list. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info - Capture effect info to generate this command from. + * @param node_id - Node id of the voice this command is generated for. + */ + void GenerateCompressorCommand(s16 buffer_offset, EffectInfoBase& effect_info, s32 node_id); + + /// Command list buffer generated commands will be added to + std::span command_list{}; + /// Input sample count, unused + u32 sample_count{}; + /// Input sample rate, unused + u32 sample_rate{}; + /// Current size of the command buffer + u64 size{}; + /// Current number of commands added + u32 count{}; + /// Current estimated processing time for all commands + u32 estimated_process_time{}; + /// Used for mapping buffers for the AudioRenderer + MemoryPoolInfo* memory_pool{}; + /// Used for estimating command process times + ICommandProcessingTimeEstimator* time_estimator{}; + /// Used to check which rendering features are currently enabled + BehaviorInfo* behavior{}; + +private: + template + T& GenerateStart(const s32 node_id); + template + void GenerateEnd(T& cmd); +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/command_generator.cpp b/src/audio_core/renderer/command/command_generator.cpp new file mode 100644 index 0000000000..2ea50d1289 --- /dev/null +++ b/src/audio_core/renderer/command/command_generator.cpp @@ -0,0 +1,796 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/common/audio_renderer_parameter.h" +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/command/command_buffer.h" +#include "audio_core/renderer/command/command_generator.h" +#include "audio_core/renderer/command/command_list_header.h" +#include "audio_core/renderer/effect/aux_.h" +#include "audio_core/renderer/effect/biquad_filter.h" +#include "audio_core/renderer/effect/buffer_mixer.h" +#include "audio_core/renderer/effect/capture.h" +#include "audio_core/renderer/effect/effect_context.h" +#include "audio_core/renderer/effect/light_limiter.h" +#include "audio_core/renderer/mix/mix_context.h" +#include "audio_core/renderer/performance/detail_aspect.h" +#include "audio_core/renderer/performance/entry_aspect.h" +#include "audio_core/renderer/sink/device_sink_info.h" +#include "audio_core/renderer/sink/sink_context.h" +#include "audio_core/renderer/splitter/splitter_context.h" +#include "audio_core/renderer/voice/voice_context.h" +#include "common/alignment.h" + +namespace AudioCore::AudioRenderer { + +CommandGenerator::CommandGenerator(CommandBuffer& command_buffer_, + const CommandListHeader& command_list_header_, + const AudioRendererSystemContext& render_context_, + VoiceContext& voice_context_, MixContext& mix_context_, + EffectContext& effect_context_, SinkContext& sink_context_, + SplitterContext& splitter_context_, + PerformanceManager* performance_manager_) + : command_buffer{command_buffer_}, command_header{command_list_header_}, + render_context{render_context_}, voice_context{voice_context_}, mix_context{mix_context_}, + effect_context{effect_context_}, sink_context{sink_context_}, + splitter_context{splitter_context_}, performance_manager{performance_manager_} { + command_buffer.GenerateClearMixCommand(InvalidNodeId); +} + +void CommandGenerator::GenerateDataSourceCommand(VoiceInfo& voice_info, + const VoiceState& voice_state, const s8 channel) { + if (voice_info.mix_id == UnusedMixId) { + if (voice_info.splitter_id != UnusedSplitterId) { + auto destination{splitter_context.GetDesintationData(voice_info.splitter_id, 0)}; + u32 dest_id{0}; + while (destination != nullptr) { + if (destination->IsConfigured()) { + auto mix_id{destination->GetMixId()}; + if (mix_id < mix_context.GetCount()) { + auto mix_info{mix_context.GetInfo(mix_id)}; + command_buffer.GenerateDepopPrepareCommand( + voice_info.node_id, voice_state, render_context.depop_buffer, + mix_info->buffer_count, mix_info->buffer_offset, + voice_info.was_playing); + } + } + dest_id++; + destination = splitter_context.GetDesintationData(voice_info.splitter_id, dest_id); + } + } + } else { + auto mix_info{mix_context.GetInfo(voice_info.mix_id)}; + command_buffer.GenerateDepopPrepareCommand( + voice_info.node_id, voice_state, render_context.depop_buffer, mix_info->buffer_count, + mix_info->buffer_offset, voice_info.was_playing); + } + + if (voice_info.was_playing) { + return; + } + + if (render_context.behavior->IsWaveBufferVer2Supported()) { + switch (voice_info.sample_format) { + case SampleFormat::PcmInt16: + command_buffer.GeneratePcmInt16Version2Command( + voice_info.node_id, voice_info, voice_state, render_context.mix_buffer_count, + channel); + break; + case SampleFormat::PcmFloat: + command_buffer.GeneratePcmFloatVersion2Command( + voice_info.node_id, voice_info, voice_state, render_context.mix_buffer_count, + channel); + break; + case SampleFormat::Adpcm: + command_buffer.GenerateAdpcmVersion2Command(voice_info.node_id, voice_info, voice_state, + render_context.mix_buffer_count, channel); + break; + default: + LOG_ERROR(Service_Audio, "Invalid SampleFormat {}", + static_cast(voice_info.sample_format)); + break; + } + } else { + switch (voice_info.sample_format) { + case SampleFormat::PcmInt16: + command_buffer.GeneratePcmInt16Version1Command( + voice_info.node_id, *command_buffer.memory_pool, voice_info, voice_state, + render_context.mix_buffer_count, channel); + break; + case SampleFormat::PcmFloat: + command_buffer.GeneratePcmFloatVersion1Command( + voice_info.node_id, *command_buffer.memory_pool, voice_info, voice_state, + render_context.mix_buffer_count, channel); + break; + case SampleFormat::Adpcm: + command_buffer.GenerateAdpcmVersion1Command( + voice_info.node_id, *command_buffer.memory_pool, voice_info, voice_state, + render_context.mix_buffer_count, channel); + break; + default: + LOG_ERROR(Service_Audio, "Invalid SampleFormat {}", + static_cast(voice_info.sample_format)); + break; + } + } +} + +void CommandGenerator::GenerateVoiceMixCommand(std::span mix_volumes, + std::span prev_mix_volumes, + const VoiceState& voice_state, s16 output_index, + const s16 buffer_count, const s16 input_index, + const s32 node_id) { + u8 precision{15}; + if (render_context.behavior->IsVolumeMixParameterPrecisionQ23Supported()) { + precision = 23; + } + + if (buffer_count > 8) { + const auto prev_samples{render_context.memory_pool_info->Translate( + CpuAddr(voice_state.previous_samples.data()), buffer_count * sizeof(s32))}; + command_buffer.GenerateMixRampGroupedCommand(node_id, buffer_count, input_index, + output_index, mix_volumes, prev_mix_volumes, + prev_samples, precision); + } else { + for (s16 i = 0; i < buffer_count; i++, output_index++) { + const auto prev_samples{render_context.memory_pool_info->Translate( + CpuAddr(&voice_state.previous_samples[i]), sizeof(s32))}; + + command_buffer.GenerateMixRampCommand(node_id, buffer_count, input_index, output_index, + mix_volumes[i], prev_mix_volumes[i], prev_samples, + precision); + } + } +} + +void CommandGenerator::GenerateBiquadFilterCommandForVoice(VoiceInfo& voice_info, + const VoiceState& voice_state, + const s16 buffer_count, const s8 channel, + const s32 node_id) { + const bool both_biquads_enabled{voice_info.biquads[0].enabled && voice_info.biquads[1].enabled}; + const auto use_float_processing{render_context.behavior->UseBiquadFilterFloatProcessing()}; + + if (both_biquads_enabled && render_context.behavior->UseMultiTapBiquadFilterProcessing() && + use_float_processing) { + command_buffer.GenerateMultitapBiquadFilterCommand(node_id, voice_info, voice_state, + buffer_count, channel); + } else { + for (u32 i = 0; i < MaxBiquadFilters; i++) { + if (voice_info.biquads[i].enabled) { + command_buffer.GenerateBiquadFilterCommand(node_id, voice_info, voice_state, + buffer_count, channel, i, + use_float_processing); + } + } + } +} + +void CommandGenerator::GenerateVoiceCommand(VoiceInfo& voice_info) { + u8 precision{15}; + if (render_context.behavior->IsVolumeMixParameterPrecisionQ23Supported()) { + precision = 23; + } + + for (s8 channel = 0; channel < voice_info.channel_count; channel++) { + const auto resource_id{voice_info.channel_resource_ids[channel]}; + auto& voice_state{voice_context.GetDspSharedState(resource_id)}; + auto& channel_resource{voice_context.GetChannelResource(resource_id)}; + + PerformanceDetailType detail_type{PerformanceDetailType::Invalid}; + switch (voice_info.sample_format) { + case SampleFormat::PcmInt16: + detail_type = PerformanceDetailType::Unk1; + break; + case SampleFormat::PcmFloat: + detail_type = PerformanceDetailType::Unk10; + break; + default: + detail_type = PerformanceDetailType::Unk2; + break; + } + + DetailAspect data_source_detail(*this, PerformanceEntryType::Voice, voice_info.node_id, + detail_type); + GenerateDataSourceCommand(voice_info, voice_state, channel); + + if (data_source_detail.initialized) { + command_buffer.GeneratePerformanceCommand(data_source_detail.node_id, + PerformanceState::Stop, + data_source_detail.performance_entry_address); + } + + if (voice_info.was_playing) { + voice_info.prev_volume = 0.0f; + continue; + } + + if (!voice_info.HasAnyConnection()) { + continue; + } + + DetailAspect biquad_detail_aspect(*this, PerformanceEntryType::Voice, voice_info.node_id, + PerformanceDetailType::Unk4); + GenerateBiquadFilterCommandForVoice( + voice_info, voice_state, render_context.mix_buffer_count, channel, voice_info.node_id); + + if (biquad_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + biquad_detail_aspect.node_id, PerformanceState::Stop, + biquad_detail_aspect.performance_entry_address); + } + + DetailAspect volume_ramp_detail_aspect(*this, PerformanceEntryType::Voice, + voice_info.node_id, PerformanceDetailType::Unk3); + command_buffer.GenerateVolumeRampCommand( + voice_info.node_id, voice_info, render_context.mix_buffer_count + channel, precision); + if (volume_ramp_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + volume_ramp_detail_aspect.node_id, PerformanceState::Stop, + volume_ramp_detail_aspect.performance_entry_address); + } + + voice_info.prev_volume = voice_info.volume; + + if (voice_info.mix_id == UnusedMixId) { + if (voice_info.splitter_id != UnusedSplitterId) { + auto i{channel}; + auto destination{splitter_context.GetDesintationData(voice_info.splitter_id, i)}; + while (destination != nullptr) { + if (destination->IsConfigured()) { + const auto mix_id{destination->GetMixId()}; + if (mix_id < mix_context.GetCount() && + static_cast(mix_id) != UnusedSplitterId) { + auto mix_info{mix_context.GetInfo(mix_id)}; + GenerateVoiceMixCommand( + destination->GetMixVolume(), destination->GetMixVolumePrev(), + voice_state, mix_info->buffer_offset, mix_info->buffer_count, + render_context.mix_buffer_count + channel, voice_info.node_id); + destination->MarkAsNeedToUpdateInternalState(); + } + } + i += voice_info.channel_count; + destination = splitter_context.GetDesintationData(voice_info.splitter_id, i); + } + } + } else { + DetailAspect volume_mix_detail_aspect(*this, PerformanceEntryType::Voice, + voice_info.node_id, PerformanceDetailType::Unk3); + auto mix_info{mix_context.GetInfo(voice_info.mix_id)}; + GenerateVoiceMixCommand(channel_resource.mix_volumes, channel_resource.prev_mix_volumes, + voice_state, mix_info->buffer_offset, mix_info->buffer_count, + render_context.mix_buffer_count + channel, voice_info.node_id); + if (volume_mix_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + volume_mix_detail_aspect.node_id, PerformanceState::Stop, + volume_mix_detail_aspect.performance_entry_address); + } + + channel_resource.prev_mix_volumes = channel_resource.mix_volumes; + } + voice_info.biquad_initialized[0] = voice_info.biquads[0].enabled; + voice_info.biquad_initialized[1] = voice_info.biquads[1].enabled; + } +} + +void CommandGenerator::GenerateVoiceCommands() { + const auto voice_count{voice_context.GetCount()}; + + for (u32 i = 0; i < voice_count; i++) { + auto sorted_info{voice_context.GetSortedInfo(i)}; + + if (sorted_info->ShouldSkip() || !sorted_info->UpdateForCommandGeneration(voice_context)) { + continue; + } + + EntryAspect voice_entry_aspect(*this, PerformanceEntryType::Voice, sorted_info->node_id); + + GenerateVoiceCommand(*sorted_info); + + if (voice_entry_aspect.initialized) { + command_buffer.GeneratePerformanceCommand(voice_entry_aspect.node_id, + PerformanceState::Stop, + voice_entry_aspect.performance_entry_address); + } + } + + splitter_context.UpdateInternalState(); +} + +void CommandGenerator::GenerateBufferMixerCommand(const s16 buffer_offset, + EffectInfoBase& effect_info, const s32 node_id) { + u8 precision{15}; + if (render_context.behavior->IsVolumeMixParameterPrecisionQ23Supported()) { + precision = 23; + } + + if (effect_info.IsEnabled()) { + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + for (u32 i = 0; i < parameter.mix_count; i++) { + if (parameter.volumes[i] != 0.0f) { + command_buffer.GenerateMixCommand(node_id, buffer_offset + parameter.inputs[i], + buffer_offset + parameter.outputs[i], + buffer_offset, parameter.volumes[i], precision); + } + } + } +} + +void CommandGenerator::GenerateDelayCommand(const s16 buffer_offset, EffectInfoBase& effect_info, + const s32 node_id) { + command_buffer.GenerateDelayCommand(node_id, effect_info, buffer_offset); +} + +void CommandGenerator::GenerateReverbCommand(const s16 buffer_offset, EffectInfoBase& effect_info, + const s32 node_id, + const bool long_size_pre_delay_supported) { + command_buffer.GenerateReverbCommand(node_id, effect_info, buffer_offset, + long_size_pre_delay_supported); +} + +void CommandGenerator::GenerateI3dl2ReverbEffectCommand(const s16 buffer_offset, + EffectInfoBase& effect_info, + const s32 node_id) { + command_buffer.GenerateI3dl2ReverbCommand(node_id, effect_info, buffer_offset); +} + +void CommandGenerator::GenerateAuxCommand(const s16 buffer_offset, EffectInfoBase& effect_info, + const s32 node_id) { + + if (effect_info.IsEnabled()) { + effect_info.GetWorkbuffer(0); + effect_info.GetWorkbuffer(1); + } + + if (effect_info.GetSendBuffer() != 0 && effect_info.GetReturnBuffer() != 0) { + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + auto channel_index{parameter.mix_buffer_count - 1}; + u32 write_offset{0}; + for (u32 i = 0; i < parameter.mix_buffer_count; i++, channel_index--) { + auto new_update_count{command_header.sample_count + write_offset}; + const auto update_count{channel_index > 0 ? 0 : new_update_count}; + command_buffer.GenerateAuxCommand(node_id, effect_info, parameter.inputs[i], + parameter.outputs[i], buffer_offset, update_count, + parameter.count_max, write_offset); + write_offset = new_update_count; + } + } +} + +void CommandGenerator::GenerateBiquadFilterEffectCommand(const s16 buffer_offset, + EffectInfoBase& effect_info, + const s32 node_id) { + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + if (effect_info.IsEnabled()) { + bool needs_init{false}; + + switch (parameter.state) { + case EffectInfoBase::ParameterState::Initialized: + needs_init = true; + break; + case EffectInfoBase::ParameterState::Updating: + case EffectInfoBase::ParameterState::Updated: + if (render_context.behavior->IsBiquadFilterEffectStateClearBugFixed()) { + needs_init = false; + } else { + needs_init = parameter.state == EffectInfoBase::ParameterState::Updating; + } + break; + default: + LOG_ERROR(Service_Audio, "Invalid biquad parameter state {}", + static_cast(parameter.state)); + break; + } + + for (s8 channel = 0; channel < parameter.channel_count; channel++) { + command_buffer.GenerateBiquadFilterCommand( + node_id, effect_info, buffer_offset, channel, needs_init, + render_context.behavior->UseBiquadFilterFloatProcessing()); + } + } else { + for (s8 channel = 0; channel < parameter.channel_count; channel++) { + command_buffer.GenerateCopyMixBufferCommand(node_id, effect_info, buffer_offset, + channel); + } + } +} + +void CommandGenerator::GenerateLightLimiterEffectCommand(const s16 buffer_offset, + EffectInfoBase& effect_info, + const s32 node_id, + const u32 effect_index) { + + const auto& state{*reinterpret_cast(effect_info.GetStateBuffer())}; + + if (render_context.behavior->IsEffectInfoVersion2Supported()) { + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + const auto& result_state{*reinterpret_cast( + &effect_context.GetDspSharedResultState(effect_index))}; + command_buffer.GenerateLightLimiterCommand(node_id, buffer_offset, parameter, result_state, + state, effect_info.IsEnabled(), + effect_info.GetWorkbuffer(-1)); + } else { + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + command_buffer.GenerateLightLimiterCommand(node_id, buffer_offset, parameter, state, + effect_info.IsEnabled(), + effect_info.GetWorkbuffer(-1)); + } +} + +void CommandGenerator::GenerateCaptureCommand(const s16 buffer_offset, EffectInfoBase& effect_info, + const s32 node_id) { + if (effect_info.IsEnabled()) { + effect_info.GetWorkbuffer(0); + } + + if (effect_info.GetSendBuffer()) { + const auto& parameter{ + *reinterpret_cast(effect_info.GetParameter())}; + auto channel_index{parameter.mix_buffer_count - 1}; + u32 write_offset{0}; + for (u32 i = 0; i < parameter.mix_buffer_count; i++, channel_index--) { + auto new_update_count{command_header.sample_count + write_offset}; + const auto update_count{channel_index > 0 ? 0 : new_update_count}; + command_buffer.GenerateCaptureCommand(node_id, effect_info, parameter.inputs[i], + parameter.outputs[i], buffer_offset, update_count, + parameter.count_max, write_offset); + write_offset = new_update_count; + } + } +} + +void CommandGenerator::GenerateCompressorCommand(const s16 buffer_offset, + EffectInfoBase& effect_info, const s32 node_id) { + command_buffer.GenerateCompressorCommand(buffer_offset, effect_info, node_id); +} + +void CommandGenerator::GenerateEffectCommand(MixInfo& mix_info) { + const auto effect_count{effect_context.GetCount()}; + for (u32 i = 0; i < effect_count; i++) { + const auto effect_index{mix_info.effect_order_buffer[i]}; + if (effect_index == -1) { + break; + } + + auto& effect_info = effect_context.GetInfo(effect_index); + if (effect_info.ShouldSkip()) { + continue; + } + + const auto entry_type{mix_info.mix_id == FinalMixId ? PerformanceEntryType::FinalMix + : PerformanceEntryType::SubMix}; + + switch (effect_info.GetType()) { + case EffectInfoBase::Type::Mix: { + DetailAspect mix_detail_aspect(*this, entry_type, mix_info.node_id, + PerformanceDetailType::Unk5); + GenerateBufferMixerCommand(mix_info.buffer_offset, effect_info, mix_info.node_id); + if (mix_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + mix_detail_aspect.node_id, PerformanceState::Stop, + mix_detail_aspect.performance_entry_address); + } + } break; + + case EffectInfoBase::Type::Aux: { + DetailAspect aux_detail_aspect(*this, entry_type, mix_info.node_id, + PerformanceDetailType::Unk7); + GenerateAuxCommand(mix_info.buffer_offset, effect_info, mix_info.node_id); + if (aux_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + aux_detail_aspect.node_id, PerformanceState::Stop, + aux_detail_aspect.performance_entry_address); + } + } break; + + case EffectInfoBase::Type::Delay: { + DetailAspect delay_detail_aspect(*this, entry_type, mix_info.node_id, + PerformanceDetailType::Unk6); + GenerateDelayCommand(mix_info.buffer_offset, effect_info, mix_info.node_id); + if (delay_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + delay_detail_aspect.node_id, PerformanceState::Stop, + delay_detail_aspect.performance_entry_address); + } + } break; + + case EffectInfoBase::Type::Reverb: { + DetailAspect reverb_detail_aspect(*this, entry_type, mix_info.node_id, + PerformanceDetailType::Unk8); + GenerateReverbCommand(mix_info.buffer_offset, effect_info, mix_info.node_id, + render_context.behavior->IsLongSizePreDelaySupported()); + if (reverb_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + reverb_detail_aspect.node_id, PerformanceState::Stop, + reverb_detail_aspect.performance_entry_address); + } + } break; + + case EffectInfoBase::Type::I3dl2Reverb: { + DetailAspect i3dl2_detail_aspect(*this, entry_type, mix_info.node_id, + PerformanceDetailType::Unk9); + GenerateI3dl2ReverbEffectCommand(mix_info.buffer_offset, effect_info, mix_info.node_id); + if (i3dl2_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + i3dl2_detail_aspect.node_id, PerformanceState::Stop, + i3dl2_detail_aspect.performance_entry_address); + } + } break; + + case EffectInfoBase::Type::BiquadFilter: { + DetailAspect biquad_detail_aspect(*this, entry_type, mix_info.node_id, + PerformanceDetailType::Unk4); + GenerateBiquadFilterEffectCommand(mix_info.buffer_offset, effect_info, + mix_info.node_id); + if (biquad_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + biquad_detail_aspect.node_id, PerformanceState::Stop, + biquad_detail_aspect.performance_entry_address); + } + } break; + + case EffectInfoBase::Type::LightLimiter: { + DetailAspect light_limiter_detail_aspect(*this, entry_type, mix_info.node_id, + PerformanceDetailType::Unk11); + GenerateLightLimiterEffectCommand(mix_info.buffer_offset, effect_info, mix_info.node_id, + effect_index); + if (light_limiter_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + light_limiter_detail_aspect.node_id, PerformanceState::Stop, + light_limiter_detail_aspect.performance_entry_address); + } + } break; + + case EffectInfoBase::Type::Capture: { + DetailAspect capture_detail_aspect(*this, entry_type, mix_info.node_id, + PerformanceDetailType::Unk12); + GenerateCaptureCommand(mix_info.buffer_offset, effect_info, mix_info.node_id); + if (capture_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + capture_detail_aspect.node_id, PerformanceState::Stop, + capture_detail_aspect.performance_entry_address); + } + } break; + + case EffectInfoBase::Type::Compressor: { + DetailAspect capture_detail_aspect(*this, entry_type, mix_info.node_id, + PerformanceDetailType::Unk13); + GenerateCompressorCommand(mix_info.buffer_offset, effect_info, mix_info.node_id); + if (capture_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + capture_detail_aspect.node_id, PerformanceState::Stop, + capture_detail_aspect.performance_entry_address); + } + } break; + + default: + LOG_ERROR(Service_Audio, "Invalid effect type {}", + static_cast(effect_info.GetType())); + break; + } + + effect_info.UpdateForCommandGeneration(); + } +} + +void CommandGenerator::GenerateMixCommands(MixInfo& mix_info) { + u8 precision{15}; + if (render_context.behavior->IsVolumeMixParameterPrecisionQ23Supported()) { + precision = 23; + } + + if (!mix_info.HasAnyConnection()) { + return; + } + + if (mix_info.dst_mix_id == UnusedMixId) { + if (mix_info.dst_splitter_id != UnusedSplitterId) { + s16 dest_id{0}; + auto destination{ + splitter_context.GetDesintationData(mix_info.dst_splitter_id, dest_id)}; + while (destination != nullptr) { + if (destination->IsConfigured()) { + auto splitter_mix_id{destination->GetMixId()}; + if (splitter_mix_id < mix_context.GetCount()) { + auto splitter_mix_info{mix_context.GetInfo(splitter_mix_id)}; + const s16 input_index{static_cast(mix_info.buffer_offset + + (dest_id % mix_info.buffer_count))}; + for (s16 i = 0; i < splitter_mix_info->buffer_count; i++) { + auto volume{mix_info.volume * destination->GetMixVolume(i)}; + if (volume != 0.0f) { + command_buffer.GenerateMixCommand( + mix_info.node_id, input_index, + splitter_mix_info->buffer_offset + i, mix_info.buffer_offset, + volume, precision); + } + } + } + } + dest_id++; + destination = + splitter_context.GetDesintationData(mix_info.dst_splitter_id, dest_id); + } + } + } else { + auto dest_mix_info{mix_context.GetInfo(mix_info.dst_mix_id)}; + for (s16 i = 0; i < mix_info.buffer_count; i++) { + for (s16 j = 0; j < dest_mix_info->buffer_count; j++) { + auto volume{mix_info.volume * mix_info.mix_volumes[i][j]}; + if (volume != 0.0f) { + command_buffer.GenerateMixCommand(mix_info.node_id, mix_info.buffer_offset + i, + dest_mix_info->buffer_offset + j, + mix_info.buffer_offset, volume, precision); + } + } + } + } +} + +void CommandGenerator::GenerateSubMixCommand(MixInfo& mix_info) { + command_buffer.GenerateDepopForMixBuffersCommand(mix_info.node_id, mix_info, + render_context.depop_buffer); + GenerateEffectCommand(mix_info); + + DetailAspect mix_detail_aspect(*this, PerformanceEntryType::SubMix, mix_info.node_id, + PerformanceDetailType::Unk5); + + GenerateMixCommands(mix_info); + + if (mix_detail_aspect.initialized) { + command_buffer.GeneratePerformanceCommand(mix_detail_aspect.node_id, PerformanceState::Stop, + mix_detail_aspect.performance_entry_address); + } +} + +void CommandGenerator::GenerateSubMixCommands() { + const auto submix_count{mix_context.GetCount()}; + for (s32 i = 0; i < submix_count; i++) { + auto sorted_info{mix_context.GetSortedInfo(i)}; + if (!sorted_info->in_use || sorted_info->mix_id == FinalMixId) { + continue; + } + + EntryAspect submix_entry_aspect(*this, PerformanceEntryType::SubMix, sorted_info->node_id); + + GenerateSubMixCommand(*sorted_info); + + if (submix_entry_aspect.initialized) { + command_buffer.GeneratePerformanceCommand( + submix_entry_aspect.node_id, PerformanceState::Stop, + submix_entry_aspect.performance_entry_address); + } + } +} + +void CommandGenerator::GenerateFinalMixCommand() { + auto& final_mix_info{*mix_context.GetFinalMixInfo()}; + + command_buffer.GenerateDepopForMixBuffersCommand(final_mix_info.node_id, final_mix_info, + render_context.depop_buffer); + GenerateEffectCommand(final_mix_info); + + u8 precision{15}; + if (render_context.behavior->IsVolumeMixParameterPrecisionQ23Supported()) { + precision = 23; + } + + for (s16 i = 0; i < final_mix_info.buffer_count; i++) { + DetailAspect volume_aspect(*this, PerformanceEntryType::FinalMix, final_mix_info.node_id, + PerformanceDetailType::Unk3); + command_buffer.GenerateVolumeCommand(final_mix_info.node_id, final_mix_info.buffer_offset, + i, final_mix_info.volume, precision); + if (volume_aspect.initialized) { + command_buffer.GeneratePerformanceCommand(volume_aspect.node_id, PerformanceState::Stop, + volume_aspect.performance_entry_address); + } + } +} + +void CommandGenerator::GenerateFinalMixCommands() { + auto final_mix_info{mix_context.GetFinalMixInfo()}; + EntryAspect final_mix_entry(*this, PerformanceEntryType::FinalMix, final_mix_info->node_id); + GenerateFinalMixCommand(); + if (final_mix_entry.initialized) { + command_buffer.GeneratePerformanceCommand(final_mix_entry.node_id, PerformanceState::Stop, + final_mix_entry.performance_entry_address); + } +} + +void CommandGenerator::GenerateSinkCommands() { + const auto sink_count{sink_context.GetCount()}; + + for (u32 i = 0; i < sink_count; i++) { + auto sink_info{sink_context.GetInfo(i)}; + if (sink_info->IsUsed() && sink_info->GetType() == SinkInfoBase::Type::DeviceSink) { + auto state{reinterpret_cast(sink_info->GetState())}; + if (command_header.sample_rate != TargetSampleRate && + state->upsampler_info == nullptr) { + auto device_state{sink_info->GetDeviceState()}; + device_state->upsampler_info = render_context.upsampler_manager->Allocate(); + } + + EntryAspect device_sink_entry(*this, PerformanceEntryType::Sink, + sink_info->GetNodeId()); + auto final_mix{mix_context.GetFinalMixInfo()}; + GenerateSinkCommand(final_mix->buffer_offset, *sink_info); + + if (device_sink_entry.initialized) { + command_buffer.GeneratePerformanceCommand( + device_sink_entry.node_id, PerformanceState::Stop, + device_sink_entry.performance_entry_address); + } + } + } + + for (u32 i = 0; i < sink_count; i++) { + auto sink_info{sink_context.GetInfo(i)}; + if (sink_info->IsUsed() && sink_info->GetType() == SinkInfoBase::Type::CircularBufferSink) { + EntryAspect circular_buffer_entry(*this, PerformanceEntryType::Sink, + sink_info->GetNodeId()); + auto final_mix{mix_context.GetFinalMixInfo()}; + GenerateSinkCommand(final_mix->buffer_offset, *sink_info); + + if (circular_buffer_entry.initialized) { + command_buffer.GeneratePerformanceCommand( + circular_buffer_entry.node_id, PerformanceState::Stop, + circular_buffer_entry.performance_entry_address); + } + } + } +} + +void CommandGenerator::GenerateSinkCommand(const s16 buffer_offset, SinkInfoBase& sink_info) { + if (sink_info.ShouldSkip()) { + return; + } + + switch (sink_info.GetType()) { + case SinkInfoBase::Type::DeviceSink: + GenerateDeviceSinkCommand(buffer_offset, sink_info); + break; + + case SinkInfoBase::Type::CircularBufferSink: + command_buffer.GenerateCircularBufferSinkCommand(sink_info.GetNodeId(), sink_info, + buffer_offset); + break; + + default: + LOG_ERROR(Service_Audio, "Invalid sink type {}", static_cast(sink_info.GetType())); + break; + } + + sink_info.UpdateForCommandGeneration(); +} + +void CommandGenerator::GenerateDeviceSinkCommand(const s16 buffer_offset, SinkInfoBase& sink_info) { + auto& parameter{ + *reinterpret_cast(sink_info.GetParameter())}; + auto state{*reinterpret_cast(sink_info.GetState())}; + + if (render_context.channels == 2 && parameter.downmix_enabled) { + command_buffer.GenerateDownMix6chTo2chCommand(InvalidNodeId, parameter.inputs, + buffer_offset, parameter.downmix_coeff); + } + + if (state.upsampler_info != nullptr) { + command_buffer.GenerateUpsampleCommand( + InvalidNodeId, buffer_offset, *state.upsampler_info, parameter.input_count, + parameter.inputs, command_header.buffer_count, command_header.sample_count, + command_header.sample_rate); + } + + command_buffer.GenerateDeviceSinkCommand(InvalidNodeId, buffer_offset, sink_info, + render_context.session_id, + command_header.samples_buffer); +} + +void CommandGenerator::GeneratePerformanceCommand( + s32 node_id, PerformanceState state, const PerformanceEntryAddresses& entry_addresses) { + command_buffer.GeneratePerformanceCommand(node_id, state, entry_addresses); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/command_generator.h b/src/audio_core/renderer/command/command_generator.h new file mode 100644 index 0000000000..d80d9b0d8b --- /dev/null +++ b/src/audio_core/renderer/command/command_generator.h @@ -0,0 +1,349 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/commands.h" +#include "audio_core/renderer/performance/performance_manager.h" +#include "common/common_types.h" + +namespace AudioCore { +struct AudioRendererSystemContext; + +namespace AudioRenderer { +class CommandBuffer; +struct CommandListHeader; +class VoiceContext; +class MixContext; +class EffectContext; +class SplitterContext; +class SinkContext; +class BehaviorInfo; +class VoiceInfo; +struct VoiceState; +class MixInfo; +class SinkInfoBase; + +/** + * Generates all commands to build up a command list, which are sent to the AudioRender for + * processing. + */ +class CommandGenerator { +public: + explicit CommandGenerator(CommandBuffer& command_buffer, + const CommandListHeader& command_list_header, + const AudioRendererSystemContext& render_context, + VoiceContext& voice_context, MixContext& mix_context, + EffectContext& effect_context, SinkContext& sink_context, + SplitterContext& splitter_context, + PerformanceManager* performance_manager); + + /** + * Calculate the buffer size needed for commands. + * + * @param behavior - Used to check what features are enabled. + * @param params - Input rendering parameters for numbers of voices/mixes/sinks etc. + */ + static u64 CalculateCommandBufferSize(const BehaviorInfo& behavior, + const AudioRendererParameterInternal& params) { + u64 size{0}; + + // Effects + size += params.effects * sizeof(EffectInfoBase); + + // Voices + u64 voice_size{0}; + if (behavior.IsWaveBufferVer2Supported()) { + voice_size = std::max(std::max(sizeof(AdpcmDataSourceVersion2Command), + sizeof(PcmInt16DataSourceVersion2Command)), + sizeof(PcmFloatDataSourceVersion2Command)); + } else { + voice_size = std::max(std::max(sizeof(AdpcmDataSourceVersion1Command), + sizeof(PcmInt16DataSourceVersion1Command)), + sizeof(PcmFloatDataSourceVersion1Command)); + } + voice_size += sizeof(BiquadFilterCommand) * MaxBiquadFilters; + voice_size += sizeof(VolumeRampCommand); + voice_size += sizeof(MixRampGroupedCommand); + + size += params.voices * (params.splitter_infos * sizeof(DepopPrepareCommand) + voice_size); + + // Sub mixes + size += sizeof(DepopForMixBuffersCommand) + + (sizeof(MixCommand) * MaxMixBuffers) * MaxMixBuffers; + + // Final mix + size += sizeof(DepopForMixBuffersCommand) + sizeof(VolumeCommand) * MaxMixBuffers; + + // Splitters + size += params.splitter_destinations * sizeof(MixRampCommand) * MaxMixBuffers; + + // Sinks + size += + params.sinks * std::max(sizeof(DeviceSinkCommand), sizeof(CircularBufferSinkCommand)); + + // Performance + size += (params.effects + params.voices + params.sinks + params.sub_mixes + 1 + + PerformanceManager::MaxDetailEntries) * + sizeof(PerformanceCommand); + return size; + } + + /** + * Get the current command buffer used to generate commands. + * + * @return The command buffer. + */ + CommandBuffer& GetCommandBuffer() { + return command_buffer; + } + + /** + * Get the current performance manager, + * + * @return The performance manager. May be nullptr. + */ + PerformanceManager* GetPerformanceManager() { + return performance_manager; + } + + /** + * Generate a data source command. + * These are the basis for all audio output. + * + * @param voice_info - Generate the command from this voice. + * @param voice_state - State used by the AudioRenderer across calls. + * @param channel - Channel index to generate the command into. + */ + void GenerateDataSourceCommand(VoiceInfo& voice_info, const VoiceState& voice_state, + s8 channel); + + /** + * Generate voice mixing commands. + * These are used to mix buffers together, to mix one input to many outputs, + * and also used as copy commands to move data around and prevent it being accidentally + * overwritten, e.g by another data source command into the same channel. + * + * @param mix_volumes - Current volumes of the mix. + * @param prev_mix_volumes - Previous volumes of the mix. + * @param voice_state - State used by the AudioRenderer across calls. + * @param output_index - Output mix buffer index. + * @param buffer_count - Number of active mix buffers. + * @param input_index - Input mix buffer index. + * @param node_id - Node id of the voice this command is generated for. + */ + void GenerateVoiceMixCommand(std::span mix_volumes, + std::span prev_mix_volumes, + const VoiceState& voice_state, s16 output_index, s16 buffer_count, + s16 input_index, s32 node_id); + + /** + * Generate a biquad filter command for a voice. + * + * @param voice_info - Voice info this command is generated from. + * @param voice_state - State used by the AudioRenderer across calls. + * @param buffer_count - Number of active mix buffers. + * @param channel - Channel index of this command. + * @param node_id - Node id of the voice this command is generated for. + */ + void GenerateBiquadFilterCommandForVoice(VoiceInfo& voice_info, const VoiceState& voice_state, + s16 buffer_count, s8 channel, s32 node_id); + + /** + * Generate commands for a voice. + * Includes a data source, biquad filter, volume and mixing. + * + * @param voice_info - Voice info these commands are generated from. + */ + void GenerateVoiceCommand(VoiceInfo& voice_info); + + /** + * Generate commands for all voices. + */ + void GenerateVoiceCommands(); + + /** + * Generate a mixing command. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info_base - BufferMixer effect info. + * @param node_id - Node id of the mix this command is generated for. + */ + void GenerateBufferMixerCommand(s16 buffer_offset, EffectInfoBase& effect_info_base, + s32 node_id); + + /** + * Generate a delay effect command. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info_base - Delay effect info. + * @param node_id - Node id of the mix this command is generated for. + */ + void GenerateDelayCommand(s16 buffer_offset, EffectInfoBase& effect_info_base, s32 node_id); + + /** + * Generate a reverb effect command. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info_base - Reverb effect info. + * @param node_id - Node id of the mix this command is generated for. + * @param long_size_pre_delay_supported - Use a longer pre-delay time before reverb starts. + */ + void GenerateReverbCommand(s16 buffer_offset, EffectInfoBase& effect_info_base, s32 node_id, + bool long_size_pre_delay_supported); + + /** + * Generate an I3DL2 reverb effect command. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info_base - I3DL2Reverb effect info. + * @param node_id - Node id of the mix this command is generated for. + */ + void GenerateI3dl2ReverbEffectCommand(s16 buffer_offset, EffectInfoBase& effect_info, + s32 node_id); + + /** + * Generate an aux effect command. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info_base - Aux effect info. + * @param node_id - Node id of the mix this command is generated for. + */ + void GenerateAuxCommand(s16 buffer_offset, EffectInfoBase& effect_info, s32 node_id); + + /** + * Generate a biquad filter effect command. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info_base - Aux effect info. + * @param node_id - Node id of the mix this command is generated for. + */ + void GenerateBiquadFilterEffectCommand(s16 buffer_offset, EffectInfoBase& effect_info, + s32 node_id); + + /** + * Generate a light limiter effect command. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info_base - Limiter effect info. + * @param node_id - Node id of the mix this command is generated for. + * @param effect_index - Index for the statistics state. + */ + void GenerateLightLimiterEffectCommand(s16 buffer_offset, EffectInfoBase& effect_info, + s32 node_id, u32 effect_index); + + /** + * Generate a capture effect command. + * Writes a mix buffer back to game memory. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info_base - Capture effect info. + * @param node_id - Node id of the mix this command is generated for. + */ + void GenerateCaptureCommand(s16 buffer_offset, EffectInfoBase& effect_info, s32 node_id); + + /** + * Generate a compressor effect command. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info_base - Compressor effect info. + * @param node_id - Node id of the mix this command is generated for. + */ + void GenerateCompressorCommand(const s16 buffer_offset, EffectInfoBase& effect_info, + const s32 node_id); + + /** + * Generate all effect commands for a mix. + * + * @param mix_info - Mix to generate effects from. + */ + void GenerateEffectCommand(MixInfo& mix_info); + + /** + * Generate all mix commands. + * + * @param mix_info - Mix to generate effects from. + */ + void GenerateMixCommands(MixInfo& mix_info); + + /** + * Generate a submix command. + * Generates all effects and all mixing commands. + * + * @param mix_info - Mix to generate effects from. + */ + void GenerateSubMixCommand(MixInfo& mix_info); + + /** + * Generate all submix command. + */ + void GenerateSubMixCommands(); + + /** + * Generate the final mix. + */ + void GenerateFinalMixCommand(); + + /** + * Generate the final mix commands. + */ + void GenerateFinalMixCommands(); + + /** + * Generate all sink commands. + */ + void GenerateSinkCommands(); + + /** + * Generate a sink command. + * Sends samples out to the backend, or a game-supplied circular buffer. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param sink_info - Sink info to generate the commands from. + */ + void GenerateSinkCommand(s16 buffer_offset, SinkInfoBase& sink_info); + + /** + * Generate a device sink command. + * Sends samples out to the backend. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param sink_info - Sink info to generate the commands from. + */ + void GenerateDeviceSinkCommand(s16 buffer_offset, SinkInfoBase& sink_info); + + /** + * Generate a performance command. + * Used to report performance metrics of the AudioRenderer back to the game. + * + * @param buffer_offset - Base mix buffer offset to use. + * @param sink_info - Sink info to generate the commands from. + */ + void GeneratePerformanceCommand(s32 node_id, PerformanceState state, + const PerformanceEntryAddresses& entry_addresses); + +private: + /// Commands will be written by this buffer + CommandBuffer& command_buffer; + /// Header information for the commands generated + const CommandListHeader& command_header; + /// Various things to control generation + const AudioRendererSystemContext& render_context; + /// Used for generating voices + VoiceContext& voice_context; + /// Used for generating mixes + MixContext& mix_context; + /// Used for generating effects + EffectContext& effect_context; + /// Used for generating sinks + SinkContext& sink_context; + /// Used for generating submixes + SplitterContext& splitter_context; + /// Used for generating performance + PerformanceManager* performance_manager; +}; + +} // namespace AudioRenderer +} // namespace AudioCore diff --git a/src/audio_core/renderer/command/command_list_header.h b/src/audio_core/renderer/command/command_list_header.h new file mode 100644 index 0000000000..988530b1f4 --- /dev/null +++ b/src/audio_core/renderer/command/command_list_header.h @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +struct CommandListHeader { + u64 buffer_size; + u32 command_count; + std::span samples_buffer; + s16 buffer_count; + u32 sample_count; + u32 sample_rate; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/command_processing_time_estimator.cpp b/src/audio_core/renderer/command/command_processing_time_estimator.cpp new file mode 100644 index 0000000000..3091f587ac --- /dev/null +++ b/src/audio_core/renderer/command/command_processing_time_estimator.cpp @@ -0,0 +1,3620 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/command/command_processing_time_estimator.h" + +namespace AudioCore::AudioRenderer { + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + const PcmInt16DataSourceVersion1Command& command) const { + return static_cast(command.pitch * 0.25f * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + const PcmInt16DataSourceVersion2Command& command) const { + return static_cast(command.pitch * 0.25f * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const PcmFloatDataSourceVersion1Command& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const PcmFloatDataSourceVersion2Command& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + const AdpcmDataSourceVersion1Command& command) const { + return static_cast(command.pitch * 0.25f * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + const AdpcmDataSourceVersion2Command& command) const { + return static_cast(command.pitch * 0.25f * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const VolumeCommand& command) const { + return static_cast((static_cast(sample_count) * 8.8f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const VolumeRampCommand& command) const { + return static_cast((static_cast(sample_count) * 9.8f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const BiquadFilterCommand& command) const { + return static_cast((static_cast(sample_count) * 58.0f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const MixCommand& command) const { + return static_cast((static_cast(sample_count) * 10.0f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const MixRampCommand& command) const { + return static_cast((static_cast(sample_count) * 14.4f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate(const MixRampGroupedCommand& command) const { + u32 count{0}; + for (u32 i = 0; i < command.buffer_count; i++) { + if (command.volumes[i] != 0.0f || command.prev_volumes[i] != 0.0f) { + count++; + } + } + + return static_cast(((static_cast(sample_count) * 14.4f) * 1.2f) * + static_cast(count)); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const DepopPrepareCommand& command) const { + return 1080; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + const DepopForMixBuffersCommand& command) const { + return static_cast((static_cast(sample_count) * 8.9f) * + static_cast(command.count)); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate(const DelayCommand& command) const { + return static_cast((static_cast(sample_count) * command.parameter.channel_count) * + 202.5f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const UpsampleCommand& command) const { + return 357915; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const DownMix6chTo2chCommand& command) const { + return 16108; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate(const AuxCommand& command) const { + if (command.enabled) { + return 15956; + } + return 3765; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const DeviceSinkCommand& command) const { + return 10042; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const CircularBufferSinkCommand& command) const { + return 55; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate(const ReverbCommand& command) const { + if (command.enabled) { + return static_cast( + (command.parameter.channel_count * static_cast(sample_count) * 750) * 1.2f); + } + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate(const I3dl2ReverbCommand& command) const { + if (command.enabled) { + return static_cast( + (command.parameter.channel_count * static_cast(sample_count) * 530) * 1.2f); + } + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const PerformanceCommand& command) const { + return 1454; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const ClearMixBufferCommand& command) const { + return static_cast( + ((static_cast(sample_count) * 0.83f) * static_cast(buffer_count)) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const CopyMixBufferCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const LightLimiterVersion1Command& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const LightLimiterVersion2Command& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const MultiTapBiquadFilterCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const CaptureCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion1::Estimate( + [[maybe_unused]] const CompressorCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + const PcmInt16DataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 749.269f + + 6138.94f); + case 240: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 1195.456f + + 7797.047f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + const PcmInt16DataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 749.269f + + 6138.94f); + case 240: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 1195.456f + + 7797.047f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + const PcmFloatDataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 749.269f + + 6138.94f); + case 240: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 1195.456f + + 7797.047f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + const PcmFloatDataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 749.269f + + 6138.94f); + case 240: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 1195.456f + + 7797.047f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + const AdpcmDataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 2125.588f + + 9039.47f); + case 240: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 3564.088 + + 6225.471); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + const AdpcmDataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 2125.588f + + 9039.47f); + case 240: + return static_cast( + (static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 2.0f) * 3564.088 + + 6225.471); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const VolumeCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1280.3f); + case 240: + return static_cast(1737.8f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const VolumeRampCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1403.9f); + case 240: + return static_cast(1884.3f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const BiquadFilterCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(4813.2f); + case 240: + return static_cast(6915.4f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const MixCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1342.2f); + case 240: + return static_cast(1833.2f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const MixRampCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1859.0f); + case 240: + return static_cast(2286.1f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate(const MixRampGroupedCommand& command) const { + u32 count{0}; + for (u32 i = 0; i < command.buffer_count; i++) { + if (command.volumes[i] != 0.0f || command.prev_volumes[i] != 0.0f) { + count++; + } + } + + switch (sample_count) { + case 160: + return static_cast((static_cast(sample_count) * 7.245f) * + static_cast(count)); + case 240: + return static_cast((static_cast(sample_count) * 7.245f) * + static_cast(count)); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const DepopPrepareCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(306.62f); + case 240: + return static_cast(293.22f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const DepopForMixBuffersCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(762.96f); + case 240: + return static_cast(726.96f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate(const DelayCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(41635.555f); + case 2: + return static_cast(97861.211f); + case 4: + return static_cast(192515.516f); + case 6: + return static_cast(301755.969f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(578.529f); + case 2: + return static_cast(663.064f); + case 4: + return static_cast(703.983f); + case 6: + return static_cast(760.032f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(8770.345f); + case 2: + return static_cast(25741.18f); + case 4: + return static_cast(47551.168f); + case 6: + return static_cast(81629.219f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(521.283f); + case 2: + return static_cast(585.396f); + case 4: + return static_cast(629.884f); + case 6: + return static_cast(713.57f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const UpsampleCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(292000.0f); + case 240: + return static_cast(0.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const DownMix6chTo2chCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(10009.0f); + case 240: + return static_cast(14577.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate(const AuxCommand& command) const { + // Is this function bugged, returning the wrong time? + // Surely the larger time should be returned when enabled... + // CMP W8, #0 + // MOV W8, #0x60; // 489.163f + // MOV W10, #0x64; // 7177.936f + // CSEL X8, X10, X8, EQ + + switch (sample_count) { + case 160: + if (command.enabled) { + return static_cast(489.163f); + } + return static_cast(7177.936f); + case 240: + if (command.enabled) { + return static_cast(485.562f); + } + return static_cast(9499.822f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate(const DeviceSinkCommand& command) const { + switch (command.input_count) { + case 2: + switch (sample_count) { + case 160: + return static_cast(9261.545f); + case 240: + return static_cast(9336.054f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + case 6: + switch (sample_count) { + case 160: + return static_cast(9336.054f); + case 240: + return static_cast(9566.728f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid input count {}", command.input_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + const CircularBufferSinkCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(static_cast(command.input_count) * 853.629f + 1284.517f); + case 240: + return static_cast(static_cast(command.input_count) * 1726.021f + 1369.683f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate(const ReverbCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(97192.227f); + case 2: + return static_cast(103278.555f); + case 4: + return static_cast(109579.039f); + case 6: + return static_cast(115065.438f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(492.009f); + case 2: + return static_cast(554.463f); + case 4: + return static_cast(595.864f); + case 6: + return static_cast(656.617f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(136463.641f); + case 2: + return static_cast(145749.047f); + case 4: + return static_cast(154796.938f); + case 6: + return static_cast(161968.406f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(495.789f); + case 2: + return static_cast(527.163f); + case 4: + return static_cast(598.752f); + case 6: + return static_cast(666.025f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate(const I3dl2ReverbCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(138836.484f); + case 2: + return static_cast(135428.172f); + case 4: + return static_cast(199181.844f); + case 6: + return static_cast(247345.906f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(718.704f); + case 2: + return static_cast(751.296f); + case 4: + return static_cast(797.464f); + case 6: + return static_cast(867.426f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(199952.734f); + case 2: + return static_cast(195199.5f); + case 4: + return static_cast(290575.875f); + case 6: + return static_cast(363494.531f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(534.24f); + case 2: + return static_cast(570.874f); + case 4: + return static_cast(660.933f); + case 6: + return static_cast(694.596f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const PerformanceCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(489.35f); + case 240: + return static_cast(491.18f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const ClearMixBufferCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(static_cast(buffer_count) * 260.4f + 139.65f); + case 240: + return static_cast(static_cast(buffer_count) * 668.85f + 193.2f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const CopyMixBufferCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(836.32f); + case 240: + return static_cast(1000.9f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const LightLimiterVersion1Command& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const LightLimiterVersion2Command& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const MultiTapBiquadFilterCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const CaptureCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const CompressorCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const PcmInt16DataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 427.52f + + 6329.442f); + case 240: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 710.143f + + 7853.286f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const PcmInt16DataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 427.52f + + 6329.442f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 371.876f + + 8049.415f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 423.43f + + 5062.659f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + case 240: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 710.143f + + 7853.286f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 610.487f + + 10138.842f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 676.722f + + 5810.962f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const PcmFloatDataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 1672.026f + + 7681.211f); + case 240: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 2550.414f + + 9663.969f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const PcmFloatDataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1672.026f + + 7681.211f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1672.982f + + 9038.011f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1673.216f + + 6027.577f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + case 240: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2550.414f + + 9663.969f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2522.303f + + 11758.571f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2537.061f + + 7369.309f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const AdpcmDataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 1827.665f + + 7913.808f); + case 240: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 2756.372f + + 9736.702f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const AdpcmDataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1827.665f + + 7913.808f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1829.285f + + 9607.814f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1824.609f + + 6517.476f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + case 240: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2756.372f + + 9736.702f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2731.308f + + 12154.379f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2732.152f + + 7929.442f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const VolumeCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1311.1f); + case 240: + return static_cast(1713.6f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const VolumeRampCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1425.3f); + case 240: + return static_cast(1700.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const BiquadFilterCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(4173.2f); + case 240: + return static_cast(5585.1f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const MixCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1402.8f); + case 240: + return static_cast(1853.2f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const MixRampCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1968.7f); + case 240: + return static_cast(2459.4f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate(const MixRampGroupedCommand& command) const { + u32 count{0}; + for (u32 i = 0; i < command.buffer_count; i++) { + if (command.volumes[i] != 0.0f || command.prev_volumes[i] != 0.0f) { + count++; + } + } + + switch (sample_count) { + case 160: + return static_cast((static_cast(sample_count) * 6.708f) * + static_cast(count)); + case 240: + return static_cast((static_cast(sample_count) * 6.443f) * + static_cast(count)); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const DepopPrepareCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const DepopForMixBuffersCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(739.64f); + case 240: + return static_cast(910.97f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate(const DelayCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(8929.042f); + case 2: + return static_cast(25500.75f); + case 4: + return static_cast(47759.617f); + case 6: + return static_cast(82203.07f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(1295.206f); + case 2: + return static_cast(1213.6f); + case 4: + return static_cast(942.028f); + case 6: + return static_cast(1001.553f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(11941.051f); + case 2: + return static_cast(37197.371f); + case 4: + return static_cast(69749.836f); + case 6: + return static_cast(120042.398f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(997.668f); + case 2: + return static_cast(977.634f); + case 4: + return static_cast(792.309f); + case 6: + return static_cast(875.427f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const UpsampleCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(312990.0f); + case 240: + return static_cast(0.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const DownMix6chTo2chCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(9949.7f); + case 240: + return static_cast(14679.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate(const AuxCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + return static_cast(7182.136f); + } + return static_cast(472.111f); + case 240: + if (command.enabled) { + return static_cast(9435.961f); + } + return static_cast(462.619f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate(const DeviceSinkCommand& command) const { + switch (command.input_count) { + case 2: + switch (sample_count) { + case 160: + return static_cast(8979.956f); + case 240: + return static_cast(9221.907f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + case 6: + switch (sample_count) { + case 160: + return static_cast(9177.903f); + case 240: + return static_cast(9725.897f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid input count {}", command.input_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const CircularBufferSinkCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(static_cast(command.input_count) * 531.069f + 0.0f); + case 240: + return static_cast(static_cast(command.input_count) * 770.257f + 0.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate(const ReverbCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(81475.055f); + case 2: + return static_cast(84975.0f); + case 4: + return static_cast(91625.148f); + case 6: + return static_cast(95332.266f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(536.298f); + case 2: + return static_cast(588.798f); + case 4: + return static_cast(643.702f); + case 6: + return static_cast(705.999f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(120174.469f); + case 2: + return static_cast(125262.219f); + case 4: + return static_cast(135751.234f); + case 6: + return static_cast(141129.234f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(617.641f); + case 2: + return static_cast(659.536f); + case 4: + return static_cast(711.438f); + case 6: + return static_cast(778.071f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate(const I3dl2ReverbCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(116754.984f); + case 2: + return static_cast(125912.055f); + case 4: + return static_cast(146336.031f); + case 6: + return static_cast(165812.656f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(735.0f); + case 2: + return static_cast(766.615f); + case 4: + return static_cast(834.067f); + case 6: + return static_cast(875.437f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(170292.344f); + case 2: + return static_cast(183875.625f); + case 4: + return static_cast(214696.188f); + case 6: + return static_cast(243846.766f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(508.473f); + case 2: + return static_cast(582.445f); + case 4: + return static_cast(626.419f); + case 6: + return static_cast(682.468f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const PerformanceCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(498.17f); + case 240: + return static_cast(489.42f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const ClearMixBufferCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(static_cast(buffer_count - 1) * 266.645f + 0.0f); + case 240: + return static_cast(static_cast(buffer_count - 1) * 440.681f + 0.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const CopyMixBufferCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(842.59f); + case 240: + return static_cast(986.72f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const LightLimiterVersion1Command& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(21392.383f); + case 2: + return static_cast(26829.389f); + case 4: + return static_cast(32405.152f); + case 6: + return static_cast(52218.586f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(897.004f); + case 2: + return static_cast(931.549f); + case 4: + return static_cast(975.387f); + case 6: + return static_cast(1016.778f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(30555.504f); + case 2: + return static_cast(39010.785f); + case 4: + return static_cast(48270.18f); + case 6: + return static_cast(76711.875f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(874.429f); + case 2: + return static_cast(921.553f); + case 4: + return static_cast(945.262f); + case 6: + return static_cast(992.26f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + const LightLimiterVersion2Command& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + if (command.parameter.statistics_enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(23308.928f); + case 2: + return static_cast(29954.062f); + case 4: + return static_cast(35807.477f); + case 6: + return static_cast(58339.773f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(21392.383f); + case 2: + return static_cast(26829.389f); + case 4: + return static_cast(32405.152f); + case 6: + return static_cast(52218.586f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(897.004f); + case 2: + return static_cast(931.549f); + case 4: + return static_cast(975.387f); + case 6: + return static_cast(1016.778f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + if (command.parameter.statistics_enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(33526.121f); + case 2: + return static_cast(43549.355f); + case 4: + return static_cast(52190.281f); + case 6: + return static_cast(85526.516f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(30555.504f); + case 2: + return static_cast(39010.785f); + case 4: + return static_cast(48270.18f); + case 6: + return static_cast(76711.875f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(874.429f); + case 2: + return static_cast(921.553f); + case 4: + return static_cast(945.262f); + case 6: + return static_cast(992.26f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const MultiTapBiquadFilterCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const CaptureCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const CompressorCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const PcmInt16DataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 427.52f + + 6329.442f); + case 240: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 710.143f + + 7853.286f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const PcmInt16DataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 427.52f + + 6329.442f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 371.876f + + 8049.415f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 423.43f + + 5062.659f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + case 240: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 710.143f + + 7853.286f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 610.487f + + 10138.842f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 676.722f + + 5810.962f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const PcmFloatDataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 1672.026f + + 7681.211f); + case 240: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 2550.414f + + 9663.969f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const PcmFloatDataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1672.026f + + 7681.211f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1672.982f + + 9038.011f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1673.216f + + 6027.577f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + case 240: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2550.414f + + 9663.969f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2522.303f + + 11758.571f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2537.061f + + 7369.309f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const AdpcmDataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 1827.665f + + 7913.808f); + case 240: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 2756.372f + + 9736.702f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const AdpcmDataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1827.665f + + 7913.808f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1829.285f + + 9607.814f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1824.609f + + 6517.476f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + case 240: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2756.372f + + 9736.702f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2731.308f + + 12154.379f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2732.152f + + 7929.442f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const VolumeCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1311.1f); + case 240: + return static_cast(1713.6f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const VolumeRampCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1425.3f); + case 240: + return static_cast(1700.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const BiquadFilterCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(4173.2f); + case 240: + return static_cast(5585.1f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const MixCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1402.8f); + case 240: + return static_cast(1853.2f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const MixRampCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1968.7f); + case 240: + return static_cast(2459.4f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate(const MixRampGroupedCommand& command) const { + u32 count{0}; + for (u32 i = 0; i < command.buffer_count; i++) { + if (command.volumes[i] != 0.0f || command.prev_volumes[i] != 0.0f) { + count++; + } + } + + switch (sample_count) { + case 160: + return static_cast((static_cast(sample_count) * 6.708f) * + static_cast(count)); + case 240: + return static_cast((static_cast(sample_count) * 6.443f) * + static_cast(count)); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const DepopPrepareCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const DepopForMixBuffersCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(739.64f); + case 240: + return static_cast(910.97f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate(const DelayCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(8929.042f); + case 2: + return static_cast(25500.75f); + case 4: + return static_cast(47759.617f); + case 6: + return static_cast(82203.07f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(1295.206f); + case 2: + return static_cast(1213.6f); + case 4: + return static_cast(942.028f); + case 6: + return static_cast(1001.553f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(11941.051f); + case 2: + return static_cast(37197.371f); + case 4: + return static_cast(69749.836f); + case 6: + return static_cast(120042.398f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(997.668f); + case 2: + return static_cast(977.634f); + case 4: + return static_cast(792.309f); + case 6: + return static_cast(875.427f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const UpsampleCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(312990.0f); + case 240: + return static_cast(0.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const DownMix6chTo2chCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(9949.7f); + case 240: + return static_cast(14679.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate(const AuxCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + return static_cast(7182.136f); + } + return static_cast(472.111f); + case 240: + if (command.enabled) { + return static_cast(9435.961f); + } + return static_cast(462.619f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate(const DeviceSinkCommand& command) const { + switch (command.input_count) { + case 2: + switch (sample_count) { + case 160: + return static_cast(8979.956f); + case 240: + return static_cast(9221.907f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + case 6: + switch (sample_count) { + case 160: + return static_cast(9177.903f); + case 240: + return static_cast(9725.897f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid input count {}", command.input_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const CircularBufferSinkCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(static_cast(command.input_count) * 531.069f + 0.0f); + case 240: + return static_cast(static_cast(command.input_count) * 770.257f + 0.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate(const ReverbCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(81475.055f); + case 2: + return static_cast(84975.0f); + case 4: + return static_cast(91625.148f); + case 6: + return static_cast(95332.266f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(536.298f); + case 2: + return static_cast(588.798f); + case 4: + return static_cast(643.702f); + case 6: + return static_cast(705.999f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(120174.469f); + case 2: + return static_cast(125262.219f); + case 4: + return static_cast(135751.234f); + case 6: + return static_cast(141129.234f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(617.641f); + case 2: + return static_cast(659.536f); + case 4: + return static_cast(711.438f); + case 6: + return static_cast(778.071f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate(const I3dl2ReverbCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(116754.984f); + case 2: + return static_cast(125912.055f); + case 4: + return static_cast(146336.031f); + case 6: + return static_cast(165812.656f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(735.0f); + case 2: + return static_cast(766.615f); + case 4: + return static_cast(834.067f); + case 6: + return static_cast(875.437f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(170292.344f); + case 2: + return static_cast(183875.625f); + case 4: + return static_cast(214696.188f); + case 6: + return static_cast(243846.766f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(508.473f); + case 2: + return static_cast(582.445f); + case 4: + return static_cast(626.419f); + case 6: + return static_cast(682.468f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const PerformanceCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(498.17f); + case 240: + return static_cast(489.42f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const ClearMixBufferCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(static_cast(buffer_count - 1) * 266.645f + 0.0f); + case 240: + return static_cast(static_cast(buffer_count - 1) * 440.681f + 0.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const CopyMixBufferCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(842.59f); + case 240: + return static_cast(986.72f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const LightLimiterVersion1Command& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(21392.383f); + case 2: + return static_cast(26829.389f); + case 4: + return static_cast(32405.152f); + case 6: + return static_cast(52218.586f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(897.004f); + case 2: + return static_cast(931.549f); + case 4: + return static_cast(975.387f); + case 6: + return static_cast(1016.778f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(30555.504f); + case 2: + return static_cast(39010.785f); + case 4: + return static_cast(48270.18f); + case 6: + return static_cast(76711.875f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(874.429f); + case 2: + return static_cast(921.553f); + case 4: + return static_cast(945.262f); + case 6: + return static_cast(992.26f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + const LightLimiterVersion2Command& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + if (command.parameter.statistics_enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(23308.928f); + case 2: + return static_cast(29954.062f); + case 4: + return static_cast(35807.477f); + case 6: + return static_cast(58339.773f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(21392.383f); + case 2: + return static_cast(26829.389f); + case 4: + return static_cast(32405.152f); + case 6: + return static_cast(52218.586f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(897.004f); + case 2: + return static_cast(931.549f); + case 4: + return static_cast(975.387f); + case 6: + return static_cast(1016.778f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + if (command.parameter.statistics_enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(33526.121f); + case 2: + return static_cast(43549.355f); + case 4: + return static_cast(52190.281f); + case 6: + return static_cast(85526.516f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(30555.504f); + case 2: + return static_cast(39010.785f); + case 4: + return static_cast(48270.18f); + case 6: + return static_cast(76711.875f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(874.429f); + case 2: + return static_cast(921.553f); + case 4: + return static_cast(945.262f); + case 6: + return static_cast(992.26f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const MultiTapBiquadFilterCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(7424.5f); + case 240: + return static_cast(9730.4f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate(const CaptureCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + return static_cast(426.982f); + } + return static_cast(4261.005f); + case 240: + if (command.enabled) { + return static_cast(435.204f); + } + return static_cast(5858.265f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const CompressorCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + const PcmInt16DataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 427.52f + + 6329.442f); + case 240: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 710.143f + + 7853.286f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + const PcmInt16DataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 427.52f + + 6329.442f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 371.876f + + 8049.415f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 423.43f + + 5062.659f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + case 240: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 710.143f + + 7853.286f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 610.487f + + 10138.842f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 676.722f + + 5810.962f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + const PcmFloatDataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 1672.026f + + 7681.211f); + case 240: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 2550.414f + + 9663.969f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + const PcmFloatDataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1672.026f + + 7681.211f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1672.982f + + 9038.011f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1673.216f + + 6027.577f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + case 240: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2550.414f + + 9663.969f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2522.303f + + 11758.571f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2537.061f + + 7369.309f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + const AdpcmDataSourceVersion1Command& command) const { + switch (sample_count) { + case 160: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 1827.665f + + 7913.808f); + case 240: + return static_cast( + ((static_cast(command.sample_rate) / 200.0f / static_cast(sample_count)) * + (command.pitch * 0.000030518f)) * + 2756.372f + + 9736.702f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + const AdpcmDataSourceVersion2Command& command) const { + switch (sample_count) { + case 160: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1827.665f + + 7913.808f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1829.285f + + 9607.814f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 1824.609f + + 6517.476f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + case 240: + switch (command.src_quality) { + case SrcQuality::Medium: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2756.372f + + 9736.702f); + case SrcQuality::High: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2731.308f + + 12154.379f); + case SrcQuality::Low: + return static_cast((((static_cast(command.sample_rate) / 200.0f / + static_cast(sample_count)) * + (command.pitch * 0.000030518f)) - + 1.0f) * + 2732.152f + + 7929.442f); + default: + LOG_ERROR(Service_Audio, "Invalid SRC quality {}", + static_cast(command.src_quality)); + return 0; + } + + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + [[maybe_unused]] const VolumeCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1311.1f); + case 240: + return static_cast(1713.6f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + [[maybe_unused]] const VolumeRampCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1425.3f); + case 240: + return static_cast(1700.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + [[maybe_unused]] const BiquadFilterCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(4173.2f); + case 240: + return static_cast(5585.1f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + [[maybe_unused]] const MixCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1402.8f); + case 240: + return static_cast(1853.2f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + [[maybe_unused]] const MixRampCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(1968.7f); + case 240: + return static_cast(2459.4f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate(const MixRampGroupedCommand& command) const { + u32 count{0}; + for (u32 i = 0; i < command.buffer_count; i++) { + if (command.volumes[i] != 0.0f || command.prev_volumes[i] != 0.0f) { + count++; + } + } + + switch (sample_count) { + case 160: + return static_cast((static_cast(sample_count) * 6.708f) * + static_cast(count)); + case 240: + return static_cast((static_cast(sample_count) * 6.443f) * + static_cast(count)); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + [[maybe_unused]] const DepopPrepareCommand& command) const { + return 0; +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + [[maybe_unused]] const DepopForMixBuffersCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(739.64f); + case 240: + return static_cast(910.97f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate(const DelayCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(8929.042f); + case 2: + return static_cast(25500.75f); + case 4: + return static_cast(47759.617f); + case 6: + return static_cast(82203.07f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(1295.206f); + case 2: + return static_cast(1213.6f); + case 4: + return static_cast(942.028f); + case 6: + return static_cast(1001.553f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(11941.051f); + case 2: + return static_cast(37197.371f); + case 4: + return static_cast(69749.836f); + case 6: + return static_cast(120042.398f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(997.668f); + case 2: + return static_cast(977.634f); + case 4: + return static_cast(792.309f); + case 6: + return static_cast(875.427f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + [[maybe_unused]] const UpsampleCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(312990.0f); + case 240: + return static_cast(0.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + [[maybe_unused]] const DownMix6chTo2chCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(9949.7f); + case 240: + return static_cast(14679.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate(const AuxCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + return static_cast(7182.136f); + } + return static_cast(472.111f); + case 240: + if (command.enabled) { + return static_cast(9435.961f); + } + return static_cast(462.619f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate(const DeviceSinkCommand& command) const { + switch (command.input_count) { + case 2: + switch (sample_count) { + case 160: + return static_cast(8979.956f); + case 240: + return static_cast(9221.907f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + case 6: + switch (sample_count) { + case 160: + return static_cast(9177.903f); + case 240: + return static_cast(9725.897f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid input count {}", command.input_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + const CircularBufferSinkCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(static_cast(command.input_count) * 531.069f + 0.0f); + case 240: + return static_cast(static_cast(command.input_count) * 770.257f + 0.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate(const ReverbCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(81475.055f); + case 2: + return static_cast(84975.0f); + case 4: + return static_cast(91625.148f); + case 6: + return static_cast(95332.266f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(536.298f); + case 2: + return static_cast(588.798f); + case 4: + return static_cast(643.702f); + case 6: + return static_cast(705.999f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(120174.469f); + case 2: + return static_cast(125262.219f); + case 4: + return static_cast(135751.234f); + case 6: + return static_cast(141129.234f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(617.641f); + case 2: + return static_cast(659.536f); + case 4: + return static_cast(711.438f); + case 6: + return static_cast(778.071f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate(const I3dl2ReverbCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(116754.984f); + case 2: + return static_cast(125912.055f); + case 4: + return static_cast(146336.031f); + case 6: + return static_cast(165812.656f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(735.0f); + case 2: + return static_cast(766.615f); + case 4: + return static_cast(834.067f); + case 6: + return static_cast(875.437f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(170292.344f); + case 2: + return static_cast(183875.625f); + case 4: + return static_cast(214696.188f); + case 6: + return static_cast(243846.766f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(508.473f); + case 2: + return static_cast(582.445f); + case 4: + return static_cast(626.419f); + case 6: + return static_cast(682.468f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + [[maybe_unused]] const PerformanceCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(498.17f); + case 240: + return static_cast(489.42f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + [[maybe_unused]] const ClearMixBufferCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(static_cast(buffer_count - 1) * 266.645f + 0.0f); + case 240: + return static_cast(static_cast(buffer_count - 1) * 440.681f + 0.0f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + [[maybe_unused]] const CopyMixBufferCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(842.59f); + case 240: + return static_cast(986.72f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + const LightLimiterVersion1Command& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(21508.01f); + case 2: + return static_cast(23120.453f); + case 4: + return static_cast(26270.053f); + case 6: + return static_cast(40471.902f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(897.004f); + case 2: + return static_cast(931.549f); + case 4: + return static_cast(975.387f); + case 6: + return static_cast(1016.778f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(30565.961f); + case 2: + return static_cast(32812.91f); + case 4: + return static_cast(37354.852f); + case 6: + return static_cast(58486.699f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(874.429f); + case 2: + return static_cast(921.553f); + case 4: + return static_cast(945.262f); + case 6: + return static_cast(992.26f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + const LightLimiterVersion2Command& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + if (command.parameter.processing_mode == LightLimiterInfo::ProcessingMode::Mode0) { + if (command.parameter.statistics_enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(23639.584f); + case 2: + return static_cast(24666.725f); + case 4: + return static_cast(28876.459f); + case 6: + return static_cast(47096.078f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } else { + if (command.parameter.statistics_enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(21508.01f); + case 2: + return static_cast(23120.453f); + case 4: + return static_cast(26270.053f); + case 6: + return static_cast(40471.902f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + } + } else if (command.parameter.processing_mode == + LightLimiterInfo::ProcessingMode::Mode1) { + if (command.parameter.statistics_enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(23639.584f); + case 2: + return static_cast(29954.062f); + case 4: + return static_cast(35807.477f); + case 6: + return static_cast(58339.773f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } else { + if (command.parameter.statistics_enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(23639.584f); + case 2: + return static_cast(29954.062f); + case 4: + return static_cast(35807.477f); + case 6: + return static_cast(58339.773f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + } + } else { + LOG_ERROR(Service_Audio, "Invalid processing mode {}", + command.parameter.processing_mode); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(897.004f); + case 2: + return static_cast(931.549f); + case 4: + return static_cast(975.387f); + case 6: + return static_cast(1016.778f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + case 240: + if (command.enabled) { + if (command.parameter.processing_mode == LightLimiterInfo::ProcessingMode::Mode0) { + if (command.parameter.statistics_enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(33875.023f); + case 2: + return static_cast(35199.938f); + case 4: + return static_cast(41371.230f); + case 6: + return static_cast(68370.914f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } else { + switch (command.parameter.channel_count) { + case 1: + return static_cast(30565.961f); + case 2: + return static_cast(32812.91f); + case 4: + return static_cast(37354.852f); + case 6: + return static_cast(58486.699f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + } else if (command.parameter.processing_mode == + LightLimiterInfo::ProcessingMode::Mode1) { + if (command.parameter.statistics_enabled) { + switch (command.parameter.channel_count) { + case 1: + return static_cast(33942.980f); + case 2: + return static_cast(28698.893f); + case 4: + return static_cast(34774.277f); + case 6: + return static_cast(61897.773f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } else { + switch (command.parameter.channel_count) { + case 1: + return static_cast(30610.248f); + case 2: + return static_cast(26322.408f); + case 4: + return static_cast(30369.000f); + case 6: + return static_cast(51892.090f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", + command.parameter.channel_count); + return 0; + } + } + } else { + LOG_ERROR(Service_Audio, "Invalid processing mode {}", + command.parameter.processing_mode); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + return static_cast(874.429f); + case 2: + return static_cast(921.553f); + case 4: + return static_cast(945.262f); + case 6: + return static_cast(992.26f); + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + [[maybe_unused]] const MultiTapBiquadFilterCommand& command) const { + switch (sample_count) { + case 160: + return static_cast(7424.5f); + case 240: + return static_cast(9730.4f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate(const CaptureCommand& command) const { + switch (sample_count) { + case 160: + if (command.enabled) { + return static_cast(426.982f); + } + return static_cast(4261.005f); + case 240: + if (command.enabled) { + return static_cast(435.204f); + } + return static_cast(5858.265f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate(const CompressorCommand& command) const { + if (command.enabled) { + switch (command.parameter.channel_count) { + case 1: + switch (sample_count) { + case 160: + return static_cast(34430.570f); + case 240: + return static_cast(51095.348f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + case 2: + switch (sample_count) { + case 160: + return static_cast(44253.320f); + case 240: + return static_cast(65693.094f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + case 4: + switch (sample_count) { + case 160: + return static_cast(63827.457f); + case 240: + return static_cast(95382.852f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + case 6: + switch (sample_count) { + case 160: + return static_cast(83361.484f); + case 240: + return static_cast(124509.906f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } + } + switch (command.parameter.channel_count) { + case 1: + switch (sample_count) { + case 160: + return static_cast(630.115f); + case 240: + return static_cast(840.136f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + case 2: + switch (sample_count) { + case 160: + return static_cast(638.274f); + case 240: + return static_cast(826.098f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + case 4: + switch (sample_count) { + case 160: + return static_cast(705.862f); + case 240: + return static_cast(901.876f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + case 6: + switch (sample_count) { + case 160: + return static_cast(782.019f); + case 240: + return static_cast(965.286f); + default: + LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count); + return 0; + } + default: + LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count); + return 0; + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/command_processing_time_estimator.h b/src/audio_core/renderer/command/command_processing_time_estimator.h new file mode 100644 index 0000000000..4522171967 --- /dev/null +++ b/src/audio_core/renderer/command/command_processing_time_estimator.h @@ -0,0 +1,254 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/command/commands.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Estimate the processing time required for all commands. + */ +class ICommandProcessingTimeEstimator { +public: + virtual ~ICommandProcessingTimeEstimator() = default; + + virtual u32 Estimate(const PcmInt16DataSourceVersion1Command& command) const = 0; + virtual u32 Estimate(const PcmInt16DataSourceVersion2Command& command) const = 0; + virtual u32 Estimate(const PcmFloatDataSourceVersion1Command& command) const = 0; + virtual u32 Estimate(const PcmFloatDataSourceVersion2Command& command) const = 0; + virtual u32 Estimate(const AdpcmDataSourceVersion1Command& command) const = 0; + virtual u32 Estimate(const AdpcmDataSourceVersion2Command& command) const = 0; + virtual u32 Estimate(const VolumeCommand& command) const = 0; + virtual u32 Estimate(const VolumeRampCommand& command) const = 0; + virtual u32 Estimate(const BiquadFilterCommand& command) const = 0; + virtual u32 Estimate(const MixCommand& command) const = 0; + virtual u32 Estimate(const MixRampCommand& command) const = 0; + virtual u32 Estimate(const MixRampGroupedCommand& command) const = 0; + virtual u32 Estimate(const DepopPrepareCommand& command) const = 0; + virtual u32 Estimate(const DepopForMixBuffersCommand& command) const = 0; + virtual u32 Estimate(const DelayCommand& command) const = 0; + virtual u32 Estimate(const UpsampleCommand& command) const = 0; + virtual u32 Estimate(const DownMix6chTo2chCommand& command) const = 0; + virtual u32 Estimate(const AuxCommand& command) const = 0; + virtual u32 Estimate(const DeviceSinkCommand& command) const = 0; + virtual u32 Estimate(const CircularBufferSinkCommand& command) const = 0; + virtual u32 Estimate(const ReverbCommand& command) const = 0; + virtual u32 Estimate(const I3dl2ReverbCommand& command) const = 0; + virtual u32 Estimate(const PerformanceCommand& command) const = 0; + virtual u32 Estimate(const ClearMixBufferCommand& command) const = 0; + virtual u32 Estimate(const CopyMixBufferCommand& command) const = 0; + virtual u32 Estimate(const LightLimiterVersion1Command& command) const = 0; + virtual u32 Estimate(const LightLimiterVersion2Command& command) const = 0; + virtual u32 Estimate(const MultiTapBiquadFilterCommand& command) const = 0; + virtual u32 Estimate(const CaptureCommand& command) const = 0; + virtual u32 Estimate(const CompressorCommand& command) const = 0; +}; + +class CommandProcessingTimeEstimatorVersion1 final : public ICommandProcessingTimeEstimator { +public: + CommandProcessingTimeEstimatorVersion1(u32 sample_count_, u32 buffer_count_) + : sample_count{sample_count_}, buffer_count{buffer_count_} {} + + u32 Estimate(const PcmInt16DataSourceVersion1Command& command) const override; + u32 Estimate(const PcmInt16DataSourceVersion2Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion1Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion2Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion1Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion2Command& command) const override; + u32 Estimate(const VolumeCommand& command) const override; + u32 Estimate(const VolumeRampCommand& command) const override; + u32 Estimate(const BiquadFilterCommand& command) const override; + u32 Estimate(const MixCommand& command) const override; + u32 Estimate(const MixRampCommand& command) const override; + u32 Estimate(const MixRampGroupedCommand& command) const override; + u32 Estimate(const DepopPrepareCommand& command) const override; + u32 Estimate(const DepopForMixBuffersCommand& command) const override; + u32 Estimate(const DelayCommand& command) const override; + u32 Estimate(const UpsampleCommand& command) const override; + u32 Estimate(const DownMix6chTo2chCommand& command) const override; + u32 Estimate(const AuxCommand& command) const override; + u32 Estimate(const DeviceSinkCommand& command) const override; + u32 Estimate(const CircularBufferSinkCommand& command) const override; + u32 Estimate(const ReverbCommand& command) const override; + u32 Estimate(const I3dl2ReverbCommand& command) const override; + u32 Estimate(const PerformanceCommand& command) const override; + u32 Estimate(const ClearMixBufferCommand& command) const override; + u32 Estimate(const CopyMixBufferCommand& command) const override; + u32 Estimate(const LightLimiterVersion1Command& command) const override; + u32 Estimate(const LightLimiterVersion2Command& command) const override; + u32 Estimate(const MultiTapBiquadFilterCommand& command) const override; + u32 Estimate(const CaptureCommand& command) const override; + u32 Estimate(const CompressorCommand& command) const override; + +private: + u32 sample_count{}; + u32 buffer_count{}; +}; + +class CommandProcessingTimeEstimatorVersion2 final : public ICommandProcessingTimeEstimator { +public: + CommandProcessingTimeEstimatorVersion2(u32 sample_count_, u32 buffer_count_) + : sample_count{sample_count_}, buffer_count{buffer_count_} {} + + u32 Estimate(const PcmInt16DataSourceVersion1Command& command) const override; + u32 Estimate(const PcmInt16DataSourceVersion2Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion1Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion2Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion1Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion2Command& command) const override; + u32 Estimate(const VolumeCommand& command) const override; + u32 Estimate(const VolumeRampCommand& command) const override; + u32 Estimate(const BiquadFilterCommand& command) const override; + u32 Estimate(const MixCommand& command) const override; + u32 Estimate(const MixRampCommand& command) const override; + u32 Estimate(const MixRampGroupedCommand& command) const override; + u32 Estimate(const DepopPrepareCommand& command) const override; + u32 Estimate(const DepopForMixBuffersCommand& command) const override; + u32 Estimate(const DelayCommand& command) const override; + u32 Estimate(const UpsampleCommand& command) const override; + u32 Estimate(const DownMix6chTo2chCommand& command) const override; + u32 Estimate(const AuxCommand& command) const override; + u32 Estimate(const DeviceSinkCommand& command) const override; + u32 Estimate(const CircularBufferSinkCommand& command) const override; + u32 Estimate(const ReverbCommand& command) const override; + u32 Estimate(const I3dl2ReverbCommand& command) const override; + u32 Estimate(const PerformanceCommand& command) const override; + u32 Estimate(const ClearMixBufferCommand& command) const override; + u32 Estimate(const CopyMixBufferCommand& command) const override; + u32 Estimate(const LightLimiterVersion1Command& command) const override; + u32 Estimate(const LightLimiterVersion2Command& command) const override; + u32 Estimate(const MultiTapBiquadFilterCommand& command) const override; + u32 Estimate(const CaptureCommand& command) const override; + u32 Estimate(const CompressorCommand& command) const override; + +private: + u32 sample_count{}; + u32 buffer_count{}; +}; + +class CommandProcessingTimeEstimatorVersion3 final : public ICommandProcessingTimeEstimator { +public: + CommandProcessingTimeEstimatorVersion3(u32 sample_count_, u32 buffer_count_) + : sample_count{sample_count_}, buffer_count{buffer_count_} {} + + u32 Estimate(const PcmInt16DataSourceVersion1Command& command) const override; + u32 Estimate(const PcmInt16DataSourceVersion2Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion1Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion2Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion1Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion2Command& command) const override; + u32 Estimate(const VolumeCommand& command) const override; + u32 Estimate(const VolumeRampCommand& command) const override; + u32 Estimate(const BiquadFilterCommand& command) const override; + u32 Estimate(const MixCommand& command) const override; + u32 Estimate(const MixRampCommand& command) const override; + u32 Estimate(const MixRampGroupedCommand& command) const override; + u32 Estimate(const DepopPrepareCommand& command) const override; + u32 Estimate(const DepopForMixBuffersCommand& command) const override; + u32 Estimate(const DelayCommand& command) const override; + u32 Estimate(const UpsampleCommand& command) const override; + u32 Estimate(const DownMix6chTo2chCommand& command) const override; + u32 Estimate(const AuxCommand& command) const override; + u32 Estimate(const DeviceSinkCommand& command) const override; + u32 Estimate(const CircularBufferSinkCommand& command) const override; + u32 Estimate(const ReverbCommand& command) const override; + u32 Estimate(const I3dl2ReverbCommand& command) const override; + u32 Estimate(const PerformanceCommand& command) const override; + u32 Estimate(const ClearMixBufferCommand& command) const override; + u32 Estimate(const CopyMixBufferCommand& command) const override; + u32 Estimate(const LightLimiterVersion1Command& command) const override; + u32 Estimate(const LightLimiterVersion2Command& command) const override; + u32 Estimate(const MultiTapBiquadFilterCommand& command) const override; + u32 Estimate(const CaptureCommand& command) const override; + u32 Estimate(const CompressorCommand& command) const override; + +private: + u32 sample_count{}; + u32 buffer_count{}; +}; + +class CommandProcessingTimeEstimatorVersion4 final : public ICommandProcessingTimeEstimator { +public: + CommandProcessingTimeEstimatorVersion4(u32 sample_count_, u32 buffer_count_) + : sample_count{sample_count_}, buffer_count{buffer_count_} {} + + u32 Estimate(const PcmInt16DataSourceVersion1Command& command) const override; + u32 Estimate(const PcmInt16DataSourceVersion2Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion1Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion2Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion1Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion2Command& command) const override; + u32 Estimate(const VolumeCommand& command) const override; + u32 Estimate(const VolumeRampCommand& command) const override; + u32 Estimate(const BiquadFilterCommand& command) const override; + u32 Estimate(const MixCommand& command) const override; + u32 Estimate(const MixRampCommand& command) const override; + u32 Estimate(const MixRampGroupedCommand& command) const override; + u32 Estimate(const DepopPrepareCommand& command) const override; + u32 Estimate(const DepopForMixBuffersCommand& command) const override; + u32 Estimate(const DelayCommand& command) const override; + u32 Estimate(const UpsampleCommand& command) const override; + u32 Estimate(const DownMix6chTo2chCommand& command) const override; + u32 Estimate(const AuxCommand& command) const override; + u32 Estimate(const DeviceSinkCommand& command) const override; + u32 Estimate(const CircularBufferSinkCommand& command) const override; + u32 Estimate(const ReverbCommand& command) const override; + u32 Estimate(const I3dl2ReverbCommand& command) const override; + u32 Estimate(const PerformanceCommand& command) const override; + u32 Estimate(const ClearMixBufferCommand& command) const override; + u32 Estimate(const CopyMixBufferCommand& command) const override; + u32 Estimate(const LightLimiterVersion1Command& command) const override; + u32 Estimate(const LightLimiterVersion2Command& command) const override; + u32 Estimate(const MultiTapBiquadFilterCommand& command) const override; + u32 Estimate(const CaptureCommand& command) const override; + u32 Estimate(const CompressorCommand& command) const override; + +private: + u32 sample_count{}; + u32 buffer_count{}; +}; + +class CommandProcessingTimeEstimatorVersion5 final : public ICommandProcessingTimeEstimator { +public: + CommandProcessingTimeEstimatorVersion5(u32 sample_count_, u32 buffer_count_) + : sample_count{sample_count_}, buffer_count{buffer_count_} {} + + u32 Estimate(const PcmInt16DataSourceVersion1Command& command) const override; + u32 Estimate(const PcmInt16DataSourceVersion2Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion1Command& command) const override; + u32 Estimate(const PcmFloatDataSourceVersion2Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion1Command& command) const override; + u32 Estimate(const AdpcmDataSourceVersion2Command& command) const override; + u32 Estimate(const VolumeCommand& command) const override; + u32 Estimate(const VolumeRampCommand& command) const override; + u32 Estimate(const BiquadFilterCommand& command) const override; + u32 Estimate(const MixCommand& command) const override; + u32 Estimate(const MixRampCommand& command) const override; + u32 Estimate(const MixRampGroupedCommand& command) const override; + u32 Estimate(const DepopPrepareCommand& command) const override; + u32 Estimate(const DepopForMixBuffersCommand& command) const override; + u32 Estimate(const DelayCommand& command) const override; + u32 Estimate(const UpsampleCommand& command) const override; + u32 Estimate(const DownMix6chTo2chCommand& command) const override; + u32 Estimate(const AuxCommand& command) const override; + u32 Estimate(const DeviceSinkCommand& command) const override; + u32 Estimate(const CircularBufferSinkCommand& command) const override; + u32 Estimate(const ReverbCommand& command) const override; + u32 Estimate(const I3dl2ReverbCommand& command) const override; + u32 Estimate(const PerformanceCommand& command) const override; + u32 Estimate(const ClearMixBufferCommand& command) const override; + u32 Estimate(const CopyMixBufferCommand& command) const override; + u32 Estimate(const LightLimiterVersion1Command& command) const override; + u32 Estimate(const LightLimiterVersion2Command& command) const override; + u32 Estimate(const MultiTapBiquadFilterCommand& command) const override; + u32 Estimate(const CaptureCommand& command) const override; + u32 Estimate(const CompressorCommand& command) const override; + +private: + u32 sample_count{}; + u32 buffer_count{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/commands.h b/src/audio_core/renderer/command/commands.h new file mode 100644 index 0000000000..6d8b8546da --- /dev/null +++ b/src/audio_core/renderer/command/commands.h @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/command/data_source/adpcm.h" +#include "audio_core/renderer/command/data_source/pcm_float.h" +#include "audio_core/renderer/command/data_source/pcm_int16.h" +#include "audio_core/renderer/command/effect/aux_.h" +#include "audio_core/renderer/command/effect/biquad_filter.h" +#include "audio_core/renderer/command/effect/capture.h" +#include "audio_core/renderer/command/effect/compressor.h" +#include "audio_core/renderer/command/effect/delay.h" +#include "audio_core/renderer/command/effect/i3dl2_reverb.h" +#include "audio_core/renderer/command/effect/light_limiter.h" +#include "audio_core/renderer/command/effect/multi_tap_biquad_filter.h" +#include "audio_core/renderer/command/effect/reverb.h" +#include "audio_core/renderer/command/icommand.h" +#include "audio_core/renderer/command/mix/clear_mix.h" +#include "audio_core/renderer/command/mix/copy_mix.h" +#include "audio_core/renderer/command/mix/depop_for_mix_buffers.h" +#include "audio_core/renderer/command/mix/depop_prepare.h" +#include "audio_core/renderer/command/mix/mix.h" +#include "audio_core/renderer/command/mix/mix_ramp.h" +#include "audio_core/renderer/command/mix/mix_ramp_grouped.h" +#include "audio_core/renderer/command/mix/volume.h" +#include "audio_core/renderer/command/mix/volume_ramp.h" +#include "audio_core/renderer/command/performance/performance.h" +#include "audio_core/renderer/command/resample/downmix_6ch_to_2ch.h" +#include "audio_core/renderer/command/resample/upsample.h" +#include "audio_core/renderer/command/sink/circular_buffer.h" +#include "audio_core/renderer/command/sink/device.h" diff --git a/src/audio_core/renderer/command/data_source/adpcm.cpp b/src/audio_core/renderer/command/data_source/adpcm.cpp new file mode 100644 index 0000000000..e66ed29909 --- /dev/null +++ b/src/audio_core/renderer/command/data_source/adpcm.cpp @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/data_source/adpcm.h" +#include "audio_core/renderer/command/data_source/decode.h" + +namespace AudioCore::AudioRenderer { + +void AdpcmDataSourceVersion1Command::Dump(const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("AdpcmDataSourceVersion1Command\n\toutput_index {:02X} source sample " + "rate {} target sample rate {} src quality {}\n", + output_index, sample_rate, processor.target_sample_rate, src_quality); +} + +void AdpcmDataSourceVersion1Command::Process(const ADSP::CommandListProcessor& processor) { + auto out_buffer{processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count)}; + + DecodeFromWaveBuffersArgs args{ + .sample_format{SampleFormat::Adpcm}, + .output{out_buffer}, + .voice_state{reinterpret_cast(voice_state)}, + .wave_buffers{wave_buffers}, + .channel{0}, + .channel_count{1}, + .src_quality{src_quality}, + .pitch{pitch}, + .source_sample_rate{sample_rate}, + .target_sample_rate{processor.target_sample_rate}, + .sample_count{processor.sample_count}, + .data_address{data_address}, + .data_size{data_size}, + .IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0}, + .IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0}, + }; + + DecodeFromWaveBuffers(*processor.memory, args); +} + +bool AdpcmDataSourceVersion1Command::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +void AdpcmDataSourceVersion2Command::Dump(const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("AdpcmDataSourceVersion2Command\n\toutput_index {:02X} source sample " + "rate {} target sample rate {} src quality {}\n", + output_index, sample_rate, processor.target_sample_rate, src_quality); +} + +void AdpcmDataSourceVersion2Command::Process(const ADSP::CommandListProcessor& processor) { + auto out_buffer{processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count)}; + + DecodeFromWaveBuffersArgs args{ + .sample_format{SampleFormat::Adpcm}, + .output{out_buffer}, + .voice_state{reinterpret_cast(voice_state)}, + .wave_buffers{wave_buffers}, + .channel{0}, + .channel_count{1}, + .src_quality{src_quality}, + .pitch{pitch}, + .source_sample_rate{sample_rate}, + .target_sample_rate{processor.target_sample_rate}, + .sample_count{processor.sample_count}, + .data_address{data_address}, + .data_size{data_size}, + .IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0}, + .IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0}, + }; + + DecodeFromWaveBuffers(*processor.memory, args); +} + +bool AdpcmDataSourceVersion2Command::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/data_source/adpcm.h b/src/audio_core/renderer/command/data_source/adpcm.h new file mode 100644 index 0000000000..a9cf9cee45 --- /dev/null +++ b/src/audio_core/renderer/command/data_source/adpcm.h @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/common/wave_buffer.h" +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command to decode ADPCM-encoded version 1 wavebuffers + * into the output_index mix buffer. + */ +struct AdpcmDataSourceVersion1Command : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Quality used for sample rate conversion + SrcQuality src_quality; + /// Mix buffer index for decoded samples + s16 output_index; + /// Flags to control decoding (see AudioCore::AudioRenderer::VoiceInfo::Flags) + u16 flags; + /// Wavebuffer sample rate + u32 sample_rate; + /// Pitch used for sample rate conversion + f32 pitch; + /// Wavebuffers containing the wavebuffer address, context address, looping information etc + std::array wave_buffers; + /// Voice state, updated each call and written back to game + CpuAddr voice_state; + /// Coefficients data address + CpuAddr data_address; + /// Coefficients data size + u64 data_size; +}; + +/** + * AudioRenderer command to decode ADPCM-encoded version 2 wavebuffers + * into the output_index mix buffer. + */ +struct AdpcmDataSourceVersion2Command : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Quality used for sample rate conversion + SrcQuality src_quality; + /// Mix buffer index for decoded samples + s16 output_index; + /// Flags to control decoding (see AudioCore::AudioRenderer::VoiceInfo::Flags) + u16 flags; + /// Wavebuffer sample rate + u32 sample_rate; + /// Pitch used for sample rate conversion + f32 pitch; + /// Target channel to read within the wavebuffer + s8 channel_index; + /// Number of channels within the wavebuffer + s8 channel_count; + /// Wavebuffers containing the wavebuffer address, context address, looping information etc + std::array wave_buffers; + /// Voice state, updated each call and written back to game + CpuAddr voice_state; + /// Coefficients data address + CpuAddr data_address; + /// Coefficients data size + u64 data_size; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/data_source/decode.cpp b/src/audio_core/renderer/command/data_source/decode.cpp new file mode 100644 index 0000000000..ff5d31bd6f --- /dev/null +++ b/src/audio_core/renderer/command/data_source/decode.cpp @@ -0,0 +1,428 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "audio_core/renderer/command/data_source/decode.h" +#include "audio_core/renderer/command/resample/resample.h" +#include "common/fixed_point.h" +#include "common/logging/log.h" +#include "core/memory.h" + +namespace AudioCore::AudioRenderer { + +constexpr u32 TempBufferSize = 0x3F00; +constexpr std::array PitchBySrcQuality = {4, 8, 4}; + +/** + * Decode PCM data. Only s16 or f32 is supported. + * + * @tparam T - Type to decode. Only s16 and f32 are supported. + * @param memory - Core memory for reading samples. + * @param out_buffer - Output mix buffer to receive the samples. + * @param req - Information for how to decode. + * @return Number of samples decoded. + */ +template +static u32 DecodePcm(Core::Memory::Memory& memory, std::span out_buffer, + const DecodeArg& req) { + constexpr s32 min{std::numeric_limits::min()}; + constexpr s32 max{std::numeric_limits::max()}; + + if (req.buffer == 0 || req.buffer_size == 0) { + return 0; + } + + if (req.start_offset >= req.end_offset) { + return 0; + } + + auto samples_to_decode{ + std::min(req.samples_to_read, req.end_offset - req.start_offset - req.offset)}; + u32 channel_count{static_cast(req.channel_count)}; + + switch (req.channel_count) { + default: { + const VAddr source{req.buffer + + (((req.start_offset + req.offset) * channel_count) * sizeof(T))}; + const u64 size{channel_count * samples_to_decode}; + const u64 size_bytes{size * sizeof(T)}; + + std::vector samples(size); + memory.ReadBlockUnsafe(source, samples.data(), size_bytes); + + if constexpr (std::is_floating_point_v) { + for (u32 i = 0; i < samples_to_decode; i++) { + auto sample{static_cast(samples[i * channel_count + req.target_channel] * + std::numeric_limits::max())}; + out_buffer[i] = static_cast(std::clamp(sample, min, max)); + } + } else { + for (u32 i = 0; i < samples_to_decode; i++) { + out_buffer[i] = samples[i * channel_count + req.target_channel]; + } + } + } break; + + case 1: + if (req.target_channel != 0) { + LOG_ERROR(Service_Audio, "Invalid target channel, expected 0, got {}", + req.target_channel); + return 0; + } + + const VAddr source{req.buffer + ((req.start_offset + req.offset) * sizeof(T))}; + std::vector samples(samples_to_decode); + memory.ReadBlockUnsafe(source, samples.data(), samples_to_decode * sizeof(T)); + + if constexpr (std::is_floating_point_v) { + for (u32 i = 0; i < samples_to_decode; i++) { + auto sample{static_cast(samples[i * channel_count + req.target_channel] * + std::numeric_limits::max())}; + out_buffer[i] = static_cast(std::clamp(sample, min, max)); + } + } else { + std::memcpy(out_buffer.data(), samples.data(), samples_to_decode * sizeof(s16)); + } + break; + } + + return samples_to_decode; +} + +/** + * Decode ADPCM data. + * + * @param memory - Core memory for reading samples. + * @param out_buffer - Output mix buffer to receive the samples. + * @param req - Information for how to decode. + * @return Number of samples decoded. + */ +static u32 DecodeAdpcm(Core::Memory::Memory& memory, std::span out_buffer, + const DecodeArg& req) { + constexpr u32 SamplesPerFrame{14}; + constexpr u32 NibblesPerFrame{16}; + + if (req.buffer == 0 || req.buffer_size == 0) { + return 0; + } + + if (req.end_offset < req.start_offset) { + return 0; + } + + auto end{(req.end_offset % SamplesPerFrame) + + NibblesPerFrame * (req.end_offset / SamplesPerFrame)}; + if (req.end_offset % SamplesPerFrame) { + end += 3; + } else { + end += 1; + } + + if (req.buffer_size < end / 2) { + return 0; + } + + auto samples_to_process{ + std::min(req.end_offset - req.start_offset - req.offset, req.samples_to_read)}; + + auto samples_to_read{samples_to_process}; + auto start_pos{req.start_offset + req.offset}; + auto samples_remaining_in_frame{start_pos % SamplesPerFrame}; + auto position_in_frame{(start_pos / SamplesPerFrame) * NibblesPerFrame + + samples_remaining_in_frame}; + + if (samples_remaining_in_frame) { + position_in_frame += 2; + } + + const auto size{std::max((samples_to_process / 8U) * SamplesPerFrame, 8U)}; + std::vector wavebuffer(size); + memory.ReadBlockUnsafe(req.buffer + position_in_frame / 2, wavebuffer.data(), + wavebuffer.size()); + + auto context{req.adpcm_context}; + auto header{context->header}; + u8 coeff_index{static_cast((header >> 4U) & 0xFU)}; + u8 scale{static_cast(header & 0xFU)}; + s32 coeff0{req.coefficients[coeff_index * 2 + 0]}; + s32 coeff1{req.coefficients[coeff_index * 2 + 1]}; + + auto yn0{context->yn0}; + auto yn1{context->yn1}; + + static constexpr std::array Steps{ + 0, 1, 2, 3, 4, 5, 6, 7, -8, -7, -6, -5, -4, -3, -2, -1, + }; + + const auto decode_sample = [&](const s32 code) -> s16 { + const auto xn = code * (1 << scale); + const auto prediction = coeff0 * yn0 + coeff1 * yn1; + const auto sample = ((xn << 11) + 0x400 + prediction) >> 11; + const auto saturated = std::clamp(sample, -0x8000, 0x7FFF); + yn1 = yn0; + yn0 = static_cast(saturated); + return yn0; + }; + + u32 read_index{0}; + u32 write_index{0}; + + while (samples_to_read > 0) { + // Are we at a new frame? + if ((position_in_frame % NibblesPerFrame) == 0) { + header = wavebuffer[read_index++]; + coeff_index = (header >> 4) & 0xF; + scale = header & 0xF; + coeff0 = req.coefficients[coeff_index * 2 + 0]; + coeff1 = req.coefficients[coeff_index * 2 + 1]; + position_in_frame += 2; + + // Can we consume all of this frame's samples? + if (samples_to_read >= SamplesPerFrame) { + // Can grab all samples until the next header + for (u32 i = 0; i < SamplesPerFrame / 2; i++) { + auto code0{Steps[(wavebuffer[read_index] >> 4) & 0xF]}; + auto code1{Steps[wavebuffer[read_index] & 0xF]}; + read_index++; + + out_buffer[write_index++] = decode_sample(code0); + out_buffer[write_index++] = decode_sample(code1); + } + + position_in_frame += SamplesPerFrame; + samples_to_read -= SamplesPerFrame; + continue; + } + } + + // Decode a single sample + auto code{wavebuffer[read_index]}; + if (position_in_frame & 1) { + code &= 0xF; + read_index++; + } else { + code >>= 4; + } + + out_buffer[write_index++] = decode_sample(Steps[code]); + + position_in_frame++; + samples_to_read--; + } + + context->header = header; + context->yn0 = yn0; + context->yn1 = yn1; + + return samples_to_process; +} + +/** + * Decode implementation. + * Decode wavebuffers according to the given args. + * + * @param memory - Core memory to read data from. + * @param args - The wavebuffer data, and information for how to decode it. + */ +void DecodeFromWaveBuffers(Core::Memory::Memory& memory, const DecodeFromWaveBuffersArgs& args) { + auto& voice_state{*args.voice_state}; + auto remaining_sample_count{args.sample_count}; + auto fraction{voice_state.fraction}; + + const auto sample_rate_ratio{ + (Common::FixedPoint<49, 15>(args.source_sample_rate) / args.target_sample_rate) * + args.pitch}; + const auto size_required{fraction + remaining_sample_count * sample_rate_ratio}; + + if (size_required < 0) { + return; + } + + auto pitch{PitchBySrcQuality[static_cast(args.src_quality)]}; + if (static_cast(pitch + size_required.to_int_floor()) > TempBufferSize) { + return; + } + + auto max_remaining_sample_count{ + ((Common::FixedPoint<17, 15>(TempBufferSize) - fraction) / sample_rate_ratio) + .to_uint_floor()}; + max_remaining_sample_count = std::min(max_remaining_sample_count, remaining_sample_count); + + auto wavebuffers_consumed{voice_state.wave_buffers_consumed}; + auto wavebuffer_index{voice_state.wave_buffer_index}; + auto played_sample_count{voice_state.played_sample_count}; + + bool is_buffer_starved{false}; + u32 offset{voice_state.offset}; + + auto output_buffer{args.output}; + std::vector temp_buffer(TempBufferSize, 0); + + while (remaining_sample_count > 0) { + const auto samples_to_write{std::min(remaining_sample_count, max_remaining_sample_count)}; + const auto samples_to_read{ + (fraction + samples_to_write * sample_rate_ratio).to_uint_floor()}; + + u32 temp_buffer_pos{0}; + + if (!args.IsVoicePitchAndSrcSkippedSupported) { + for (u32 i = 0; i < pitch; i++) { + temp_buffer[i] = voice_state.sample_history[i]; + } + temp_buffer_pos = pitch; + } + + u32 samples_read{0}; + while (samples_read < samples_to_read) { + if (wavebuffer_index >= MaxWaveBuffers) { + LOG_ERROR(Service_Audio, "Invalid wavebuffer index! {}", wavebuffer_index); + wavebuffer_index = 0; + voice_state.wave_buffer_valid.fill(false); + wavebuffers_consumed = MaxWaveBuffers; + } + + if (!voice_state.wave_buffer_valid[wavebuffer_index]) { + is_buffer_starved = true; + break; + } + + auto& wavebuffer{args.wave_buffers[wavebuffer_index]}; + + if (offset == 0 && args.sample_format == SampleFormat::Adpcm && + wavebuffer.context != 0) { + memory.ReadBlockUnsafe(wavebuffer.context, &voice_state.adpcm_context, + wavebuffer.context_size); + } + + auto start_offset{wavebuffer.start_offset}; + auto end_offset{wavebuffer.end_offset}; + + if (wavebuffer.loop && voice_state.loop_count > 0 && + wavebuffer.loop_start_offset != 0 && wavebuffer.loop_end_offset != 0 && + wavebuffer.loop_start_offset <= wavebuffer.loop_end_offset) { + start_offset = wavebuffer.loop_start_offset; + end_offset = wavebuffer.loop_end_offset; + } + + DecodeArg decode_arg{.buffer{wavebuffer.buffer}, + .buffer_size{wavebuffer.buffer_size}, + .start_offset{start_offset}, + .end_offset{end_offset}, + .channel_count{args.channel_count}, + .coefficients{}, + .adpcm_context{nullptr}, + .target_channel{args.channel}, + .offset{offset}, + .samples_to_read{samples_to_read - samples_read}}; + + s32 samples_decoded{0}; + + switch (args.sample_format) { + case SampleFormat::PcmInt16: + samples_decoded = DecodePcm( + memory, {&temp_buffer[temp_buffer_pos], TempBufferSize - temp_buffer_pos}, + decode_arg); + break; + + case SampleFormat::PcmFloat: + samples_decoded = DecodePcm( + memory, {&temp_buffer[temp_buffer_pos], TempBufferSize - temp_buffer_pos}, + decode_arg); + break; + + case SampleFormat::Adpcm: { + decode_arg.adpcm_context = &voice_state.adpcm_context; + memory.ReadBlockUnsafe(args.data_address, &decode_arg.coefficients, args.data_size); + samples_decoded = DecodeAdpcm( + memory, {&temp_buffer[temp_buffer_pos], TempBufferSize - temp_buffer_pos}, + decode_arg); + } break; + + default: + LOG_ERROR(Service_Audio, "Invalid sample format to decode {}", + static_cast(args.sample_format)); + samples_decoded = 0; + break; + } + + played_sample_count += samples_decoded; + samples_read += samples_decoded; + temp_buffer_pos += samples_decoded; + offset += samples_decoded; + + if (samples_decoded == 0 || offset >= end_offset - start_offset) { + offset = 0; + if (!wavebuffer.loop) { + voice_state.wave_buffer_valid[wavebuffer_index] = false; + voice_state.loop_count = 0; + + if (wavebuffer.stream_ended) { + played_sample_count = 0; + } + + wavebuffer_index = (wavebuffer_index + 1) % MaxWaveBuffers; + wavebuffers_consumed++; + } else { + voice_state.loop_count++; + if (wavebuffer.loop_count > 0 && + (voice_state.loop_count > wavebuffer.loop_count || samples_decoded == 0)) { + voice_state.wave_buffer_valid[wavebuffer_index] = false; + voice_state.loop_count = 0; + + if (wavebuffer.stream_ended) { + played_sample_count = 0; + } + + wavebuffer_index = (wavebuffer_index + 1) % MaxWaveBuffers; + wavebuffers_consumed++; + } + + if (samples_decoded == 0) { + is_buffer_starved = true; + break; + } + + if (args.IsVoicePlayedSampleCountResetAtLoopPointSupported) { + played_sample_count = 0; + } + } + } + } + + if (args.IsVoicePitchAndSrcSkippedSupported) { + if (samples_read > output_buffer.size()) { + LOG_ERROR(Service_Audio, "Attempting to write past the end of output buffer!"); + } + for (u32 i = 0; i < samples_read; i++) { + output_buffer[i] = temp_buffer[i]; + } + } else { + std::memset(&temp_buffer[temp_buffer_pos], 0, + (samples_to_read - samples_read) * sizeof(s16)); + + Resample(output_buffer, temp_buffer, sample_rate_ratio, fraction, samples_to_write, + args.src_quality); + + std::memcpy(voice_state.sample_history.data(), &temp_buffer[samples_to_read], + pitch * sizeof(s16)); + } + + remaining_sample_count -= samples_to_write; + if (remaining_sample_count != 0 && is_buffer_starved) { + LOG_ERROR(Service_Audio, "Samples remaining but buffer is starving??"); + break; + } + + output_buffer = output_buffer.subspan(samples_to_write); + } + + voice_state.wave_buffers_consumed = wavebuffers_consumed; + voice_state.played_sample_count = played_sample_count; + voice_state.wave_buffer_index = wavebuffer_index; + voice_state.offset = offset; + voice_state.fraction = fraction; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/data_source/decode.h b/src/audio_core/renderer/command/data_source/decode.h new file mode 100644 index 0000000000..4d63d6fa8a --- /dev/null +++ b/src/audio_core/renderer/command/data_source/decode.h @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/common/wave_buffer.h" +#include "audio_core/renderer/voice/voice_state.h" +#include "common/common_types.h" + +namespace Core::Memory { +class Memory; +} + +namespace AudioCore::AudioRenderer { + +struct DecodeFromWaveBuffersArgs { + SampleFormat sample_format; + std::span output; + VoiceState* voice_state; + std::span wave_buffers; + s8 channel; + s8 channel_count; + SrcQuality src_quality; + f32 pitch; + u32 source_sample_rate; + u32 target_sample_rate; + u32 sample_count; + CpuAddr data_address; + u64 data_size; + bool IsVoicePlayedSampleCountResetAtLoopPointSupported; + bool IsVoicePitchAndSrcSkippedSupported; +}; + +struct DecodeArg { + CpuAddr buffer; + u64 buffer_size; + u32 start_offset; + u32 end_offset; + s8 channel_count; + std::array coefficients; + VoiceState::AdpcmContext* adpcm_context; + s8 target_channel; + u32 offset; + u32 samples_to_read; +}; + +/** + * Decode wavebuffers according to the given args. + * + * @param memory - Core memory to read data from. + * @param args - The wavebuffer data, and information for how to decode it. + */ +void DecodeFromWaveBuffers(Core::Memory::Memory& memory, const DecodeFromWaveBuffersArgs& args); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/data_source/pcm_float.cpp b/src/audio_core/renderer/command/data_source/pcm_float.cpp new file mode 100644 index 0000000000..be77fab694 --- /dev/null +++ b/src/audio_core/renderer/command/data_source/pcm_float.cpp @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/data_source/decode.h" +#include "audio_core/renderer/command/data_source/pcm_float.h" + +namespace AudioCore::AudioRenderer { + +void PcmFloatDataSourceVersion1Command::Dump(const ADSP::CommandListProcessor& processor, + std::string& string) { + string += + fmt::format("PcmFloatDataSourceVersion1Command\n\toutput_index {:02X} channel {} " + "channel count {} source sample rate {} target sample rate {} src quality {}\n", + output_index, channel_index, channel_count, sample_rate, + processor.target_sample_rate, src_quality); +} + +void PcmFloatDataSourceVersion1Command::Process(const ADSP::CommandListProcessor& processor) { + auto out_buffer = processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count); + + DecodeFromWaveBuffersArgs args{ + .sample_format{SampleFormat::PcmFloat}, + .output{out_buffer}, + .voice_state{reinterpret_cast(voice_state)}, + .wave_buffers{wave_buffers}, + .channel{channel_index}, + .channel_count{channel_count}, + .src_quality{src_quality}, + .pitch{pitch}, + .source_sample_rate{sample_rate}, + .target_sample_rate{processor.target_sample_rate}, + .sample_count{processor.sample_count}, + .data_address{0}, + .data_size{0}, + .IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0}, + .IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0}, + }; + + DecodeFromWaveBuffers(*processor.memory, args); +} + +bool PcmFloatDataSourceVersion1Command::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +void PcmFloatDataSourceVersion2Command::Dump(const ADSP::CommandListProcessor& processor, + std::string& string) { + string += + fmt::format("PcmFloatDataSourceVersion2Command\n\toutput_index {:02X} channel {} " + "channel count {} source sample rate {} target sample rate {} src quality {}\n", + output_index, channel_index, channel_count, sample_rate, + processor.target_sample_rate, src_quality); +} + +void PcmFloatDataSourceVersion2Command::Process(const ADSP::CommandListProcessor& processor) { + auto out_buffer = processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count); + + DecodeFromWaveBuffersArgs args{ + .sample_format{SampleFormat::PcmFloat}, + .output{out_buffer}, + .voice_state{reinterpret_cast(voice_state)}, + .wave_buffers{wave_buffers}, + .channel{channel_index}, + .channel_count{channel_count}, + .src_quality{src_quality}, + .pitch{pitch}, + .source_sample_rate{sample_rate}, + .target_sample_rate{processor.target_sample_rate}, + .sample_count{processor.sample_count}, + .data_address{0}, + .data_size{0}, + .IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0}, + .IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0}, + }; + + DecodeFromWaveBuffers(*processor.memory, args); +} + +bool PcmFloatDataSourceVersion2Command::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/data_source/pcm_float.h b/src/audio_core/renderer/command/data_source/pcm_float.h new file mode 100644 index 0000000000..e4af77c200 --- /dev/null +++ b/src/audio_core/renderer/command/data_source/pcm_float.h @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/wave_buffer.h" +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command to decode PCM float-encoded version 1 wavebuffers + * into the output_index mix buffer. + */ +struct PcmFloatDataSourceVersion1Command : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Quality used for sample rate conversion + SrcQuality src_quality; + /// Mix buffer index for decoded samples + s16 output_index; + /// Flags to control decoding (see AudioCore::AudioRenderer::VoiceInfo::Flags) + u16 flags; + /// Wavebuffer sample rate + u32 sample_rate; + /// Pitch used for sample rate conversion + f32 pitch; + /// Target channel to read within the wavebuffer + s8 channel_index; + /// Number of channels within the wavebuffer + s8 channel_count; + /// Wavebuffers containing the wavebuffer address, context address, looping information etc + std::array wave_buffers; + /// Voice state, updated each call and written back to game + CpuAddr voice_state; +}; + +/** + * AudioRenderer command to decode PCM float-encoded version 2 wavebuffers + * into the output_index mix buffer. + */ +struct PcmFloatDataSourceVersion2Command : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Quality used for sample rate conversion + SrcQuality src_quality; + /// Mix buffer index for decoded samples + s16 output_index; + /// Flags to control decoding (see AudioCore::AudioRenderer::VoiceInfo::Flags) + u16 flags; + /// Wavebuffer sample rate + u32 sample_rate; + /// Pitch used for sample rate conversion + f32 pitch; + /// Target channel to read within the wavebuffer + s8 channel_index; + /// Number of channels within the wavebuffer + s8 channel_count; + /// Wavebuffers containing the wavebuffer address, context address, looping information etc + std::array wave_buffers; + /// Voice state, updated each call and written back to game + CpuAddr voice_state; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/data_source/pcm_int16.cpp b/src/audio_core/renderer/command/data_source/pcm_int16.cpp new file mode 100644 index 0000000000..7a27463e4f --- /dev/null +++ b/src/audio_core/renderer/command/data_source/pcm_int16.cpp @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/data_source/decode.h" +#include "audio_core/renderer/command/data_source/pcm_int16.h" + +namespace AudioCore::AudioRenderer { + +void PcmInt16DataSourceVersion1Command::Dump(const ADSP::CommandListProcessor& processor, + std::string& string) { + string += + fmt::format("PcmInt16DataSourceVersion1Command\n\toutput_index {:02X} channel {} " + "channel count {} source sample rate {} target sample rate {} src quality {}\n", + output_index, channel_index, channel_count, sample_rate, + processor.target_sample_rate, src_quality); +} + +void PcmInt16DataSourceVersion1Command::Process(const ADSP::CommandListProcessor& processor) { + auto out_buffer = processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count); + + DecodeFromWaveBuffersArgs args{ + .sample_format{SampleFormat::PcmInt16}, + .output{out_buffer}, + .voice_state{reinterpret_cast(voice_state)}, + .wave_buffers{wave_buffers}, + .channel{channel_index}, + .channel_count{channel_count}, + .src_quality{src_quality}, + .pitch{pitch}, + .source_sample_rate{sample_rate}, + .target_sample_rate{processor.target_sample_rate}, + .sample_count{processor.sample_count}, + .data_address{0}, + .data_size{0}, + .IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0}, + .IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0}, + }; + + DecodeFromWaveBuffers(*processor.memory, args); +} + +bool PcmInt16DataSourceVersion1Command::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +void PcmInt16DataSourceVersion2Command::Dump(const ADSP::CommandListProcessor& processor, + std::string& string) { + string += + fmt::format("PcmInt16DataSourceVersion2Command\n\toutput_index {:02X} channel {} " + "channel count {} source sample rate {} target sample rate {} src quality {}\n", + output_index, channel_index, channel_count, sample_rate, + processor.target_sample_rate, src_quality); +} + +void PcmInt16DataSourceVersion2Command::Process(const ADSP::CommandListProcessor& processor) { + auto out_buffer = processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count); + DecodeFromWaveBuffersArgs args{ + .sample_format{SampleFormat::PcmInt16}, + .output{out_buffer}, + .voice_state{reinterpret_cast(voice_state)}, + .wave_buffers{wave_buffers}, + .channel{channel_index}, + .channel_count{channel_count}, + .src_quality{src_quality}, + .pitch{pitch}, + .source_sample_rate{sample_rate}, + .target_sample_rate{processor.target_sample_rate}, + .sample_count{processor.sample_count}, + .data_address{0}, + .data_size{0}, + .IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0}, + .IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0}, + }; + + DecodeFromWaveBuffers(*processor.memory, args); +} + +bool PcmInt16DataSourceVersion2Command::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/data_source/pcm_int16.h b/src/audio_core/renderer/command/data_source/pcm_int16.h new file mode 100644 index 0000000000..5de1ad60d5 --- /dev/null +++ b/src/audio_core/renderer/command/data_source/pcm_int16.h @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/wave_buffer.h" +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command to decode PCM s16-encoded version 1 wavebuffers + * into the output_index mix buffer. + */ +struct PcmInt16DataSourceVersion1Command : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Quality used for sample rate conversion + SrcQuality src_quality; + /// Mix buffer index for decoded samples + s16 output_index; + /// Flags to control decoding (see AudioCore::AudioRenderer::VoiceInfo::Flags) + u16 flags; + /// Wavebuffer sample rate + u32 sample_rate; + /// Pitch used for sample rate conversion + f32 pitch; + /// Target channel to read within the wavebuffer + s8 channel_index; + /// Number of channels within the wavebuffer + s8 channel_count; + /// Wavebuffers containing the wavebuffer address, context address, looping information etc + std::array wave_buffers; + /// Voice state, updated each call and written back to game + CpuAddr voice_state; +}; + +/** + * AudioRenderer command to decode PCM s16-encoded version 2 wavebuffers + * into the output_index mix buffer. + */ +struct PcmInt16DataSourceVersion2Command : ICommand { + /** + * Print this command's information to a string. + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Quality used for sample rate conversion + SrcQuality src_quality; + /// Mix buffer index for decoded samples + s16 output_index; + /// Flags to control decoding (see AudioCore::AudioRenderer::VoiceInfo::Flags) + u16 flags; + /// Wavebuffer sample rate + u32 sample_rate; + /// Pitch used for sample rate conversion + f32 pitch; + /// Target channel to read within the wavebuffer + s8 channel_index; + /// Number of channels within the wavebuffer + s8 channel_count; + /// Wavebuffers containing the wavebuffer address, context address, looping information etc + std::array wave_buffers; + /// Voice state, updated each call and written back to game + CpuAddr voice_state; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/aux_.cpp b/src/audio_core/renderer/command/effect/aux_.cpp new file mode 100644 index 0000000000..e76db893fb --- /dev/null +++ b/src/audio_core/renderer/command/effect/aux_.cpp @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/effect/aux_.h" +#include "audio_core/renderer/effect/aux_.h" +#include "core/memory.h" + +namespace AudioCore::AudioRenderer { +/** + * Reset an AuxBuffer. + * + * @param memory - Core memory for writing. + * @param aux_info - Memory address pointing to the AuxInfo to reset. + */ +static void ResetAuxBufferDsp(Core::Memory::Memory& memory, const CpuAddr aux_info) { + if (aux_info == 0) { + LOG_ERROR(Service_Audio, "Aux info is 0!"); + return; + } + + auto info{reinterpret_cast(memory.GetPointer(aux_info))}; + info->read_offset = 0; + info->write_offset = 0; + info->total_sample_count = 0; +} + +/** + * Write the given input mix buffer to the memory at send_buffer, and update send_info_ if + * update_count is set, to notify the game that an update happened. + * + * @param memory - Core memory for writing. + * @param send_info_ - Meta information for where to write the mix buffer. + * @param sample_count - Unused. + * @param send_buffer - Memory address to write the mix buffer to. + * @param count_max - Maximum number of samples in the receiving buffer. + * @param input - Input mix buffer to write. + * @param write_count_ - Number of samples to write. + * @param write_offset - Current offset to begin writing the receiving buffer at. + * @param update_count - If non-zero, send_info_ will be updated. + * @return Number of samples written. + */ +static u32 WriteAuxBufferDsp(Core::Memory::Memory& memory, const CpuAddr send_info_, + [[maybe_unused]] u32 sample_count, const CpuAddr send_buffer, + const u32 count_max, std::span input, + const u32 write_count_, const u32 write_offset, + const u32 update_count) { + if (write_count_ > count_max) { + LOG_ERROR(Service_Audio, + "write_count must be smaller than count_max! write_count {}, count_max {}", + write_count_, count_max); + return 0; + } + + if (input.empty()) { + LOG_ERROR(Service_Audio, "input buffer is empty!"); + return 0; + } + + if (send_buffer == 0) { + LOG_ERROR(Service_Audio, "send_buffer is 0!"); + return 0; + } + + if (count_max == 0) { + return 0; + } + + AuxInfo::AuxInfoDsp send_info{}; + memory.ReadBlockUnsafe(send_info_, &send_info, sizeof(AuxInfo::AuxInfoDsp)); + + u32 target_write_offset{send_info.write_offset + write_offset}; + if (target_write_offset > count_max || write_count_ == 0) { + return 0; + } + + u32 write_count{write_count_}; + u32 write_pos{0}; + while (write_count > 0) { + u32 to_write{std::min(count_max - target_write_offset, write_count)}; + + if (to_write > 0) { + memory.WriteBlockUnsafe(send_buffer + target_write_offset * sizeof(s32), + &input[write_pos], to_write * sizeof(s32)); + } + + target_write_offset = (target_write_offset + to_write) % count_max; + write_count -= to_write; + write_pos += to_write; + } + + if (update_count) { + send_info.write_offset = (send_info.write_offset + update_count) % count_max; + } + + memory.WriteBlockUnsafe(send_info_, &send_info, sizeof(AuxInfo::AuxInfoDsp)); + + return write_count_; +} + +/** + * Read the given memory at return_buffer into the output mix buffer, and update return_info_ if + * update_count is set, to notify the game that an update happened. + * + * @param memory - Core memory for writing. + * @param return_info_ - Meta information for where to read the mix buffer. + * @param return_buffer - Memory address to read the samples from. + * @param count_max - Maximum number of samples in the receiving buffer. + * @param output - Output mix buffer which will receive the samples. + * @param count_ - Number of samples to read. + * @param read_offset - Current offset to begin reading the return_buffer at. + * @param update_count - If non-zero, send_info_ will be updated. + * @return Number of samples read. + */ +static u32 ReadAuxBufferDsp(Core::Memory::Memory& memory, const CpuAddr return_info_, + const CpuAddr return_buffer, const u32 count_max, std::span output, + const u32 count_, const u32 read_offset, const u32 update_count) { + if (count_max == 0) { + return 0; + } + + if (count_ > count_max) { + LOG_ERROR(Service_Audio, "count must be smaller than count_max! count {}, count_max {}", + count_, count_max); + return 0; + } + + if (output.empty()) { + LOG_ERROR(Service_Audio, "output buffer is empty!"); + return 0; + } + + if (return_buffer == 0) { + LOG_ERROR(Service_Audio, "return_buffer is 0!"); + return 0; + } + + AuxInfo::AuxInfoDsp return_info{}; + memory.ReadBlockUnsafe(return_info_, &return_info, sizeof(AuxInfo::AuxInfoDsp)); + + u32 target_read_offset{return_info.read_offset + read_offset}; + if (target_read_offset > count_max) { + return 0; + } + + u32 read_count{count_}; + u32 read_pos{0}; + while (read_count > 0) { + u32 to_read{std::min(count_max - target_read_offset, read_count)}; + + if (to_read > 0) { + memory.ReadBlockUnsafe(return_buffer + target_read_offset * sizeof(s32), + &output[read_pos], to_read * sizeof(s32)); + } + + target_read_offset = (target_read_offset + to_read) % count_max; + read_count -= to_read; + read_pos += to_read; + } + + if (update_count) { + return_info.read_offset = (return_info.read_offset + update_count) % count_max; + } + + memory.WriteBlockUnsafe(return_info_, &return_info, sizeof(AuxInfo::AuxInfoDsp)); + + return count_; +} + +void AuxCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("AuxCommand\n\tenabled {} input {:02X} output {:02X}\n", effect_enabled, + input, output); +} + +void AuxCommand::Process(const ADSP::CommandListProcessor& processor) { + auto input_buffer{ + processor.mix_buffers.subspan(input * processor.sample_count, processor.sample_count)}; + auto output_buffer{ + processor.mix_buffers.subspan(output * processor.sample_count, processor.sample_count)}; + + if (effect_enabled) { + WriteAuxBufferDsp(*processor.memory, send_buffer_info, processor.sample_count, send_buffer, + count_max, input_buffer, processor.sample_count, write_offset, + update_count); + + auto read{ReadAuxBufferDsp(*processor.memory, return_buffer_info, return_buffer, count_max, + output_buffer, processor.sample_count, write_offset, + update_count)}; + + if (read != processor.sample_count) { + std::memset(&output_buffer[read], 0, processor.sample_count - read); + } + } else { + ResetAuxBufferDsp(*processor.memory, send_buffer_info); + ResetAuxBufferDsp(*processor.memory, return_buffer_info); + if (input != output) { + std::memcpy(output_buffer.data(), input_buffer.data(), output_buffer.size_bytes()); + } + } +} + +bool AuxCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/aux_.h b/src/audio_core/renderer/command/effect/aux_.h new file mode 100644 index 0000000000..825c93732b --- /dev/null +++ b/src/audio_core/renderer/command/effect/aux_.h @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command to read and write an auxiliary buffer, writing the input mix buffer to game + * memory, and reading into the output buffer from game memory. + */ +struct AuxCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer index + s16 input; + /// Output mix buffer index + s16 output; + /// Meta info for writing + CpuAddr send_buffer_info; + /// Meta info for reading + CpuAddr return_buffer_info; + /// Game memory write buffer + CpuAddr send_buffer; + /// Game memory read buffer + CpuAddr return_buffer; + /// Max samples to read/write + u32 count_max; + /// Current read/write offset + u32 write_offset; + /// Number of samples to update per call + u32 update_count; + /// is this effect enabled? + bool effect_enabled; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/biquad_filter.cpp b/src/audio_core/renderer/command/effect/biquad_filter.cpp new file mode 100644 index 0000000000..1baae74fd6 --- /dev/null +++ b/src/audio_core/renderer/command/effect/biquad_filter.cpp @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/effect/biquad_filter.h" +#include "audio_core/renderer/voice/voice_state.h" + +namespace AudioCore::AudioRenderer { +/** + * Biquad filter float implementation. + * + * @param output - Output container for filtered samples. + * @param input - Input container for samples to be filtered. + * @param b - Feedforward coefficients. + * @param a - Feedback coefficients. + * @param state - State to track previous samples between calls. + * @param sample_count - Number of samples to process. + */ +void ApplyBiquadFilterFloat(std::span output, std::span input, + std::array& b_, std::array& a_, + VoiceState::BiquadFilterState& state, const u32 sample_count) { + constexpr s64 min{std::numeric_limits::min()}; + constexpr s64 max{std::numeric_limits::max()}; + std::array b{Common::FixedPoint<50, 14>::from_base(b_[0]).to_double(), + Common::FixedPoint<50, 14>::from_base(b_[1]).to_double(), + Common::FixedPoint<50, 14>::from_base(b_[2]).to_double()}; + std::array a{Common::FixedPoint<50, 14>::from_base(a_[0]).to_double(), + Common::FixedPoint<50, 14>::from_base(a_[1]).to_double()}; + std::array s{state.s0.to_double(), state.s1.to_double(), state.s2.to_double(), + state.s3.to_double()}; + + for (u32 i = 0; i < sample_count; i++) { + f64 in_sample{static_cast(input[i])}; + auto sample{in_sample * b[0] + s[0] * b[1] + s[1] * b[2] + s[2] * a[0] + s[3] * a[1]}; + + output[i] = static_cast(std::clamp(static_cast(sample), min, max)); + + s[1] = s[0]; + s[0] = in_sample; + s[3] = s[2]; + s[2] = sample; + } + + state.s0 = s[0]; + state.s1 = s[1]; + state.s2 = s[2]; + state.s3 = s[3]; +} + +/** + * Biquad filter s32 implementation. + * + * @param output - Output container for filtered samples. + * @param input - Input container for samples to be filtered. + * @param b - Feedforward coefficients. + * @param a - Feedback coefficients. + * @param state - State to track previous samples between calls. + * @param sample_count - Number of samples to process. + */ +static void ApplyBiquadFilterInt(std::span output, std::span input, + std::array& b_, std::array& a_, + VoiceState::BiquadFilterState& state, const u32 sample_count) { + constexpr s64 min{std::numeric_limits::min()}; + constexpr s64 max{std::numeric_limits::max()}; + std::array, 3> b{ + Common::FixedPoint<50, 14>::from_base(b_[0]), + Common::FixedPoint<50, 14>::from_base(b_[1]), + Common::FixedPoint<50, 14>::from_base(b_[2]), + }; + std::array, 3> a{ + Common::FixedPoint<50, 14>::from_base(a_[0]), + Common::FixedPoint<50, 14>::from_base(a_[1]), + }; + + for (u32 i = 0; i < sample_count; i++) { + s64 in_sample{input[i]}; + auto sample{in_sample * b[0] + state.s0}; + const auto out_sample{std::clamp(sample.to_long(), min, max)}; + + output[i] = static_cast(out_sample); + + state.s0 = state.s1 + b[1] * in_sample + a[0] * out_sample; + state.s1 = 0 + b[2] * in_sample + a[1] * out_sample; + } +} + +void BiquadFilterCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format( + "BiquadFilterCommand\n\tinput {:02X} output {:02X} needs_init {} use_float_processing {}\n", + input, output, needs_init, use_float_processing); +} + +void BiquadFilterCommand::Process(const ADSP::CommandListProcessor& processor) { + auto state_{reinterpret_cast(state)}; + if (needs_init) { + std::memset(state_, 0, sizeof(VoiceState::BiquadFilterState)); + } + + auto input_buffer{ + processor.mix_buffers.subspan(input * processor.sample_count, processor.sample_count)}; + auto output_buffer{ + processor.mix_buffers.subspan(output * processor.sample_count, processor.sample_count)}; + + if (use_float_processing) { + ApplyBiquadFilterFloat(output_buffer, input_buffer, biquad.b, biquad.a, *state_, + processor.sample_count); + } else { + ApplyBiquadFilterInt(output_buffer, input_buffer, biquad.b, biquad.a, *state_, + processor.sample_count); + } +} + +bool BiquadFilterCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/biquad_filter.h b/src/audio_core/renderer/command/effect/biquad_filter.h new file mode 100644 index 0000000000..4c9c42d29a --- /dev/null +++ b/src/audio_core/renderer/command/effect/biquad_filter.h @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "audio_core/renderer/voice/voice_info.h" +#include "audio_core/renderer/voice/voice_state.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for applying a biquad filter to the input mix buffer, saving the results to + * the output mix buffer. + */ +struct BiquadFilterCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer index + s16 input; + /// Output mix buffer index + s16 output; + /// Input parameters for biquad + VoiceInfo::BiquadFilterParameter biquad; + /// Biquad state, updated each call + CpuAddr state; + /// If true, reset the state + bool needs_init; + /// If true, use float processing rather than int + bool use_float_processing; +}; + +/** + * Biquad filter float implementation. + * + * @param output - Output container for filtered samples. + * @param input - Input container for samples to be filtered. + * @param b - Feedforward coefficients. + * @param a - Feedback coefficients. + * @param state - State to track previous samples. + * @param sample_count - Number of samples to process. + */ +void ApplyBiquadFilterFloat(std::span output, std::span input, + std::array& b, std::array& a, + VoiceState::BiquadFilterState& state, const u32 sample_count); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/capture.cpp b/src/audio_core/renderer/command/effect/capture.cpp new file mode 100644 index 0000000000..042fd286e4 --- /dev/null +++ b/src/audio_core/renderer/command/effect/capture.cpp @@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/effect/capture.h" +#include "audio_core/renderer/effect/aux_.h" +#include "core/memory.h" + +namespace AudioCore::AudioRenderer { +/** + * Reset an AuxBuffer. + * + * @param memory - Core memory for writing. + * @param aux_info - Memory address pointing to the AuxInfo to reset. + */ +static void ResetAuxBufferDsp(Core::Memory::Memory& memory, const CpuAddr aux_info) { + if (aux_info == 0) { + LOG_ERROR(Service_Audio, "Aux info is 0!"); + return; + } + + memory.Write32(VAddr(aux_info + offsetof(AuxInfo::AuxInfoDsp, read_offset)), 0); + memory.Write32(VAddr(aux_info + offsetof(AuxInfo::AuxInfoDsp, write_offset)), 0); + memory.Write32(VAddr(aux_info + offsetof(AuxInfo::AuxInfoDsp, total_sample_count)), 0); +} + +/** + * Write the given input mix buffer to the memory at send_buffer, and update send_info_ if + * update_count is set, to notify the game that an update happened. + * + * @param memory - Core memory for writing. + * @param send_info_ - Header information for where to write the mix buffer. + * @param send_buffer - Memory address to write the mix buffer to. + * @param count_max - Maximum number of samples in the receiving buffer. + * @param input - Input mix buffer to write. + * @param write_count_ - Number of samples to write. + * @param write_offset - Current offset to begin writing the receiving buffer at. + * @param update_count - If non-zero, send_info_ will be updated. + * @return Number of samples written. + */ +static u32 WriteAuxBufferDsp(Core::Memory::Memory& memory, const CpuAddr send_info_, + const CpuAddr send_buffer, u32 count_max, std::span input, + const u32 write_count_, const u32 write_offset, + const u32 update_count) { + if (write_count_ > count_max) { + LOG_ERROR(Service_Audio, + "write_count must be smaller than count_max! write_count {}, count_max {}", + write_count_, count_max); + return 0; + } + + if (send_info_ == 0) { + LOG_ERROR(Service_Audio, "send_info is 0!"); + return 0; + } + + if (input.empty()) { + LOG_ERROR(Service_Audio, "input buffer is empty!"); + return 0; + } + + if (send_buffer == 0) { + LOG_ERROR(Service_Audio, "send_buffer is 0!"); + return 0; + } + + if (count_max == 0) { + return 0; + } + + AuxInfo::AuxBufferInfo send_info{}; + memory.ReadBlockUnsafe(send_info_, &send_info, sizeof(AuxInfo::AuxBufferInfo)); + + u32 target_write_offset{send_info.dsp_info.write_offset + write_offset}; + if (target_write_offset > count_max || write_count_ == 0) { + return 0; + } + + u32 write_count{write_count_}; + u32 write_pos{0}; + while (write_count > 0) { + u32 to_write{std::min(count_max - target_write_offset, write_count)}; + + if (to_write > 0) { + memory.WriteBlockUnsafe(send_buffer + target_write_offset * sizeof(s32), + &input[write_pos], to_write * sizeof(s32)); + } + + target_write_offset = (target_write_offset + to_write) % count_max; + write_count -= to_write; + write_pos += to_write; + } + + if (update_count) { + const auto count_diff{send_info.dsp_info.total_sample_count - + send_info.cpu_info.total_sample_count}; + if (count_diff >= count_max) { + auto dsp_lost_count{send_info.dsp_info.lost_sample_count + update_count}; + if (dsp_lost_count - send_info.cpu_info.lost_sample_count < + send_info.dsp_info.lost_sample_count - send_info.cpu_info.lost_sample_count) { + dsp_lost_count = send_info.cpu_info.lost_sample_count - 1; + } + send_info.dsp_info.lost_sample_count = dsp_lost_count; + } + + send_info.dsp_info.write_offset = + (send_info.dsp_info.write_offset + update_count + count_max) % count_max; + + auto new_sample_count{send_info.dsp_info.total_sample_count + update_count}; + if (new_sample_count - send_info.cpu_info.total_sample_count < count_diff) { + new_sample_count = send_info.cpu_info.total_sample_count - 1; + } + send_info.dsp_info.total_sample_count = new_sample_count; + } + + memory.WriteBlockUnsafe(send_info_, &send_info, sizeof(AuxInfo::AuxBufferInfo)); + + return write_count_; +} + +void CaptureCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("CaptureCommand\n\tenabled {} input {:02X} output {:02X}", effect_enabled, + input, output); +} + +void CaptureCommand::Process(const ADSP::CommandListProcessor& processor) { + if (effect_enabled) { + auto input_buffer{ + processor.mix_buffers.subspan(input * processor.sample_count, processor.sample_count)}; + WriteAuxBufferDsp(*processor.memory, send_buffer_info, send_buffer, count_max, input_buffer, + processor.sample_count, write_offset, update_count); + } else { + ResetAuxBufferDsp(*processor.memory, send_buffer_info); + } +} + +bool CaptureCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/capture.h b/src/audio_core/renderer/command/effect/capture.h new file mode 100644 index 0000000000..8670acb24e --- /dev/null +++ b/src/audio_core/renderer/command/effect/capture.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for capturing a mix buffer. That is, writing it back to a given game memory + * address. + */ +struct CaptureCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer index + s16 input; + /// Output mix buffer index + s16 output; + /// Meta info for writing + CpuAddr send_buffer_info; + /// Game memory write buffer + CpuAddr send_buffer; + /// Max samples to read/write + u32 count_max; + /// Current read/write offset + u32 write_offset; + /// Number of samples to update per call + u32 update_count; + /// is this effect enabled? + bool effect_enabled; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/compressor.cpp b/src/audio_core/renderer/command/effect/compressor.cpp new file mode 100644 index 0000000000..2ebc140f13 --- /dev/null +++ b/src/audio_core/renderer/command/effect/compressor.cpp @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/effect/compressor.h" +#include "audio_core/renderer/effect/compressor.h" + +namespace AudioCore::AudioRenderer { + +static void SetCompressorEffectParameter(CompressorInfo::ParameterVersion2& params, + CompressorInfo::State& state) { + const auto ratio{1.0f / params.compressor_ratio}; + auto makeup_gain{0.0f}; + if (params.makeup_gain_enabled) { + makeup_gain = (params.threshold * 0.5f) * (ratio - 1.0f) - 3.0f; + } + state.makeup_gain = makeup_gain; + state.unk_18 = params.unk_28; + + const auto a{(params.out_gain + makeup_gain) / 20.0f * 3.3219f}; + const auto b{(a - std::trunc(a)) * 0.69315f}; + const auto c{std::pow(2.0f, b)}; + + state.unk_0C = (1.0f - ratio) / 6.0f; + state.unk_14 = params.threshold + 1.5f; + state.unk_10 = params.threshold - 1.5f; + state.unk_20 = c; +} + +static void InitializeCompressorEffect(CompressorInfo::ParameterVersion2& params, + CompressorInfo::State& state) { + std::memset(&state, 0, sizeof(CompressorInfo::State)); + + state.unk_00 = 0; + state.unk_04 = 1.0f; + state.unk_08 = 1.0f; + + SetCompressorEffectParameter(params, state); +} + +static void ApplyCompressorEffect(CompressorInfo::ParameterVersion2& params, + CompressorInfo::State& state, bool enabled, + std::vector> input_buffers, + std::vector> output_buffers, u32 sample_count) { + if (enabled) { + auto state_00{state.unk_00}; + auto state_04{state.unk_04}; + auto state_08{state.unk_08}; + auto state_18{state.unk_18}; + + for (u32 i = 0; i < sample_count; i++) { + auto a{0.0f}; + for (s16 channel = 0; channel < params.channel_count; channel++) { + const auto input_sample{Common::FixedPoint<49, 15>(input_buffers[channel][i])}; + a += (input_sample * input_sample).to_float(); + } + + state_00 += params.unk_24 * ((a / params.channel_count) - state.unk_00); + + auto b{-100.0f}; + auto c{0.0f}; + if (state_00 >= 1.0e-10) { + b = std::log10(state_00) * 10.0f; + c = 1.0f; + } + + if (b >= state.unk_10) { + const auto d{b >= state.unk_14 + ? ((1.0f / params.compressor_ratio) - 1.0f) * + (b - params.threshold) + : (b - state.unk_10) * (b - state.unk_10) * -state.unk_0C}; + const auto e{d / 20.0f * 3.3219f}; + const auto f{(e - std::trunc(e)) * 0.69315f}; + c = std::pow(2.0f, f); + } + + state_18 = params.unk_28; + auto tmp{c}; + if ((state_04 - c) <= 0.08f) { + state_18 = params.unk_2C; + if (((state_04 - c) >= -0.08f) && (std::abs(state_08 - c) >= 0.001f)) { + tmp = state_04; + } + } + + state_04 = tmp; + state_08 += (c - state_08) * state_18; + + for (s16 channel = 0; channel < params.channel_count; channel++) { + output_buffers[channel][i] = static_cast( + static_cast(input_buffers[channel][i]) * state_08 * state.unk_20); + } + } + + state.unk_00 = state_00; + state.unk_04 = state_04; + state.unk_08 = state_08; + state.unk_18 = state_18; + } else { + for (s16 channel = 0; channel < params.channel_count; channel++) { + if (params.inputs[channel] != params.outputs[channel]) { + std::memcpy((char*)output_buffers[channel].data(), + (char*)input_buffers[channel].data(), + output_buffers[channel].size_bytes()); + } + } + } +} + +void CompressorCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("CompressorCommand\n\tenabled {} \n\tinputs: ", effect_enabled); + for (s16 i = 0; i < parameter.channel_count; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n\toutputs: "; + for (s16 i = 0; i < parameter.channel_count; i++) { + string += fmt::format("{:02X}, ", outputs[i]); + } + string += "\n"; +} + +void CompressorCommand::Process(const ADSP::CommandListProcessor& processor) { + std::vector> input_buffers(parameter.channel_count); + std::vector> output_buffers(parameter.channel_count); + + for (s16 i = 0; i < parameter.channel_count; i++) { + input_buffers[i] = processor.mix_buffers.subspan(inputs[i] * processor.sample_count, + processor.sample_count); + output_buffers[i] = processor.mix_buffers.subspan(outputs[i] * processor.sample_count, + processor.sample_count); + } + + auto state_{reinterpret_cast(state)}; + + if (effect_enabled) { + if (parameter.state == CompressorInfo::ParameterState::Updating) { + SetCompressorEffectParameter(parameter, *state_); + } else if (parameter.state == CompressorInfo::ParameterState::Initialized) { + InitializeCompressorEffect(parameter, *state_); + } + } + + ApplyCompressorEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers, + processor.sample_count); +} + +bool CompressorCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/compressor.h b/src/audio_core/renderer/command/effect/compressor.h new file mode 100644 index 0000000000..f8e96cb43f --- /dev/null +++ b/src/audio_core/renderer/command/effect/compressor.h @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/command/icommand.h" +#include "audio_core/renderer/effect/compressor.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for limiting volume between a high and low threshold. + * Version 1. + */ +struct CompressorCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer offsets for each channel + std::array inputs; + /// Output mix buffer offsets for each channel + std::array outputs; + /// Input parameters + CompressorInfo::ParameterVersion2 parameter; + /// State, updated each call + CpuAddr state; + /// Game-supplied workbuffer (Unused) + CpuAddr workbuffer; + /// Is this effect enabled? + bool effect_enabled; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/delay.cpp b/src/audio_core/renderer/command/effect/delay.cpp new file mode 100644 index 0000000000..a4e408d403 --- /dev/null +++ b/src/audio_core/renderer/command/effect/delay.cpp @@ -0,0 +1,238 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/effect/delay.h" + +namespace AudioCore::AudioRenderer { +/** + * Update the DelayInfo state according to the given parameters. + * + * @param params - Input parameters to update the state. + * @param state - State to be updated. + */ +static void SetDelayEffectParameter(const DelayInfo::ParameterVersion1& params, + DelayInfo::State& state) { + auto channel_spread{params.channel_spread}; + state.feedback_gain = params.feedback_gain * 0.97998046875f; + state.delay_feedback_gain = state.feedback_gain * (1.0f - channel_spread); + if (params.channel_count == 4 || params.channel_count == 6) { + channel_spread >>= 1; + } + state.delay_feedback_cross_gain = channel_spread * state.feedback_gain; + state.lowpass_feedback_gain = params.lowpass_amount * 0.949951171875f; + state.lowpass_gain = 1.0f - state.lowpass_feedback_gain; +} + +/** + * Initialize a new DelayInfo state according to the given parameters. + * + * @param params - Input parameters to update the state. + * @param state - State to be updated. + * @param workbuffer - Game-supplied memory for the state. (Unused) + */ +static void InitializeDelayEffect(const DelayInfo::ParameterVersion1& params, + DelayInfo::State& state, + [[maybe_unused]] const CpuAddr workbuffer) { + state = {}; + + for (u32 channel = 0; channel < params.channel_count; channel++) { + Common::FixedPoint<32, 32> sample_count_max{0.064f}; + sample_count_max *= params.sample_rate.to_int_floor() * params.delay_time_max; + + Common::FixedPoint<18, 14> delay_time{params.delay_time}; + delay_time *= params.sample_rate / 1000; + Common::FixedPoint<32, 32> sample_count{delay_time}; + + if (sample_count > sample_count_max) { + sample_count = sample_count_max; + } + + state.delay_lines[channel].sample_count_max = sample_count_max.to_int_floor(); + state.delay_lines[channel].sample_count = sample_count.to_int_floor(); + state.delay_lines[channel].buffer.resize(state.delay_lines[channel].sample_count, 0); + if (state.delay_lines[channel].buffer.size() == 0) { + state.delay_lines[channel].buffer.push_back(0); + } + state.delay_lines[channel].buffer_pos = 0; + state.delay_lines[channel].decay_rate = 1.0f; + } + + SetDelayEffectParameter(params, state); +} + +/** + * Delay effect impl, according to the parameters and current state, on the input mix buffers, + * saving the results to the output mix buffers. + * + * @tparam NumChannels - Number of channels to process. 1-6. + * @param params - Input parameters to use. + * @param state - State to use, must be initialized (see InitializeDelayEffect). + * @param inputs - Input mix buffers to performan the delay on. + * @param outputs - Output mix buffers to receive the delayed samples. + * @param sample_count - Number of samples to process. + */ +template +static void ApplyDelay(const DelayInfo::ParameterVersion1& params, DelayInfo::State& state, + std::vector>& inputs, + std::vector>& outputs, const u32 sample_count) { + for (u32 sample_index = 0; sample_index < sample_count; sample_index++) { + std::array, NumChannels> input_samples{}; + for (u32 channel = 0; channel < NumChannels; channel++) { + input_samples[channel] = inputs[channel][sample_index] * 64; + } + + std::array, NumChannels> delay_samples{}; + for (u32 channel = 0; channel < NumChannels; channel++) { + delay_samples[channel] = state.delay_lines[channel].Read(); + } + + // clang-format off + std::array, NumChannels>, NumChannels> matrix{}; + if constexpr (NumChannels == 1) { + matrix = {{ + {state.feedback_gain}, + }}; + } else if constexpr (NumChannels == 2) { + matrix = {{ + {state.delay_feedback_gain, state.delay_feedback_cross_gain}, + {state.delay_feedback_cross_gain, state.delay_feedback_gain}, + }}; + } else if constexpr (NumChannels == 4) { + matrix = {{ + {state.delay_feedback_gain, state.delay_feedback_cross_gain, state.delay_feedback_cross_gain, 0.0f}, + {state.delay_feedback_cross_gain, state.delay_feedback_gain, 0.0f, state.delay_feedback_cross_gain}, + {state.delay_feedback_cross_gain, 0.0f, state.delay_feedback_gain, state.delay_feedback_cross_gain}, + {0.0f, state.delay_feedback_cross_gain, state.delay_feedback_cross_gain, state.delay_feedback_gain}, + }}; + } else if constexpr (NumChannels == 6) { + matrix = {{ + {state.delay_feedback_gain, 0.0f, state.delay_feedback_cross_gain, 0.0f, state.delay_feedback_cross_gain, 0.0f}, + {0.0f, state.delay_feedback_gain, state.delay_feedback_cross_gain, 0.0f, 0.0f, state.delay_feedback_cross_gain}, + {state.delay_feedback_cross_gain, state.delay_feedback_cross_gain, state.delay_feedback_gain, 0.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 0.0f, params.feedback_gain, 0.0f, 0.0f}, + {state.delay_feedback_cross_gain, 0.0f, 0.0f, 0.0f, state.delay_feedback_gain, state.delay_feedback_cross_gain}, + {0.0f, state.delay_feedback_cross_gain, 0.0f, 0.0f, state.delay_feedback_cross_gain, state.delay_feedback_gain}, + }}; + } + // clang-format on + + std::array, NumChannels> gained_samples{}; + for (u32 channel = 0; channel < NumChannels; channel++) { + Common::FixedPoint<50, 14> delay{}; + for (u32 j = 0; j < NumChannels; j++) { + delay += delay_samples[j] * matrix[j][channel]; + } + gained_samples[channel] = input_samples[channel] * params.in_gain + delay; + } + + for (u32 channel = 0; channel < NumChannels; channel++) { + state.lowpass_z[channel] = gained_samples[channel] * state.lowpass_gain + + state.lowpass_z[channel] * state.lowpass_feedback_gain; + state.delay_lines[channel].Write(state.lowpass_z[channel]); + } + + for (u32 channel = 0; channel < NumChannels; channel++) { + outputs[channel][sample_index] = (input_samples[channel] * params.dry_gain + + delay_samples[channel] * params.wet_gain) + .to_int_floor() / + 64; + } + } +} + +/** + * Apply a delay effect if enabled, according to the parameters and current state, on the input mix + * buffers, saving the results to the output mix buffers. + * + * @param params - Input parameters to use. + * @param state - State to use, must be initialized (see InitializeDelayEffect). + * @param enabled - If enabled, delay will be applied, otherwise input is copied to output. + * @param inputs - Input mix buffers to performan the delay on. + * @param outputs - Output mix buffers to receive the delayed samples. + * @param sample_count - Number of samples to process. + */ +static void ApplyDelayEffect(const DelayInfo::ParameterVersion1& params, DelayInfo::State& state, + const bool enabled, std::vector>& inputs, + std::vector>& outputs, const u32 sample_count) { + + if (!IsChannelCountValid(params.channel_count)) { + LOG_ERROR(Service_Audio, "Invalid delay channels {}", params.channel_count); + return; + } + + if (enabled) { + switch (params.channel_count) { + case 1: + ApplyDelay<1>(params, state, inputs, outputs, sample_count); + break; + case 2: + ApplyDelay<2>(params, state, inputs, outputs, sample_count); + break; + case 4: + ApplyDelay<4>(params, state, inputs, outputs, sample_count); + break; + case 6: + ApplyDelay<6>(params, state, inputs, outputs, sample_count); + break; + default: + for (u32 channel = 0; channel < params.channel_count; channel++) { + if (inputs[channel].data() != outputs[channel].data()) { + std::memcpy(outputs[channel].data(), inputs[channel].data(), + sample_count * sizeof(s32)); + } + } + break; + } + } else { + for (u32 channel = 0; channel < params.channel_count; channel++) { + if (inputs[channel].data() != outputs[channel].data()) { + std::memcpy(outputs[channel].data(), inputs[channel].data(), + sample_count * sizeof(s32)); + } + } + } +} + +void DelayCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("DelayCommand\n\tenabled {} \n\tinputs: ", effect_enabled); + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n\toutputs: "; + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", outputs[i]); + } + string += "\n"; +} + +void DelayCommand::Process(const ADSP::CommandListProcessor& processor) { + std::vector> input_buffers(parameter.channel_count); + std::vector> output_buffers(parameter.channel_count); + + for (s16 i = 0; i < parameter.channel_count; i++) { + input_buffers[i] = processor.mix_buffers.subspan(inputs[i] * processor.sample_count, + processor.sample_count); + output_buffers[i] = processor.mix_buffers.subspan(outputs[i] * processor.sample_count, + processor.sample_count); + } + + auto state_{reinterpret_cast(state)}; + + if (effect_enabled) { + if (parameter.state == DelayInfo::ParameterState::Updating) { + SetDelayEffectParameter(parameter, *state_); + } else if (parameter.state == DelayInfo::ParameterState::Initialized) { + InitializeDelayEffect(parameter, *state_, workbuffer); + } + } + ApplyDelayEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers, + processor.sample_count); +} + +bool DelayCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/delay.h b/src/audio_core/renderer/command/effect/delay.h new file mode 100644 index 0000000000..b7a15ae6b1 --- /dev/null +++ b/src/audio_core/renderer/command/effect/delay.h @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/command/icommand.h" +#include "audio_core/renderer/effect/delay.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for a delay effect. Delays inputs mix buffers according to the parameters + * and state, outputs receives the delayed samples. + */ +struct DelayCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer offsets for each channel + std::array inputs; + /// Output mix buffer offsets for each channel + std::array outputs; + /// Input parameters + DelayInfo::ParameterVersion1 parameter; + /// State, updated each call + CpuAddr state; + /// Game-supplied workbuffer (Unused) + CpuAddr workbuffer; + /// Is this effect enabled? + bool effect_enabled; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/i3dl2_reverb.cpp b/src/audio_core/renderer/command/effect/i3dl2_reverb.cpp new file mode 100644 index 0000000000..c4bf3943a1 --- /dev/null +++ b/src/audio_core/renderer/command/effect/i3dl2_reverb.cpp @@ -0,0 +1,437 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/effect/i3dl2_reverb.h" + +namespace AudioCore::AudioRenderer { + +constexpr std::array MinDelayLineTimes{ + 5.0f, + 6.0f, + 13.0f, + 14.0f, +}; +constexpr std::array MaxDelayLineTimes{ + 45.7042007446f, + 82.7817001343f, + 149.938293457f, + 271.575805664f, +}; +constexpr std::array Decay0MaxDelayLineTimes{17.0f, 13.0f, + 9.0f, 7.0f}; +constexpr std::array Decay1MaxDelayLineTimes{19.0f, 11.0f, + 10.0f, 6.0f}; +constexpr std::array EarlyTapTimes{ + 0.0171360000968f, + 0.0591540001333f, + 0.161733001471f, + 0.390186011791f, + 0.425262004137f, + 0.455410987139f, + 0.689737021923f, + 0.74590998888f, + 0.833844006062f, + 0.859502017498f, + 0.0f, + 0.0750240013003f, + 0.168788000941f, + 0.299901008606f, + 0.337442994118f, + 0.371903002262f, + 0.599011003971f, + 0.716741025448f, + 0.817858994007f, + 0.85166400671f, +}; + +constexpr std::array EarlyGains{ + 0.67096f, 0.61027f, 1.0f, 0.3568f, 0.68361f, 0.65978f, 0.51939f, + 0.24712f, 0.45945f, 0.45021f, 0.64196f, 0.54879f, 0.92925f, 0.3827f, + 0.72867f, 0.69794f, 0.5464f, 0.24563f, 0.45214f, 0.44042f}; + +/** + * Update the I3dl2ReverbInfo state according to the given parameters. + * + * @param params - Input parameters to update the state. + * @param state - State to be updated. + * @param reset - If enabled, the state buffers will be reset. Only set this on initialize. + */ +static void UpdateI3dl2ReverbEffectParameter(const I3dl2ReverbInfo::ParameterVersion1& params, + I3dl2ReverbInfo::State& state, const bool reset) { + const auto pow_10 = [](f32 val) -> f32 { + return (val >= 0.0f) ? 1.0f : (val <= -5.3f) ? 0.0f : std::pow(10.0f, val); + }; + const auto sin = [](f32 degrees) -> f32 { + return std::sin(degrees * std::numbers::pi_v / 180.0f); + }; + const auto cos = [](f32 degrees) -> f32 { + return std::cos(degrees * std::numbers::pi_v / 180.0f); + }; + + Common::FixedPoint<50, 14> delay{static_cast(params.sample_rate) / 1000.0f}; + + state.dry_gain = params.dry_gain; + Common::FixedPoint<50, 14> early_gain{ + std::min(params.room_gain + params.reflection_gain, 5000.0f) / 2000.0f}; + state.early_gain = pow_10(early_gain.to_float()); + Common::FixedPoint<50, 14> late_gain{std::min(params.room_gain + params.reverb_gain, 5000.0f) / + 2000.0f}; + state.late_gain = pow_10(late_gain.to_float()); + + Common::FixedPoint<50, 14> hf_gain{pow_10(params.room_HF_gain / 2000.0f)}; + if (hf_gain >= 1.0f) { + state.lowpass_1 = 0.0f; + state.lowpass_2 = 1.0f; + } else { + const auto reference_hf{(params.reference_HF * 256.0f) / + static_cast(params.sample_rate)}; + const Common::FixedPoint<50, 14> a{1.0f - hf_gain.to_float()}; + const Common::FixedPoint<50, 14> b{2.0f + (-cos(reference_hf) * (hf_gain * 2.0f))}; + const Common::FixedPoint<50, 14> c{ + std::sqrt(std::pow(b.to_float(), 2.0f) + (std::pow(a.to_float(), 2.0f) * -4.0f))}; + + state.lowpass_1 = std::min(((b - c) / (a * 2.0f)).to_float(), 0.99723f); + state.lowpass_2 = 1.0f - state.lowpass_1; + } + + state.early_to_late_taps = + (((params.reflection_delay + params.late_reverb_delay_time) * 1000.0f) * delay).to_int(); + state.last_reverb_echo = params.late_reverb_diffusion * 0.6f * 0.01f; + + for (u32 i = 0; i < I3dl2ReverbInfo::MaxDelayLines; i++) { + auto curr_delay{ + ((MinDelayLineTimes[i] + (params.late_reverb_density / 100.0f) * + (MaxDelayLineTimes[i] - MinDelayLineTimes[i])) * + delay) + .to_int()}; + state.fdn_delay_lines[i].SetDelay(curr_delay); + + const auto a{ + (static_cast(state.fdn_delay_lines[i].delay + state.decay_delay_lines0[i].delay + + state.decay_delay_lines1[i].delay) * + -60.0f) / + (params.late_reverb_decay_time * static_cast(params.sample_rate))}; + const auto b{a / params.late_reverb_HF_decay_ratio}; + const auto c{ + cos(((params.reference_HF * 0.5f) * 128.0f) / static_cast(params.sample_rate)) / + sin(((params.reference_HF * 0.5f) * 128.0f) / static_cast(params.sample_rate))}; + const auto d{pow_10((b - a) / 40.0f)}; + const auto e{pow_10((b + a) / 40.0f) * 0.7071f}; + + state.lowpass_coeff[i][0] = ((c * d + 1.0f) * e) / (c + d); + state.lowpass_coeff[i][1] = ((1.0f - (c * d)) * e) / (c + d); + state.lowpass_coeff[i][2] = (c - d) / (c + d); + + state.decay_delay_lines0[i].wet_gain = state.last_reverb_echo; + state.decay_delay_lines1[i].wet_gain = state.last_reverb_echo * -0.9f; + } + + if (reset) { + state.shelf_filter.fill(0.0f); + state.lowpass_0 = 0.0f; + for (u32 i = 0; i < I3dl2ReverbInfo::MaxDelayLines; i++) { + std::ranges::fill(state.fdn_delay_lines[i].buffer, 0); + std::ranges::fill(state.decay_delay_lines0[i].buffer, 0); + std::ranges::fill(state.decay_delay_lines1[i].buffer, 0); + } + std::ranges::fill(state.center_delay_line.buffer, 0); + std::ranges::fill(state.early_delay_line.buffer, 0); + } + + const auto reflection_time{(params.late_reverb_delay_time * 0.9998f + 0.02f) * 1000.0f}; + const auto reflection_delay{params.reflection_delay * 1000.0f}; + for (u32 i = 0; i < I3dl2ReverbInfo::MaxDelayTaps; i++) { + auto length{((reflection_delay + reflection_time * EarlyTapTimes[i]) * delay).to_int()}; + if (length >= state.early_delay_line.max_delay) { + length = state.early_delay_line.max_delay; + } + state.early_tap_steps[i] = length; + } +} + +/** + * Initialize a new I3dl2ReverbInfo state according to the given parameters. + * + * @param params - Input parameters to update the state. + * @param state - State to be updated. + * @param workbuffer - Game-supplied memory for the state. (Unused) + */ +static void InitializeI3dl2ReverbEffect(const I3dl2ReverbInfo::ParameterVersion1& params, + I3dl2ReverbInfo::State& state, const CpuAddr workbuffer) { + state = {}; + Common::FixedPoint<50, 14> delay{static_cast(params.sample_rate) / 1000}; + + for (u32 i = 0; i < I3dl2ReverbInfo::MaxDelayLines; i++) { + auto fdn_delay_time{(MaxDelayLineTimes[i] * delay).to_uint_floor()}; + state.fdn_delay_lines[i].Initialize(fdn_delay_time); + + auto decay0_delay_time{(Decay0MaxDelayLineTimes[i] * delay).to_uint_floor()}; + state.decay_delay_lines0[i].Initialize(decay0_delay_time); + + auto decay1_delay_time{(Decay1MaxDelayLineTimes[i] * delay).to_uint_floor()}; + state.decay_delay_lines1[i].Initialize(decay1_delay_time); + } + + const auto center_delay_time{(5 * delay).to_uint_floor()}; + state.center_delay_line.Initialize(center_delay_time); + + const auto early_delay_time{(400 * delay).to_uint_floor()}; + state.early_delay_line.Initialize(early_delay_time); + + UpdateI3dl2ReverbEffectParameter(params, state, true); +} + +/** + * Pass-through the effect, copying input to output directly, with no reverb applied. + * + * @param inputs - Array of input mix buffers to copy. + * @param outputs - Array of output mix buffers to receive copy. + * @param channel_count - Number of channels in inputs and outputs. + * @param sample_count - Number of samples within each channel (unused). + */ +static void ApplyI3dl2ReverbEffectBypass(std::span> inputs, + std::span> outputs, const u32 channel_count, + [[maybe_unused]] const u32 sample_count) { + for (u32 i = 0; i < channel_count; i++) { + if (inputs[i].data() != outputs[i].data()) { + std::memcpy(outputs[i].data(), inputs[i].data(), outputs[i].size_bytes()); + } + } +} + +/** + * Tick the delay lines, reading and returning their current output, and writing a new decaying + * sample (mix). + * + * @param decay0 - The first decay line. + * @param decay1 - The second decay line. + * @param fdn - Feedback delay network. + * @param mix - The new calculated sample to be written and decayed. + * @return The next delayed and decayed sample. + */ +static Common::FixedPoint<50, 14> Axfx2AllPassTick(I3dl2ReverbInfo::I3dl2DelayLine& decay0, + I3dl2ReverbInfo::I3dl2DelayLine& decay1, + I3dl2ReverbInfo::I3dl2DelayLine& fdn, + const Common::FixedPoint<50, 14> mix) { + auto val{decay0.Read()}; + auto mixed{mix - (val * decay0.wet_gain)}; + auto out{decay0.Tick(mixed) + (mixed * decay0.wet_gain)}; + + val = decay1.Read(); + mixed = out - (val * decay1.wet_gain); + out = decay1.Tick(mixed) + (mixed * decay1.wet_gain); + + fdn.Tick(out); + return out; +} + +/** + * Impl. Apply a I3DL2 reverb according to the current state, on the input mix buffers, + * saving the results to the output mix buffers. + * + * @tparam NumChannels - Number of channels to process. 1-6. + Inputs/outputs should have this many buffers. + * @param state - State to use, must be initialized (see InitializeI3dl2ReverbEffect). + * @param inputs - Input mix buffers to perform the reverb on. + * @param outputs - Output mix buffers to receive the reverbed samples. + * @param sample_count - Number of samples to process. + */ +template +static void ApplyI3dl2ReverbEffect(I3dl2ReverbInfo::State& state, + std::span> inputs, + std::span> outputs, const u32 sample_count) { + constexpr std::array OutTapIndexes1Ch{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + constexpr std::array OutTapIndexes2Ch{ + 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, + }; + constexpr std::array OutTapIndexes4Ch{ + 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 3, 3, 3, + }; + constexpr std::array OutTapIndexes6Ch{ + 2, 0, 0, 1, 1, 1, 1, 4, 4, 4, 1, 1, 1, 0, 0, 0, 0, 5, 5, 5, + }; + + std::span tap_indexes{}; + if constexpr (NumChannels == 1) { + tap_indexes = OutTapIndexes1Ch; + } else if constexpr (NumChannels == 2) { + tap_indexes = OutTapIndexes2Ch; + } else if constexpr (NumChannels == 4) { + tap_indexes = OutTapIndexes4Ch; + } else if constexpr (NumChannels == 6) { + tap_indexes = OutTapIndexes6Ch; + } + + for (u32 sample_index = 0; sample_index < sample_count; sample_index++) { + Common::FixedPoint<50, 14> early_to_late_tap{ + state.early_delay_line.TapOut(state.early_to_late_taps)}; + std::array, NumChannels> output_samples{}; + + for (u32 early_tap = 0; early_tap < I3dl2ReverbInfo::MaxDelayTaps; early_tap++) { + output_samples[tap_indexes[early_tap]] += + state.early_delay_line.TapOut(state.early_tap_steps[early_tap]) * + EarlyGains[early_tap]; + if constexpr (NumChannels == 6) { + output_samples[static_cast(Channels::LFE)] += + state.early_delay_line.TapOut(state.early_tap_steps[early_tap]) * + EarlyGains[early_tap]; + } + } + + Common::FixedPoint<50, 14> current_sample{}; + for (u32 channel = 0; channel < NumChannels; channel++) { + current_sample += inputs[channel][sample_index]; + } + + state.lowpass_0 = + (current_sample * state.lowpass_2 + state.lowpass_0 * state.lowpass_1).to_float(); + state.early_delay_line.Tick(state.lowpass_0); + + for (u32 channel = 0; channel < NumChannels; channel++) { + output_samples[channel] *= state.early_gain; + } + + std::array, I3dl2ReverbInfo::MaxDelayLines> filtered_samples{}; + for (u32 delay_line = 0; delay_line < I3dl2ReverbInfo::MaxDelayLines; delay_line++) { + filtered_samples[delay_line] = + state.fdn_delay_lines[delay_line].Read() * state.lowpass_coeff[delay_line][0] + + state.shelf_filter[delay_line]; + state.shelf_filter[delay_line] = + (filtered_samples[delay_line] * state.lowpass_coeff[delay_line][2] + + state.fdn_delay_lines[delay_line].Read() * state.lowpass_coeff[delay_line][1]) + .to_float(); + } + + const std::array, I3dl2ReverbInfo::MaxDelayLines> mix_matrix{ + filtered_samples[1] + filtered_samples[2] + early_to_late_tap * state.late_gain, + -filtered_samples[0] - filtered_samples[3] + early_to_late_tap * state.late_gain, + filtered_samples[0] - filtered_samples[3] + early_to_late_tap * state.late_gain, + filtered_samples[1] - filtered_samples[2] + early_to_late_tap * state.late_gain, + }; + + std::array, I3dl2ReverbInfo::MaxDelayLines> allpass_samples{}; + for (u32 delay_line = 0; delay_line < I3dl2ReverbInfo::MaxDelayLines; delay_line++) { + allpass_samples[delay_line] = Axfx2AllPassTick( + state.decay_delay_lines0[delay_line], state.decay_delay_lines1[delay_line], + state.fdn_delay_lines[delay_line], mix_matrix[delay_line]); + } + + if constexpr (NumChannels == 6) { + const std::array, MaxChannels> allpass_outputs{ + allpass_samples[0], allpass_samples[1], allpass_samples[2] - allpass_samples[3], + allpass_samples[3], allpass_samples[2], allpass_samples[3], + }; + + for (u32 channel = 0; channel < NumChannels; channel++) { + Common::FixedPoint<50, 14> allpass{}; + + if (channel == static_cast(Channels::Center)) { + allpass = state.center_delay_line.Tick(allpass_outputs[channel] * 0.5f); + } else { + allpass = allpass_outputs[channel]; + } + + auto out_sample{output_samples[channel] + allpass + + state.dry_gain * static_cast(inputs[channel][sample_index])}; + + outputs[channel][sample_index] = + static_cast(std::clamp(out_sample.to_float(), -8388600.0f, 8388600.0f)); + } + } else { + for (u32 channel = 0; channel < NumChannels; channel++) { + auto out_sample{output_samples[channel] + allpass_samples[channel] + + state.dry_gain * static_cast(inputs[channel][sample_index])}; + outputs[channel][sample_index] = + static_cast(std::clamp(out_sample.to_float(), -8388600.0f, 8388600.0f)); + } + } + } +} + +/** + * Apply a I3DL2 reverb if enabled, according to the current state, on the input mix buffers, + * saving the results to the output mix buffers. + * + * @param params - Input parameters to use. + * @param state - State to use, must be initialized (see InitializeI3dl2ReverbEffect). + * @param enabled - If enabled, delay will be applied, otherwise input is copied to output. + * @param inputs - Input mix buffers to performan the delay on. + * @param outputs - Output mix buffers to receive the delayed samples. + * @param sample_count - Number of samples to process. + */ +static void ApplyI3dl2ReverbEffect(const I3dl2ReverbInfo::ParameterVersion1& params, + I3dl2ReverbInfo::State& state, const bool enabled, + std::span> inputs, + std::span> outputs, const u32 sample_count) { + if (enabled) { + switch (params.channel_count) { + case 0: + return; + case 1: + ApplyI3dl2ReverbEffect<1>(state, inputs, outputs, sample_count); + break; + case 2: + ApplyI3dl2ReverbEffect<2>(state, inputs, outputs, sample_count); + break; + case 4: + ApplyI3dl2ReverbEffect<4>(state, inputs, outputs, sample_count); + break; + case 6: + ApplyI3dl2ReverbEffect<6>(state, inputs, outputs, sample_count); + break; + default: + ApplyI3dl2ReverbEffectBypass(inputs, outputs, params.channel_count, sample_count); + break; + } + } else { + ApplyI3dl2ReverbEffectBypass(inputs, outputs, params.channel_count, sample_count); + } +} + +void I3dl2ReverbCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("I3dl2ReverbCommand\n\tenabled {} \n\tinputs: ", effect_enabled); + for (u32 i = 0; i < parameter.channel_count; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n\toutputs: "; + for (u32 i = 0; i < parameter.channel_count; i++) { + string += fmt::format("{:02X}, ", outputs[i]); + } + string += "\n"; +} + +void I3dl2ReverbCommand::Process(const ADSP::CommandListProcessor& processor) { + std::vector> input_buffers(parameter.channel_count); + std::vector> output_buffers(parameter.channel_count); + + for (u32 i = 0; i < parameter.channel_count; i++) { + input_buffers[i] = processor.mix_buffers.subspan(inputs[i] * processor.sample_count, + processor.sample_count); + output_buffers[i] = processor.mix_buffers.subspan(outputs[i] * processor.sample_count, + processor.sample_count); + } + + auto state_{reinterpret_cast(state)}; + + if (effect_enabled) { + if (parameter.state == I3dl2ReverbInfo::ParameterState::Updating) { + UpdateI3dl2ReverbEffectParameter(parameter, *state_, false); + } else if (parameter.state == I3dl2ReverbInfo::ParameterState::Initialized) { + InitializeI3dl2ReverbEffect(parameter, *state_, workbuffer); + } + } + ApplyI3dl2ReverbEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers, + processor.sample_count); +} + +bool I3dl2ReverbCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/i3dl2_reverb.h b/src/audio_core/renderer/command/effect/i3dl2_reverb.h new file mode 100644 index 0000000000..243877056d --- /dev/null +++ b/src/audio_core/renderer/command/effect/i3dl2_reverb.h @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/command/icommand.h" +#include "audio_core/renderer/effect/i3dl2.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for a I3DL2Reverb effect. Apply a reverb to inputs mix buffer according to + * the I3DL2 spec, outputs receives the results. + */ +struct I3dl2ReverbCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer offsets for each channel + std::array inputs; + /// Output mix buffer offsets for each channel + std::array outputs; + /// Input parameters + I3dl2ReverbInfo::ParameterVersion1 parameter; + /// State, updated each call + CpuAddr state; + /// Game-supplied workbuffer (Unused) + CpuAddr workbuffer; + /// Is this effect enabled? + bool effect_enabled; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/light_limiter.cpp b/src/audio_core/renderer/command/effect/light_limiter.cpp new file mode 100644 index 0000000000..e8fb0e2fc7 --- /dev/null +++ b/src/audio_core/renderer/command/effect/light_limiter.cpp @@ -0,0 +1,222 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/effect/light_limiter.h" + +namespace AudioCore::AudioRenderer { +/** + * Update the LightLimiterInfo state according to the given parameters. + * A no-op. + * + * @param params - Input parameters to update the state. + * @param state - State to be updated. + */ +static void UpdateLightLimiterEffectParameter(const LightLimiterInfo::ParameterVersion2& params, + LightLimiterInfo::State& state) {} + +/** + * Initialize a new LightLimiterInfo state according to the given parameters. + * + * @param params - Input parameters to update the state. + * @param state - State to be updated. + * @param workbuffer - Game-supplied memory for the state. (Unused) + */ +static void InitializeLightLimiterEffect(const LightLimiterInfo::ParameterVersion2& params, + LightLimiterInfo::State& state, const CpuAddr workbuffer) { + state = {}; + state.samples_average.fill(0.0f); + state.compression_gain.fill(1.0f); + state.look_ahead_sample_offsets.fill(0); + for (u32 i = 0; i < params.channel_count; i++) { + state.look_ahead_sample_buffers[i].resize(params.look_ahead_samples_max, 0.0f); + } +} + +/** + * Apply a light limiter effect if enabled, according to the current state, on the input mix + * buffers, saving the results to the output mix buffers. + * + * @param params - Input parameters to use. + * @param state - State to use, must be initialized (see InitializeLightLimiterEffect). + * @param enabled - If enabled, limiter will be applied, otherwise input is copied to output. + * @param inputs - Input mix buffers to perform the limiter on. + * @param outputs - Output mix buffers to receive the limited samples. + * @param sample_count - Number of samples to process. + * @params statistics - Optional output statistics, only used with version 2. + */ +static void ApplyLightLimiterEffect(const LightLimiterInfo::ParameterVersion2& params, + LightLimiterInfo::State& state, const bool enabled, + std::vector>& inputs, + std::vector>& outputs, const u32 sample_count, + LightLimiterInfo::StatisticsInternal* statistics) { + constexpr s64 min{std::numeric_limits::min()}; + constexpr s64 max{std::numeric_limits::max()}; + + const auto recip_estimate = [](f64 a) -> f64 { + s32 q, s; + f64 r; + q = (s32)(a * 512.0); /* a in units of 1/512 rounded down */ + r = 1.0 / (((f64)q + 0.5) / 512.0); /* reciprocal r */ + s = (s32)(256.0 * r + 0.5); /* r in units of 1/256 rounded to nearest */ + return ((f64)s / 256.0); + }; + + if (enabled) { + if (statistics && params.statistics_reset_required) { + for (u32 i = 0; i < params.channel_count; i++) { + statistics->channel_compression_gain_min[i] = 1.0f; + statistics->channel_max_sample[i] = 0; + } + } + + for (u32 sample_index = 0; sample_index < sample_count; sample_index++) { + for (u32 channel = 0; channel < params.channel_count; channel++) { + auto sample{(Common::FixedPoint<49, 15>(inputs[channel][sample_index]) / + Common::FixedPoint<49, 15>::one) * + params.input_gain}; + auto abs_sample{sample}; + if (sample < 0.0f) { + abs_sample = -sample; + } + auto coeff{abs_sample > state.samples_average[channel] ? params.attack_coeff + : params.release_coeff}; + state.samples_average[channel] += + ((abs_sample - state.samples_average[channel]) * coeff).to_float(); + + // Reciprocal estimate + auto new_average_sample{Common::FixedPoint<49, 15>( + recip_estimate(state.samples_average[channel].to_double()))}; + if (params.processing_mode != LightLimiterInfo::ProcessingMode::Mode1) { + // Two Newton-Raphson steps + auto temp{2.0 - (state.samples_average[channel] * new_average_sample)}; + new_average_sample = 2.0 - (state.samples_average[channel] * temp); + } + + auto above_threshold{state.samples_average[channel] > params.threshold}; + auto attenuation{above_threshold ? params.threshold * new_average_sample : 1.0f}; + coeff = attenuation < state.compression_gain[channel] ? params.attack_coeff + : params.release_coeff; + state.compression_gain[channel] += + (attenuation - state.compression_gain[channel]) * coeff; + + auto lookahead_sample{ + state.look_ahead_sample_buffers[channel] + [state.look_ahead_sample_offsets[channel]]}; + + state.look_ahead_sample_buffers[channel][state.look_ahead_sample_offsets[channel]] = + sample; + state.look_ahead_sample_offsets[channel] = + (state.look_ahead_sample_offsets[channel] + 1) % params.look_ahead_samples_min; + + outputs[channel][sample_index] = static_cast( + std::clamp((lookahead_sample * state.compression_gain[channel] * + params.output_gain * Common::FixedPoint<49, 15>::one) + .to_long(), + min, max)); + + if (statistics) { + statistics->channel_max_sample[channel] = + std::max(statistics->channel_max_sample[channel], abs_sample.to_float()); + statistics->channel_compression_gain_min[channel] = + std::min(statistics->channel_compression_gain_min[channel], + state.compression_gain[channel].to_float()); + } + } + } + } else { + for (u32 i = 0; i < params.channel_count; i++) { + if (params.inputs[i] != params.outputs[i]) { + std::memcpy(outputs[i].data(), inputs[i].data(), outputs[i].size_bytes()); + } + } + } +} + +void LightLimiterVersion1Command::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("LightLimiterVersion1Command\n\tinputs: "); + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n\toutputs: "; + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", outputs[i]); + } + string += "\n"; +} + +void LightLimiterVersion1Command::Process(const ADSP::CommandListProcessor& processor) { + std::vector> input_buffers(parameter.channel_count); + std::vector> output_buffers(parameter.channel_count); + + for (u32 i = 0; i < parameter.channel_count; i++) { + input_buffers[i] = processor.mix_buffers.subspan(inputs[i] * processor.sample_count, + processor.sample_count); + output_buffers[i] = processor.mix_buffers.subspan(outputs[i] * processor.sample_count, + processor.sample_count); + } + + auto state_{reinterpret_cast(state)}; + + if (effect_enabled) { + if (parameter.state == LightLimiterInfo::ParameterState::Updating) { + UpdateLightLimiterEffectParameter(parameter, *state_); + } else if (parameter.state == LightLimiterInfo::ParameterState::Initialized) { + InitializeLightLimiterEffect(parameter, *state_, workbuffer); + } + } + + LightLimiterInfo::StatisticsInternal* statistics{nullptr}; + ApplyLightLimiterEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers, + processor.sample_count, statistics); +} + +bool LightLimiterVersion1Command::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +void LightLimiterVersion2Command::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("LightLimiterVersion2Command\n\tinputs: \n"); + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n\toutputs: "; + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", outputs[i]); + } + string += "\n"; +} + +void LightLimiterVersion2Command::Process(const ADSP::CommandListProcessor& processor) { + std::vector> input_buffers(parameter.channel_count); + std::vector> output_buffers(parameter.channel_count); + + for (u32 i = 0; i < parameter.channel_count; i++) { + input_buffers[i] = processor.mix_buffers.subspan(inputs[i] * processor.sample_count, + processor.sample_count); + output_buffers[i] = processor.mix_buffers.subspan(outputs[i] * processor.sample_count, + processor.sample_count); + } + + auto state_{reinterpret_cast(state)}; + + if (effect_enabled) { + if (parameter.state == LightLimiterInfo::ParameterState::Updating) { + UpdateLightLimiterEffectParameter(parameter, *state_); + } else if (parameter.state == LightLimiterInfo::ParameterState::Initialized) { + InitializeLightLimiterEffect(parameter, *state_, workbuffer); + } + } + + auto statistics{reinterpret_cast(result_state)}; + ApplyLightLimiterEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers, + processor.sample_count, statistics); +} + +bool LightLimiterVersion2Command::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/light_limiter.h b/src/audio_core/renderer/command/effect/light_limiter.h new file mode 100644 index 0000000000..5d98272c79 --- /dev/null +++ b/src/audio_core/renderer/command/effect/light_limiter.h @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/command/icommand.h" +#include "audio_core/renderer/effect/light_limiter.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for limiting volume between a high and low threshold. + * Version 1. + */ +struct LightLimiterVersion1Command : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer offsets for each channel + std::array inputs; + /// Output mix buffer offsets for each channel + std::array outputs; + /// Input parameters + LightLimiterInfo::ParameterVersion2 parameter; + /// State, updated each call + CpuAddr state; + /// Game-supplied workbuffer (Unused) + CpuAddr workbuffer; + /// Is this effect enabled? + bool effect_enabled; +}; + +/** + * AudioRenderer command for limiting volume between a high and low threshold. + * Version 2 with output statistics. + */ +struct LightLimiterVersion2Command : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer offsets for each channel + std::array inputs; + /// Output mix buffer offsets for each channel + std::array outputs; + /// Input parameters + LightLimiterInfo::ParameterVersion2 parameter; + /// State, updated each call + CpuAddr state; + /// Game-supplied workbuffer (Unused) + CpuAddr workbuffer; + /// Optional statistics, sent back to the sysmodule + CpuAddr result_state; + /// Is this effect enabled? + bool effect_enabled; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.cpp b/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.cpp new file mode 100644 index 0000000000..b3c3ba4ba5 --- /dev/null +++ b/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.cpp @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/effect/biquad_filter.h" +#include "audio_core/renderer/command/effect/multi_tap_biquad_filter.h" + +namespace AudioCore::AudioRenderer { + +void MultiTapBiquadFilterCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format( + "MultiTapBiquadFilterCommand\n\tinput {:02X}\n\toutput {:02X}\n\tneeds_init ({}, {})\n", + input, output, needs_init[0], needs_init[1]); +} + +void MultiTapBiquadFilterCommand::Process(const ADSP::CommandListProcessor& processor) { + if (filter_tap_count > MaxBiquadFilters) { + LOG_ERROR(Service_Audio, "Too many filter taps! {}", filter_tap_count); + filter_tap_count = MaxBiquadFilters; + } + + auto input_buffer{ + processor.mix_buffers.subspan(input * processor.sample_count, processor.sample_count)}; + auto output_buffer{ + processor.mix_buffers.subspan(output * processor.sample_count, processor.sample_count)}; + + // TODO: Fix this, currently just applies the filter to the input twice, + // and doesn't chain the biquads together at all. + for (u32 i = 0; i < filter_tap_count; i++) { + auto state{reinterpret_cast(states[i])}; + if (needs_init[i]) { + std::memset(state, 0, sizeof(VoiceState::BiquadFilterState)); + } + + ApplyBiquadFilterFloat(output_buffer, input_buffer, biquads[i].b, biquads[i].a, *state, + processor.sample_count); + } +} + +bool MultiTapBiquadFilterCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.h b/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.h new file mode 100644 index 0000000000..99c2c08301 --- /dev/null +++ b/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.h @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/command/icommand.h" +#include "audio_core/renderer/voice/voice_info.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for applying multiple biquads at once. + */ +struct MultiTapBiquadFilterCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer index + s16 input; + /// Output mix buffer index + s16 output; + /// Biquad parameters + std::array biquads; + /// Biquad states, updated each call + std::array states; + /// If each biquad needs initialisation + std::array needs_init; + /// Number of active biquads + u8 filter_tap_count; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/reverb.cpp b/src/audio_core/renderer/command/effect/reverb.cpp new file mode 100644 index 0000000000..fe2b1eb431 --- /dev/null +++ b/src/audio_core/renderer/command/effect/reverb.cpp @@ -0,0 +1,440 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/effect/reverb.h" + +namespace AudioCore::AudioRenderer { + +constexpr std::array FdnMaxDelayLineTimes = { + 53.9532470703125f, + 79.19256591796875f, + 116.23876953125f, + 170.61529541015625f, +}; + +constexpr std::array DecayMaxDelayLineTimes = { + 7.0f, + 9.0f, + 13.0f, + 17.0f, +}; + +constexpr std::array, ReverbInfo::NumEarlyModes> + EarlyDelayTimes = { + {{0.000000f, 3.500000f, 2.799988f, 3.899963f, 2.699951f, 13.399963f, 7.899963f, 8.399963f, + 9.899963f, 12.000000f, 12.500000f}, + {0.000000f, 11.799988f, 5.500000f, 11.199951f, 10.399963f, 38.099976f, 22.199951f, + 29.599976f, 21.199951f, 24.799988f, 40.000000f}, + {0.000000f, 41.500000f, 20.500000f, 41.299988f, 0.000000f, 29.500000f, 33.799988f, + 45.199951f, 46.799988f, 0.000000f, 50.000000f}, + {33.099976f, 43.299988f, 22.799988f, 37.899963f, 14.899963f, 35.299988f, 17.899963f, + 34.199951f, 0.000000f, 43.299988f, 50.000000f}, + {0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, + 0.000000f, 0.000000f, 0.000000f}}, +}; + +constexpr std::array, ReverbInfo::NumEarlyModes> + EarlyDelayGains = {{ + {0.699951f, 0.679993f, 0.699951f, 0.679993f, 0.699951f, 0.679993f, 0.699951f, 0.679993f, + 0.679993f, 0.679993f}, + {0.699951f, 0.679993f, 0.699951f, 0.679993f, 0.699951f, 0.679993f, 0.679993f, 0.679993f, + 0.679993f, 0.679993f}, + {0.500000f, 0.699951f, 0.699951f, 0.679993f, 0.500000f, 0.679993f, 0.679993f, 0.699951f, + 0.679993f, 0.000000f}, + {0.929993f, 0.919983f, 0.869995f, 0.859985f, 0.939941f, 0.809998f, 0.799988f, 0.769958f, + 0.759949f, 0.649963f}, + {0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, + 0.000000f, 0.000000f}, + }}; + +constexpr std::array, ReverbInfo::NumLateModes> + FdnDelayTimes = {{ + {53.953247f, 79.192566f, 116.238770f, 130.615295f}, + {53.953247f, 79.192566f, 116.238770f, 170.615295f}, + {5.000000f, 10.000000f, 5.000000f, 10.000000f}, + {47.029968f, 71.000000f, 103.000000f, 170.000000f}, + {53.953247f, 79.192566f, 116.238770f, 170.615295f}, + }}; + +constexpr std::array, ReverbInfo::NumLateModes> + DecayDelayTimes = {{ + {7.000000f, 9.000000f, 13.000000f, 17.000000f}, + {7.000000f, 9.000000f, 13.000000f, 17.000000f}, + {1.000000f, 1.000000f, 1.000000f, 1.000000f}, + {7.000000f, 7.000000f, 13.000000f, 9.000000f}, + {7.000000f, 9.000000f, 13.000000f, 17.000000f}, + }}; + +/** + * Update the ReverbInfo state according to the given parameters. + * + * @param params - Input parameters to update the state. + * @param state - State to be updated. + */ +static void UpdateReverbEffectParameter(const ReverbInfo::ParameterVersion2& params, + ReverbInfo::State& state) { + const auto pow_10 = [](f32 val) -> f32 { + return (val >= 0.0f) ? 1.0f : (val <= -5.3f) ? 0.0f : std::pow(10.0f, val); + }; + const auto cos = [](f32 degrees) -> f32 { + return std::cos(degrees * std::numbers::pi_v / 180.0f); + }; + + static bool unk_initialized{false}; + static Common::FixedPoint<50, 14> unk_value{}; + + const auto sample_rate{Common::FixedPoint<50, 14>::from_base(params.sample_rate)}; + const auto pre_delay_time{Common::FixedPoint<50, 14>::from_base(params.pre_delay)}; + + for (u32 i = 0; i < ReverbInfo::MaxDelayTaps; i++) { + auto early_delay{ + ((pre_delay_time + EarlyDelayTimes[params.early_mode][i]) * sample_rate).to_int()}; + early_delay = std::min(early_delay, state.pre_delay_line.sample_count_max); + state.early_delay_times[i] = early_delay + 1; + state.early_gains[i] = Common::FixedPoint<50, 14>::from_base(params.early_gain) * + EarlyDelayGains[params.early_mode][i]; + } + + if (params.channel_count == 2) { + state.early_gains[4] * 0.5f; + state.early_gains[5] * 0.5f; + } + + auto pre_time{ + ((pre_delay_time + EarlyDelayTimes[params.early_mode][10]) * sample_rate).to_int()}; + state.pre_delay_time = std::min(pre_time, state.pre_delay_line.sample_count_max); + + if (!unk_initialized) { + unk_value = cos((1280.0f / sample_rate).to_float()); + unk_initialized = true; + } + + for (u32 i = 0; i < ReverbInfo::MaxDelayLines; i++) { + const auto fdn_delay{(FdnDelayTimes[params.late_mode][i] * sample_rate).to_int()}; + state.fdn_delay_lines[i].sample_count = + std::min(fdn_delay, state.fdn_delay_lines[i].sample_count_max); + state.fdn_delay_lines[i].buffer_end = + &state.fdn_delay_lines[i].buffer[state.fdn_delay_lines[i].sample_count - 1]; + + const auto decay_delay{(DecayDelayTimes[params.late_mode][i] * sample_rate).to_int()}; + state.decay_delay_lines[i].sample_count = + std::min(decay_delay, state.decay_delay_lines[i].sample_count_max); + state.decay_delay_lines[i].buffer_end = + &state.decay_delay_lines[i].buffer[state.decay_delay_lines[i].sample_count - 1]; + + state.decay_delay_lines[i].decay = + 0.5999755859375f * (1.0f - Common::FixedPoint<50, 14>::from_base(params.colouration)); + + auto a{(Common::FixedPoint<50, 14>(state.fdn_delay_lines[i].sample_count_max) + + state.decay_delay_lines[i].sample_count_max) * + -3}; + auto b{a / (Common::FixedPoint<50, 14>::from_base(params.decay_time) * sample_rate)}; + Common::FixedPoint<50, 14> c{0.0f}; + Common::FixedPoint<50, 14> d{0.0f}; + auto hf_decay_ratio{Common::FixedPoint<50, 14>::from_base(params.high_freq_decay_ratio)}; + + if (hf_decay_ratio > 0.99493408203125f) { + c = 0.0f; + d = 1.0f; + } else { + const auto e{ + pow_10(((((1.0f / hf_decay_ratio) - 1.0f) * 2) / 100 * (b / 10)).to_float())}; + const auto f{1.0f - e}; + const auto g{2.0f - (unk_value * e * 2)}; + const auto h{std::sqrt(std::pow(g.to_float(), 2.0f) - (std::pow(f, 2.0f) * 4))}; + + c = (g - h) / (f * 2.0f); + d = 1.0f - c; + } + + state.hf_decay_prev_gain[i] = c; + state.hf_decay_gain[i] = pow_10((b / 1000).to_float()) * d * 0.70709228515625f; + state.prev_feedback_output[i] = 0; + } +} + +/** + * Initialize a new ReverbInfo state according to the given parameters. + * + * @param params - Input parameters to update the state. + * @param state - State to be updated. + * @param workbuffer - Game-supplied memory for the state. (Unused) + * @param long_size_pre_delay_supported - Use a longer pre-delay time before reverb begins. + */ +static void InitializeReverbEffect(const ReverbInfo::ParameterVersion2& params, + ReverbInfo::State& state, const CpuAddr workbuffer, + const bool long_size_pre_delay_supported) { + state = {}; + + auto delay{Common::FixedPoint<50, 14>::from_base(params.sample_rate)}; + + for (u32 i = 0; i < ReverbInfo::MaxDelayLines; i++) { + auto fdn_delay_time{(FdnMaxDelayLineTimes[i] * delay).to_uint_floor()}; + state.fdn_delay_lines[i].Initialize(fdn_delay_time, 1.0f); + + auto decay_delay_time{(DecayMaxDelayLineTimes[i] * delay).to_uint_floor()}; + state.decay_delay_lines[i].Initialize(decay_delay_time, 0.0f); + } + + const auto pre_delay{long_size_pre_delay_supported ? 350.0f : 150.0f}; + const auto pre_delay_line{(pre_delay * delay).to_uint_floor()}; + state.pre_delay_line.Initialize(pre_delay_line, 1.0f); + + const auto center_delay_time{(5 * delay).to_uint_floor()}; + state.center_delay_line.Initialize(center_delay_time, 1.0f); + + UpdateReverbEffectParameter(params, state); + + for (u32 i = 0; i < ReverbInfo::MaxDelayLines; i++) { + std::ranges::fill(state.fdn_delay_lines[i].buffer, 0); + std::ranges::fill(state.decay_delay_lines[i].buffer, 0); + } + std::ranges::fill(state.center_delay_line.buffer, 0); + std::ranges::fill(state.pre_delay_line.buffer, 0); +} + +/** + * Pass-through the effect, copying input to output directly, with no reverb applied. + * + * @param inputs - Array of input mix buffers to copy. + * @param outputs - Array of output mix buffers to receive copy. + * @param channel_count - Number of channels in inputs and outputs. + * @param sample_count - Number of samples within each channel. + */ +static void ApplyReverbEffectBypass(std::span> inputs, + std::span> outputs, const u32 channel_count, + const u32 sample_count) { + for (u32 i = 0; i < channel_count; i++) { + if (inputs[i].data() != outputs[i].data()) { + std::memcpy(outputs[i].data(), inputs[i].data(), outputs[i].size_bytes()); + } + } +} + +/** + * Tick the delay lines, reading and returning their current output, and writing a new decaying + * sample (mix). + * + * @param decay - The decay line. + * @param fdn - Feedback delay network. + * @param mix - The new calculated sample to be written and decayed. + * @return The next delayed and decayed sample. + */ +static Common::FixedPoint<50, 14> Axfx2AllPassTick(ReverbInfo::ReverbDelayLine& decay, + ReverbInfo::ReverbDelayLine& fdn, + const Common::FixedPoint<50, 14> mix) { + const auto val{decay.Read()}; + const auto mixed{mix - (val * decay.decay)}; + const auto out{decay.Tick(mixed) + (mixed * decay.decay)}; + + fdn.Tick(out); + return out; +} + +/** + * Impl. Apply a Reverb according to the current state, on the input mix buffers, + * saving the results to the output mix buffers. + * + * @tparam NumChannels - Number of channels to process. 1-6. + Inputs/outputs should have this many buffers. + * @param params - Input parameters to update the state. + * @param state - State to use, must be initialized (see InitializeReverbEffect). + * @param inputs - Input mix buffers to perform the reverb on. + * @param outputs - Output mix buffers to receive the reverbed samples. + * @param sample_count - Number of samples to process. + */ +template +static void ApplyReverbEffect(const ReverbInfo::ParameterVersion2& params, ReverbInfo::State& state, + std::vector>& inputs, + std::vector>& outputs, const u32 sample_count) { + constexpr std::array OutTapIndexes1Ch{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + constexpr std::array OutTapIndexes2Ch{ + 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, + }; + constexpr std::array OutTapIndexes4Ch{ + 0, 0, 1, 1, 0, 1, 2, 2, 3, 3, + }; + constexpr std::array OutTapIndexes6Ch{ + 0, 0, 1, 1, 2, 2, 4, 4, 5, 5, + }; + + std::span tap_indexes{}; + if constexpr (NumChannels == 1) { + tap_indexes = OutTapIndexes1Ch; + } else if constexpr (NumChannels == 2) { + tap_indexes = OutTapIndexes2Ch; + } else if constexpr (NumChannels == 4) { + tap_indexes = OutTapIndexes4Ch; + } else if constexpr (NumChannels == 6) { + tap_indexes = OutTapIndexes6Ch; + } + + for (u32 sample_index = 0; sample_index < sample_count; sample_index++) { + std::array, NumChannels> output_samples{}; + + for (u32 early_tap = 0; early_tap < ReverbInfo::MaxDelayTaps; early_tap++) { + const auto sample{state.pre_delay_line.TapOut(state.early_delay_times[early_tap]) * + state.early_gains[early_tap]}; + output_samples[tap_indexes[early_tap]] += sample; + if constexpr (NumChannels == 6) { + output_samples[static_cast(Channels::LFE)] += sample; + } + } + + if constexpr (NumChannels == 6) { + output_samples[static_cast(Channels::LFE)] *= 0.2f; + } + + Common::FixedPoint<50, 14> input_sample{}; + for (u32 channel = 0; channel < NumChannels; channel++) { + input_sample += inputs[channel][sample_index]; + } + + input_sample *= 64; + input_sample *= Common::FixedPoint<50, 14>::from_base(params.base_gain); + state.pre_delay_line.Write(input_sample); + + for (u32 i = 0; i < ReverbInfo::MaxDelayLines; i++) { + state.prev_feedback_output[i] = + state.prev_feedback_output[i] * state.hf_decay_prev_gain[i] + + state.fdn_delay_lines[i].Read() * state.hf_decay_gain[i]; + } + + Common::FixedPoint<50, 14> pre_delay_sample{ + state.pre_delay_line.Read() * Common::FixedPoint<50, 14>::from_base(params.late_gain)}; + + std::array, ReverbInfo::MaxDelayLines> mix_matrix{ + state.prev_feedback_output[2] + state.prev_feedback_output[1] + pre_delay_sample, + -state.prev_feedback_output[0] - state.prev_feedback_output[3] + pre_delay_sample, + state.prev_feedback_output[0] - state.prev_feedback_output[3] + pre_delay_sample, + state.prev_feedback_output[1] - state.prev_feedback_output[2] + pre_delay_sample, + }; + + std::array, ReverbInfo::MaxDelayLines> allpass_samples{}; + for (u32 i = 0; i < ReverbInfo::MaxDelayLines; i++) { + allpass_samples[i] = Axfx2AllPassTick(state.decay_delay_lines[i], + state.fdn_delay_lines[i], mix_matrix[i]); + } + + const auto dry_gain{Common::FixedPoint<50, 14>::from_base(params.dry_gain)}; + const auto wet_gain{Common::FixedPoint<50, 14>::from_base(params.wet_gain)}; + + if constexpr (NumChannels == 6) { + const std::array, MaxChannels> allpass_outputs{ + allpass_samples[0], allpass_samples[1], allpass_samples[2] - allpass_samples[3], + allpass_samples[3], allpass_samples[2], allpass_samples[3], + }; + + for (u32 channel = 0; channel < NumChannels; channel++) { + auto in_sample{inputs[channel][sample_index] * dry_gain}; + + Common::FixedPoint<50, 14> allpass{}; + if (channel == static_cast(Channels::Center)) { + allpass = state.center_delay_line.Tick(allpass_outputs[channel] * 0.5f); + } else { + allpass = allpass_outputs[channel]; + } + + auto out_sample{((output_samples[channel] + allpass) * wet_gain) / 64}; + outputs[channel][sample_index] = (in_sample + out_sample).to_int(); + } + } else { + for (u32 channel = 0; channel < NumChannels; channel++) { + auto in_sample{inputs[channel][sample_index] * dry_gain}; + auto out_sample{((output_samples[channel] + allpass_samples[channel]) * wet_gain) / + 64}; + outputs[channel][sample_index] = (in_sample + out_sample).to_int(); + } + } + } +} + +/** + * Apply a Reverb if enabled, according to the current state, on the input mix buffers, + * saving the results to the output mix buffers. + * + * @param params - Input parameters to use. + * @param state - State to use, must be initialized (see InitializeReverbEffect). + * @param enabled - If enabled, delay will be applied, otherwise input is copied to output. + * @param inputs - Input mix buffers to performan the reverb on. + * @param outputs - Output mix buffers to receive the reverbed samples. + * @param sample_count - Number of samples to process. + */ +static void ApplyReverbEffect(const ReverbInfo::ParameterVersion2& params, ReverbInfo::State& state, + const bool enabled, std::vector>& inputs, + std::vector>& outputs, const u32 sample_count) { + if (enabled) { + switch (params.channel_count) { + case 0: + return; + case 1: + ApplyReverbEffect<1>(params, state, inputs, outputs, sample_count); + break; + case 2: + ApplyReverbEffect<2>(params, state, inputs, outputs, sample_count); + break; + case 4: + ApplyReverbEffect<4>(params, state, inputs, outputs, sample_count); + break; + case 6: + ApplyReverbEffect<6>(params, state, inputs, outputs, sample_count); + break; + default: + ApplyReverbEffectBypass(inputs, outputs, params.channel_count, sample_count); + break; + } + } else { + ApplyReverbEffectBypass(inputs, outputs, params.channel_count, sample_count); + } +} + +void ReverbCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format( + "ReverbCommand\n\tenabled {} long_size_pre_delay_supported {}\n\tinputs: ", effect_enabled, + long_size_pre_delay_supported); + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n\toutputs: "; + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", outputs[i]); + } + string += "\n"; +} + +void ReverbCommand::Process(const ADSP::CommandListProcessor& processor) { + std::vector> input_buffers(parameter.channel_count); + std::vector> output_buffers(parameter.channel_count); + + for (u32 i = 0; i < parameter.channel_count; i++) { + input_buffers[i] = processor.mix_buffers.subspan(inputs[i] * processor.sample_count, + processor.sample_count); + output_buffers[i] = processor.mix_buffers.subspan(outputs[i] * processor.sample_count, + processor.sample_count); + } + + auto state_{reinterpret_cast(state)}; + + if (effect_enabled) { + if (parameter.state == ReverbInfo::ParameterState::Updating) { + UpdateReverbEffectParameter(parameter, *state_); + } else if (parameter.state == ReverbInfo::ParameterState::Initialized) { + InitializeReverbEffect(parameter, *state_, workbuffer, long_size_pre_delay_supported); + } + } + ApplyReverbEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers, + processor.sample_count); +} + +bool ReverbCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/effect/reverb.h b/src/audio_core/renderer/command/effect/reverb.h new file mode 100644 index 0000000000..328756150f --- /dev/null +++ b/src/audio_core/renderer/command/effect/reverb.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/command/icommand.h" +#include "audio_core/renderer/effect/reverb.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for a Reverb effect. Apply a reverb to inputs mix buffer, outputs receives + * the results. + */ +struct ReverbCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer offsets for each channel + std::array inputs; + /// Output mix buffer offsets for each channel + std::array outputs; + /// Input parameters + ReverbInfo::ParameterVersion2 parameter; + /// State, updated each call + CpuAddr state; + /// Game-supplied workbuffer (Unused) + CpuAddr workbuffer; + /// Is this effect enabled? + bool effect_enabled; + /// Is a longer pre-delay time supported? + bool long_size_pre_delay_supported; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/icommand.h b/src/audio_core/renderer/command/icommand.h new file mode 100644 index 0000000000..f2dd412541 --- /dev/null +++ b/src/audio_core/renderer/command/icommand.h @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +enum class CommandId : u8 { + /* 0x00 */ Invalid, + /* 0x01 */ DataSourcePcmInt16Version1, + /* 0x02 */ DataSourcePcmInt16Version2, + /* 0x03 */ DataSourcePcmFloatVersion1, + /* 0x04 */ DataSourcePcmFloatVersion2, + /* 0x05 */ DataSourceAdpcmVersion1, + /* 0x06 */ DataSourceAdpcmVersion2, + /* 0x07 */ Volume, + /* 0x08 */ VolumeRamp, + /* 0x09 */ BiquadFilter, + /* 0x0A */ Mix, + /* 0x0B */ MixRamp, + /* 0x0C */ MixRampGrouped, + /* 0x0D */ DepopPrepare, + /* 0x0E */ DepopForMixBuffers, + /* 0x0F */ Delay, + /* 0x10 */ Upsample, + /* 0x11 */ DownMix6chTo2ch, + /* 0x12 */ Aux, + /* 0x13 */ DeviceSink, + /* 0x14 */ CircularBufferSink, + /* 0x15 */ Reverb, + /* 0x16 */ I3dl2Reverb, + /* 0x17 */ Performance, + /* 0x18 */ ClearMixBuffer, + /* 0x19 */ CopyMixBuffer, + /* 0x1A */ LightLimiterVersion1, + /* 0x1B */ LightLimiterVersion2, + /* 0x1C */ MultiTapBiquadFilter, + /* 0x1D */ Capture, + /* 0x1E */ Compressor, +}; + +constexpr u32 CommandMagic{0xCAFEBABE}; + +/** + * A command, generated by the host, and processed by the ADSP's AudioRenderer. + */ +struct ICommand { + virtual ~ICommand() = default; + + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + virtual void Dump(const ADSP::CommandListProcessor& processor, std::string& string) = 0; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + virtual void Process(const ADSP::CommandListProcessor& processor) = 0; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + virtual bool Verify(const ADSP::CommandListProcessor& processor) = 0; + + /// Command magic 0xCAFEBABE + u32 magic{}; + /// Command enabled + bool enabled{}; + /// Type of this command (see CommandId) + CommandId type{}; + /// Size of this command + s16 size{}; + /// Estimated processing time for this command + u32 estimated_process_time{}; + /// Node id of the voice or mix this command was generated from + u32 node_id{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/clear_mix.cpp b/src/audio_core/renderer/command/mix/clear_mix.cpp new file mode 100644 index 0000000000..4f649d6a87 --- /dev/null +++ b/src/audio_core/renderer/command/mix/clear_mix.cpp @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/clear_mix.h" + +namespace AudioCore::AudioRenderer { + +void ClearMixBufferCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("ClearMixBufferCommand\n"); +} + +void ClearMixBufferCommand::Process(const ADSP::CommandListProcessor& processor) { + memset(processor.mix_buffers.data(), 0, processor.mix_buffers.size_bytes()); +} + +bool ClearMixBufferCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/clear_mix.h b/src/audio_core/renderer/command/mix/clear_mix.h new file mode 100644 index 0000000000..956ec0b65b --- /dev/null +++ b/src/audio_core/renderer/command/mix/clear_mix.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for a clearing the mix buffers. + * Used at the start of each command list. + */ +struct ClearMixBufferCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/copy_mix.cpp b/src/audio_core/renderer/command/mix/copy_mix.cpp new file mode 100644 index 0000000000..1d49f1644f --- /dev/null +++ b/src/audio_core/renderer/command/mix/copy_mix.cpp @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/copy_mix.h" + +namespace AudioCore::AudioRenderer { + +void CopyMixBufferCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("CopyMixBufferCommand\n\tinput {:02X} output {:02X}\n", input_index, + output_index); +} + +void CopyMixBufferCommand::Process(const ADSP::CommandListProcessor& processor) { + auto output{processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count)}; + auto input{processor.mix_buffers.subspan(input_index * processor.sample_count, + processor.sample_count)}; + std::memcpy(output.data(), input.data(), processor.sample_count * sizeof(s32)); +} + +bool CopyMixBufferCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/copy_mix.h b/src/audio_core/renderer/command/mix/copy_mix.h new file mode 100644 index 0000000000..a59007fb63 --- /dev/null +++ b/src/audio_core/renderer/command/mix/copy_mix.h @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for a copying a mix buffer from input to output. + */ +struct CopyMixBufferCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer index + s16 input_index; + /// Output mix buffer index + s16 output_index; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/depop_for_mix_buffers.cpp b/src/audio_core/renderer/command/mix/depop_for_mix_buffers.cpp new file mode 100644 index 0000000000..c2bc10061c --- /dev/null +++ b/src/audio_core/renderer/command/mix/depop_for_mix_buffers.cpp @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/common/common.h" +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/depop_for_mix_buffers.h" + +namespace AudioCore::AudioRenderer { +/** + * Apply depopping. Add the depopped sample to each incoming new sample, decaying it each time + * according to decay. + * + * @param output - Output buffer to be depopped. + * @param depop_sample - Depopped sample to apply to output samples. + * @param decay_ - Amount to decay the depopped sample for every output sample. + * @param sample_count - Samples to process. + * @return Final decayed depop sample. + */ +static s32 ApplyDepopMix(std::span output, const s32 depop_sample, + Common::FixedPoint<49, 15>& decay_, const u32 sample_count) { + auto sample{std::abs(depop_sample)}; + auto decay{decay_.to_raw()}; + + if (depop_sample <= 0) { + for (u32 i = 0; i < sample_count; i++) { + sample = static_cast((static_cast(sample) * decay) >> 15); + output[i] -= sample; + } + return -sample; + } else { + for (u32 i = 0; i < sample_count; i++) { + sample = static_cast((static_cast(sample) * decay) >> 15); + output[i] += sample; + } + return sample; + } +} + +void DepopForMixBuffersCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("DepopForMixBuffersCommand\n\tinput {:02X} count {} decay {}\n", input, + count, decay.to_float()); +} + +void DepopForMixBuffersCommand::Process(const ADSP::CommandListProcessor& processor) { + auto end_index{std::min(processor.buffer_count, input + count)}; + std::span depop_buff{reinterpret_cast(depop_buffer), end_index}; + + for (u32 index = input; index < end_index; index++) { + const auto depop_sample{depop_buff[index]}; + if (depop_sample != 0) { + auto input_buffer{processor.mix_buffers.subspan(index * processor.sample_count, + processor.sample_count)}; + depop_buff[index] = + ApplyDepopMix(input_buffer, depop_sample, decay, processor.sample_count); + } + } +} + +bool DepopForMixBuffersCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/depop_for_mix_buffers.h b/src/audio_core/renderer/command/mix/depop_for_mix_buffers.h new file mode 100644 index 0000000000..e7268ff278 --- /dev/null +++ b/src/audio_core/renderer/command/mix/depop_for_mix_buffers.h @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for depopping a mix buffer. + * Adds a cumulation of previous samples to the current mix buffer with a decay. + */ +struct DepopForMixBuffersCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Starting input mix buffer index + u32 input; + /// Number of mix buffers to depop + u32 count; + /// Amount to decay the depop sample for each new sample + Common::FixedPoint<49, 15> decay; + /// Address of the depop buffer, holding the last sample for every mix buffer + CpuAddr depop_buffer; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/depop_prepare.cpp b/src/audio_core/renderer/command/mix/depop_prepare.cpp new file mode 100644 index 0000000000..2ee076ef6b --- /dev/null +++ b/src/audio_core/renderer/command/mix/depop_prepare.cpp @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/depop_prepare.h" +#include "audio_core/renderer/voice/voice_state.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { + +void DepopPrepareCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("DepopPrepareCommand\n\tinputs: "); + for (u32 i = 0; i < buffer_count; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n"; +} + +void DepopPrepareCommand::Process(const ADSP::CommandListProcessor& processor) { + auto samples{reinterpret_cast(previous_samples)}; + auto buffer{std::span(reinterpret_cast(depop_buffer), buffer_count)}; + + for (u32 i = 0; i < buffer_count; i++) { + if (samples[i]) { + buffer[inputs[i]] += samples[i]; + samples[i] = 0; + } + } +} + +bool DepopPrepareCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/depop_prepare.h b/src/audio_core/renderer/command/mix/depop_prepare.h new file mode 100644 index 0000000000..a5465da9ae --- /dev/null +++ b/src/audio_core/renderer/command/mix/depop_prepare.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for preparing depop. + * Adds the previusly output last samples to the depop buffer. + */ +struct DepopPrepareCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Depop buffer offset for each mix buffer + std::array inputs; + /// Pointer to the previous mix buffer samples + CpuAddr previous_samples; + /// Number of mix buffers to use + u32 buffer_count; + /// Pointer to the current depop values + CpuAddr depop_buffer; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/mix.cpp b/src/audio_core/renderer/command/mix/mix.cpp new file mode 100644 index 0000000000..8ecf9b05a9 --- /dev/null +++ b/src/audio_core/renderer/command/mix/mix.cpp @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/mix.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { +/** + * Mix input mix buffer into output mix buffer, with volume applied to the input. + * + * @tparam Q - Number of bits for fixed point operations. + * @param output - Output mix buffer. + * @param input - Input mix buffer. + * @param volume - Volume applied to the input. + * @param sample_count - Number of samples to process. + */ +template +static void ApplyMix(std::span output, std::span input, const f32 volume_, + const u32 sample_count) { + const Common::FixedPoint<64 - Q, Q> volume{volume_}; + for (u32 i = 0; i < sample_count; i++) { + output[i] = (output[i] + input[i] * volume).to_int(); + } +} + +void MixCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("MixCommand"); + string += fmt::format("\n\tinput {:02X}", input_index); + string += fmt::format("\n\toutput {:02X}", output_index); + string += fmt::format("\n\tvolume {:.8f}", volume); + string += "\n"; +} + +void MixCommand::Process(const ADSP::CommandListProcessor& processor) { + auto output{processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count)}; + auto input{processor.mix_buffers.subspan(input_index * processor.sample_count, + processor.sample_count)}; + + // If volume is 0, nothing will be added to the output, so just skip. + if (volume == 0.0f) { + return; + } + + switch (precision) { + case 15: + ApplyMix<15>(output, input, volume, processor.sample_count); + break; + + case 23: + ApplyMix<23>(output, input, volume, processor.sample_count); + break; + + default: + LOG_ERROR(Service_Audio, "Invalid precision {}", precision); + break; + } +} + +bool MixCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/mix.h b/src/audio_core/renderer/command/mix/mix.h new file mode 100644 index 0000000000..0201cf1718 --- /dev/null +++ b/src/audio_core/renderer/command/mix/mix.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for mixing an input mix buffer to an output mix buffer, with a volume + * applied to the input. + */ +struct MixCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Fixed point precision + u8 precision; + /// Input mix buffer index + s16 input_index; + /// Output mix buffer index + s16 output_index; + /// Mix volume applied to the input + f32 volume; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/mix_ramp.cpp b/src/audio_core/renderer/command/mix/mix_ramp.cpp new file mode 100644 index 0000000000..ffdafa1c8d --- /dev/null +++ b/src/audio_core/renderer/command/mix/mix_ramp.cpp @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/mix_ramp.h" +#include "common/fixed_point.h" +#include "common/logging/log.h" + +namespace AudioCore::AudioRenderer { +/** + * Mix input mix buffer into output mix buffer, with volume applied to the input. + * + * @tparam Q - Number of bits for fixed point operations. + * @param output - Output mix buffer. + * @param input - Input mix buffer. + * @param volume - Volume applied to the input. + * @param ramp - Ramp applied to volume every sample. + * @param sample_count - Number of samples to process. + * @return The final gained input sample, used for depopping. + */ +template +s32 ApplyMixRamp(std::span output, std::span input, const f32 volume_, + const f32 ramp_, const u32 sample_count) { + Common::FixedPoint<64 - Q, Q> volume{volume_}; + Common::FixedPoint<64 - Q, Q> sample{0}; + + if (ramp_ == 0.0f) { + for (u32 i = 0; i < sample_count; i++) { + sample = input[i] * volume; + output[i] = (output[i] + sample).to_int(); + } + } else { + Common::FixedPoint<64 - Q, Q> ramp{ramp_}; + for (u32 i = 0; i < sample_count; i++) { + sample = input[i] * volume; + output[i] = (output[i] + sample).to_int(); + volume += ramp; + } + } + return sample.to_int(); +} + +template s32 ApplyMixRamp<15>(std::span, std::span, const f32, const f32, + const u32); +template s32 ApplyMixRamp<23>(std::span, std::span, const f32, const f32, + const u32); + +void MixRampCommand::Dump(const ADSP::CommandListProcessor& processor, std::string& string) { + const auto ramp{(volume - prev_volume) / static_cast(processor.sample_count)}; + string += fmt::format("MixRampCommand"); + string += fmt::format("\n\tinput {:02X}", input_index); + string += fmt::format("\n\toutput {:02X}", output_index); + string += fmt::format("\n\tvolume {:.8f}", volume); + string += fmt::format("\n\tprev_volume {:.8f}", prev_volume); + string += fmt::format("\n\tramp {:.8f}", ramp); + string += "\n"; +} + +void MixRampCommand::Process(const ADSP::CommandListProcessor& processor) { + auto output{processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count)}; + auto input{processor.mix_buffers.subspan(input_index * processor.sample_count, + processor.sample_count)}; + const auto ramp{(volume - prev_volume) / static_cast(processor.sample_count)}; + auto prev_sample_ptr{reinterpret_cast(previous_sample)}; + + // If previous volume and ramp are both 0, nothing will be added to the output, so just skip. + if (prev_volume == 0.0f && ramp == 0.0f) { + *prev_sample_ptr = 0; + return; + } + + switch (precision) { + case 15: + *prev_sample_ptr = + ApplyMixRamp<15>(output, input, prev_volume, ramp, processor.sample_count); + break; + + case 23: + *prev_sample_ptr = + ApplyMixRamp<23>(output, input, prev_volume, ramp, processor.sample_count); + break; + + default: + LOG_ERROR(Service_Audio, "Invalid precision {}", precision); + break; + } +} + +bool MixRampCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/mix_ramp.h b/src/audio_core/renderer/command/mix/mix_ramp.h new file mode 100644 index 0000000000..770f57e802 --- /dev/null +++ b/src/audio_core/renderer/command/mix/mix_ramp.h @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for mixing an input mix buffer to an output mix buffer, with a volume + * applied to the input, and volume ramping to smooth out the transition. + */ +struct MixRampCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Fixed point precision + u8 precision; + /// Input mix buffer index + s16 input_index; + /// Output mix buffer index + s16 output_index; + /// Previous mix volume + f32 prev_volume; + /// Current mix volume + f32 volume; + /// Pointer to the previous sample buffer, used for depopping + CpuAddr previous_sample; +}; + +/** + * Mix input mix buffer into output mix buffer, with volume applied to the input. + * @tparam Q - Number of bits for fixed point operations. + * @param output - Output mix buffer. + * @param input - Input mix buffer. + * @param volume - Volume applied to the input. + * @param ramp - Ramp applied to volume every sample. + * @param sample_count - Number of samples to process. + * @return The final gained input sample, used for depopping. + */ +template +s32 ApplyMixRamp(std::span output, std::span input, const f32 volume_, + const f32 ramp_, const u32 sample_count); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/mix_ramp_grouped.cpp b/src/audio_core/renderer/command/mix/mix_ramp_grouped.cpp new file mode 100644 index 0000000000..43dbef9fca --- /dev/null +++ b/src/audio_core/renderer/command/mix/mix_ramp_grouped.cpp @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/mix_ramp.h" +#include "audio_core/renderer/command/mix/mix_ramp_grouped.h" + +namespace AudioCore::AudioRenderer { + +void MixRampGroupedCommand::Dump(const ADSP::CommandListProcessor& processor, std::string& string) { + string += "MixRampGroupedCommand"; + for (u32 i = 0; i < buffer_count; i++) { + string += fmt::format("\n\t{}", i); + const auto ramp{(volumes[i] - prev_volumes[i]) / static_cast(processor.sample_count)}; + string += fmt::format("\n\t\tinput {:02X}", inputs[i]); + string += fmt::format("\n\t\toutput {:02X}", outputs[i]); + string += fmt::format("\n\t\tvolume {:.8f}", volumes[i]); + string += fmt::format("\n\t\tprev_volume {:.8f}", prev_volumes[i]); + string += fmt::format("\n\t\tramp {:.8f}", ramp); + string += "\n"; + } +} + +void MixRampGroupedCommand::Process(const ADSP::CommandListProcessor& processor) { + std::span prev_samples = {reinterpret_cast(previous_samples), MaxMixBuffers}; + + for (u32 i = 0; i < buffer_count; i++) { + auto last_sample{0}; + if (prev_volumes[i] != 0.0f || volumes[i] != 0.0f) { + const auto output{processor.mix_buffers.subspan(outputs[i] * processor.sample_count, + processor.sample_count)}; + const auto input{processor.mix_buffers.subspan(inputs[i] * processor.sample_count, + processor.sample_count)}; + const auto ramp{(volumes[i] - prev_volumes[i]) / + static_cast(processor.sample_count)}; + + if (prev_volumes[i] == 0.0f && ramp == 0.0f) { + prev_samples[i] = 0; + continue; + } + + switch (precision) { + case 15: + last_sample = + ApplyMixRamp<15>(output, input, prev_volumes[i], ramp, processor.sample_count); + break; + case 23: + last_sample = + ApplyMixRamp<23>(output, input, prev_volumes[i], ramp, processor.sample_count); + break; + default: + LOG_ERROR(Service_Audio, "Invalid precision {}", precision); + break; + } + } + + prev_samples[i] = last_sample; + } +} + +bool MixRampGroupedCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/mix_ramp_grouped.h b/src/audio_core/renderer/command/mix/mix_ramp_grouped.h new file mode 100644 index 0000000000..027276e5a4 --- /dev/null +++ b/src/audio_core/renderer/command/mix/mix_ramp_grouped.h @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for mixing multiple input mix buffers to multiple output mix buffers, with + * a volume applied to the input, and volume ramping to smooth out the transition. + */ +struct MixRampGroupedCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Fixed point precision + u8 precision; + /// Number of mix buffers to mix + u32 buffer_count; + /// Input mix buffer indexes for each mix buffer + std::array inputs; + /// Output mix buffer indexes for each mix buffer + std::array outputs; + /// Previous mix vloumes for each mix buffer + std::array prev_volumes; + /// Current mix vloumes for each mix buffer + std::array volumes; + /// Pointer to the previous sample buffer, used for depop + CpuAddr previous_samples; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/volume.cpp b/src/audio_core/renderer/command/mix/volume.cpp new file mode 100644 index 0000000000..b045fb0625 --- /dev/null +++ b/src/audio_core/renderer/command/mix/volume.cpp @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/volume.h" +#include "common/fixed_point.h" +#include "common/logging/log.h" + +namespace AudioCore::AudioRenderer { +/** + * Apply volume to the input mix buffer, saving to the output buffer. + * + * @tparam Q - Number of bits for fixed point operations. + * @param output - Output mix buffer. + * @param input - Input mix buffer. + * @param volume - Volume applied to the input. + * @param sample_count - Number of samples to process. + */ +template +static void ApplyUniformGain(std::span output, std::span input, const f32 volume, + const u32 sample_count) { + if (volume == 1.0f) { + std::memcpy(output.data(), input.data(), input.size_bytes()); + } else { + const Common::FixedPoint<64 - Q, Q> gain{volume}; + for (u32 i = 0; i < sample_count; i++) { + output[i] = (input[i] * gain).to_int(); + } + } +} + +void VolumeCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("VolumeCommand"); + string += fmt::format("\n\tinput {:02X}", input_index); + string += fmt::format("\n\toutput {:02X}", output_index); + string += fmt::format("\n\tvolume {:.8f}", volume); + string += "\n"; +} + +void VolumeCommand::Process(const ADSP::CommandListProcessor& processor) { + // If input and output buffers are the same, and the volume is 1.0f, this won't do + // anything, so just skip. + if (input_index == output_index && volume == 1.0f) { + return; + } + + auto output{processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count)}; + auto input{processor.mix_buffers.subspan(input_index * processor.sample_count, + processor.sample_count)}; + + switch (precision) { + case 15: + ApplyUniformGain<15>(output, input, volume, processor.sample_count); + break; + + case 23: + ApplyUniformGain<23>(output, input, volume, processor.sample_count); + break; + + default: + LOG_ERROR(Service_Audio, "Invalid precision {}", precision); + break; + } +} + +bool VolumeCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/volume.h b/src/audio_core/renderer/command/mix/volume.h new file mode 100644 index 0000000000..6ae9fb7943 --- /dev/null +++ b/src/audio_core/renderer/command/mix/volume.h @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for applying volume to a mix buffer. + */ +struct VolumeCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Fixed point precision + u8 precision; + /// Input mix buffer index + s16 input_index; + /// Output mix buffer index + s16 output_index; + /// Mix volume applied to the input + f32 volume; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/volume_ramp.cpp b/src/audio_core/renderer/command/mix/volume_ramp.cpp new file mode 100644 index 0000000000..4243071481 --- /dev/null +++ b/src/audio_core/renderer/command/mix/volume_ramp.cpp @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/mix/volume_ramp.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { +/** + * Apply volume with ramping to the input mix buffer, saving to the output buffer. + * + * @tparam Q - Number of bits for fixed point operations. + * @param output - Output mix buffers. + * @param input - Input mix buffers. + * @param volume - Volume applied to the input. + * @param ramp - Ramp applied to volume every sample. + * @param sample_count - Number of samples to process. + */ +template +static void ApplyLinearEnvelopeGain(std::span output, std::span input, + const f32 volume, const f32 ramp_, const u32 sample_count) { + if (volume == 0.0f && ramp_ == 0.0f) { + std::memset(output.data(), 0, output.size_bytes()); + } else if (volume == 1.0f && ramp_ == 0.0f) { + std::memcpy(output.data(), input.data(), output.size_bytes()); + } else if (ramp_ == 0.0f) { + const Common::FixedPoint<64 - Q, Q> gain{volume}; + for (u32 i = 0; i < sample_count; i++) { + output[i] = (input[i] * gain).to_int(); + } + } else { + Common::FixedPoint<64 - Q, Q> gain{volume}; + const Common::FixedPoint<64 - Q, Q> ramp{ramp_}; + for (u32 i = 0; i < sample_count; i++) { + output[i] = (input[i] * gain).to_int(); + gain += ramp; + } + } +} + +void VolumeRampCommand::Dump(const ADSP::CommandListProcessor& processor, std::string& string) { + const auto ramp{(volume - prev_volume) / static_cast(processor.sample_count)}; + string += fmt::format("VolumeRampCommand"); + string += fmt::format("\n\tinput {:02X}", input_index); + string += fmt::format("\n\toutput {:02X}", output_index); + string += fmt::format("\n\tvolume {:.8f}", volume); + string += fmt::format("\n\tprev_volume {:.8f}", prev_volume); + string += fmt::format("\n\tramp {:.8f}", ramp); + string += "\n"; +} + +void VolumeRampCommand::Process(const ADSP::CommandListProcessor& processor) { + auto output{processor.mix_buffers.subspan(output_index * processor.sample_count, + processor.sample_count)}; + auto input{processor.mix_buffers.subspan(input_index * processor.sample_count, + processor.sample_count)}; + const auto ramp{(volume - prev_volume) / static_cast(processor.sample_count)}; + + // If input and output buffers are the same, and the volume is 1.0f, and there's no ramping, + // this won't do anything, so just skip. + if (input_index == output_index && prev_volume == 1.0f && ramp == 0.0f) { + return; + } + + switch (precision) { + case 15: + ApplyLinearEnvelopeGain<15>(output, input, prev_volume, ramp, processor.sample_count); + break; + + case 23: + ApplyLinearEnvelopeGain<23>(output, input, prev_volume, ramp, processor.sample_count); + break; + + default: + LOG_ERROR(Service_Audio, "Invalid precision {}", precision); + break; + } +} + +bool VolumeRampCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/volume_ramp.h b/src/audio_core/renderer/command/mix/volume_ramp.h new file mode 100644 index 0000000000..77b61547ee --- /dev/null +++ b/src/audio_core/renderer/command/mix/volume_ramp.h @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for applying volume to a mix buffer, with ramping for the volume to smooth + * out the transition. + */ +struct VolumeRampCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Fixed point precision + u8 precision; + /// Input mix buffer index + s16 input_index; + /// Output mix buffer index + s16 output_index; + /// Previous mix volume applied to the input + f32 prev_volume; + /// Current mix volume applied to the input + f32 volume; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/performance/performance.cpp b/src/audio_core/renderer/command/performance/performance.cpp new file mode 100644 index 0000000000..985958b039 --- /dev/null +++ b/src/audio_core/renderer/command/performance/performance.cpp @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/performance/performance.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/core_timing_util.h" + +namespace AudioCore::AudioRenderer { + +void PerformanceCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("PerformanceCommand\n\tstate {}\n", static_cast(state)); +} + +void PerformanceCommand::Process(const ADSP::CommandListProcessor& processor) { + auto base{entry_address.translated_address}; + if (state == PerformanceState::Start) { + auto start_time_ptr{reinterpret_cast(base + entry_address.entry_start_time_offset)}; + *start_time_ptr = static_cast( + Core::Timing::CyclesToUs(processor.system->CoreTiming().GetClockTicks() - + processor.start_time - processor.current_processing_time) + .count()); + } else if (state == PerformanceState::Stop) { + auto processed_time_ptr{ + reinterpret_cast(base + entry_address.entry_processed_time_offset)}; + auto entry_count_ptr{ + reinterpret_cast(base + entry_address.header_entry_count_offset)}; + + *processed_time_ptr = static_cast( + Core::Timing::CyclesToUs(processor.system->CoreTiming().GetClockTicks() - + processor.start_time - processor.current_processing_time) + .count()); + (*entry_count_ptr)++; + } +} + +bool PerformanceCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/performance/performance.h b/src/audio_core/renderer/command/performance/performance.h new file mode 100644 index 0000000000..11a7d6c081 --- /dev/null +++ b/src/audio_core/renderer/command/performance/performance.h @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "audio_core/renderer/performance/performance_entry_addresses.h" +#include "audio_core/renderer/performance/performance_manager.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for writing AudioRenderer performance metrics back to the sysmodule. + */ +struct PerformanceCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// State of the performance + PerformanceState state; + /// Pointers to be written + PerformanceEntryAddresses entry_address; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.cpp b/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.cpp new file mode 100644 index 0000000000..1fd90308ad --- /dev/null +++ b/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.cpp @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/resample/downmix_6ch_to_2ch.h" + +namespace AudioCore::AudioRenderer { + +void DownMix6chTo2chCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("DownMix6chTo2chCommand\n\tinputs: "); + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n\toutputs: "; + for (u32 i = 0; i < MaxChannels; i++) { + string += fmt::format("{:02X}, ", outputs[i]); + } + string += "\n"; +} + +void DownMix6chTo2chCommand::Process(const ADSP::CommandListProcessor& processor) { + auto in_front_left{ + processor.mix_buffers.subspan(inputs[0] * processor.sample_count, processor.sample_count)}; + auto in_front_right{ + processor.mix_buffers.subspan(inputs[1] * processor.sample_count, processor.sample_count)}; + auto in_center{ + processor.mix_buffers.subspan(inputs[2] * processor.sample_count, processor.sample_count)}; + auto in_lfe{ + processor.mix_buffers.subspan(inputs[3] * processor.sample_count, processor.sample_count)}; + auto in_back_left{ + processor.mix_buffers.subspan(inputs[4] * processor.sample_count, processor.sample_count)}; + auto in_back_right{ + processor.mix_buffers.subspan(inputs[5] * processor.sample_count, processor.sample_count)}; + + auto out_front_left{ + processor.mix_buffers.subspan(outputs[0] * processor.sample_count, processor.sample_count)}; + auto out_front_right{ + processor.mix_buffers.subspan(outputs[1] * processor.sample_count, processor.sample_count)}; + auto out_center{ + processor.mix_buffers.subspan(outputs[2] * processor.sample_count, processor.sample_count)}; + auto out_lfe{ + processor.mix_buffers.subspan(outputs[3] * processor.sample_count, processor.sample_count)}; + auto out_back_left{ + processor.mix_buffers.subspan(outputs[4] * processor.sample_count, processor.sample_count)}; + auto out_back_right{ + processor.mix_buffers.subspan(outputs[5] * processor.sample_count, processor.sample_count)}; + + for (u32 i = 0; i < processor.sample_count; i++) { + const auto left_sample{(in_front_left[i] * down_mix_coeff[0] + + in_center[i] * down_mix_coeff[1] + in_lfe[i] * down_mix_coeff[2] + + in_back_left[i] * down_mix_coeff[3]) + .to_int()}; + + const auto right_sample{(in_front_right[i] * down_mix_coeff[0] + + in_center[i] * down_mix_coeff[1] + in_lfe[i] * down_mix_coeff[2] + + in_back_right[i] * down_mix_coeff[3]) + .to_int()}; + + out_front_left[i] = left_sample; + out_front_right[i] = right_sample; + } + + std::memset(out_center.data(), 0, out_center.size_bytes()); + std::memset(out_lfe.data(), 0, out_lfe.size_bytes()); + std::memset(out_back_left.data(), 0, out_back_left.size_bytes()); + std::memset(out_back_right.data(), 0, out_back_right.size_bytes()); +} + +bool DownMix6chTo2chCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.h b/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.h new file mode 100644 index 0000000000..dc133a73be --- /dev/null +++ b/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.h @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for downmixing 6 channels to 2. + * Channel layout (SMPTE): + * 0 - front left + * 1 - front right + * 2 - center + * 3 - lfe + * 4 - back left + * 5 - back right + */ +struct DownMix6chTo2chCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Input mix buffer offsets for each channel + std::array inputs; + /// Output mix buffer offsets for each channel + std::array outputs; + /// Coefficients used for downmixing + std::array, 4> down_mix_coeff; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/resample/resample.cpp b/src/audio_core/renderer/command/resample/resample.cpp new file mode 100644 index 0000000000..070c9d2b88 --- /dev/null +++ b/src/audio_core/renderer/command/resample/resample.cpp @@ -0,0 +1,883 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/command/resample/resample.h" + +namespace AudioCore::AudioRenderer { + +static void ResampleLowQuality(std::span output, std::span input, + const Common::FixedPoint<49, 15>& sample_rate_ratio, + Common::FixedPoint<49, 15>& fraction, const u32 samples_to_write) { + if (sample_rate_ratio == 1.0f) { + for (u32 i = 0; i < samples_to_write; i++) { + output[i] = input[i]; + } + } else { + u32 read_index{0}; + for (u32 i = 0; i < samples_to_write; i++) { + output[i] = input[read_index + (fraction >= 0.5f)]; + fraction += sample_rate_ratio; + read_index += static_cast(fraction.to_int_floor()); + fraction.clear_int(); + } + } +} + +static void ResampleNormalQuality(std::span output, std::span input, + const Common::FixedPoint<49, 15>& sample_rate_ratio, + Common::FixedPoint<49, 15>& fraction, + const u32 samples_to_write) { + static constexpr std::array lut0 = { + 0.20141602f, 0.59283447f, 0.20513916f, 0.00009155f, 0.19772339f, 0.59277344f, 0.20889282f, + 0.00027466f, 0.19406128f, 0.59262085f, 0.21264648f, 0.00045776f, 0.19039917f, 0.59240723f, + 0.21646118f, 0.00067139f, 0.18679810f, 0.59213257f, 0.22030640f, 0.00085449f, 0.18322754f, + 0.59176636f, 0.22415161f, 0.00103760f, 0.17968750f, 0.59133911f, 0.22802734f, 0.00125122f, + 0.17617798f, 0.59085083f, 0.23193359f, 0.00146484f, 0.17269897f, 0.59027100f, 0.23583984f, + 0.00167847f, 0.16925049f, 0.58963013f, 0.23977661f, 0.00189209f, 0.16583252f, 0.58892822f, + 0.24374390f, 0.00210571f, 0.16244507f, 0.58816528f, 0.24774170f, 0.00234985f, 0.15908813f, + 0.58731079f, 0.25173950f, 0.00256348f, 0.15576172f, 0.58639526f, 0.25576782f, 0.00280762f, + 0.15249634f, 0.58541870f, 0.25979614f, 0.00308228f, 0.14923096f, 0.58435059f, 0.26385498f, + 0.00332642f, 0.14602661f, 0.58325195f, 0.26794434f, 0.00360107f, 0.14285278f, 0.58206177f, + 0.27203369f, 0.00387573f, 0.13973999f, 0.58078003f, 0.27612305f, 0.00418091f, 0.13662720f, + 0.57946777f, 0.28024292f, 0.00448608f, 0.13357544f, 0.57806396f, 0.28436279f, 0.00479126f, + 0.13052368f, 0.57662964f, 0.28851318f, 0.00512695f, 0.12753296f, 0.57510376f, 0.29266357f, + 0.00546265f, 0.12460327f, 0.57351685f, 0.29681396f, 0.00579834f, 0.12167358f, 0.57183838f, + 0.30099487f, 0.00616455f, 0.11880493f, 0.57012939f, 0.30517578f, 0.00656128f, 0.11596680f, + 0.56835938f, 0.30935669f, 0.00695801f, 0.11318970f, 0.56649780f, 0.31353760f, 0.00735474f, + 0.11041260f, 0.56457520f, 0.31771851f, 0.00778198f, 0.10769653f, 0.56262207f, 0.32192993f, + 0.00823975f, 0.10501099f, 0.56057739f, 0.32614136f, 0.00869751f, 0.10238647f, 0.55847168f, + 0.33032227f, 0.00915527f, 0.09976196f, 0.55633545f, 0.33453369f, 0.00967407f, 0.09722900f, + 0.55410767f, 0.33874512f, 0.01019287f, 0.09469604f, 0.55181885f, 0.34295654f, 0.01071167f, + 0.09222412f, 0.54949951f, 0.34713745f, 0.01126099f, 0.08978271f, 0.54708862f, 0.35134888f, + 0.01184082f, 0.08737183f, 0.54464722f, 0.35552979f, 0.01245117f, 0.08499146f, 0.54214478f, + 0.35974121f, 0.01306152f, 0.08267212f, 0.53958130f, 0.36392212f, 0.01370239f, 0.08041382f, + 0.53695679f, 0.36810303f, 0.01437378f, 0.07815552f, 0.53427124f, 0.37225342f, 0.01507568f, + 0.07595825f, 0.53155518f, 0.37640381f, 0.01577759f, 0.07379150f, 0.52877808f, 0.38055420f, + 0.01651001f, 0.07165527f, 0.52593994f, 0.38470459f, 0.01727295f, 0.06958008f, 0.52307129f, + 0.38882446f, 0.01806641f, 0.06753540f, 0.52014160f, 0.39294434f, 0.01889038f, 0.06552124f, + 0.51715088f, 0.39703369f, 0.01974487f, 0.06356812f, 0.51409912f, 0.40112305f, 0.02059937f, + 0.06164551f, 0.51101685f, 0.40518188f, 0.02148438f, 0.05975342f, 0.50790405f, 0.40921021f, + 0.02243042f, 0.05789185f, 0.50473022f, 0.41323853f, 0.02337646f, 0.05609131f, 0.50152588f, + 0.41726685f, 0.02435303f, 0.05432129f, 0.49826050f, 0.42123413f, 0.02539062f, 0.05258179f, + 0.49493408f, 0.42520142f, 0.02642822f, 0.05087280f, 0.49160767f, 0.42913818f, 0.02749634f, + 0.04922485f, 0.48822021f, 0.43307495f, 0.02859497f, 0.04760742f, 0.48477173f, 0.43695068f, + 0.02975464f, 0.04602051f, 0.48132324f, 0.44082642f, 0.03091431f, 0.04446411f, 0.47781372f, + 0.44467163f, 0.03210449f, 0.04293823f, 0.47424316f, 0.44845581f, 0.03335571f, 0.04147339f, + 0.47067261f, 0.45223999f, 0.03460693f, 0.04003906f, 0.46704102f, 0.45599365f, 0.03591919f, + 0.03863525f, 0.46340942f, 0.45971680f, 0.03726196f, 0.03726196f, 0.45971680f, 0.46340942f, + 0.03863525f, 0.03591919f, 0.45599365f, 0.46704102f, 0.04003906f, 0.03460693f, 0.45223999f, + 0.47067261f, 0.04147339f, 0.03335571f, 0.44845581f, 0.47424316f, 0.04293823f, 0.03210449f, + 0.44467163f, 0.47781372f, 0.04446411f, 0.03091431f, 0.44082642f, 0.48132324f, 0.04602051f, + 0.02975464f, 0.43695068f, 0.48477173f, 0.04760742f, 0.02859497f, 0.43307495f, 0.48822021f, + 0.04922485f, 0.02749634f, 0.42913818f, 0.49160767f, 0.05087280f, 0.02642822f, 0.42520142f, + 0.49493408f, 0.05258179f, 0.02539062f, 0.42123413f, 0.49826050f, 0.05432129f, 0.02435303f, + 0.41726685f, 0.50152588f, 0.05609131f, 0.02337646f, 0.41323853f, 0.50473022f, 0.05789185f, + 0.02243042f, 0.40921021f, 0.50790405f, 0.05975342f, 0.02148438f, 0.40518188f, 0.51101685f, + 0.06164551f, 0.02059937f, 0.40112305f, 0.51409912f, 0.06356812f, 0.01974487f, 0.39703369f, + 0.51715088f, 0.06552124f, 0.01889038f, 0.39294434f, 0.52014160f, 0.06753540f, 0.01806641f, + 0.38882446f, 0.52307129f, 0.06958008f, 0.01727295f, 0.38470459f, 0.52593994f, 0.07165527f, + 0.01651001f, 0.38055420f, 0.52877808f, 0.07379150f, 0.01577759f, 0.37640381f, 0.53155518f, + 0.07595825f, 0.01507568f, 0.37225342f, 0.53427124f, 0.07815552f, 0.01437378f, 0.36810303f, + 0.53695679f, 0.08041382f, 0.01370239f, 0.36392212f, 0.53958130f, 0.08267212f, 0.01306152f, + 0.35974121f, 0.54214478f, 0.08499146f, 0.01245117f, 0.35552979f, 0.54464722f, 0.08737183f, + 0.01184082f, 0.35134888f, 0.54708862f, 0.08978271f, 0.01126099f, 0.34713745f, 0.54949951f, + 0.09222412f, 0.01071167f, 0.34295654f, 0.55181885f, 0.09469604f, 0.01019287f, 0.33874512f, + 0.55410767f, 0.09722900f, 0.00967407f, 0.33453369f, 0.55633545f, 0.09976196f, 0.00915527f, + 0.33032227f, 0.55847168f, 0.10238647f, 0.00869751f, 0.32614136f, 0.56057739f, 0.10501099f, + 0.00823975f, 0.32192993f, 0.56262207f, 0.10769653f, 0.00778198f, 0.31771851f, 0.56457520f, + 0.11041260f, 0.00735474f, 0.31353760f, 0.56649780f, 0.11318970f, 0.00695801f, 0.30935669f, + 0.56835938f, 0.11596680f, 0.00656128f, 0.30517578f, 0.57012939f, 0.11880493f, 0.00616455f, + 0.30099487f, 0.57183838f, 0.12167358f, 0.00579834f, 0.29681396f, 0.57351685f, 0.12460327f, + 0.00546265f, 0.29266357f, 0.57510376f, 0.12753296f, 0.00512695f, 0.28851318f, 0.57662964f, + 0.13052368f, 0.00479126f, 0.28436279f, 0.57806396f, 0.13357544f, 0.00448608f, 0.28024292f, + 0.57946777f, 0.13662720f, 0.00418091f, 0.27612305f, 0.58078003f, 0.13973999f, 0.00387573f, + 0.27203369f, 0.58206177f, 0.14285278f, 0.00360107f, 0.26794434f, 0.58325195f, 0.14602661f, + 0.00332642f, 0.26385498f, 0.58435059f, 0.14923096f, 0.00308228f, 0.25979614f, 0.58541870f, + 0.15249634f, 0.00280762f, 0.25576782f, 0.58639526f, 0.15576172f, 0.00256348f, 0.25173950f, + 0.58731079f, 0.15908813f, 0.00234985f, 0.24774170f, 0.58816528f, 0.16244507f, 0.00210571f, + 0.24374390f, 0.58892822f, 0.16583252f, 0.00189209f, 0.23977661f, 0.58963013f, 0.16925049f, + 0.00167847f, 0.23583984f, 0.59027100f, 0.17269897f, 0.00146484f, 0.23193359f, 0.59085083f, + 0.17617798f, 0.00125122f, 0.22802734f, 0.59133911f, 0.17968750f, 0.00103760f, 0.22415161f, + 0.59176636f, 0.18322754f, 0.00085449f, 0.22030640f, 0.59213257f, 0.18679810f, 0.00067139f, + 0.21646118f, 0.59240723f, 0.19039917f, 0.00045776f, 0.21264648f, 0.59262085f, 0.19406128f, + 0.00027466f, 0.20889282f, 0.59277344f, 0.19772339f, 0.00009155f, 0.20513916f, 0.59283447f, + 0.20141602f, + }; + + static constexpr std::array lut1 = { + 0.00207520f, 0.99606323f, 0.00210571f, -0.00015259f, -0.00610352f, 0.99578857f, + 0.00646973f, -0.00045776f, -0.01000977f, 0.99526978f, 0.01095581f, -0.00079346f, + -0.01373291f, 0.99444580f, 0.01562500f, -0.00109863f, -0.01733398f, 0.99337769f, + 0.02041626f, -0.00143433f, -0.02075195f, 0.99203491f, 0.02539062f, -0.00177002f, + -0.02404785f, 0.99041748f, 0.03051758f, -0.00210571f, -0.02719116f, 0.98855591f, + 0.03582764f, -0.00244141f, -0.03021240f, 0.98641968f, 0.04125977f, -0.00280762f, + -0.03308105f, 0.98400879f, 0.04687500f, -0.00314331f, -0.03579712f, 0.98135376f, + 0.05261230f, -0.00350952f, -0.03839111f, 0.97842407f, 0.05856323f, -0.00390625f, + -0.04083252f, 0.97521973f, 0.06463623f, -0.00427246f, -0.04315186f, 0.97180176f, + 0.07086182f, -0.00466919f, -0.04534912f, 0.96810913f, 0.07727051f, -0.00509644f, + -0.04742432f, 0.96414185f, 0.08383179f, -0.00549316f, -0.04934692f, 0.95996094f, + 0.09054565f, -0.00592041f, -0.05114746f, 0.95550537f, 0.09741211f, -0.00637817f, + -0.05285645f, 0.95083618f, 0.10443115f, -0.00683594f, -0.05441284f, 0.94589233f, + 0.11160278f, -0.00732422f, -0.05584717f, 0.94073486f, 0.11892700f, -0.00781250f, + -0.05718994f, 0.93533325f, 0.12643433f, -0.00830078f, -0.05841064f, 0.92968750f, + 0.13406372f, -0.00881958f, -0.05953979f, 0.92382812f, 0.14184570f, -0.00936890f, + -0.06054688f, 0.91772461f, 0.14978027f, -0.00991821f, -0.06146240f, 0.91143799f, + 0.15783691f, -0.01046753f, -0.06225586f, 0.90490723f, 0.16607666f, -0.01104736f, + -0.06295776f, 0.89816284f, 0.17443848f, -0.01165771f, -0.06356812f, 0.89120483f, + 0.18292236f, -0.01229858f, -0.06408691f, 0.88403320f, 0.19155884f, -0.01293945f, + -0.06451416f, 0.87667847f, 0.20034790f, -0.01358032f, -0.06484985f, 0.86914062f, + 0.20925903f, -0.01428223f, -0.06509399f, 0.86138916f, 0.21829224f, -0.01495361f, + -0.06527710f, 0.85345459f, 0.22744751f, -0.01568604f, -0.06536865f, 0.84533691f, + 0.23675537f, -0.01641846f, -0.06536865f, 0.83703613f, 0.24615479f, -0.01718140f, + -0.06533813f, 0.82858276f, 0.25567627f, -0.01794434f, -0.06518555f, 0.81991577f, + 0.26531982f, -0.01873779f, -0.06500244f, 0.81112671f, 0.27505493f, -0.01956177f, + -0.06472778f, 0.80215454f, 0.28491211f, -0.02038574f, -0.06442261f, 0.79306030f, + 0.29489136f, -0.02124023f, -0.06402588f, 0.78378296f, 0.30496216f, -0.02209473f, + -0.06359863f, 0.77438354f, 0.31512451f, -0.02297974f, -0.06307983f, 0.76486206f, + 0.32537842f, -0.02389526f, -0.06253052f, 0.75518799f, 0.33569336f, -0.02481079f, + -0.06195068f, 0.74539185f, 0.34613037f, -0.02575684f, -0.06130981f, 0.73547363f, + 0.35662842f, -0.02670288f, -0.06060791f, 0.72543335f, 0.36721802f, -0.02767944f, + -0.05987549f, 0.71527100f, 0.37786865f, -0.02865601f, -0.05911255f, 0.70504761f, + 0.38858032f, -0.02966309f, -0.05831909f, 0.69470215f, 0.39935303f, -0.03067017f, + -0.05746460f, 0.68426514f, 0.41018677f, -0.03170776f, -0.05661011f, 0.67373657f, + 0.42108154f, -0.03271484f, -0.05569458f, 0.66311646f, 0.43200684f, -0.03378296f, + -0.05477905f, 0.65246582f, 0.44299316f, -0.03482056f, -0.05383301f, 0.64169312f, + 0.45401001f, -0.03588867f, -0.05285645f, 0.63088989f, 0.46505737f, -0.03695679f, + -0.05187988f, 0.62002563f, 0.47613525f, -0.03802490f, -0.05087280f, 0.60910034f, + 0.48721313f, -0.03912354f, -0.04983521f, 0.59814453f, 0.49832153f, -0.04019165f, + -0.04879761f, 0.58712769f, 0.50946045f, -0.04129028f, -0.04772949f, 0.57611084f, + 0.52056885f, -0.04235840f, -0.04669189f, 0.56503296f, 0.53170776f, -0.04345703f, + -0.04562378f, 0.55392456f, 0.54281616f, -0.04452515f, -0.04452515f, 0.54281616f, + 0.55392456f, -0.04562378f, -0.04345703f, 0.53170776f, 0.56503296f, -0.04669189f, + -0.04235840f, 0.52056885f, 0.57611084f, -0.04772949f, -0.04129028f, 0.50946045f, + 0.58712769f, -0.04879761f, -0.04019165f, 0.49832153f, 0.59814453f, -0.04983521f, + -0.03912354f, 0.48721313f, 0.60910034f, -0.05087280f, -0.03802490f, 0.47613525f, + 0.62002563f, -0.05187988f, -0.03695679f, 0.46505737f, 0.63088989f, -0.05285645f, + -0.03588867f, 0.45401001f, 0.64169312f, -0.05383301f, -0.03482056f, 0.44299316f, + 0.65246582f, -0.05477905f, -0.03378296f, 0.43200684f, 0.66311646f, -0.05569458f, + -0.03271484f, 0.42108154f, 0.67373657f, -0.05661011f, -0.03170776f, 0.41018677f, + 0.68426514f, -0.05746460f, -0.03067017f, 0.39935303f, 0.69470215f, -0.05831909f, + -0.02966309f, 0.38858032f, 0.70504761f, -0.05911255f, -0.02865601f, 0.37786865f, + 0.71527100f, -0.05987549f, -0.02767944f, 0.36721802f, 0.72543335f, -0.06060791f, + -0.02670288f, 0.35662842f, 0.73547363f, -0.06130981f, -0.02575684f, 0.34613037f, + 0.74539185f, -0.06195068f, -0.02481079f, 0.33569336f, 0.75518799f, -0.06253052f, + -0.02389526f, 0.32537842f, 0.76486206f, -0.06307983f, -0.02297974f, 0.31512451f, + 0.77438354f, -0.06359863f, -0.02209473f, 0.30496216f, 0.78378296f, -0.06402588f, + -0.02124023f, 0.29489136f, 0.79306030f, -0.06442261f, -0.02038574f, 0.28491211f, + 0.80215454f, -0.06472778f, -0.01956177f, 0.27505493f, 0.81112671f, -0.06500244f, + -0.01873779f, 0.26531982f, 0.81991577f, -0.06518555f, -0.01794434f, 0.25567627f, + 0.82858276f, -0.06533813f, -0.01718140f, 0.24615479f, 0.83703613f, -0.06536865f, + -0.01641846f, 0.23675537f, 0.84533691f, -0.06536865f, -0.01568604f, 0.22744751f, + 0.85345459f, -0.06527710f, -0.01495361f, 0.21829224f, 0.86138916f, -0.06509399f, + -0.01428223f, 0.20925903f, 0.86914062f, -0.06484985f, -0.01358032f, 0.20034790f, + 0.87667847f, -0.06451416f, -0.01293945f, 0.19155884f, 0.88403320f, -0.06408691f, + -0.01229858f, 0.18292236f, 0.89120483f, -0.06356812f, -0.01165771f, 0.17443848f, + 0.89816284f, -0.06295776f, -0.01104736f, 0.16607666f, 0.90490723f, -0.06225586f, + -0.01046753f, 0.15783691f, 0.91143799f, -0.06146240f, -0.00991821f, 0.14978027f, + 0.91772461f, -0.06054688f, -0.00936890f, 0.14184570f, 0.92382812f, -0.05953979f, + -0.00881958f, 0.13406372f, 0.92968750f, -0.05841064f, -0.00830078f, 0.12643433f, + 0.93533325f, -0.05718994f, -0.00781250f, 0.11892700f, 0.94073486f, -0.05584717f, + -0.00732422f, 0.11160278f, 0.94589233f, -0.05441284f, -0.00683594f, 0.10443115f, + 0.95083618f, -0.05285645f, -0.00637817f, 0.09741211f, 0.95550537f, -0.05114746f, + -0.00592041f, 0.09054565f, 0.95996094f, -0.04934692f, -0.00549316f, 0.08383179f, + 0.96414185f, -0.04742432f, -0.00509644f, 0.07727051f, 0.96810913f, -0.04534912f, + -0.00466919f, 0.07086182f, 0.97180176f, -0.04315186f, -0.00427246f, 0.06463623f, + 0.97521973f, -0.04083252f, -0.00390625f, 0.05856323f, 0.97842407f, -0.03839111f, + -0.00350952f, 0.05261230f, 0.98135376f, -0.03579712f, -0.00314331f, 0.04687500f, + 0.98400879f, -0.03308105f, -0.00280762f, 0.04125977f, 0.98641968f, -0.03021240f, + -0.00244141f, 0.03582764f, 0.98855591f, -0.02719116f, -0.00210571f, 0.03051758f, + 0.99041748f, -0.02404785f, -0.00177002f, 0.02539062f, 0.99203491f, -0.02075195f, + -0.00143433f, 0.02041626f, 0.99337769f, -0.01733398f, -0.00109863f, 0.01562500f, + 0.99444580f, -0.01373291f, -0.00079346f, 0.01095581f, 0.99526978f, -0.01000977f, + -0.00045776f, 0.00646973f, 0.99578857f, -0.00610352f, -0.00015259f, 0.00210571f, + 0.99606323f, -0.00207520f, + }; + + static constexpr std::array lut2 = { + 0.09750366f, 0.80221558f, 0.10159302f, -0.00097656f, 0.09350586f, 0.80203247f, + 0.10580444f, -0.00103760f, 0.08959961f, 0.80169678f, 0.11010742f, -0.00115967f, + 0.08578491f, 0.80117798f, 0.11447144f, -0.00128174f, 0.08203125f, 0.80047607f, + 0.11892700f, -0.00140381f, 0.07836914f, 0.79962158f, 0.12347412f, -0.00152588f, + 0.07479858f, 0.79861450f, 0.12814331f, -0.00164795f, 0.07135010f, 0.79742432f, + 0.13287354f, -0.00177002f, 0.06796265f, 0.79605103f, 0.13769531f, -0.00192261f, + 0.06469727f, 0.79452515f, 0.14260864f, -0.00204468f, 0.06149292f, 0.79284668f, + 0.14761353f, -0.00219727f, 0.05834961f, 0.79098511f, 0.15270996f, -0.00231934f, + 0.05532837f, 0.78894043f, 0.15789795f, -0.00247192f, 0.05236816f, 0.78674316f, + 0.16317749f, -0.00265503f, 0.04949951f, 0.78442383f, 0.16851807f, -0.00280762f, + 0.04672241f, 0.78189087f, 0.17398071f, -0.00299072f, 0.04400635f, 0.77920532f, + 0.17950439f, -0.00314331f, 0.04141235f, 0.77636719f, 0.18511963f, -0.00332642f, + 0.03887939f, 0.77337646f, 0.19082642f, -0.00350952f, 0.03640747f, 0.77023315f, + 0.19659424f, -0.00369263f, 0.03402710f, 0.76693726f, 0.20248413f, -0.00387573f, + 0.03173828f, 0.76348877f, 0.20843506f, -0.00405884f, 0.02951050f, 0.75985718f, + 0.21444702f, -0.00427246f, 0.02737427f, 0.75610352f, 0.22055054f, -0.00445557f, + 0.02529907f, 0.75219727f, 0.22674561f, -0.00466919f, 0.02331543f, 0.74816895f, + 0.23300171f, -0.00485229f, 0.02139282f, 0.74398804f, 0.23931885f, -0.00506592f, + 0.01956177f, 0.73965454f, 0.24572754f, -0.00531006f, 0.01779175f, 0.73519897f, + 0.25219727f, -0.00552368f, 0.01605225f, 0.73059082f, 0.25872803f, -0.00570679f, + 0.01440430f, 0.72586060f, 0.26535034f, -0.00592041f, 0.01281738f, 0.72100830f, + 0.27203369f, -0.00616455f, 0.01132202f, 0.71600342f, 0.27877808f, -0.00637817f, + 0.00988770f, 0.71090698f, 0.28558350f, -0.00656128f, 0.00851440f, 0.70565796f, + 0.29244995f, -0.00677490f, 0.00720215f, 0.70031738f, 0.29934692f, -0.00701904f, + 0.00592041f, 0.69485474f, 0.30633545f, -0.00723267f, 0.00469971f, 0.68927002f, + 0.31338501f, -0.00741577f, 0.00357056f, 0.68356323f, 0.32046509f, -0.00762939f, + 0.00247192f, 0.67773438f, 0.32760620f, -0.00787354f, 0.00143433f, 0.67184448f, + 0.33477783f, -0.00808716f, 0.00045776f, 0.66583252f, 0.34197998f, -0.00827026f, + -0.00048828f, 0.65972900f, 0.34924316f, -0.00845337f, -0.00134277f, 0.65353394f, + 0.35656738f, -0.00863647f, -0.00216675f, 0.64721680f, 0.36389160f, -0.00885010f, + -0.00296021f, 0.64083862f, 0.37127686f, -0.00903320f, -0.00369263f, 0.63433838f, + 0.37869263f, -0.00921631f, -0.00436401f, 0.62777710f, 0.38613892f, -0.00933838f, + -0.00497437f, 0.62115479f, 0.39361572f, -0.00949097f, -0.00558472f, 0.61444092f, + 0.40109253f, -0.00964355f, -0.00613403f, 0.60763550f, 0.40859985f, -0.00979614f, + -0.00665283f, 0.60076904f, 0.41610718f, -0.00991821f, -0.00714111f, 0.59384155f, + 0.42364502f, -0.01000977f, -0.00756836f, 0.58685303f, 0.43121338f, -0.01013184f, + -0.00796509f, 0.57977295f, 0.43875122f, -0.01022339f, -0.00833130f, 0.57266235f, + 0.44631958f, -0.01028442f, -0.00866699f, 0.56552124f, 0.45388794f, -0.01034546f, + -0.00897217f, 0.55831909f, 0.46145630f, -0.01040649f, -0.00921631f, 0.55105591f, + 0.46902466f, -0.01040649f, -0.00946045f, 0.54373169f, 0.47659302f, -0.01040649f, + -0.00967407f, 0.53640747f, 0.48413086f, -0.01037598f, -0.00985718f, 0.52902222f, + 0.49166870f, -0.01037598f, -0.01000977f, 0.52160645f, 0.49917603f, -0.01031494f, + -0.01013184f, 0.51416016f, 0.50668335f, -0.01025391f, -0.01025391f, 0.50668335f, + 0.51416016f, -0.01013184f, -0.01031494f, 0.49917603f, 0.52160645f, -0.01000977f, + -0.01037598f, 0.49166870f, 0.52902222f, -0.00985718f, -0.01037598f, 0.48413086f, + 0.53640747f, -0.00967407f, -0.01040649f, 0.47659302f, 0.54373169f, -0.00946045f, + -0.01040649f, 0.46902466f, 0.55105591f, -0.00921631f, -0.01040649f, 0.46145630f, + 0.55831909f, -0.00897217f, -0.01034546f, 0.45388794f, 0.56552124f, -0.00866699f, + -0.01028442f, 0.44631958f, 0.57266235f, -0.00833130f, -0.01022339f, 0.43875122f, + 0.57977295f, -0.00796509f, -0.01013184f, 0.43121338f, 0.58685303f, -0.00756836f, + -0.01000977f, 0.42364502f, 0.59384155f, -0.00714111f, -0.00991821f, 0.41610718f, + 0.60076904f, -0.00665283f, -0.00979614f, 0.40859985f, 0.60763550f, -0.00613403f, + -0.00964355f, 0.40109253f, 0.61444092f, -0.00558472f, -0.00949097f, 0.39361572f, + 0.62115479f, -0.00497437f, -0.00933838f, 0.38613892f, 0.62777710f, -0.00436401f, + -0.00921631f, 0.37869263f, 0.63433838f, -0.00369263f, -0.00903320f, 0.37127686f, + 0.64083862f, -0.00296021f, -0.00885010f, 0.36389160f, 0.64721680f, -0.00216675f, + -0.00863647f, 0.35656738f, 0.65353394f, -0.00134277f, -0.00845337f, 0.34924316f, + 0.65972900f, -0.00048828f, -0.00827026f, 0.34197998f, 0.66583252f, 0.00045776f, + -0.00808716f, 0.33477783f, 0.67184448f, 0.00143433f, -0.00787354f, 0.32760620f, + 0.67773438f, 0.00247192f, -0.00762939f, 0.32046509f, 0.68356323f, 0.00357056f, + -0.00741577f, 0.31338501f, 0.68927002f, 0.00469971f, -0.00723267f, 0.30633545f, + 0.69485474f, 0.00592041f, -0.00701904f, 0.29934692f, 0.70031738f, 0.00720215f, + -0.00677490f, 0.29244995f, 0.70565796f, 0.00851440f, -0.00656128f, 0.28558350f, + 0.71090698f, 0.00988770f, -0.00637817f, 0.27877808f, 0.71600342f, 0.01132202f, + -0.00616455f, 0.27203369f, 0.72100830f, 0.01281738f, -0.00592041f, 0.26535034f, + 0.72586060f, 0.01440430f, -0.00570679f, 0.25872803f, 0.73059082f, 0.01605225f, + -0.00552368f, 0.25219727f, 0.73519897f, 0.01779175f, -0.00531006f, 0.24572754f, + 0.73965454f, 0.01956177f, -0.00506592f, 0.23931885f, 0.74398804f, 0.02139282f, + -0.00485229f, 0.23300171f, 0.74816895f, 0.02331543f, -0.00466919f, 0.22674561f, + 0.75219727f, 0.02529907f, -0.00445557f, 0.22055054f, 0.75610352f, 0.02737427f, + -0.00427246f, 0.21444702f, 0.75985718f, 0.02951050f, -0.00405884f, 0.20843506f, + 0.76348877f, 0.03173828f, -0.00387573f, 0.20248413f, 0.76693726f, 0.03402710f, + -0.00369263f, 0.19659424f, 0.77023315f, 0.03640747f, -0.00350952f, 0.19082642f, + 0.77337646f, 0.03887939f, -0.00332642f, 0.18511963f, 0.77636719f, 0.04141235f, + -0.00314331f, 0.17950439f, 0.77920532f, 0.04400635f, -0.00299072f, 0.17398071f, + 0.78189087f, 0.04672241f, -0.00280762f, 0.16851807f, 0.78442383f, 0.04949951f, + -0.00265503f, 0.16317749f, 0.78674316f, 0.05236816f, -0.00247192f, 0.15789795f, + 0.78894043f, 0.05532837f, -0.00231934f, 0.15270996f, 0.79098511f, 0.05834961f, + -0.00219727f, 0.14761353f, 0.79284668f, 0.06149292f, -0.00204468f, 0.14260864f, + 0.79452515f, 0.06469727f, -0.00192261f, 0.13769531f, 0.79605103f, 0.06796265f, + -0.00177002f, 0.13287354f, 0.79742432f, 0.07135010f, -0.00164795f, 0.12814331f, + 0.79861450f, 0.07479858f, -0.00152588f, 0.12347412f, 0.79962158f, 0.07836914f, + -0.00140381f, 0.11892700f, 0.80047607f, 0.08203125f, -0.00128174f, 0.11447144f, + 0.80117798f, 0.08578491f, -0.00115967f, 0.11010742f, 0.80169678f, 0.08959961f, + -0.00103760f, 0.10580444f, 0.80203247f, 0.09350586f, -0.00097656f, 0.10159302f, + 0.80221558f, 0.09750366f, + }; + + const auto get_lut = [&]() -> std::span { + if (sample_rate_ratio <= 1.0f) { + return std::span(lut2.data(), lut2.size()); + } else if (sample_rate_ratio < 1.3f) { + return std::span(lut1.data(), lut1.size()); + } else { + return std::span(lut0.data(), lut0.size()); + } + }; + + auto lut{get_lut()}; + u32 read_index{0}; + for (u32 i = 0; i < samples_to_write; i++) { + const auto lut_index{(fraction.get_frac() >> 8) * 4}; + const Common::FixedPoint<56, 8> sample0{input[read_index + 0] * lut[lut_index + 0]}; + const Common::FixedPoint<56, 8> sample1{input[read_index + 1] * lut[lut_index + 1]}; + const Common::FixedPoint<56, 8> sample2{input[read_index + 2] * lut[lut_index + 2]}; + const Common::FixedPoint<56, 8> sample3{input[read_index + 3] * lut[lut_index + 3]}; + output[i] = (sample0 + sample1 + sample2 + sample3).to_int_floor(); + fraction += sample_rate_ratio; + read_index += static_cast(fraction.to_int_floor()); + fraction.clear_int(); + } +} + +static void ResampleHighQuality(std::span output, std::span input, + const Common::FixedPoint<49, 15>& sample_rate_ratio, + Common::FixedPoint<49, 15>& fraction, const u32 samples_to_write) { + static constexpr std::array lut0 = { + -0.01776123f, -0.00070190f, 0.26672363f, 0.50006104f, 0.26956177f, 0.00024414f, + -0.01800537f, 0.00000000f, -0.01748657f, -0.00164795f, 0.26388550f, 0.50003052f, + 0.27236938f, 0.00122070f, -0.01824951f, -0.00003052f, -0.01724243f, -0.00256348f, + 0.26107788f, 0.49996948f, 0.27520752f, 0.00219727f, -0.01849365f, -0.00003052f, + -0.01699829f, -0.00344849f, 0.25823975f, 0.49984741f, 0.27801514f, 0.00320435f, + -0.01873779f, -0.00006104f, -0.01675415f, -0.00433350f, 0.25543213f, 0.49972534f, + 0.28085327f, 0.00424194f, -0.01898193f, -0.00006104f, -0.01651001f, -0.00518799f, + 0.25259399f, 0.49954224f, 0.28366089f, 0.00527954f, -0.01922607f, -0.00009155f, + -0.01626587f, -0.00604248f, 0.24978638f, 0.49932861f, 0.28646851f, 0.00634766f, + -0.01947021f, -0.00012207f, -0.01602173f, -0.00686646f, 0.24697876f, 0.49908447f, + 0.28930664f, 0.00744629f, -0.01971436f, -0.00015259f, -0.01574707f, -0.00765991f, + 0.24414062f, 0.49877930f, 0.29211426f, 0.00854492f, -0.01995850f, -0.00015259f, + -0.01550293f, -0.00845337f, 0.24133301f, 0.49847412f, 0.29492188f, 0.00967407f, + -0.02020264f, -0.00018311f, -0.01525879f, -0.00921631f, 0.23852539f, 0.49810791f, + 0.29772949f, 0.01083374f, -0.02044678f, -0.00021362f, -0.01501465f, -0.00997925f, + 0.23571777f, 0.49774170f, 0.30050659f, 0.01199341f, -0.02069092f, -0.00024414f, + -0.01477051f, -0.01071167f, 0.23291016f, 0.49731445f, 0.30331421f, 0.01318359f, + -0.02093506f, -0.00027466f, -0.01452637f, -0.01141357f, 0.23010254f, 0.49685669f, + 0.30609131f, 0.01437378f, -0.02117920f, -0.00030518f, -0.01428223f, -0.01211548f, + 0.22732544f, 0.49636841f, 0.30886841f, 0.01559448f, -0.02142334f, -0.00033569f, + -0.01403809f, -0.01278687f, 0.22451782f, 0.49581909f, 0.31164551f, 0.01684570f, + -0.02163696f, -0.00039673f, -0.01379395f, -0.01345825f, 0.22174072f, 0.49526978f, + 0.31442261f, 0.01809692f, -0.02188110f, -0.00042725f, -0.01358032f, -0.01409912f, + 0.21896362f, 0.49465942f, 0.31719971f, 0.01937866f, -0.02209473f, -0.00045776f, + -0.01333618f, -0.01473999f, 0.21618652f, 0.49404907f, 0.31994629f, 0.02069092f, + -0.02233887f, -0.00048828f, -0.01309204f, -0.01535034f, 0.21343994f, 0.49337769f, + 0.32269287f, 0.02203369f, -0.02255249f, -0.00054932f, -0.01284790f, -0.01596069f, + 0.21066284f, 0.49267578f, 0.32543945f, 0.02337646f, -0.02279663f, -0.00057983f, + -0.01263428f, -0.01654053f, 0.20791626f, 0.49194336f, 0.32818604f, 0.02471924f, + -0.02301025f, -0.00064087f, -0.01239014f, -0.01708984f, 0.20516968f, 0.49118042f, + 0.33090210f, 0.02612305f, -0.02322388f, -0.00067139f, -0.01214600f, -0.01763916f, + 0.20242310f, 0.49035645f, 0.33361816f, 0.02752686f, -0.02343750f, -0.00073242f, + -0.01193237f, -0.01818848f, 0.19970703f, 0.48953247f, 0.33633423f, 0.02896118f, + -0.02365112f, -0.00079346f, -0.01168823f, -0.01867676f, 0.19696045f, 0.48864746f, + 0.33901978f, 0.03039551f, -0.02386475f, -0.00082397f, -0.01147461f, -0.01919556f, + 0.19427490f, 0.48776245f, 0.34170532f, 0.03186035f, -0.02407837f, -0.00088501f, + -0.01123047f, -0.01968384f, 0.19155884f, 0.48681641f, 0.34439087f, 0.03335571f, + -0.02429199f, -0.00094604f, -0.01101685f, -0.02014160f, 0.18887329f, 0.48583984f, + 0.34704590f, 0.03485107f, -0.02447510f, -0.00100708f, -0.01080322f, -0.02059937f, + 0.18615723f, 0.48483276f, 0.34970093f, 0.03637695f, -0.02468872f, -0.00106812f, + -0.01058960f, -0.02102661f, 0.18350220f, 0.48379517f, 0.35235596f, 0.03793335f, + -0.02487183f, -0.00112915f, -0.01034546f, -0.02145386f, 0.18081665f, 0.48272705f, + 0.35498047f, 0.03948975f, -0.02505493f, -0.00119019f, -0.01013184f, -0.02188110f, + 0.17816162f, 0.48162842f, 0.35760498f, 0.04107666f, -0.02523804f, -0.00125122f, + -0.00991821f, -0.02227783f, 0.17550659f, 0.48049927f, 0.36019897f, 0.04269409f, + -0.02542114f, -0.00131226f, -0.00970459f, -0.02264404f, 0.17288208f, 0.47933960f, + 0.36279297f, 0.04431152f, -0.02560425f, -0.00140381f, -0.00952148f, -0.02301025f, + 0.17025757f, 0.47814941f, 0.36538696f, 0.04595947f, -0.02578735f, -0.00146484f, + -0.00930786f, -0.02337646f, 0.16763306f, 0.47689819f, 0.36795044f, 0.04763794f, + -0.02593994f, -0.00152588f, -0.00909424f, -0.02371216f, 0.16503906f, 0.47564697f, + 0.37048340f, 0.04931641f, -0.02609253f, -0.00161743f, -0.00888062f, -0.02401733f, + 0.16244507f, 0.47436523f, 0.37304688f, 0.05102539f, -0.02627563f, -0.00170898f, + -0.00869751f, -0.02435303f, 0.15988159f, 0.47302246f, 0.37554932f, 0.05276489f, + -0.02642822f, -0.00177002f, -0.00848389f, -0.02462769f, 0.15731812f, 0.47167969f, + 0.37805176f, 0.05450439f, -0.02658081f, -0.00186157f, -0.00830078f, -0.02493286f, + 0.15475464f, 0.47027588f, 0.38055420f, 0.05627441f, -0.02670288f, -0.00195312f, + -0.00808716f, -0.02520752f, 0.15222168f, 0.46887207f, 0.38302612f, 0.05804443f, + -0.02685547f, -0.00204468f, -0.00790405f, -0.02545166f, 0.14968872f, 0.46743774f, + 0.38546753f, 0.05987549f, -0.02697754f, -0.00213623f, -0.00772095f, -0.02569580f, + 0.14718628f, 0.46594238f, 0.38790894f, 0.06170654f, -0.02709961f, -0.00222778f, + -0.00753784f, -0.02593994f, 0.14468384f, 0.46444702f, 0.39031982f, 0.06353760f, + -0.02722168f, -0.00231934f, -0.00735474f, -0.02615356f, 0.14218140f, 0.46289062f, + 0.39273071f, 0.06539917f, -0.02734375f, -0.00241089f, -0.00717163f, -0.02636719f, + 0.13970947f, 0.46133423f, 0.39511108f, 0.06729126f, -0.02743530f, -0.00250244f, + -0.00698853f, -0.02655029f, 0.13726807f, 0.45974731f, 0.39749146f, 0.06918335f, + -0.02755737f, -0.00259399f, -0.00680542f, -0.02673340f, 0.13479614f, 0.45812988f, + 0.39984131f, 0.07113647f, -0.02764893f, -0.00271606f, -0.00662231f, -0.02691650f, + 0.13238525f, 0.45648193f, 0.40216064f, 0.07305908f, -0.02774048f, -0.00280762f, + -0.00643921f, -0.02706909f, 0.12997437f, 0.45480347f, 0.40447998f, 0.07504272f, + -0.02780151f, -0.00292969f, -0.00628662f, -0.02722168f, 0.12756348f, 0.45309448f, + 0.40676880f, 0.07699585f, -0.02789307f, -0.00305176f, -0.00610352f, -0.02734375f, + 0.12518311f, 0.45135498f, 0.40902710f, 0.07901001f, -0.02795410f, -0.00314331f, + -0.00595093f, -0.02746582f, 0.12280273f, 0.44958496f, 0.41128540f, 0.08102417f, + -0.02801514f, -0.00326538f, -0.00579834f, -0.02758789f, 0.12045288f, 0.44778442f, + 0.41351318f, 0.08306885f, -0.02804565f, -0.00338745f, -0.00561523f, -0.02770996f, + 0.11813354f, 0.44598389f, 0.41571045f, 0.08511353f, -0.02810669f, -0.00350952f, + -0.00546265f, -0.02780151f, 0.11581421f, 0.44412231f, 0.41787720f, 0.08718872f, + -0.02813721f, -0.00363159f, -0.00531006f, -0.02786255f, 0.11349487f, 0.44226074f, + 0.42004395f, 0.08929443f, -0.02816772f, -0.00375366f, -0.00515747f, -0.02795410f, + 0.11120605f, 0.44036865f, 0.42218018f, 0.09140015f, -0.02816772f, -0.00387573f, + -0.00500488f, -0.02801514f, 0.10894775f, 0.43844604f, 0.42431641f, 0.09353638f, + -0.02819824f, -0.00402832f, -0.00485229f, -0.02807617f, 0.10668945f, 0.43649292f, + 0.42639160f, 0.09570312f, -0.02819824f, -0.00415039f, -0.00469971f, -0.02810669f, + 0.10446167f, 0.43453979f, 0.42846680f, 0.09786987f, -0.02819824f, -0.00427246f, + -0.00457764f, -0.02813721f, 0.10223389f, 0.43252563f, 0.43051147f, 0.10003662f, + -0.02816772f, -0.00442505f, -0.00442505f, -0.02816772f, 0.10003662f, 0.43051147f, + 0.43252563f, 0.10223389f, -0.02813721f, -0.00457764f, -0.00427246f, -0.02819824f, + 0.09786987f, 0.42846680f, 0.43453979f, 0.10446167f, -0.02810669f, -0.00469971f, + -0.00415039f, -0.02819824f, 0.09570312f, 0.42639160f, 0.43649292f, 0.10668945f, + -0.02807617f, -0.00485229f, -0.00402832f, -0.02819824f, 0.09353638f, 0.42431641f, + 0.43844604f, 0.10894775f, -0.02801514f, -0.00500488f, -0.00387573f, -0.02816772f, + 0.09140015f, 0.42218018f, 0.44036865f, 0.11120605f, -0.02795410f, -0.00515747f, + -0.00375366f, -0.02816772f, 0.08929443f, 0.42004395f, 0.44226074f, 0.11349487f, + -0.02786255f, -0.00531006f, -0.00363159f, -0.02813721f, 0.08718872f, 0.41787720f, + 0.44412231f, 0.11581421f, -0.02780151f, -0.00546265f, -0.00350952f, -0.02810669f, + 0.08511353f, 0.41571045f, 0.44598389f, 0.11813354f, -0.02770996f, -0.00561523f, + -0.00338745f, -0.02804565f, 0.08306885f, 0.41351318f, 0.44778442f, 0.12045288f, + -0.02758789f, -0.00579834f, -0.00326538f, -0.02801514f, 0.08102417f, 0.41128540f, + 0.44958496f, 0.12280273f, -0.02746582f, -0.00595093f, -0.00314331f, -0.02795410f, + 0.07901001f, 0.40902710f, 0.45135498f, 0.12518311f, -0.02734375f, -0.00610352f, + -0.00305176f, -0.02789307f, 0.07699585f, 0.40676880f, 0.45309448f, 0.12756348f, + -0.02722168f, -0.00628662f, -0.00292969f, -0.02780151f, 0.07504272f, 0.40447998f, + 0.45480347f, 0.12997437f, -0.02706909f, -0.00643921f, -0.00280762f, -0.02774048f, + 0.07305908f, 0.40216064f, 0.45648193f, 0.13238525f, -0.02691650f, -0.00662231f, + -0.00271606f, -0.02764893f, 0.07113647f, 0.39984131f, 0.45812988f, 0.13479614f, + -0.02673340f, -0.00680542f, -0.00259399f, -0.02755737f, 0.06918335f, 0.39749146f, + 0.45974731f, 0.13726807f, -0.02655029f, -0.00698853f, -0.00250244f, -0.02743530f, + 0.06729126f, 0.39511108f, 0.46133423f, 0.13970947f, -0.02636719f, -0.00717163f, + -0.00241089f, -0.02734375f, 0.06539917f, 0.39273071f, 0.46289062f, 0.14218140f, + -0.02615356f, -0.00735474f, -0.00231934f, -0.02722168f, 0.06353760f, 0.39031982f, + 0.46444702f, 0.14468384f, -0.02593994f, -0.00753784f, -0.00222778f, -0.02709961f, + 0.06170654f, 0.38790894f, 0.46594238f, 0.14718628f, -0.02569580f, -0.00772095f, + -0.00213623f, -0.02697754f, 0.05987549f, 0.38546753f, 0.46743774f, 0.14968872f, + -0.02545166f, -0.00790405f, -0.00204468f, -0.02685547f, 0.05804443f, 0.38302612f, + 0.46887207f, 0.15222168f, -0.02520752f, -0.00808716f, -0.00195312f, -0.02670288f, + 0.05627441f, 0.38055420f, 0.47027588f, 0.15475464f, -0.02493286f, -0.00830078f, + -0.00186157f, -0.02658081f, 0.05450439f, 0.37805176f, 0.47167969f, 0.15731812f, + -0.02462769f, -0.00848389f, -0.00177002f, -0.02642822f, 0.05276489f, 0.37554932f, + 0.47302246f, 0.15988159f, -0.02435303f, -0.00869751f, -0.00170898f, -0.02627563f, + 0.05102539f, 0.37304688f, 0.47436523f, 0.16244507f, -0.02401733f, -0.00888062f, + -0.00161743f, -0.02609253f, 0.04931641f, 0.37048340f, 0.47564697f, 0.16503906f, + -0.02371216f, -0.00909424f, -0.00152588f, -0.02593994f, 0.04763794f, 0.36795044f, + 0.47689819f, 0.16763306f, -0.02337646f, -0.00930786f, -0.00146484f, -0.02578735f, + 0.04595947f, 0.36538696f, 0.47814941f, 0.17025757f, -0.02301025f, -0.00952148f, + -0.00140381f, -0.02560425f, 0.04431152f, 0.36279297f, 0.47933960f, 0.17288208f, + -0.02264404f, -0.00970459f, -0.00131226f, -0.02542114f, 0.04269409f, 0.36019897f, + 0.48049927f, 0.17550659f, -0.02227783f, -0.00991821f, -0.00125122f, -0.02523804f, + 0.04107666f, 0.35760498f, 0.48162842f, 0.17816162f, -0.02188110f, -0.01013184f, + -0.00119019f, -0.02505493f, 0.03948975f, 0.35498047f, 0.48272705f, 0.18081665f, + -0.02145386f, -0.01034546f, -0.00112915f, -0.02487183f, 0.03793335f, 0.35235596f, + 0.48379517f, 0.18350220f, -0.02102661f, -0.01058960f, -0.00106812f, -0.02468872f, + 0.03637695f, 0.34970093f, 0.48483276f, 0.18615723f, -0.02059937f, -0.01080322f, + -0.00100708f, -0.02447510f, 0.03485107f, 0.34704590f, 0.48583984f, 0.18887329f, + -0.02014160f, -0.01101685f, -0.00094604f, -0.02429199f, 0.03335571f, 0.34439087f, + 0.48681641f, 0.19155884f, -0.01968384f, -0.01123047f, -0.00088501f, -0.02407837f, + 0.03186035f, 0.34170532f, 0.48776245f, 0.19427490f, -0.01919556f, -0.01147461f, + -0.00082397f, -0.02386475f, 0.03039551f, 0.33901978f, 0.48864746f, 0.19696045f, + -0.01867676f, -0.01168823f, -0.00079346f, -0.02365112f, 0.02896118f, 0.33633423f, + 0.48953247f, 0.19970703f, -0.01818848f, -0.01193237f, -0.00073242f, -0.02343750f, + 0.02752686f, 0.33361816f, 0.49035645f, 0.20242310f, -0.01763916f, -0.01214600f, + -0.00067139f, -0.02322388f, 0.02612305f, 0.33090210f, 0.49118042f, 0.20516968f, + -0.01708984f, -0.01239014f, -0.00064087f, -0.02301025f, 0.02471924f, 0.32818604f, + 0.49194336f, 0.20791626f, -0.01654053f, -0.01263428f, -0.00057983f, -0.02279663f, + 0.02337646f, 0.32543945f, 0.49267578f, 0.21066284f, -0.01596069f, -0.01284790f, + -0.00054932f, -0.02255249f, 0.02203369f, 0.32269287f, 0.49337769f, 0.21343994f, + -0.01535034f, -0.01309204f, -0.00048828f, -0.02233887f, 0.02069092f, 0.31994629f, + 0.49404907f, 0.21618652f, -0.01473999f, -0.01333618f, -0.00045776f, -0.02209473f, + 0.01937866f, 0.31719971f, 0.49465942f, 0.21896362f, -0.01409912f, -0.01358032f, + -0.00042725f, -0.02188110f, 0.01809692f, 0.31442261f, 0.49526978f, 0.22174072f, + -0.01345825f, -0.01379395f, -0.00039673f, -0.02163696f, 0.01684570f, 0.31164551f, + 0.49581909f, 0.22451782f, -0.01278687f, -0.01403809f, -0.00033569f, -0.02142334f, + 0.01559448f, 0.30886841f, 0.49636841f, 0.22732544f, -0.01211548f, -0.01428223f, + -0.00030518f, -0.02117920f, 0.01437378f, 0.30609131f, 0.49685669f, 0.23010254f, + -0.01141357f, -0.01452637f, -0.00027466f, -0.02093506f, 0.01318359f, 0.30331421f, + 0.49731445f, 0.23291016f, -0.01071167f, -0.01477051f, -0.00024414f, -0.02069092f, + 0.01199341f, 0.30050659f, 0.49774170f, 0.23571777f, -0.00997925f, -0.01501465f, + -0.00021362f, -0.02044678f, 0.01083374f, 0.29772949f, 0.49810791f, 0.23852539f, + -0.00921631f, -0.01525879f, -0.00018311f, -0.02020264f, 0.00967407f, 0.29492188f, + 0.49847412f, 0.24133301f, -0.00845337f, -0.01550293f, -0.00015259f, -0.01995850f, + 0.00854492f, 0.29211426f, 0.49877930f, 0.24414062f, -0.00765991f, -0.01574707f, + -0.00015259f, -0.01971436f, 0.00744629f, 0.28930664f, 0.49908447f, 0.24697876f, + -0.00686646f, -0.01602173f, -0.00012207f, -0.01947021f, 0.00634766f, 0.28646851f, + 0.49932861f, 0.24978638f, -0.00604248f, -0.01626587f, -0.00009155f, -0.01922607f, + 0.00527954f, 0.28366089f, 0.49954224f, 0.25259399f, -0.00518799f, -0.01651001f, + -0.00006104f, -0.01898193f, 0.00424194f, 0.28085327f, 0.49972534f, 0.25543213f, + -0.00433350f, -0.01675415f, -0.00006104f, -0.01873779f, 0.00320435f, 0.27801514f, + 0.49984741f, 0.25823975f, -0.00344849f, -0.01699829f, -0.00003052f, -0.01849365f, + 0.00219727f, 0.27520752f, 0.49996948f, 0.26107788f, -0.00256348f, -0.01724243f, + -0.00003052f, -0.01824951f, 0.00122070f, 0.27236938f, 0.50003052f, 0.26388550f, + -0.00164795f, -0.01748657f, 0.00000000f, -0.01800537f, 0.00024414f, 0.26956177f, + 0.50006104f, 0.26672363f, -0.00070190f, -0.01776123f, + }; + + static constexpr std::array lut1 = { + 0.01275635f, -0.07745361f, 0.18670654f, 0.75119019f, 0.19219971f, -0.07821655f, + 0.01272583f, 0.00000000f, 0.01281738f, -0.07666016f, 0.18124390f, 0.75106812f, + 0.19772339f, -0.07897949f, 0.01266479f, 0.00003052f, 0.01284790f, -0.07583618f, + 0.17581177f, 0.75088501f, 0.20330811f, -0.07971191f, 0.01257324f, 0.00006104f, + 0.01287842f, -0.07501221f, 0.17044067f, 0.75057983f, 0.20892334f, -0.08041382f, + 0.01248169f, 0.00009155f, 0.01290894f, -0.07415771f, 0.16510010f, 0.75018311f, + 0.21453857f, -0.08111572f, 0.01239014f, 0.00012207f, 0.01290894f, -0.07330322f, + 0.15979004f, 0.74966431f, 0.22021484f, -0.08178711f, 0.01229858f, 0.00015259f, + 0.01290894f, -0.07241821f, 0.15454102f, 0.74908447f, 0.22592163f, -0.08242798f, + 0.01217651f, 0.00018311f, 0.01290894f, -0.07150269f, 0.14932251f, 0.74838257f, + 0.23165894f, -0.08303833f, 0.01205444f, 0.00021362f, 0.01290894f, -0.07058716f, + 0.14416504f, 0.74755859f, 0.23742676f, -0.08364868f, 0.01193237f, 0.00024414f, + 0.01287842f, -0.06967163f, 0.13903809f, 0.74667358f, 0.24322510f, -0.08419800f, + 0.01177979f, 0.00027466f, 0.01284790f, -0.06872559f, 0.13397217f, 0.74566650f, + 0.24905396f, -0.08474731f, 0.01162720f, 0.00033569f, 0.01281738f, -0.06777954f, + 0.12893677f, 0.74456787f, 0.25491333f, -0.08526611f, 0.01147461f, 0.00036621f, + 0.01278687f, -0.06683350f, 0.12396240f, 0.74337769f, 0.26077271f, -0.08575439f, + 0.01129150f, 0.00042725f, 0.01275635f, -0.06585693f, 0.11901855f, 0.74206543f, + 0.26669312f, -0.08621216f, 0.01110840f, 0.00045776f, 0.01269531f, -0.06488037f, + 0.11413574f, 0.74069214f, 0.27261353f, -0.08663940f, 0.01092529f, 0.00051880f, + 0.01263428f, -0.06387329f, 0.10931396f, 0.73919678f, 0.27853394f, -0.08700562f, + 0.01071167f, 0.00057983f, 0.01257324f, -0.06286621f, 0.10452271f, 0.73760986f, + 0.28451538f, -0.08737183f, 0.01049805f, 0.00064087f, 0.01251221f, -0.06185913f, + 0.09979248f, 0.73593140f, 0.29049683f, -0.08770752f, 0.01025391f, 0.00067139f, + 0.01242065f, -0.06082153f, 0.09512329f, 0.73413086f, 0.29647827f, -0.08801270f, + 0.01000977f, 0.00073242f, 0.01232910f, -0.05981445f, 0.09051514f, 0.73226929f, + 0.30249023f, -0.08828735f, 0.00973511f, 0.00079346f, 0.01226807f, -0.05877686f, + 0.08593750f, 0.73028564f, 0.30853271f, -0.08850098f, 0.00949097f, 0.00088501f, + 0.01214600f, -0.05773926f, 0.08142090f, 0.72824097f, 0.31457520f, -0.08871460f, + 0.00918579f, 0.00094604f, 0.01205444f, -0.05670166f, 0.07696533f, 0.72607422f, + 0.32061768f, -0.08886719f, 0.00891113f, 0.00100708f, 0.01196289f, -0.05563354f, + 0.07257080f, 0.72381592f, 0.32669067f, -0.08898926f, 0.00860596f, 0.00106812f, + 0.01187134f, -0.05459595f, 0.06820679f, 0.72146606f, 0.33276367f, -0.08908081f, + 0.00827026f, 0.00115967f, 0.01174927f, -0.05352783f, 0.06393433f, 0.71902466f, + 0.33883667f, -0.08911133f, 0.00796509f, 0.00122070f, 0.01162720f, -0.05245972f, + 0.05969238f, 0.71649170f, 0.34494019f, -0.08914185f, 0.00759888f, 0.00131226f, + 0.01150513f, -0.05139160f, 0.05551147f, 0.71389771f, 0.35101318f, -0.08911133f, + 0.00726318f, 0.00137329f, 0.01138306f, -0.05032349f, 0.05139160f, 0.71118164f, + 0.35711670f, -0.08901978f, 0.00686646f, 0.00146484f, 0.01126099f, -0.04928589f, + 0.04733276f, 0.70837402f, 0.36322021f, -0.08892822f, 0.00650024f, 0.00155640f, + 0.01113892f, -0.04821777f, 0.04333496f, 0.70550537f, 0.36932373f, -0.08877563f, + 0.00610352f, 0.00164795f, 0.01101685f, -0.04714966f, 0.03939819f, 0.70251465f, + 0.37542725f, -0.08856201f, 0.00567627f, 0.00173950f, 0.01086426f, -0.04608154f, + 0.03549194f, 0.69946289f, 0.38153076f, -0.08834839f, 0.00527954f, 0.00183105f, + 0.01074219f, -0.04501343f, 0.03167725f, 0.69631958f, 0.38763428f, -0.08804321f, + 0.00482178f, 0.00192261f, 0.01058960f, -0.04394531f, 0.02792358f, 0.69308472f, + 0.39370728f, -0.08773804f, 0.00436401f, 0.00201416f, 0.01043701f, -0.04287720f, + 0.02420044f, 0.68975830f, 0.39981079f, -0.08737183f, 0.00390625f, 0.00210571f, + 0.01031494f, -0.04180908f, 0.02056885f, 0.68637085f, 0.40588379f, -0.08694458f, + 0.00344849f, 0.00222778f, 0.01016235f, -0.04074097f, 0.01699829f, 0.68289185f, + 0.41195679f, -0.08648682f, 0.00296021f, 0.00231934f, 0.01000977f, -0.03970337f, + 0.01345825f, 0.67932129f, 0.41802979f, -0.08596802f, 0.00244141f, 0.00244141f, + 0.00985718f, -0.03863525f, 0.01000977f, 0.67568970f, 0.42407227f, -0.08541870f, + 0.00192261f, 0.00253296f, 0.00970459f, -0.03759766f, 0.00662231f, 0.67196655f, + 0.43011475f, -0.08480835f, 0.00140381f, 0.00265503f, 0.00955200f, -0.03652954f, + 0.00326538f, 0.66815186f, 0.43612671f, -0.08416748f, 0.00085449f, 0.00277710f, + 0.00936890f, -0.03549194f, 0.00000000f, 0.66427612f, 0.44213867f, -0.08346558f, + 0.00027466f, 0.00289917f, 0.00921631f, -0.03445435f, -0.00320435f, 0.66030884f, + 0.44812012f, -0.08270264f, -0.00027466f, 0.00299072f, 0.00906372f, -0.03344727f, + -0.00634766f, 0.65631104f, 0.45407104f, -0.08190918f, -0.00088501f, 0.00311279f, + 0.00891113f, -0.03240967f, -0.00946045f, 0.65219116f, 0.46002197f, -0.08105469f, + -0.00146484f, 0.00323486f, 0.00872803f, -0.03140259f, -0.01248169f, 0.64801025f, + 0.46594238f, -0.08013916f, -0.00210571f, 0.00338745f, 0.00857544f, -0.03039551f, + -0.01544189f, 0.64376831f, 0.47183228f, -0.07919312f, -0.00271606f, 0.00350952f, + 0.00842285f, -0.02938843f, -0.01834106f, 0.63946533f, 0.47772217f, -0.07818604f, + -0.00335693f, 0.00363159f, 0.00823975f, -0.02838135f, -0.02117920f, 0.63507080f, + 0.48358154f, -0.07711792f, -0.00402832f, 0.00375366f, 0.00808716f, -0.02740479f, + -0.02395630f, 0.63061523f, 0.48937988f, -0.07598877f, -0.00469971f, 0.00390625f, + 0.00793457f, -0.02642822f, -0.02667236f, 0.62609863f, 0.49517822f, -0.07482910f, + -0.00537109f, 0.00402832f, 0.00775146f, -0.02545166f, -0.02932739f, 0.62152100f, + 0.50094604f, -0.07357788f, -0.00607300f, 0.00418091f, 0.00759888f, -0.02450562f, + -0.03192139f, 0.61685181f, 0.50665283f, -0.07229614f, -0.00677490f, 0.00430298f, + 0.00741577f, -0.02352905f, -0.03445435f, 0.61215210f, 0.51235962f, -0.07098389f, + -0.00750732f, 0.00445557f, 0.00726318f, -0.02258301f, -0.03689575f, 0.60736084f, + 0.51800537f, -0.06958008f, -0.00823975f, 0.00460815f, 0.00711060f, -0.02166748f, + -0.03930664f, 0.60253906f, 0.52362061f, -0.06811523f, -0.00897217f, 0.00476074f, + 0.00692749f, -0.02075195f, -0.04165649f, 0.59762573f, 0.52920532f, -0.06661987f, + -0.00973511f, 0.00488281f, 0.00677490f, -0.01983643f, -0.04394531f, 0.59268188f, + 0.53475952f, -0.06506348f, -0.01052856f, 0.00503540f, 0.00662231f, -0.01892090f, + -0.04617310f, 0.58767700f, 0.54025269f, -0.06344604f, -0.01129150f, 0.00518799f, + 0.00643921f, -0.01803589f, -0.04830933f, 0.58261108f, 0.54571533f, -0.06173706f, + -0.01208496f, 0.00534058f, 0.00628662f, -0.01715088f, -0.05041504f, 0.57748413f, + 0.55111694f, -0.05999756f, -0.01290894f, 0.00549316f, 0.00613403f, -0.01626587f, + -0.05245972f, 0.57232666f, 0.55648804f, -0.05819702f, -0.01373291f, 0.00564575f, + 0.00598145f, -0.01541138f, -0.05444336f, 0.56707764f, 0.56182861f, -0.05636597f, + -0.01455688f, 0.00582886f, 0.00582886f, -0.01455688f, -0.05636597f, 0.56182861f, + 0.56707764f, -0.05444336f, -0.01541138f, 0.00598145f, 0.00564575f, -0.01373291f, + -0.05819702f, 0.55648804f, 0.57232666f, -0.05245972f, -0.01626587f, 0.00613403f, + 0.00549316f, -0.01290894f, -0.05999756f, 0.55111694f, 0.57748413f, -0.05041504f, + -0.01715088f, 0.00628662f, 0.00534058f, -0.01208496f, -0.06173706f, 0.54571533f, + 0.58261108f, -0.04830933f, -0.01803589f, 0.00643921f, 0.00518799f, -0.01129150f, + -0.06344604f, 0.54025269f, 0.58767700f, -0.04617310f, -0.01892090f, 0.00662231f, + 0.00503540f, -0.01052856f, -0.06506348f, 0.53475952f, 0.59268188f, -0.04394531f, + -0.01983643f, 0.00677490f, 0.00488281f, -0.00973511f, -0.06661987f, 0.52920532f, + 0.59762573f, -0.04165649f, -0.02075195f, 0.00692749f, 0.00476074f, -0.00897217f, + -0.06811523f, 0.52362061f, 0.60253906f, -0.03930664f, -0.02166748f, 0.00711060f, + 0.00460815f, -0.00823975f, -0.06958008f, 0.51800537f, 0.60736084f, -0.03689575f, + -0.02258301f, 0.00726318f, 0.00445557f, -0.00750732f, -0.07098389f, 0.51235962f, + 0.61215210f, -0.03445435f, -0.02352905f, 0.00741577f, 0.00430298f, -0.00677490f, + -0.07229614f, 0.50665283f, 0.61685181f, -0.03192139f, -0.02450562f, 0.00759888f, + 0.00418091f, -0.00607300f, -0.07357788f, 0.50094604f, 0.62152100f, -0.02932739f, + -0.02545166f, 0.00775146f, 0.00402832f, -0.00537109f, -0.07482910f, 0.49517822f, + 0.62609863f, -0.02667236f, -0.02642822f, 0.00793457f, 0.00390625f, -0.00469971f, + -0.07598877f, 0.48937988f, 0.63061523f, -0.02395630f, -0.02740479f, 0.00808716f, + 0.00375366f, -0.00402832f, -0.07711792f, 0.48358154f, 0.63507080f, -0.02117920f, + -0.02838135f, 0.00823975f, 0.00363159f, -0.00335693f, -0.07818604f, 0.47772217f, + 0.63946533f, -0.01834106f, -0.02938843f, 0.00842285f, 0.00350952f, -0.00271606f, + -0.07919312f, 0.47183228f, 0.64376831f, -0.01544189f, -0.03039551f, 0.00857544f, + 0.00338745f, -0.00210571f, -0.08013916f, 0.46594238f, 0.64801025f, -0.01248169f, + -0.03140259f, 0.00872803f, 0.00323486f, -0.00146484f, -0.08105469f, 0.46002197f, + 0.65219116f, -0.00946045f, -0.03240967f, 0.00891113f, 0.00311279f, -0.00088501f, + -0.08190918f, 0.45407104f, 0.65631104f, -0.00634766f, -0.03344727f, 0.00906372f, + 0.00299072f, -0.00027466f, -0.08270264f, 0.44812012f, 0.66030884f, -0.00320435f, + -0.03445435f, 0.00921631f, 0.00289917f, 0.00027466f, -0.08346558f, 0.44213867f, + 0.66427612f, 0.00000000f, -0.03549194f, 0.00936890f, 0.00277710f, 0.00085449f, + -0.08416748f, 0.43612671f, 0.66815186f, 0.00326538f, -0.03652954f, 0.00955200f, + 0.00265503f, 0.00140381f, -0.08480835f, 0.43011475f, 0.67196655f, 0.00662231f, + -0.03759766f, 0.00970459f, 0.00253296f, 0.00192261f, -0.08541870f, 0.42407227f, + 0.67568970f, 0.01000977f, -0.03863525f, 0.00985718f, 0.00244141f, 0.00244141f, + -0.08596802f, 0.41802979f, 0.67932129f, 0.01345825f, -0.03970337f, 0.01000977f, + 0.00231934f, 0.00296021f, -0.08648682f, 0.41195679f, 0.68289185f, 0.01699829f, + -0.04074097f, 0.01016235f, 0.00222778f, 0.00344849f, -0.08694458f, 0.40588379f, + 0.68637085f, 0.02056885f, -0.04180908f, 0.01031494f, 0.00210571f, 0.00390625f, + -0.08737183f, 0.39981079f, 0.68975830f, 0.02420044f, -0.04287720f, 0.01043701f, + 0.00201416f, 0.00436401f, -0.08773804f, 0.39370728f, 0.69308472f, 0.02792358f, + -0.04394531f, 0.01058960f, 0.00192261f, 0.00482178f, -0.08804321f, 0.38763428f, + 0.69631958f, 0.03167725f, -0.04501343f, 0.01074219f, 0.00183105f, 0.00527954f, + -0.08834839f, 0.38153076f, 0.69946289f, 0.03549194f, -0.04608154f, 0.01086426f, + 0.00173950f, 0.00567627f, -0.08856201f, 0.37542725f, 0.70251465f, 0.03939819f, + -0.04714966f, 0.01101685f, 0.00164795f, 0.00610352f, -0.08877563f, 0.36932373f, + 0.70550537f, 0.04333496f, -0.04821777f, 0.01113892f, 0.00155640f, 0.00650024f, + -0.08892822f, 0.36322021f, 0.70837402f, 0.04733276f, -0.04928589f, 0.01126099f, + 0.00146484f, 0.00686646f, -0.08901978f, 0.35711670f, 0.71118164f, 0.05139160f, + -0.05032349f, 0.01138306f, 0.00137329f, 0.00726318f, -0.08911133f, 0.35101318f, + 0.71389771f, 0.05551147f, -0.05139160f, 0.01150513f, 0.00131226f, 0.00759888f, + -0.08914185f, 0.34494019f, 0.71649170f, 0.05969238f, -0.05245972f, 0.01162720f, + 0.00122070f, 0.00796509f, -0.08911133f, 0.33883667f, 0.71902466f, 0.06393433f, + -0.05352783f, 0.01174927f, 0.00115967f, 0.00827026f, -0.08908081f, 0.33276367f, + 0.72146606f, 0.06820679f, -0.05459595f, 0.01187134f, 0.00106812f, 0.00860596f, + -0.08898926f, 0.32669067f, 0.72381592f, 0.07257080f, -0.05563354f, 0.01196289f, + 0.00100708f, 0.00891113f, -0.08886719f, 0.32061768f, 0.72607422f, 0.07696533f, + -0.05670166f, 0.01205444f, 0.00094604f, 0.00918579f, -0.08871460f, 0.31457520f, + 0.72824097f, 0.08142090f, -0.05773926f, 0.01214600f, 0.00088501f, 0.00949097f, + -0.08850098f, 0.30853271f, 0.73028564f, 0.08593750f, -0.05877686f, 0.01226807f, + 0.00079346f, 0.00973511f, -0.08828735f, 0.30249023f, 0.73226929f, 0.09051514f, + -0.05981445f, 0.01232910f, 0.00073242f, 0.01000977f, -0.08801270f, 0.29647827f, + 0.73413086f, 0.09512329f, -0.06082153f, 0.01242065f, 0.00067139f, 0.01025391f, + -0.08770752f, 0.29049683f, 0.73593140f, 0.09979248f, -0.06185913f, 0.01251221f, + 0.00064087f, 0.01049805f, -0.08737183f, 0.28451538f, 0.73760986f, 0.10452271f, + -0.06286621f, 0.01257324f, 0.00057983f, 0.01071167f, -0.08700562f, 0.27853394f, + 0.73919678f, 0.10931396f, -0.06387329f, 0.01263428f, 0.00051880f, 0.01092529f, + -0.08663940f, 0.27261353f, 0.74069214f, 0.11413574f, -0.06488037f, 0.01269531f, + 0.00045776f, 0.01110840f, -0.08621216f, 0.26669312f, 0.74206543f, 0.11901855f, + -0.06585693f, 0.01275635f, 0.00042725f, 0.01129150f, -0.08575439f, 0.26077271f, + 0.74337769f, 0.12396240f, -0.06683350f, 0.01278687f, 0.00036621f, 0.01147461f, + -0.08526611f, 0.25491333f, 0.74456787f, 0.12893677f, -0.06777954f, 0.01281738f, + 0.00033569f, 0.01162720f, -0.08474731f, 0.24905396f, 0.74566650f, 0.13397217f, + -0.06872559f, 0.01284790f, 0.00027466f, 0.01177979f, -0.08419800f, 0.24322510f, + 0.74667358f, 0.13903809f, -0.06967163f, 0.01287842f, 0.00024414f, 0.01193237f, + -0.08364868f, 0.23742676f, 0.74755859f, 0.14416504f, -0.07058716f, 0.01290894f, + 0.00021362f, 0.01205444f, -0.08303833f, 0.23165894f, 0.74838257f, 0.14932251f, + -0.07150269f, 0.01290894f, 0.00018311f, 0.01217651f, -0.08242798f, 0.22592163f, + 0.74908447f, 0.15454102f, -0.07241821f, 0.01290894f, 0.00015259f, 0.01229858f, + -0.08178711f, 0.22021484f, 0.74966431f, 0.15979004f, -0.07330322f, 0.01290894f, + 0.00012207f, 0.01239014f, -0.08111572f, 0.21453857f, 0.75018311f, 0.16510010f, + -0.07415771f, 0.01290894f, 0.00009155f, 0.01248169f, -0.08041382f, 0.20892334f, + 0.75057983f, 0.17044067f, -0.07501221f, 0.01287842f, 0.00006104f, 0.01257324f, + -0.07971191f, 0.20330811f, 0.75088501f, 0.17581177f, -0.07583618f, 0.01284790f, + 0.00003052f, 0.01266479f, -0.07897949f, 0.19772339f, 0.75106812f, 0.18124390f, + -0.07666016f, 0.01281738f, 0.00000000f, 0.01272583f, -0.07821655f, 0.19219971f, + 0.75119019f, 0.18670654f, -0.07745361f, 0.01275635f, + }; + + static constexpr std::array lut2 = { + -0.00036621f, 0.00143433f, -0.00408936f, 0.99996948f, 0.00247192f, -0.00048828f, + 0.00006104f, 0.00000000f, -0.00079346f, 0.00329590f, -0.01052856f, 0.99975586f, + 0.00918579f, -0.00241089f, 0.00051880f, -0.00003052f, -0.00122070f, 0.00512695f, + -0.01684570f, 0.99929810f, 0.01605225f, -0.00439453f, 0.00097656f, -0.00006104f, + -0.00161743f, 0.00689697f, -0.02297974f, 0.99862671f, 0.02304077f, -0.00640869f, + 0.00143433f, -0.00009155f, -0.00201416f, 0.00866699f, -0.02899170f, 0.99774170f, + 0.03018188f, -0.00845337f, 0.00192261f, -0.00015259f, -0.00238037f, 0.01037598f, + -0.03488159f, 0.99664307f, 0.03741455f, -0.01055908f, 0.00241089f, -0.00018311f, + -0.00274658f, 0.01202393f, -0.04061890f, 0.99533081f, 0.04483032f, -0.01266479f, + 0.00292969f, -0.00024414f, -0.00308228f, 0.01364136f, -0.04620361f, 0.99377441f, + 0.05233765f, -0.01483154f, 0.00344849f, -0.00027466f, -0.00341797f, 0.01522827f, + -0.05163574f, 0.99200439f, 0.05999756f, -0.01699829f, 0.00396729f, -0.00033569f, + -0.00375366f, 0.01678467f, -0.05691528f, 0.99002075f, 0.06777954f, -0.01922607f, + 0.00451660f, -0.00039673f, -0.00405884f, 0.01828003f, -0.06207275f, 0.98782349f, + 0.07568359f, -0.02145386f, 0.00506592f, -0.00042725f, -0.00436401f, 0.01971436f, + -0.06707764f, 0.98541260f, 0.08370972f, -0.02374268f, 0.00564575f, -0.00048828f, + -0.00463867f, 0.02114868f, -0.07192993f, 0.98278809f, 0.09185791f, -0.02603149f, + 0.00622559f, -0.00054932f, -0.00494385f, 0.02252197f, -0.07666016f, 0.97991943f, + 0.10012817f, -0.02835083f, 0.00680542f, -0.00061035f, -0.00518799f, 0.02383423f, + -0.08123779f, 0.97686768f, 0.10848999f, -0.03073120f, 0.00738525f, -0.00070190f, + -0.00543213f, 0.02511597f, -0.08566284f, 0.97360229f, 0.11700439f, -0.03308105f, + 0.00799561f, -0.00076294f, -0.00567627f, 0.02636719f, -0.08993530f, 0.97012329f, + 0.12561035f, -0.03549194f, 0.00860596f, -0.00082397f, -0.00592041f, 0.02755737f, + -0.09405518f, 0.96643066f, 0.13436890f, -0.03790283f, 0.00924683f, -0.00091553f, + -0.00613403f, 0.02868652f, -0.09805298f, 0.96252441f, 0.14318848f, -0.04034424f, + 0.00985718f, -0.00097656f, -0.00631714f, 0.02981567f, -0.10189819f, 0.95843506f, + 0.15213013f, -0.04281616f, 0.01049805f, -0.00106812f, -0.00653076f, 0.03085327f, + -0.10559082f, 0.95413208f, 0.16119385f, -0.04528809f, 0.01113892f, -0.00112915f, + -0.00671387f, 0.03189087f, -0.10916138f, 0.94961548f, 0.17034912f, -0.04779053f, + 0.01181030f, -0.00122070f, -0.00686646f, 0.03286743f, -0.11254883f, 0.94491577f, + 0.17959595f, -0.05029297f, 0.01248169f, -0.00131226f, -0.00701904f, 0.03378296f, + -0.11584473f, 0.94000244f, 0.18893433f, -0.05279541f, 0.01315308f, -0.00140381f, + -0.00717163f, 0.03466797f, -0.11895752f, 0.93490601f, 0.19839478f, -0.05532837f, + 0.01382446f, -0.00149536f, -0.00732422f, 0.03552246f, -0.12194824f, 0.92962646f, + 0.20791626f, -0.05786133f, 0.01449585f, -0.00158691f, -0.00744629f, 0.03631592f, + -0.12478638f, 0.92413330f, 0.21752930f, -0.06042480f, 0.01519775f, -0.00167847f, + -0.00753784f, 0.03707886f, -0.12750244f, 0.91848755f, 0.22723389f, -0.06298828f, + 0.01586914f, -0.00177002f, -0.00765991f, 0.03781128f, -0.13006592f, 0.91262817f, + 0.23703003f, -0.06555176f, 0.01657104f, -0.00189209f, -0.00775146f, 0.03848267f, + -0.13250732f, 0.90658569f, 0.24691772f, -0.06808472f, 0.01727295f, -0.00198364f, + -0.00784302f, 0.03909302f, -0.13479614f, 0.90036011f, 0.25683594f, -0.07064819f, + 0.01797485f, -0.00210571f, -0.00790405f, 0.03970337f, -0.13696289f, 0.89395142f, + 0.26687622f, -0.07321167f, 0.01870728f, -0.00219727f, -0.00796509f, 0.04025269f, + -0.13900757f, 0.88739014f, 0.27694702f, -0.07577515f, 0.01940918f, -0.00231934f, + -0.00802612f, 0.04077148f, -0.14089966f, 0.88064575f, 0.28710938f, -0.07833862f, + 0.02011108f, -0.00244141f, -0.00808716f, 0.04122925f, -0.14263916f, 0.87374878f, + 0.29733276f, -0.08090210f, 0.02084351f, -0.00253296f, -0.00811768f, 0.04165649f, + -0.14428711f, 0.86666870f, 0.30761719f, -0.08343506f, 0.02154541f, -0.00265503f, + -0.00814819f, 0.04205322f, -0.14578247f, 0.85940552f, 0.31793213f, -0.08596802f, + 0.02227783f, -0.00277710f, -0.00814819f, 0.04238892f, -0.14715576f, 0.85202026f, + 0.32833862f, -0.08847046f, 0.02297974f, -0.00289917f, -0.00817871f, 0.04272461f, + -0.14840698f, 0.84445190f, 0.33874512f, -0.09097290f, 0.02371216f, -0.00302124f, + -0.00817871f, 0.04299927f, -0.14953613f, 0.83673096f, 0.34924316f, -0.09347534f, + 0.02441406f, -0.00314331f, -0.00817871f, 0.04321289f, -0.15054321f, 0.82888794f, + 0.35977173f, -0.09594727f, 0.02514648f, -0.00326538f, -0.00814819f, 0.04342651f, + -0.15142822f, 0.82086182f, 0.37033081f, -0.09838867f, 0.02584839f, -0.00341797f, + -0.00814819f, 0.04357910f, -0.15219116f, 0.81271362f, 0.38092041f, -0.10079956f, + 0.02655029f, -0.00354004f, -0.00811768f, 0.04373169f, -0.15283203f, 0.80441284f, + 0.39154053f, -0.10321045f, 0.02725220f, -0.00366211f, -0.00808716f, 0.04382324f, + -0.15338135f, 0.79598999f, 0.40219116f, -0.10559082f, 0.02795410f, -0.00381470f, + -0.00805664f, 0.04388428f, -0.15377808f, 0.78741455f, 0.41287231f, -0.10794067f, + 0.02865601f, -0.00393677f, -0.00799561f, 0.04388428f, -0.15408325f, 0.77871704f, + 0.42358398f, -0.11026001f, 0.02935791f, -0.00405884f, -0.00793457f, 0.04388428f, + -0.15426636f, 0.76989746f, 0.43429565f, -0.11251831f, 0.03002930f, -0.00421143f, + -0.00787354f, 0.04385376f, -0.15435791f, 0.76095581f, 0.44500732f, -0.11477661f, + 0.03070068f, -0.00433350f, -0.00781250f, 0.04379272f, -0.15435791f, 0.75192261f, + 0.45574951f, -0.11697388f, 0.03137207f, -0.00448608f, -0.00775146f, 0.04367065f, + -0.15420532f, 0.74273682f, 0.46649170f, -0.11914062f, 0.03201294f, -0.00460815f, + -0.00769043f, 0.04354858f, -0.15399170f, 0.73345947f, 0.47723389f, -0.12127686f, + 0.03268433f, -0.00473022f, -0.00759888f, 0.04339600f, -0.15365601f, 0.72406006f, + 0.48794556f, -0.12335205f, 0.03329468f, -0.00488281f, -0.00750732f, 0.04321289f, + -0.15322876f, 0.71456909f, 0.49868774f, -0.12539673f, 0.03393555f, -0.00500488f, + -0.00741577f, 0.04296875f, -0.15270996f, 0.70498657f, 0.50936890f, -0.12738037f, + 0.03454590f, -0.00515747f, -0.00732422f, 0.04272461f, -0.15209961f, 0.69528198f, + 0.52008057f, -0.12930298f, 0.03515625f, -0.00527954f, -0.00723267f, 0.04248047f, + -0.15136719f, 0.68551636f, 0.53076172f, -0.13119507f, 0.03573608f, -0.00543213f, + -0.00714111f, 0.04217529f, -0.15057373f, 0.67565918f, 0.54138184f, -0.13299561f, + 0.03631592f, -0.00555420f, -0.00701904f, 0.04183960f, -0.14968872f, 0.66571045f, + 0.55200195f, -0.13476562f, 0.03689575f, -0.00567627f, -0.00692749f, 0.04150391f, + -0.14871216f, 0.65567017f, 0.56259155f, -0.13647461f, 0.03741455f, -0.00582886f, + -0.00680542f, 0.04113770f, -0.14767456f, 0.64556885f, 0.57315063f, -0.13812256f, + 0.03796387f, -0.00595093f, -0.00668335f, 0.04074097f, -0.14651489f, 0.63540649f, + 0.58364868f, -0.13970947f, 0.03845215f, -0.00607300f, -0.00656128f, 0.04031372f, + -0.14529419f, 0.62518311f, 0.59411621f, -0.14120483f, 0.03897095f, -0.00619507f, + -0.00643921f, 0.03988647f, -0.14401245f, 0.61486816f, 0.60452271f, -0.14263916f, + 0.03942871f, -0.00631714f, -0.00631714f, 0.03942871f, -0.14263916f, 0.60452271f, + 0.61486816f, -0.14401245f, 0.03988647f, -0.00643921f, -0.00619507f, 0.03897095f, + -0.14120483f, 0.59411621f, 0.62518311f, -0.14529419f, 0.04031372f, -0.00656128f, + -0.00607300f, 0.03845215f, -0.13970947f, 0.58364868f, 0.63540649f, -0.14651489f, + 0.04074097f, -0.00668335f, -0.00595093f, 0.03796387f, -0.13812256f, 0.57315063f, + 0.64556885f, -0.14767456f, 0.04113770f, -0.00680542f, -0.00582886f, 0.03741455f, + -0.13647461f, 0.56259155f, 0.65567017f, -0.14871216f, 0.04150391f, -0.00692749f, + -0.00567627f, 0.03689575f, -0.13476562f, 0.55200195f, 0.66571045f, -0.14968872f, + 0.04183960f, -0.00701904f, -0.00555420f, 0.03631592f, -0.13299561f, 0.54138184f, + 0.67565918f, -0.15057373f, 0.04217529f, -0.00714111f, -0.00543213f, 0.03573608f, + -0.13119507f, 0.53076172f, 0.68551636f, -0.15136719f, 0.04248047f, -0.00723267f, + -0.00527954f, 0.03515625f, -0.12930298f, 0.52008057f, 0.69528198f, -0.15209961f, + 0.04272461f, -0.00732422f, -0.00515747f, 0.03454590f, -0.12738037f, 0.50936890f, + 0.70498657f, -0.15270996f, 0.04296875f, -0.00741577f, -0.00500488f, 0.03393555f, + -0.12539673f, 0.49868774f, 0.71456909f, -0.15322876f, 0.04321289f, -0.00750732f, + -0.00488281f, 0.03329468f, -0.12335205f, 0.48794556f, 0.72406006f, -0.15365601f, + 0.04339600f, -0.00759888f, -0.00473022f, 0.03268433f, -0.12127686f, 0.47723389f, + 0.73345947f, -0.15399170f, 0.04354858f, -0.00769043f, -0.00460815f, 0.03201294f, + -0.11914062f, 0.46649170f, 0.74273682f, -0.15420532f, 0.04367065f, -0.00775146f, + -0.00448608f, 0.03137207f, -0.11697388f, 0.45574951f, 0.75192261f, -0.15435791f, + 0.04379272f, -0.00781250f, -0.00433350f, 0.03070068f, -0.11477661f, 0.44500732f, + 0.76095581f, -0.15435791f, 0.04385376f, -0.00787354f, -0.00421143f, 0.03002930f, + -0.11251831f, 0.43429565f, 0.76989746f, -0.15426636f, 0.04388428f, -0.00793457f, + -0.00405884f, 0.02935791f, -0.11026001f, 0.42358398f, 0.77871704f, -0.15408325f, + 0.04388428f, -0.00799561f, -0.00393677f, 0.02865601f, -0.10794067f, 0.41287231f, + 0.78741455f, -0.15377808f, 0.04388428f, -0.00805664f, -0.00381470f, 0.02795410f, + -0.10559082f, 0.40219116f, 0.79598999f, -0.15338135f, 0.04382324f, -0.00808716f, + -0.00366211f, 0.02725220f, -0.10321045f, 0.39154053f, 0.80441284f, -0.15283203f, + 0.04373169f, -0.00811768f, -0.00354004f, 0.02655029f, -0.10079956f, 0.38092041f, + 0.81271362f, -0.15219116f, 0.04357910f, -0.00814819f, -0.00341797f, 0.02584839f, + -0.09838867f, 0.37033081f, 0.82086182f, -0.15142822f, 0.04342651f, -0.00814819f, + -0.00326538f, 0.02514648f, -0.09594727f, 0.35977173f, 0.82888794f, -0.15054321f, + 0.04321289f, -0.00817871f, -0.00314331f, 0.02441406f, -0.09347534f, 0.34924316f, + 0.83673096f, -0.14953613f, 0.04299927f, -0.00817871f, -0.00302124f, 0.02371216f, + -0.09097290f, 0.33874512f, 0.84445190f, -0.14840698f, 0.04272461f, -0.00817871f, + -0.00289917f, 0.02297974f, -0.08847046f, 0.32833862f, 0.85202026f, -0.14715576f, + 0.04238892f, -0.00814819f, -0.00277710f, 0.02227783f, -0.08596802f, 0.31793213f, + 0.85940552f, -0.14578247f, 0.04205322f, -0.00814819f, -0.00265503f, 0.02154541f, + -0.08343506f, 0.30761719f, 0.86666870f, -0.14428711f, 0.04165649f, -0.00811768f, + -0.00253296f, 0.02084351f, -0.08090210f, 0.29733276f, 0.87374878f, -0.14263916f, + 0.04122925f, -0.00808716f, -0.00244141f, 0.02011108f, -0.07833862f, 0.28710938f, + 0.88064575f, -0.14089966f, 0.04077148f, -0.00802612f, -0.00231934f, 0.01940918f, + -0.07577515f, 0.27694702f, 0.88739014f, -0.13900757f, 0.04025269f, -0.00796509f, + -0.00219727f, 0.01870728f, -0.07321167f, 0.26687622f, 0.89395142f, -0.13696289f, + 0.03970337f, -0.00790405f, -0.00210571f, 0.01797485f, -0.07064819f, 0.25683594f, + 0.90036011f, -0.13479614f, 0.03909302f, -0.00784302f, -0.00198364f, 0.01727295f, + -0.06808472f, 0.24691772f, 0.90658569f, -0.13250732f, 0.03848267f, -0.00775146f, + -0.00189209f, 0.01657104f, -0.06555176f, 0.23703003f, 0.91262817f, -0.13006592f, + 0.03781128f, -0.00765991f, -0.00177002f, 0.01586914f, -0.06298828f, 0.22723389f, + 0.91848755f, -0.12750244f, 0.03707886f, -0.00753784f, -0.00167847f, 0.01519775f, + -0.06042480f, 0.21752930f, 0.92413330f, -0.12478638f, 0.03631592f, -0.00744629f, + -0.00158691f, 0.01449585f, -0.05786133f, 0.20791626f, 0.92962646f, -0.12194824f, + 0.03552246f, -0.00732422f, -0.00149536f, 0.01382446f, -0.05532837f, 0.19839478f, + 0.93490601f, -0.11895752f, 0.03466797f, -0.00717163f, -0.00140381f, 0.01315308f, + -0.05279541f, 0.18893433f, 0.94000244f, -0.11584473f, 0.03378296f, -0.00701904f, + -0.00131226f, 0.01248169f, -0.05029297f, 0.17959595f, 0.94491577f, -0.11254883f, + 0.03286743f, -0.00686646f, -0.00122070f, 0.01181030f, -0.04779053f, 0.17034912f, + 0.94961548f, -0.10916138f, 0.03189087f, -0.00671387f, -0.00112915f, 0.01113892f, + -0.04528809f, 0.16119385f, 0.95413208f, -0.10559082f, 0.03085327f, -0.00653076f, + -0.00106812f, 0.01049805f, -0.04281616f, 0.15213013f, 0.95843506f, -0.10189819f, + 0.02981567f, -0.00631714f, -0.00097656f, 0.00985718f, -0.04034424f, 0.14318848f, + 0.96252441f, -0.09805298f, 0.02868652f, -0.00613403f, -0.00091553f, 0.00924683f, + -0.03790283f, 0.13436890f, 0.96643066f, -0.09405518f, 0.02755737f, -0.00592041f, + -0.00082397f, 0.00860596f, -0.03549194f, 0.12561035f, 0.97012329f, -0.08993530f, + 0.02636719f, -0.00567627f, -0.00076294f, 0.00799561f, -0.03308105f, 0.11700439f, + 0.97360229f, -0.08566284f, 0.02511597f, -0.00543213f, -0.00070190f, 0.00738525f, + -0.03073120f, 0.10848999f, 0.97686768f, -0.08123779f, 0.02383423f, -0.00518799f, + -0.00061035f, 0.00680542f, -0.02835083f, 0.10012817f, 0.97991943f, -0.07666016f, + 0.02252197f, -0.00494385f, -0.00054932f, 0.00622559f, -0.02603149f, 0.09185791f, + 0.98278809f, -0.07192993f, 0.02114868f, -0.00463867f, -0.00048828f, 0.00564575f, + -0.02374268f, 0.08370972f, 0.98541260f, -0.06707764f, 0.01971436f, -0.00436401f, + -0.00042725f, 0.00506592f, -0.02145386f, 0.07568359f, 0.98782349f, -0.06207275f, + 0.01828003f, -0.00405884f, -0.00039673f, 0.00451660f, -0.01922607f, 0.06777954f, + 0.99002075f, -0.05691528f, 0.01678467f, -0.00375366f, -0.00033569f, 0.00396729f, + -0.01699829f, 0.05999756f, 0.99200439f, -0.05163574f, 0.01522827f, -0.00341797f, + -0.00027466f, 0.00344849f, -0.01483154f, 0.05233765f, 0.99377441f, -0.04620361f, + 0.01364136f, -0.00308228f, -0.00024414f, 0.00292969f, -0.01266479f, 0.04483032f, + 0.99533081f, -0.04061890f, 0.01202393f, -0.00274658f, -0.00018311f, 0.00241089f, + -0.01055908f, 0.03741455f, 0.99664307f, -0.03488159f, 0.01037598f, -0.00238037f, + -0.00015259f, 0.00192261f, -0.00845337f, 0.03018188f, 0.99774170f, -0.02899170f, + 0.00866699f, -0.00201416f, -0.00009155f, 0.00143433f, -0.00640869f, 0.02304077f, + 0.99862671f, -0.02297974f, 0.00689697f, -0.00161743f, -0.00006104f, 0.00097656f, + -0.00439453f, 0.01605225f, 0.99929810f, -0.01684570f, 0.00512695f, -0.00122070f, + -0.00003052f, 0.00051880f, -0.00241089f, 0.00918579f, 0.99975586f, -0.01052856f, + 0.00329590f, -0.00079346f, 0.00000000f, 0.00006104f, -0.00048828f, 0.00247192f, + 0.99996948f, -0.00408936f, 0.00143433f, -0.00036621f, + }; + + const auto get_lut = [&]() -> std::span { + if (sample_rate_ratio <= 1.0f) { + return std::span(lut2.data(), lut2.size()); + } else if (sample_rate_ratio < 1.3f) { + return std::span(lut1.data(), lut1.size()); + } else { + return std::span(lut0.data(), lut0.size()); + } + }; + + auto lut{get_lut()}; + u32 read_index{0}; + for (u32 i = 0; i < samples_to_write; i++) { + const auto lut_index{(fraction.get_frac() >> 8) * 8}; + const Common::FixedPoint<56, 8> sample0{input[read_index + 0] * lut[lut_index + 0]}; + const Common::FixedPoint<56, 8> sample1{input[read_index + 1] * lut[lut_index + 1]}; + const Common::FixedPoint<56, 8> sample2{input[read_index + 2] * lut[lut_index + 2]}; + const Common::FixedPoint<56, 8> sample3{input[read_index + 3] * lut[lut_index + 3]}; + const Common::FixedPoint<56, 8> sample4{input[read_index + 4] * lut[lut_index + 4]}; + const Common::FixedPoint<56, 8> sample5{input[read_index + 5] * lut[lut_index + 5]}; + const Common::FixedPoint<56, 8> sample6{input[read_index + 6] * lut[lut_index + 6]}; + const Common::FixedPoint<56, 8> sample7{input[read_index + 7] * lut[lut_index + 7]}; + output[i] = (sample0 + sample1 + sample2 + sample3 + sample4 + sample5 + sample6 + sample7) + .to_int_floor(); + fraction += sample_rate_ratio; + read_index += static_cast(fraction.to_int_floor()); + fraction.clear_int(); + } +} + +void Resample(std::span output, std::span input, + const Common::FixedPoint<49, 15>& sample_rate_ratio, + Common::FixedPoint<49, 15>& fraction, const u32 samples_to_write, + const SrcQuality src_quality) { + + switch (src_quality) { + case SrcQuality::Low: + ResampleLowQuality(output, input, sample_rate_ratio, fraction, samples_to_write); + break; + case SrcQuality::Medium: + ResampleNormalQuality(output, input, sample_rate_ratio, fraction, samples_to_write); + break; + case SrcQuality::High: + ResampleHighQuality(output, input, sample_rate_ratio, fraction, samples_to_write); + break; + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/resample/resample.h b/src/audio_core/renderer/command/resample/resample.h new file mode 100644 index 0000000000..ba9209b82c --- /dev/null +++ b/src/audio_core/renderer/command/resample/resample.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { +/** + * Resample an input buffer into an output buffer, according to the sample_rate_ratio. + * + * @param output - Output buffer. + * @param input - Input buffer. + * @param sample_rate_ratio - Ratio for resampling. + e.g 32000/48000 = 0.666 input samples read per output. + * @param fraction - Current read fraction, written to and should be passed back in for + * multiple calls. + * @param samples_to_write - Number of samples to write. + * @param src_quality - Resampling quality. + */ +void Resample(std::span output, std::span input, + const Common::FixedPoint<49, 15>& sample_rate_ratio, + Common::FixedPoint<49, 15>& fraction, u32 samples_to_write, SrcQuality src_quality); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/resample/upsample.cpp b/src/audio_core/renderer/command/resample/upsample.cpp new file mode 100644 index 0000000000..6c3ff31f7d --- /dev/null +++ b/src/audio_core/renderer/command/resample/upsample.cpp @@ -0,0 +1,262 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/resample/upsample.h" +#include "audio_core/renderer/upsampler/upsampler_info.h" + +namespace AudioCore::AudioRenderer { +/** + * Upsampling impl. Input must be 8K, 16K or 32K, output is 48K. + * + * @param output - Output buffer. + * @param input - Input buffer. + * @param target_sample_count - Number of samples for output. + * @param state - Upsampler state, updated each call. + */ +static void SrcProcessFrame(std::span output, std::span input, + const u32 target_sample_count, const u32 source_sample_count, + UpsamplerState* state) { + constexpr u32 WindowSize = 10; + constexpr std::array, WindowSize> SincWindow1{ + 51.93359375f, -18.80078125f, 9.73046875f, -5.33203125f, 2.84375f, + -1.41015625f, 0.62109375f, -0.2265625f, 0.0625f, -0.00390625f, + }; + constexpr std::array, WindowSize> SincWindow2{ + 105.35546875f, -24.52734375f, 11.9609375f, -6.515625f, 3.52734375f, + -1.796875f, 0.828125f, -0.32421875f, 0.1015625f, -0.015625f, + }; + constexpr std::array, WindowSize> SincWindow3{ + 122.08203125f, -16.47656250f, 7.68359375f, -4.15625000f, 2.26171875f, + -1.16796875f, 0.54687500f, -0.22265625f, 0.07421875f, -0.01171875f, + }; + constexpr std::array, WindowSize> SincWindow4{ + 23.73437500f, -9.62109375f, 5.07812500f, -2.78125000f, 1.46875000f, + -0.71484375f, 0.30859375f, -0.10546875f, 0.02734375f, 0.00000000f, + }; + constexpr std::array, WindowSize> SincWindow5{ + 80.62500000f, -24.67187500f, 12.44921875f, -6.80859375f, 3.66406250f, + -1.83984375f, 0.83203125f, -0.31640625f, 0.09375000f, -0.01171875f, + }; + + if (!state->initialized) { + switch (source_sample_count) { + case 40: + state->window_size = WindowSize; + state->ratio = 6.0f; + state->history.fill(0); + break; + + case 80: + state->window_size = WindowSize; + state->ratio = 3.0f; + state->history.fill(0); + break; + + case 160: + state->window_size = WindowSize; + state->ratio = 1.5f; + state->history.fill(0); + break; + + default: + LOG_ERROR(Service_Audio, "Invalid upsampling source count {}!", source_sample_count); + // This continues anyway, but let's assume 160 for sanity + state->window_size = WindowSize; + state->ratio = 1.5f; + state->history.fill(0); + break; + } + + state->history_input_index = 0; + state->history_output_index = 9; + state->history_start_index = 0; + state->history_end_index = UpsamplerState::HistorySize - 1; + state->initialized = true; + } + + if (target_sample_count == 0) { + return; + } + + u32 read_index{0}; + + auto increment = [&]() -> void { + state->history[state->history_input_index] = input[read_index++]; + state->history_input_index = + static_cast((state->history_input_index + 1) % UpsamplerState::HistorySize); + state->history_output_index = + static_cast((state->history_output_index + 1) % UpsamplerState::HistorySize); + }; + + auto calculate_sample = [&state](std::span> coeffs1, + std::span> coeffs2) -> s32 { + auto output_index{state->history_output_index}; + auto start_pos{output_index - state->history_start_index + 1U}; + auto end_pos{10U}; + + if (start_pos < 10) { + end_pos = start_pos; + } + + u64 prev_contrib{0}; + u32 coeff_index{0}; + for (; coeff_index < end_pos; coeff_index++, output_index--) { + prev_contrib += static_cast(state->history[output_index].to_raw()) * + coeffs1[coeff_index].to_raw(); + } + + auto end_index{state->history_end_index}; + for (; start_pos < 9; start_pos++, coeff_index++, end_index--) { + prev_contrib += static_cast(state->history[end_index].to_raw()) * + coeffs1[coeff_index].to_raw(); + } + + output_index = + static_cast((state->history_output_index + 1) % UpsamplerState::HistorySize); + start_pos = state->history_end_index - output_index + 1U; + end_pos = 10U; + + if (start_pos < 10) { + end_pos = start_pos; + } + + u64 next_contrib{0}; + coeff_index = 0; + for (; coeff_index < end_pos; coeff_index++, output_index++) { + next_contrib += static_cast(state->history[output_index].to_raw()) * + coeffs2[coeff_index].to_raw(); + } + + auto start_index{state->history_start_index}; + for (; start_pos < 9; start_pos++, start_index++, coeff_index++) { + next_contrib += static_cast(state->history[start_index].to_raw()) * + coeffs2[coeff_index].to_raw(); + } + + return static_cast(((prev_contrib >> 15) + (next_contrib >> 15)) >> 8); + }; + + switch (state->ratio.to_int_floor()) { + // 40 -> 240 + case 6: + for (u32 write_index = 0; write_index < target_sample_count; write_index++) { + switch (state->sample_index) { + case 0: + increment(); + output[write_index] = state->history[state->history_output_index].to_int_floor(); + break; + + case 1: + output[write_index] = calculate_sample(SincWindow3, SincWindow4); + break; + + case 2: + output[write_index] = calculate_sample(SincWindow2, SincWindow1); + break; + + case 3: + output[write_index] = calculate_sample(SincWindow5, SincWindow5); + break; + + case 4: + output[write_index] = calculate_sample(SincWindow1, SincWindow2); + break; + + case 5: + output[write_index] = calculate_sample(SincWindow4, SincWindow3); + break; + } + state->sample_index = static_cast((state->sample_index + 1) % 6); + } + break; + + // 80 -> 240 + case 3: + for (u32 write_index = 0; write_index < target_sample_count; write_index++) { + switch (state->sample_index) { + case 0: + increment(); + output[write_index] = state->history[state->history_output_index].to_int_floor(); + break; + + case 1: + output[write_index] = calculate_sample(SincWindow2, SincWindow1); + break; + + case 2: + output[write_index] = calculate_sample(SincWindow1, SincWindow2); + break; + } + state->sample_index = static_cast((state->sample_index + 1) % 3); + } + break; + + // 160 -> 240 + default: + for (u32 write_index = 0; write_index < target_sample_count; write_index++) { + switch (state->sample_index) { + case 0: + increment(); + output[write_index] = state->history[state->history_output_index].to_int_floor(); + break; + + case 1: + output[write_index] = calculate_sample(SincWindow1, SincWindow2); + break; + + case 2: + increment(); + output[write_index] = calculate_sample(SincWindow2, SincWindow1); + break; + } + state->sample_index = static_cast((state->sample_index + 1) % 3); + } + + break; + } +} + +auto UpsampleCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) -> void { + string += fmt::format("UpsampleCommand\n\tsource_sample_count {} source_sample_rate {}", + source_sample_count, source_sample_rate); + const auto upsampler{reinterpret_cast(upsampler_info)}; + if (upsampler != nullptr) { + string += fmt::format("\n\tUpsampler\n\t\tenabled {} sample count {}\n\tinputs: ", + upsampler->enabled, upsampler->sample_count); + for (u32 i = 0; i < upsampler->input_count; i++) { + string += fmt::format("{:02X}, ", upsampler->inputs[i]); + } + } + string += "\n"; +} + +void UpsampleCommand::Process(const ADSP::CommandListProcessor& processor) { + const auto info{reinterpret_cast(upsampler_info)}; + const auto input_count{std::min(info->input_count, buffer_count)}; + const std::span inputs_{reinterpret_cast(inputs), input_count}; + + for (u32 i = 0; i < input_count; i++) { + const auto channel{inputs_[i]}; + + if (channel >= 0 && channel < static_cast(processor.buffer_count)) { + auto state{&info->states[i]}; + std::span output{ + reinterpret_cast(samples_buffer + info->sample_count * channel * sizeof(s32)), + info->sample_count}; + auto input{processor.mix_buffers.subspan(channel * processor.sample_count, + processor.sample_count)}; + + SrcProcessFrame(output, input, info->sample_count, source_sample_count, state); + } + } +} + +bool UpsampleCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/resample/upsample.h b/src/audio_core/renderer/command/resample/upsample.h new file mode 100644 index 0000000000..bfc94e8aff --- /dev/null +++ b/src/audio_core/renderer/command/resample/upsample.h @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for upsampling a mix buffer to 48Khz. + * Input must be 8Khz, 16Khz or 32Khz, and output will be 48Khz. + */ +struct UpsampleCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Pointer to the output samples buffer. + CpuAddr samples_buffer; + /// Pointer to input mix buffer indexes. + CpuAddr inputs; + /// Number of input mix buffers. + u32 buffer_count; + /// Unknown, unused. + u32 unk_20; + /// Source data sample count. + u32 source_sample_count; + /// Source data sample rate. + u32 source_sample_rate; + /// Pointer to the upsampler info for this command. + CpuAddr upsampler_info; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/sink/circular_buffer.cpp b/src/audio_core/renderer/command/sink/circular_buffer.cpp new file mode 100644 index 0000000000..ded5afc94f --- /dev/null +++ b/src/audio_core/renderer/command/sink/circular_buffer.cpp @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/sink/circular_buffer.h" +#include "core/memory.h" + +namespace AudioCore::AudioRenderer { + +void CircularBufferSinkCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format( + "CircularBufferSinkCommand\n\tinput_count {} ring size {:04X} ring pos {:04X}\n\tinputs: ", + input_count, size, pos); + for (u32 i = 0; i < input_count; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n"; +} + +void CircularBufferSinkCommand::Process(const ADSP::CommandListProcessor& processor) { + constexpr s32 min{std::numeric_limits::min()}; + constexpr s32 max{std::numeric_limits::max()}; + + std::vector output(processor.sample_count); + for (u32 channel = 0; channel < input_count; channel++) { + auto input{processor.mix_buffers.subspan(inputs[channel] * processor.sample_count, + processor.sample_count)}; + for (u32 sample_index = 0; sample_index < processor.sample_count; sample_index++) { + output[sample_index] = static_cast(std::clamp(input[sample_index], min, max)); + } + + processor.memory->WriteBlockUnsafe(address + pos, output.data(), + output.size() * sizeof(s16)); + pos += static_cast(processor.sample_count * sizeof(s16)); + if (pos >= size) { + pos = 0; + } + } +} + +bool CircularBufferSinkCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/sink/circular_buffer.h b/src/audio_core/renderer/command/sink/circular_buffer.h new file mode 100644 index 0000000000..e7d5be26e5 --- /dev/null +++ b/src/audio_core/renderer/command/sink/circular_buffer.h @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for sinking samples to a circular buffer. + */ +struct CircularBufferSinkCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Number of input mix buffers + u32 input_count; + /// Input mix buffer indexes + std::array inputs; + /// Circular buffer address + CpuAddr address; + /// Circular buffer size + u32 size; + /// Current buffer offset + u32 pos; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/sink/device.cpp b/src/audio_core/renderer/command/sink/device.cpp new file mode 100644 index 0000000000..47e0c67226 --- /dev/null +++ b/src/audio_core/renderer/command/sink/device.cpp @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/adsp/command_list_processor.h" +#include "audio_core/renderer/command/sink/device.h" +#include "audio_core/sink/sink.h" + +namespace AudioCore::AudioRenderer { + +void DeviceSinkCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor, + std::string& string) { + string += fmt::format("DeviceSinkCommand\n\t{} session {} input_count {}\n\tinputs: ", + std::string_view(name), session_id, input_count); + for (u32 i = 0; i < input_count; i++) { + string += fmt::format("{:02X}, ", inputs[i]); + } + string += "\n"; +} + +void DeviceSinkCommand::Process(const ADSP::CommandListProcessor& processor) { + constexpr s32 min = std::numeric_limits::min(); + constexpr s32 max = std::numeric_limits::max(); + + auto stream{processor.GetOutputSinkStream()}; + stream->SetSystemChannels(input_count); + + Sink::SinkBuffer out_buffer{ + .frames{TargetSampleCount}, + .frames_played{0}, + .tag{0}, + .consumed{false}, + }; + + std::vector samples(out_buffer.frames * input_count); + + for (u32 channel = 0; channel < input_count; channel++) { + const auto offset{inputs[channel] * out_buffer.frames}; + + for (u32 index = 0; index < out_buffer.frames; index++) { + samples[index * input_count + channel] = + static_cast(std::clamp(sample_buffer[offset + index], min, max)); + } + } + + out_buffer.tag = reinterpret_cast(samples.data()); + stream->AppendBuffer(out_buffer, samples); +} + +bool DeviceSinkCommand::Verify(const ADSP::CommandListProcessor& processor) { + return true; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/sink/device.h b/src/audio_core/renderer/command/sink/device.h new file mode 100644 index 0000000000..1099bcf8c8 --- /dev/null +++ b/src/audio_core/renderer/command/sink/device.h @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "audio_core/renderer/command/icommand.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class CommandListProcessor; +} + +/** + * AudioRenderer command for sinking samples to an output device. + */ +struct DeviceSinkCommand : ICommand { + /** + * Print this command's information to a string. + * + * @param processor - The CommandListProcessor processing this command. + * @param string - The string to print into. + */ + void Dump(const ADSP::CommandListProcessor& processor, std::string& string) override; + + /** + * Process this command. + * + * @param processor - The CommandListProcessor processing this command. + */ + void Process(const ADSP::CommandListProcessor& processor) override; + + /** + * Verify this command's data is valid. + * + * @param processor - The CommandListProcessor processing this command. + * @return True if the command is valid, otherwise false. + */ + bool Verify(const ADSP::CommandListProcessor& processor) override; + + /// Device name + char name[0x100]; + /// System session id (unused) + s32 session_id; + /// Sample buffer to sink + std::span sample_buffer; + /// Number of input channels + u32 input_count; + /// Mix buffer indexes for each channel + std::array inputs; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/aux_.cpp b/src/audio_core/renderer/effect/aux_.cpp new file mode 100644 index 0000000000..51e780ef1f --- /dev/null +++ b/src/audio_core/renderer/effect/aux_.cpp @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/aux_.h" + +namespace AudioCore::AudioRenderer { + +void AuxInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + if (buffer_unmapped || in_params.is_new) { + const bool send_unmapped{!pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_specific->send_buffer_info_address, + sizeof(AuxBufferInfo) + in_specific->count_max * sizeof(s32))}; + const bool return_unmapped{!pool_mapper.TryAttachBuffer( + error_info, workbuffers[1], in_specific->return_buffer_info_address, + sizeof(AuxBufferInfo) + in_specific->count_max * sizeof(s32))}; + + buffer_unmapped = send_unmapped || return_unmapped; + + if (!buffer_unmapped) { + auto send{workbuffers[0].GetReference(false)}; + send_buffer_info = send + sizeof(AuxInfoDsp); + send_buffer = send + sizeof(AuxBufferInfo); + + auto ret{workbuffers[1].GetReference(false)}; + return_buffer_info = ret + sizeof(AuxInfoDsp); + return_buffer = ret + sizeof(AuxBufferInfo); + } + } else { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } +} + +void AuxInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(ParameterVersion2)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (buffer_unmapped || in_params.is_new) { + const bool send_unmapped{!pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], params->send_buffer_info_address, + sizeof(AuxBufferInfo) + params->count_max * sizeof(s32))}; + const bool return_unmapped{!pool_mapper.TryAttachBuffer( + error_info, workbuffers[1], params->return_buffer_info_address, + sizeof(AuxBufferInfo) + params->count_max * sizeof(s32))}; + + buffer_unmapped = send_unmapped || return_unmapped; + + if (!buffer_unmapped) { + auto send{workbuffers[0].GetReference(false)}; + send_buffer_info = send + sizeof(AuxInfoDsp); + send_buffer = send + sizeof(AuxBufferInfo); + + auto ret{workbuffers[1].GetReference(false)}; + return_buffer_info = ret + sizeof(AuxInfoDsp); + return_buffer = ret + sizeof(AuxBufferInfo); + } + } else { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } +} + +void AuxInfo::UpdateForCommandGeneration() { + if (enabled) { + usage_state = UsageState::Enabled; + } else { + usage_state = UsageState::Disabled; + } +} + +void AuxInfo::InitializeResultState(EffectResultState& result_state) {} + +void AuxInfo::UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) {} + +CpuAddr AuxInfo::GetWorkbuffer(s32 index) { + return workbuffers[index].GetReference(true); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/aux_.h b/src/audio_core/renderer/effect/aux_.h new file mode 100644 index 0000000000..4d3d9e3d95 --- /dev/null +++ b/src/audio_core/renderer/effect/aux_.h @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Auxiliary Buffer used for Aux commands. + * Send and return buffers are available (names from the game's perspective). + * Send is read by the host, containing a buffer of samples to be used for whatever purpose. + * Return is written by the host, writing a mix buffer back to the game. + * This allows the game to use pre-processed samples skipping the other render processing, + * and to examine or modify what the audio renderer has generated. + */ +class AuxInfo : public EffectInfoBase { +public: + struct ParameterVersion1 { + /* 0x00 */ std::array inputs; + /* 0x18 */ std::array outputs; + /* 0x30 */ u32 mix_buffer_count; + /* 0x34 */ u32 sample_rate; + /* 0x38 */ u32 count_max; + /* 0x3C */ u32 mix_buffer_count_max; + /* 0x40 */ CpuAddr send_buffer_info_address; + /* 0x48 */ CpuAddr send_buffer_address; + /* 0x50 */ CpuAddr return_buffer_info_address; + /* 0x58 */ CpuAddr return_buffer_address; + /* 0x60 */ u32 mix_buffer_sample_size; + /* 0x64 */ u32 sample_count; + /* 0x68 */ u32 mix_buffer_sample_count; + }; + static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1), + "AuxInfo::ParameterVersion1 has the wrong size!"); + + struct ParameterVersion2 { + /* 0x00 */ std::array inputs; + /* 0x18 */ std::array outputs; + /* 0x30 */ u32 mix_buffer_count; + /* 0x34 */ u32 sample_rate; + /* 0x38 */ u32 count_max; + /* 0x3C */ u32 mix_buffer_count_max; + /* 0x40 */ CpuAddr send_buffer_info_address; + /* 0x48 */ CpuAddr send_buffer_address; + /* 0x50 */ CpuAddr return_buffer_info_address; + /* 0x58 */ CpuAddr return_buffer_address; + /* 0x60 */ u32 mix_buffer_sample_size; + /* 0x64 */ u32 sample_count; + /* 0x68 */ u32 mix_buffer_sample_count; + }; + static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2), + "AuxInfo::ParameterVersion2 has the wrong size!"); + + struct AuxInfoDsp { + /* 0x00 */ u32 read_offset; + /* 0x04 */ u32 write_offset; + /* 0x08 */ u32 lost_sample_count; + /* 0x0C */ u32 total_sample_count; + /* 0x10 */ char unk10[0x30]; + }; + static_assert(sizeof(AuxInfoDsp) == 0x40, "AuxInfo::AuxInfoDsp has the wrong size!"); + + struct AuxBufferInfo { + /* 0x00 */ AuxInfoDsp cpu_info; + /* 0x40 */ AuxInfoDsp dsp_info; + }; + static_assert(sizeof(AuxBufferInfo) == 0x80, "AuxInfo::AuxBufferInfo has the wrong size!"); + + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info after command generation. Usually only changes its state. + */ + void UpdateForCommandGeneration() override; + + /** + * Initialize a new result state. Version 2 only, unused. + * + * @param result_state - Result state to initialize. + */ + void InitializeResultState(EffectResultState& result_state) override; + + /** + * Update the host-side state with the ADSP-side state. Version 2 only, unused. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override; + + /** + * Get a workbuffer assigned to this effect with the given index. + * + * @param index - Workbuffer index. + * @return Address of the buffer. + */ + CpuAddr GetWorkbuffer(s32 index) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/biquad_filter.cpp b/src/audio_core/renderer/effect/biquad_filter.cpp new file mode 100644 index 0000000000..a1efb32312 --- /dev/null +++ b/src/audio_core/renderer/effect/biquad_filter.cpp @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/biquad_filter.h" + +namespace AudioCore::AudioRenderer { + +void BiquadFilterInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion1& in_params, const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void BiquadFilterInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion2& in_params, const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(ParameterVersion2)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void BiquadFilterInfo::UpdateForCommandGeneration() { + if (enabled) { + usage_state = UsageState::Enabled; + } else { + usage_state = UsageState::Disabled; + } + + auto params{reinterpret_cast(parameter.data())}; + params->state = ParameterState::Updated; +} + +void BiquadFilterInfo::InitializeResultState(EffectResultState& result_state) {} + +void BiquadFilterInfo::UpdateResultState(EffectResultState& cpu_state, + EffectResultState& dsp_state) {} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/biquad_filter.h b/src/audio_core/renderer/effect/biquad_filter.h new file mode 100644 index 0000000000..f53fd5babc --- /dev/null +++ b/src/audio_core/renderer/effect/biquad_filter.h @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +class BiquadFilterInfo : public EffectInfoBase { +public: + struct ParameterVersion1 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ std::array b; + /* 0x12 */ std::array a; + /* 0x16 */ s8 channel_count; + /* 0x17 */ ParameterState state; + }; + static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1), + "BiquadFilterInfo::ParameterVersion1 has the wrong size!"); + + struct ParameterVersion2 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ std::array b; + /* 0x12 */ std::array a; + /* 0x16 */ s8 channel_count; + /* 0x17 */ ParameterState state; + }; + static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2), + "BiquadFilterInfo::ParameterVersion2 has the wrong size!"); + + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info after command generation. Usually only changes its state. + */ + void UpdateForCommandGeneration() override; + + /** + * Initialize a new result state. Version 2 only, unused. + * + * @param result_state - Result state to initialize. + */ + void InitializeResultState(EffectResultState& result_state) override; + + /** + * Update the host-side state with the ADSP-side state. Version 2 only, unused. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/buffer_mixer.cpp b/src/audio_core/renderer/effect/buffer_mixer.cpp new file mode 100644 index 0000000000..9c8877f012 --- /dev/null +++ b/src/audio_core/renderer/effect/buffer_mixer.cpp @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/buffer_mixer.h" + +namespace AudioCore::AudioRenderer { + +void BufferMixerInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion1& in_params, const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void BufferMixerInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion2& in_params, const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(ParameterVersion2)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void BufferMixerInfo::UpdateForCommandGeneration() { + if (enabled) { + usage_state = UsageState::Enabled; + } else { + usage_state = UsageState::Disabled; + } +} + +void BufferMixerInfo::InitializeResultState(EffectResultState& result_state) {} + +void BufferMixerInfo::UpdateResultState(EffectResultState& cpu_state, + EffectResultState& dsp_state) {} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/buffer_mixer.h b/src/audio_core/renderer/effect/buffer_mixer.h new file mode 100644 index 0000000000..23eed4a8be --- /dev/null +++ b/src/audio_core/renderer/effect/buffer_mixer.h @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +class BufferMixerInfo : public EffectInfoBase { +public: + struct ParameterVersion1 { + /* 0x00 */ std::array inputs; + /* 0x18 */ std::array outputs; + /* 0x30 */ std::array volumes; + /* 0x90 */ u32 mix_count; + }; + static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1), + "BufferMixerInfo::ParameterVersion1 has the wrong size!"); + + struct ParameterVersion2 { + /* 0x00 */ std::array inputs; + /* 0x18 */ std::array outputs; + /* 0x30 */ std::array volumes; + /* 0x90 */ u32 mix_count; + }; + static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2), + "BufferMixerInfo::ParameterVersion2 has the wrong size!"); + + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info after command generation. Usually only changes its state. + */ + void UpdateForCommandGeneration() override; + + /** + * Initialize a new result state. Version 2 only, unused. + * + * @param result_state - Result state to initialize. + */ + void InitializeResultState(EffectResultState& result_state) override; + + /** + * Update the host-side state with the ADSP-side state. Version 2 only, unused. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/capture.cpp b/src/audio_core/renderer/effect/capture.cpp new file mode 100644 index 0000000000..3f038efdb3 --- /dev/null +++ b/src/audio_core/renderer/effect/capture.cpp @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/aux_.h" +#include "audio_core/renderer/effect/capture.h" + +namespace AudioCore::AudioRenderer { + +void CaptureInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) { + auto in_specific{ + reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(AuxInfo::ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + if (buffer_unmapped || in_params.is_new) { + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_specific->send_buffer_info_address, + in_specific->count_max * sizeof(s32) + sizeof(AuxInfo::AuxBufferInfo)); + + if (!buffer_unmapped) { + const auto send_address{workbuffers[0].GetReference(false)}; + send_buffer_info = send_address + sizeof(AuxInfo::AuxInfoDsp); + send_buffer = send_address + sizeof(AuxInfo::AuxBufferInfo); + return_buffer_info = 0; + return_buffer = 0; + } + } else { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } +} + +void CaptureInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) { + auto in_specific{ + reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(AuxInfo::ParameterVersion2)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (buffer_unmapped || in_params.is_new) { + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], params->send_buffer_info_address, + params->count_max * sizeof(s32) + sizeof(AuxInfo::AuxBufferInfo)); + + if (!buffer_unmapped) { + const auto send_address{workbuffers[0].GetReference(false)}; + send_buffer_info = send_address + sizeof(AuxInfo::AuxInfoDsp); + send_buffer = send_address + sizeof(AuxInfo::AuxBufferInfo); + return_buffer_info = 0; + return_buffer = 0; + } + } else { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } +} + +void CaptureInfo::UpdateForCommandGeneration() { + if (enabled) { + usage_state = UsageState::Enabled; + } else { + usage_state = UsageState::Disabled; + } +} + +void CaptureInfo::InitializeResultState(EffectResultState& result_state) {} + +void CaptureInfo::UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) {} + +CpuAddr CaptureInfo::GetWorkbuffer(s32 index) { + return workbuffers[index].GetReference(true); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/capture.h b/src/audio_core/renderer/effect/capture.h new file mode 100644 index 0000000000..6fbed8e6be --- /dev/null +++ b/src/audio_core/renderer/effect/capture.h @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +class CaptureInfo : public EffectInfoBase { +public: + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info after command generation. Usually only changes its state. + */ + void UpdateForCommandGeneration() override; + + /** + * Initialize a new result state. Version 2 only, unused. + * + * @param result_state - Result state to initialize. + */ + void InitializeResultState(EffectResultState& result_state) override; + + /** + * Update the host-side state with the ADSP-side state. Version 2 only, unused. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override; + + /** + * Get a workbuffer assigned to this effect with the given index. + * + * @param index - Workbuffer index. + * @return Address of the buffer. + */ + CpuAddr GetWorkbuffer(s32 index) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/compressor.cpp b/src/audio_core/renderer/effect/compressor.cpp new file mode 100644 index 0000000000..220ae02f96 --- /dev/null +++ b/src/audio_core/renderer/effect/compressor.cpp @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/compressor.h" + +namespace AudioCore::AudioRenderer { + +void CompressorInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion1& in_params, const PoolMapper& pool_mapper) {} + +void CompressorInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion2& in_params, const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void CompressorInfo::UpdateForCommandGeneration() { + if (enabled) { + usage_state = UsageState::Enabled; + } else { + usage_state = UsageState::Disabled; + } + + auto params{reinterpret_cast(parameter.data())}; + params->state = ParameterState::Updated; +} + +CpuAddr CompressorInfo::GetWorkbuffer(s32 index) { + return GetSingleBuffer(index); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/compressor.h b/src/audio_core/renderer/effect/compressor.h new file mode 100644 index 0000000000..019a5ae58a --- /dev/null +++ b/src/audio_core/renderer/effect/compressor.h @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { + +class CompressorInfo : public EffectInfoBase { +public: + struct ParameterVersion1 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ s16 channel_count_max; + /* 0x0E */ s16 channel_count; + /* 0x10 */ s32 sample_rate; + /* 0x14 */ f32 threshold; + /* 0x18 */ f32 compressor_ratio; + /* 0x1C */ s32 attack_time; + /* 0x20 */ s32 release_time; + /* 0x24 */ f32 unk_24; + /* 0x28 */ f32 unk_28; + /* 0x2C */ f32 unk_2C; + /* 0x30 */ f32 out_gain; + /* 0x34 */ ParameterState state; + /* 0x35 */ bool makeup_gain_enabled; + }; + static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1), + "CompressorInfo::ParameterVersion1 has the wrong size!"); + + struct ParameterVersion2 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ s16 channel_count_max; + /* 0x0E */ s16 channel_count; + /* 0x10 */ s32 sample_rate; + /* 0x14 */ f32 threshold; + /* 0x18 */ f32 compressor_ratio; + /* 0x1C */ s32 attack_time; + /* 0x20 */ s32 release_time; + /* 0x24 */ f32 unk_24; + /* 0x28 */ f32 unk_28; + /* 0x2C */ f32 unk_2C; + /* 0x30 */ f32 out_gain; + /* 0x34 */ ParameterState state; + /* 0x35 */ bool makeup_gain_enabled; + }; + static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2), + "CompressorInfo::ParameterVersion2 has the wrong size!"); + + struct State { + f32 unk_00; + f32 unk_04; + f32 unk_08; + f32 unk_0C; + f32 unk_10; + f32 unk_14; + f32 unk_18; + f32 makeup_gain; + f32 unk_20; + char unk_24[0x1C]; + }; + static_assert(sizeof(State) <= sizeof(EffectInfoBase::State), + "CompressorInfo::State has the wrong size!"); + + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info after command generation. Usually only changes its state. + */ + void UpdateForCommandGeneration() override; + + /** + * Get a workbuffer assigned to this effect with the given index. + * + * @param index - Workbuffer index. + * @return Address of the buffer. + */ + CpuAddr GetWorkbuffer(s32 index) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/delay.cpp b/src/audio_core/renderer/effect/delay.cpp new file mode 100644 index 0000000000..d9853efd92 --- /dev/null +++ b/src/audio_core/renderer/effect/delay.cpp @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/delay.h" + +namespace AudioCore::AudioRenderer { + +void DelayInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + if (IsChannelCountValid(in_specific->channel_count_max)) { + const auto old_state{params->state}; + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (!IsChannelCountValid(in_specific->channel_count)) { + params->channel_count = params->channel_count_max; + } + + if (!IsChannelCountValid(in_specific->channel_count) || + old_state != ParameterState::Updated) { + params->state = old_state; + } + + if (buffer_unmapped || in_params.is_new) { + usage_state = UsageState::New; + params->state = ParameterState::Initialized; + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size); + return; + } + } + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void DelayInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + if (IsChannelCountValid(in_specific->channel_count_max)) { + const auto old_state{params->state}; + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (!IsChannelCountValid(in_specific->channel_count)) { + params->channel_count = params->channel_count_max; + } + + if (!IsChannelCountValid(in_specific->channel_count) || + old_state != ParameterState::Updated) { + params->state = old_state; + } + + if (buffer_unmapped || in_params.is_new) { + usage_state = UsageState::New; + params->state = ParameterState::Initialized; + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size); + return; + } + } + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void DelayInfo::UpdateForCommandGeneration() { + if (enabled) { + usage_state = UsageState::Enabled; + } else { + usage_state = UsageState::Disabled; + } + + auto params{reinterpret_cast(parameter.data())}; + params->state = ParameterState::Updated; +} + +void DelayInfo::InitializeResultState(EffectResultState& result_state) {} + +void DelayInfo::UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) {} + +CpuAddr DelayInfo::GetWorkbuffer(s32 index) { + return GetSingleBuffer(index); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/delay.h b/src/audio_core/renderer/effect/delay.h new file mode 100644 index 0000000000..accc42a060 --- /dev/null +++ b/src/audio_core/renderer/effect/delay.h @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { + +class DelayInfo : public EffectInfoBase { +public: + struct ParameterVersion1 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ u16 channel_count_max; + /* 0x0E */ u16 channel_count; + /* 0x10 */ u32 delay_time_max; + /* 0x14 */ u32 delay_time; + /* 0x18 */ Common::FixedPoint<18, 14> sample_rate; + /* 0x1C */ Common::FixedPoint<18, 14> in_gain; + /* 0x20 */ Common::FixedPoint<18, 14> feedback_gain; + /* 0x24 */ Common::FixedPoint<18, 14> wet_gain; + /* 0x28 */ Common::FixedPoint<18, 14> dry_gain; + /* 0x2C */ Common::FixedPoint<18, 14> channel_spread; + /* 0x30 */ Common::FixedPoint<18, 14> lowpass_amount; + /* 0x34 */ ParameterState state; + }; + static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1), + "DelayInfo::ParameterVersion1 has the wrong size!"); + + struct ParameterVersion2 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ s16 channel_count_max; + /* 0x0E */ s16 channel_count; + /* 0x10 */ s32 delay_time_max; + /* 0x14 */ s32 delay_time; + /* 0x18 */ s32 sample_rate; + /* 0x1C */ s32 in_gain; + /* 0x20 */ s32 feedback_gain; + /* 0x24 */ s32 wet_gain; + /* 0x28 */ s32 dry_gain; + /* 0x2C */ s32 channel_spread; + /* 0x30 */ s32 lowpass_amount; + /* 0x34 */ ParameterState state; + }; + static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2), + "DelayInfo::ParameterVersion2 has the wrong size!"); + + struct DelayLine { + Common::FixedPoint<50, 14> Read() const { + return buffer[buffer_pos]; + } + + void Write(const Common::FixedPoint<50, 14> value) { + buffer[buffer_pos] = value; + buffer_pos = static_cast((buffer_pos + 1) % buffer.size()); + } + + s32 sample_count_max{}; + s32 sample_count{}; + std::vector> buffer{}; + u32 buffer_pos{}; + Common::FixedPoint<18, 14> decay_rate{}; + }; + + struct State { + /* 0x000 */ std::array unk_000; + /* 0x020 */ std::array delay_lines; + /* 0x0B0 */ Common::FixedPoint<18, 14> feedback_gain; + /* 0x0B4 */ Common::FixedPoint<18, 14> delay_feedback_gain; + /* 0x0B8 */ Common::FixedPoint<18, 14> delay_feedback_cross_gain; + /* 0x0BC */ Common::FixedPoint<18, 14> lowpass_gain; + /* 0x0C0 */ Common::FixedPoint<18, 14> lowpass_feedback_gain; + /* 0x0C4 */ std::array, MaxChannels> lowpass_z; + }; + static_assert(sizeof(State) <= sizeof(EffectInfoBase::State), + "DelayInfo::State has the wrong size!"); + + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info after command generation. Usually only changes its state. + */ + void UpdateForCommandGeneration() override; + + /** + * Initialize a new result state. Version 2 only, unused. + * + * @param result_state - Result state to initialize. + */ + void InitializeResultState(EffectResultState& result_state) override; + + /** + * Update the host-side state with the ADSP-side state. Version 2 only, unused. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override; + + /** + * Get a workbuffer assigned to this effect with the given index. + * + * @param index - Workbuffer index. + * @return Address of the buffer. + */ + CpuAddr GetWorkbuffer(s32 index) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_context.cpp b/src/audio_core/renderer/effect/effect_context.cpp new file mode 100644 index 0000000000..74c7801c9a --- /dev/null +++ b/src/audio_core/renderer/effect/effect_context.cpp @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/effect_context.h" + +namespace AudioCore::AudioRenderer { + +void EffectContext::Initialize(std::span effect_infos_, const u32 effect_count_, + std::span result_states_cpu_, + std::span result_states_dsp_, + const size_t dsp_state_count_) { + effect_infos = effect_infos_; + effect_count = effect_count_; + result_states_cpu = result_states_cpu_; + result_states_dsp = result_states_dsp_; + dsp_state_count = dsp_state_count_; +} + +EffectInfoBase& EffectContext::GetInfo(const u32 index) { + return effect_infos[index]; +} + +EffectResultState& EffectContext::GetResultState(const u32 index) { + return result_states_cpu[index]; +} + +EffectResultState& EffectContext::GetDspSharedResultState(const u32 index) { + return result_states_dsp[index]; +} + +u32 EffectContext::GetCount() const { + return effect_count; +} + +void EffectContext::UpdateStateByDspShared() { + for (size_t i = 0; i < dsp_state_count; i++) { + effect_infos[i].UpdateResultState(result_states_cpu[i], result_states_dsp[i]); + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_context.h b/src/audio_core/renderer/effect/effect_context.h new file mode 100644 index 0000000000..85955bd9c9 --- /dev/null +++ b/src/audio_core/renderer/effect/effect_context.h @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/effect/effect_info_base.h" +#include "audio_core/renderer/effect/effect_result_state.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +class EffectContext { +public: + /** + * Initialize the effect context + * @param effect_infos List of effect infos for this context + * @param effect_count The number of effects in the list + * @param result_states_cpu The workbuffer of result states for the CPU for this context + * @param result_states_dsp The workbuffer of result states for the DSP for this context + * @param state_count The number of result states + */ + void Initialize(std::span effect_infos_, const u32 effect_count_, + std::span result_states_cpu_, + std::span result_states_dsp_, const size_t dsp_state_count); + + /** + * Get the EffectInfo for a given index + * @param index Which effect to return + * @return Pointer to the effect + */ + EffectInfoBase& GetInfo(const u32 index); + + /** + * Get the CPU result state for a given index + * @param index Which result to return + * @return Pointer to the effect result state + */ + EffectResultState& GetResultState(const u32 index); + + /** + * Get the DSP result state for a given index + * @param index Which result to return + * @return Pointer to the effect result state + */ + EffectResultState& GetDspSharedResultState(const u32 index); + + /** + * Get the number of effects in this context + * @return The number of effects + */ + u32 GetCount() const; + + /** + * Update the CPU and DSP result states for all effects + */ + void UpdateStateByDspShared(); + +private: + /// Workbuffer for all of the effects + std::span effect_infos{}; + /// Number of effects in the workbuffer + u32 effect_count{}; + /// Workbuffer of states for all effects, kept host-side and not directly modified, dsp states + /// are copied here on the next render frame + std::span result_states_cpu{}; + /// Workbuffer of states for all effects, used by the AudioRenderer to track effect state + /// between calls + std::span result_states_dsp{}; + /// Number of result states in the workbuffers + size_t dsp_state_count{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_info_base.h b/src/audio_core/renderer/effect/effect_info_base.h new file mode 100644 index 0000000000..43d0589cc9 --- /dev/null +++ b/src/audio_core/renderer/effect/effect_info_base.h @@ -0,0 +1,435 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/effect/effect_result_state.h" +#include "audio_core/renderer/memory/address_info.h" +#include "audio_core/renderer/memory/pool_mapper.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Base of all effects. Holds various data and functions used for all derived effects. + * Should not be used directly. + */ +class EffectInfoBase { +public: + enum class Type : u8 { + Invalid, + Mix, + Aux, + Delay, + Reverb, + I3dl2Reverb, + BiquadFilter, + LightLimiter, + Capture, + Compressor, + }; + + enum class UsageState { + Invalid, + New, + Enabled, + Disabled, + }; + + enum class OutStatus : u8 { + Invalid, + New, + Initialized, + Used, + Removed, + }; + + enum class ParameterState : u8 { + Initialized, + Updating, + Updated, + }; + + struct InParameterVersion1 { + /* 0x00 */ Type type; + /* 0x01 */ bool is_new; + /* 0x02 */ bool enabled; + /* 0x04 */ u32 mix_id; + /* 0x08 */ CpuAddr workbuffer; + /* 0x10 */ CpuAddr workbuffer_size; + /* 0x18 */ u32 process_order; + /* 0x1C */ char unk1C[0x4]; + /* 0x20 */ std::array specific; + }; + static_assert(sizeof(InParameterVersion1) == 0xC0, + "EffectInfoBase::InParameterVersion1 has the wrong size!"); + + struct InParameterVersion2 { + /* 0x00 */ Type type; + /* 0x01 */ bool is_new; + /* 0x02 */ bool enabled; + /* 0x04 */ u32 mix_id; + /* 0x08 */ CpuAddr workbuffer; + /* 0x10 */ CpuAddr workbuffer_size; + /* 0x18 */ u32 process_order; + /* 0x1C */ char unk1C[0x4]; + /* 0x20 */ std::array specific; + }; + static_assert(sizeof(InParameterVersion2) == 0xC0, + "EffectInfoBase::InParameterVersion2 has the wrong size!"); + + struct OutStatusVersion1 { + /* 0x00 */ OutStatus state; + /* 0x01 */ char unk01[0xF]; + }; + static_assert(sizeof(OutStatusVersion1) == 0x10, + "EffectInfoBase::OutStatusVersion1 has the wrong size!"); + + struct OutStatusVersion2 { + /* 0x00 */ OutStatus state; + /* 0x01 */ char unk01[0xF]; + /* 0x10 */ EffectResultState result_state; + }; + static_assert(sizeof(OutStatusVersion2) == 0x90, + "EffectInfoBase::OutStatusVersion2 has the wrong size!"); + + struct State { + std::array buffer; + }; + static_assert(sizeof(State) == 0x500, "EffectInfoBase::State has the wrong size!"); + + EffectInfoBase() { + Cleanup(); + } + + virtual ~EffectInfoBase() = default; + + /** + * Cleanup this effect, resetting it to a starting state. + */ + void Cleanup() { + type = Type::Invalid; + enabled = false; + mix_id = UnusedMixId; + process_order = InvalidProcessOrder; + buffer_unmapped = false; + parameter = {}; + for (auto& workbuffer : workbuffers) { + workbuffer.Setup(CpuAddr(0), 0); + } + } + + /** + * Forcibly unmap all assigned workbuffers from the AudioRenderer. + * + * @param pool_mapper - Mapper to unmap the buffers. + */ + void ForceUnmapBuffers(const PoolMapper& pool_mapper) { + for (auto& workbuffer : workbuffers) { + if (workbuffer.GetReference(false) != 0) { + pool_mapper.ForceUnmapPointer(workbuffer); + } + } + } + + /** + * Check if this effect is enabled. + * + * @return True if effect is enabled, otherwise false. + */ + bool IsEnabled() const { + return enabled; + } + + /** + * Check if this effect should not be generated. + * + * @return True if effect should be skipped, otherwise false. + */ + bool ShouldSkip() const { + return buffer_unmapped; + } + + /** + * Get the type of this effect. + * + * @return The type of this effect. See EffectInfoBase::Type + */ + Type GetType() const { + return type; + } + + /** + * Set the type of this effect. + * + * @param type_ - The new type of this effect. + */ + void SetType(const Type type_) { + type = type_; + } + + /** + * Get the mix id of this effect. + * + * @return Mix id of this effect. + */ + s32 GetMixId() const { + return mix_id; + } + + /** + * Get the processing order of this effect. + * + * @return Process order of this effect. + */ + s32 GetProcessingOrder() const { + return process_order; + } + + /** + * Get this effect's parameter data. + * + * @return Pointer to the parametter, must be cast to the correct type. + */ + u8* GetParameter() { + return parameter.data(); + } + + /** + * Get this effect's parameter data. + * + * @return Pointer to the parametter, must be cast to the correct type. + */ + u8* GetStateBuffer() { + return state.data(); + } + + /** + * Set this effect's usage state. + * + * @param usage - new usage state of this effect. + */ + void SetUsage(const UsageState usage) { + usage_state = usage; + } + + /** + * Check if this effects need to have its workbuffer information updated. + * Version 1. + * + * @param params - Input parameters. + * @return True if workbuffers need updating, otherwise false. + */ + bool ShouldUpdateWorkBufferInfo(const InParameterVersion1& params) const { + return buffer_unmapped || params.is_new; + } + + /** + * Check if this effects need to have its workbuffer information updated. + * Version 2. + * + * @param params - Input parameters. + * @return True if workbuffers need updating, otherwise false. + */ + bool ShouldUpdateWorkBufferInfo(const InParameterVersion2& params) const { + return buffer_unmapped || params.is_new; + } + + /** + * Get the current usage state of this effect. + * + * @return The current usage state. + */ + UsageState GetUsage() const { + return usage_state; + } + + /** + * Write the current state. Version 1. + * + * @param out_status - Status to write. + * @param renderer_active - Is the AudioRenderer active? + */ + void StoreStatus(OutStatusVersion1& out_status, const bool renderer_active) const { + if (renderer_active) { + if (usage_state != UsageState::Disabled) { + out_status.state = OutStatus::Used; + } else { + out_status.state = OutStatus::Removed; + } + } else if (usage_state == UsageState::New) { + out_status.state = OutStatus::Used; + } else { + out_status.state = OutStatus::Removed; + } + } + + /** + * Write the current state. Version 2. + * + * @param out_status - Status to write. + * @param renderer_active - Is the AudioRenderer active? + */ + void StoreStatus(OutStatusVersion2& out_status, const bool renderer_active) const { + if (renderer_active) { + if (usage_state != UsageState::Disabled) { + out_status.state = OutStatus::Used; + } else { + out_status.state = OutStatus::Removed; + } + } else if (usage_state == UsageState::New) { + out_status.state = OutStatus::Used; + } else { + out_status.state = OutStatus::Removed; + } + } + + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + virtual void Update(BehaviorInfo::ErrorInfo& error_info, + [[maybe_unused]] const InParameterVersion1& params, + [[maybe_unused]] const PoolMapper& pool_mapper) { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + virtual void Update(BehaviorInfo::ErrorInfo& error_info, + [[maybe_unused]] const InParameterVersion2& params, + [[maybe_unused]] const PoolMapper& pool_mapper) { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } + + /** + * Update the info after command generation. Usually only changes its state. + */ + virtual void UpdateForCommandGeneration() {} + + /** + * Initialize a new result state. Version 2 only, unused. + * + * @param result_state - Result state to initialize. + */ + virtual void InitializeResultState([[maybe_unused]] EffectResultState& result_state) {} + + /** + * Update the host-side state with the ADSP-side state. Version 2 only, unused. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + virtual void UpdateResultState([[maybe_unused]] EffectResultState& cpu_state, + [[maybe_unused]] EffectResultState& dsp_state) {} + + /** + * Get a workbuffer assigned to this effect with the given index. + * + * @param index - Workbuffer index. + * @return Address of the buffer. + */ + virtual CpuAddr GetWorkbuffer([[maybe_unused]] s32 index) { + return 0; + } + + /** + * Get the first workbuffer assigned to this effect. + * + * @param index - Workbuffer index. Unused. + * @return Address of the buffer. + */ + CpuAddr GetSingleBuffer([[maybe_unused]] const s32 index) { + if (enabled) { + return workbuffers[0].GetReference(true); + } + + if (usage_state != UsageState::Disabled) { + const auto ref{workbuffers[0].GetReference(false)}; + const auto size{workbuffers[0].GetSize()}; + if (ref != 0 && size > 0) { + // Invalidate DSP cache + } + } + return 0; + } + + /** + * Get the send buffer info, used by Aux and Capture. + * + * @return Address of the buffer info. + */ + CpuAddr GetSendBufferInfo() const { + return send_buffer_info; + } + + /** + * Get the send buffer, used by Aux and Capture. + * + * @return Address of the buffer. + */ + CpuAddr GetSendBuffer() const { + return send_buffer; + } + + /** + * Get the return buffer info, used by Aux and Capture. + * + * @return Address of the buffer info. + */ + CpuAddr GetReturnBufferInfo() const { + return return_buffer_info; + } + + /** + * Get the return buffer, used by Aux and Capture. + * + * @return Address of the buffer. + */ + CpuAddr GetReturnBuffer() const { + return return_buffer; + } + +protected: + /// Type of this effect. May be changed + Type type{Type::Invalid}; + /// Is this effect enabled? + bool enabled{}; + /// Are this effect's buffers unmapped? + bool buffer_unmapped{}; + /// Current usage state + UsageState usage_state{UsageState::Invalid}; + /// Mix id of this effect + s32 mix_id{UnusedMixId}; + /// Process order of this effect + s32 process_order{InvalidProcessOrder}; + /// Workbuffers assigned to this effect + std::array workbuffers{AddressInfo(CpuAddr(0), 0), AddressInfo(CpuAddr(0), 0)}; + /// Aux/Capture buffer info for reading + CpuAddr send_buffer_info; + /// Aux/Capture buffer for reading + CpuAddr send_buffer; + /// Aux/Capture buffer info for writing + CpuAddr return_buffer_info; + /// Aux/Capture buffer for writing + CpuAddr return_buffer; + /// Parameters of this effect + std::array parameter{}; + /// State of this effect used by the AudioRenderer across calls + std::array state{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_reset.h b/src/audio_core/renderer/effect/effect_reset.h new file mode 100644 index 0000000000..1ea67e3347 --- /dev/null +++ b/src/audio_core/renderer/effect/effect_reset.h @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/effect/aux_.h" +#include "audio_core/renderer/effect/biquad_filter.h" +#include "audio_core/renderer/effect/buffer_mixer.h" +#include "audio_core/renderer/effect/capture.h" +#include "audio_core/renderer/effect/compressor.h" +#include "audio_core/renderer/effect/delay.h" +#include "audio_core/renderer/effect/i3dl2.h" +#include "audio_core/renderer/effect/light_limiter.h" +#include "audio_core/renderer/effect/reverb.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Reset an effect, and create a new one of the given type. + * + * @param effect - Effect to reset and re-construct. + * @param type - Type of the new effect to create. + */ +static void ResetEffect(EffectInfoBase* effect, const EffectInfoBase::Type type) { + *effect = {}; + + switch (type) { + case EffectInfoBase::Type::Invalid: + std::construct_at(effect); + effect->SetType(EffectInfoBase::Type::Invalid); + break; + case EffectInfoBase::Type::Mix: + std::construct_at(reinterpret_cast(effect)); + effect->SetType(EffectInfoBase::Type::Mix); + break; + case EffectInfoBase::Type::Aux: + std::construct_at(reinterpret_cast(effect)); + effect->SetType(EffectInfoBase::Type::Aux); + break; + case EffectInfoBase::Type::Delay: + std::construct_at(reinterpret_cast(effect)); + effect->SetType(EffectInfoBase::Type::Delay); + break; + case EffectInfoBase::Type::Reverb: + std::construct_at(reinterpret_cast(effect)); + effect->SetType(EffectInfoBase::Type::Reverb); + break; + case EffectInfoBase::Type::I3dl2Reverb: + std::construct_at(reinterpret_cast(effect)); + effect->SetType(EffectInfoBase::Type::I3dl2Reverb); + break; + case EffectInfoBase::Type::BiquadFilter: + std::construct_at(reinterpret_cast(effect)); + effect->SetType(EffectInfoBase::Type::BiquadFilter); + break; + case EffectInfoBase::Type::LightLimiter: + std::construct_at(reinterpret_cast(effect)); + effect->SetType(EffectInfoBase::Type::LightLimiter); + break; + case EffectInfoBase::Type::Capture: + std::construct_at(reinterpret_cast(effect)); + effect->SetType(EffectInfoBase::Type::Capture); + break; + case EffectInfoBase::Type::Compressor: + std::construct_at(reinterpret_cast(effect)); + effect->SetType(EffectInfoBase::Type::Compressor); + break; + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/effect_result_state.h b/src/audio_core/renderer/effect/effect_result_state.h new file mode 100644 index 0000000000..ae096ad69f --- /dev/null +++ b/src/audio_core/renderer/effect/effect_result_state.h @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +struct EffectResultState { + std::array state; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/i3dl2.cpp b/src/audio_core/renderer/effect/i3dl2.cpp new file mode 100644 index 0000000000..960b29cfc7 --- /dev/null +++ b/src/audio_core/renderer/effect/i3dl2.cpp @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/i3dl2.h" + +namespace AudioCore::AudioRenderer { + +void I3dl2ReverbInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion1& in_params, const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + if (IsChannelCountValid(in_specific->channel_count_max)) { + const auto old_state{params->state}; + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (!IsChannelCountValid(in_specific->channel_count)) { + params->channel_count = params->channel_count_max; + } + + if (!IsChannelCountValid(in_specific->channel_count) || + old_state != ParameterState::Updated) { + params->state = old_state; + } + + if (buffer_unmapped || in_params.is_new) { + usage_state = UsageState::New; + params->state = ParameterState::Initialized; + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size); + return; + } + } + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void I3dl2ReverbInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion2& in_params, const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + if (IsChannelCountValid(in_specific->channel_count_max)) { + const auto old_state{params->state}; + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (!IsChannelCountValid(in_specific->channel_count)) { + params->channel_count = params->channel_count_max; + } + + if (!IsChannelCountValid(in_specific->channel_count) || + old_state != ParameterState::Updated) { + params->state = old_state; + } + + if (buffer_unmapped || in_params.is_new) { + usage_state = UsageState::New; + params->state = ParameterState::Initialized; + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size); + return; + } + } + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void I3dl2ReverbInfo::UpdateForCommandGeneration() { + if (enabled) { + usage_state = UsageState::Enabled; + } else { + usage_state = UsageState::Disabled; + } + + auto params{reinterpret_cast(parameter.data())}; + params->state = ParameterState::Updated; +} + +void I3dl2ReverbInfo::InitializeResultState(EffectResultState& result_state) {} + +void I3dl2ReverbInfo::UpdateResultState(EffectResultState& cpu_state, + EffectResultState& dsp_state) {} + +CpuAddr I3dl2ReverbInfo::GetWorkbuffer(s32 index) { + return GetSingleBuffer(index); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/i3dl2.h b/src/audio_core/renderer/effect/i3dl2.h new file mode 100644 index 0000000000..7a088a6270 --- /dev/null +++ b/src/audio_core/renderer/effect/i3dl2.h @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { + +class I3dl2ReverbInfo : public EffectInfoBase { +public: + struct ParameterVersion1 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ u16 channel_count_max; + /* 0x0E */ u16 channel_count; + /* 0x10 */ char unk10[0x4]; + /* 0x14 */ u32 sample_rate; + /* 0x18 */ f32 room_HF_gain; + /* 0x1C */ f32 reference_HF; + /* 0x20 */ f32 late_reverb_decay_time; + /* 0x24 */ f32 late_reverb_HF_decay_ratio; + /* 0x28 */ f32 room_gain; + /* 0x2C */ f32 reflection_gain; + /* 0x30 */ f32 reverb_gain; + /* 0x34 */ f32 late_reverb_diffusion; + /* 0x38 */ f32 reflection_delay; + /* 0x3C */ f32 late_reverb_delay_time; + /* 0x40 */ f32 late_reverb_density; + /* 0x44 */ f32 dry_gain; + /* 0x48 */ ParameterState state; + /* 0x49 */ char unk49[0x3]; + }; + static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1), + "I3dl2ReverbInfo::ParameterVersion1 has the wrong size!"); + + struct ParameterVersion2 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ u16 channel_count_max; + /* 0x0E */ u16 channel_count; + /* 0x10 */ char unk10[0x4]; + /* 0x14 */ u32 sample_rate; + /* 0x18 */ f32 room_HF_gain; + /* 0x1C */ f32 reference_HF; + /* 0x20 */ f32 late_reverb_decay_time; + /* 0x24 */ f32 late_reverb_HF_decay_ratio; + /* 0x28 */ f32 room_gain; + /* 0x2C */ f32 reflection_gain; + /* 0x30 */ f32 reverb_gain; + /* 0x34 */ f32 late_reverb_diffusion; + /* 0x38 */ f32 reflection_delay; + /* 0x3C */ f32 late_reverb_delay_time; + /* 0x40 */ f32 late_reverb_density; + /* 0x44 */ f32 dry_gain; + /* 0x48 */ ParameterState state; + /* 0x49 */ char unk49[0x3]; + }; + static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2), + "I3dl2ReverbInfo::ParameterVersion2 has the wrong size!"); + + static constexpr u32 MaxDelayLines = 4; + static constexpr u32 MaxDelayTaps = 20; + + struct I3dl2DelayLine { + void Initialize(const s32 delay_time) { + max_delay = delay_time; + buffer.resize(delay_time + 1, 0); + buffer_end = &buffer[delay_time]; + output = &buffer[0]; + SetDelay(delay_time); + wet_gain = 0.0f; + } + + void SetDelay(const s32 delay_time) { + if (max_delay < delay_time) { + return; + } + delay = delay_time; + input = &buffer[(output - buffer.data() + delay) % (max_delay + 1)]; + } + + Common::FixedPoint<50, 14> Tick(const Common::FixedPoint<50, 14> sample) { + Write(sample); + + auto out_sample{Read()}; + + output++; + if (output >= buffer_end) { + output = buffer.data(); + } + + return out_sample; + } + + Common::FixedPoint<50, 14> Read() { + return *output; + } + + void Write(const Common::FixedPoint<50, 14> sample) { + *(input++) = sample; + if (input >= buffer_end) { + input = buffer.data(); + } + } + + Common::FixedPoint<50, 14> TapOut(const s32 index) { + auto out{input - (index + 1)}; + if (out < buffer.data()) { + out += max_delay + 1; + } + return *out; + } + + std::vector> buffer{}; + Common::FixedPoint<50, 14>* buffer_end{}; + s32 max_delay{}; + Common::FixedPoint<50, 14>* input{}; + Common::FixedPoint<50, 14>* output{}; + s32 delay{}; + f32 wet_gain{}; + }; + + struct State { + f32 lowpass_0; + f32 lowpass_1; + f32 lowpass_2; + I3dl2DelayLine early_delay_line; + std::array early_tap_steps; + f32 early_gain; + f32 late_gain; + s32 early_to_late_taps; + std::array fdn_delay_lines; + std::array decay_delay_lines0; + std::array decay_delay_lines1; + f32 last_reverb_echo; + I3dl2DelayLine center_delay_line; + std::array, MaxDelayLines> lowpass_coeff; + std::array shelf_filter; + f32 dry_gain; + }; + static_assert(sizeof(State) <= sizeof(EffectInfoBase::State), + "I3dl2ReverbInfo::State is too large!"); + + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info after command generation. Usually only changes its state. + */ + void UpdateForCommandGeneration() override; + + /** + * Initialize a new result state. Version 2 only, unused. + * + * @param result_state - Result state to initialize. + */ + void InitializeResultState(EffectResultState& result_state) override; + + /** + * Update the host-side state with the ADSP-side state. Version 2 only, unused. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override; + + /** + * Get a workbuffer assigned to this effect with the given index. + * + * @param index - Workbuffer index. + * @return Address of the buffer. + */ + CpuAddr GetWorkbuffer(s32 index) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/light_limiter.cpp b/src/audio_core/renderer/effect/light_limiter.cpp new file mode 100644 index 0000000000..1635a952da --- /dev/null +++ b/src/audio_core/renderer/effect/light_limiter.cpp @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/light_limiter.h" + +namespace AudioCore::AudioRenderer { + +void LightLimiterInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion1& in_params, const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (buffer_unmapped || in_params.is_new) { + usage_state = UsageState::New; + params->state = ParameterState::Initialized; + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size); + } else { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } +} + +void LightLimiterInfo::Update(BehaviorInfo::ErrorInfo& error_info, + const InParameterVersion2& in_params, const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (buffer_unmapped || in_params.is_new) { + usage_state = UsageState::New; + params->state = ParameterState::Initialized; + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size); + } else { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } +} + +void LightLimiterInfo::UpdateForCommandGeneration() { + if (enabled) { + usage_state = UsageState::Enabled; + } else { + usage_state = UsageState::Disabled; + } + + auto params{reinterpret_cast(parameter.data())}; + params->state = ParameterState::Updated; + params->statistics_reset_required = false; +} + +void LightLimiterInfo::InitializeResultState(EffectResultState& result_state) { + auto result_state_{reinterpret_cast(result_state.state.data())}; + + result_state_->channel_max_sample.fill(0); + result_state_->channel_compression_gain_min.fill(1.0f); +} + +void LightLimiterInfo::UpdateResultState(EffectResultState& cpu_state, + EffectResultState& dsp_state) { + auto cpu_statistics{reinterpret_cast(cpu_state.state.data())}; + auto dsp_statistics{reinterpret_cast(dsp_state.state.data())}; + + *cpu_statistics = *dsp_statistics; +} + +CpuAddr LightLimiterInfo::GetWorkbuffer(s32 index) { + return GetSingleBuffer(index); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/light_limiter.h b/src/audio_core/renderer/effect/light_limiter.h new file mode 100644 index 0000000000..338d67bbc5 --- /dev/null +++ b/src/audio_core/renderer/effect/light_limiter.h @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { + +class LightLimiterInfo : public EffectInfoBase { +public: + enum class ProcessingMode { + Mode0, + Mode1, + }; + + struct ParameterVersion1 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ u16 channel_count_max; + /* 0x0E */ u16 channel_count; + /* 0x0C */ u32 sample_rate; + /* 0x14 */ s32 look_ahead_time_max; + /* 0x18 */ s32 attack_time; + /* 0x1C */ s32 release_time; + /* 0x20 */ s32 look_ahead_time; + /* 0x24 */ f32 attack_coeff; + /* 0x28 */ f32 release_coeff; + /* 0x2C */ f32 threshold; + /* 0x30 */ f32 input_gain; + /* 0x34 */ f32 output_gain; + /* 0x38 */ s32 look_ahead_samples_min; + /* 0x3C */ s32 look_ahead_samples_max; + /* 0x40 */ ParameterState state; + /* 0x41 */ bool statistics_enabled; + /* 0x42 */ bool statistics_reset_required; + /* 0x43 */ ProcessingMode processing_mode; + }; + static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1), + "LightLimiterInfo::ParameterVersion1 has the wrong size!"); + + struct ParameterVersion2 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ u16 channel_count_max; + /* 0x0E */ u16 channel_count; + /* 0x0C */ u32 sample_rate; + /* 0x14 */ s32 look_ahead_time_max; + /* 0x18 */ s32 attack_time; + /* 0x1C */ s32 release_time; + /* 0x20 */ s32 look_ahead_time; + /* 0x24 */ f32 attack_coeff; + /* 0x28 */ f32 release_coeff; + /* 0x2C */ f32 threshold; + /* 0x30 */ f32 input_gain; + /* 0x34 */ f32 output_gain; + /* 0x38 */ s32 look_ahead_samples_min; + /* 0x3C */ s32 look_ahead_samples_max; + /* 0x40 */ ParameterState state; + /* 0x41 */ bool statistics_enabled; + /* 0x42 */ bool statistics_reset_required; + /* 0x43 */ ProcessingMode processing_mode; + }; + static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2), + "LightLimiterInfo::ParameterVersion2 has the wrong size!"); + + struct State { + std::array, MaxChannels> samples_average; + std::array, MaxChannels> compression_gain; + std::array look_ahead_sample_offsets; + std::array>, MaxChannels> look_ahead_sample_buffers; + }; + static_assert(sizeof(State) <= sizeof(EffectInfoBase::State), + "LightLimiterInfo::State has the wrong size!"); + + struct StatisticsInternal { + /* 0x00 */ std::array channel_max_sample; + /* 0x18 */ std::array channel_compression_gain_min; + }; + static_assert(sizeof(StatisticsInternal) == 0x30, + "LightLimiterInfo::StatisticsInternal has the wrong size!"); + + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info after command generation. Usually only changes its state. + */ + void UpdateForCommandGeneration() override; + + /** + * Initialize a new limiter statistics result state. Version 2 only. + * + * @param result_state - Result state to initialize. + */ + void InitializeResultState(EffectResultState& result_state) override; + + /** + * Update the host-side limiter statistics with the ADSP-side one. Version 2 only. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override; + + /** + * Get a workbuffer assigned to this effect with the given index. + * + * @param index - Workbuffer index. + * @return Address of the buffer. + */ + CpuAddr GetWorkbuffer(s32 index) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/reverb.cpp b/src/audio_core/renderer/effect/reverb.cpp new file mode 100644 index 0000000000..2d32383d02 --- /dev/null +++ b/src/audio_core/renderer/effect/reverb.cpp @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/effect/reverb.h" + +namespace AudioCore::AudioRenderer { + +void ReverbInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + if (IsChannelCountValid(in_specific->channel_count_max)) { + const auto old_state{params->state}; + std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (!IsChannelCountValid(in_specific->channel_count)) { + params->channel_count = params->channel_count_max; + } + + if (!IsChannelCountValid(in_specific->channel_count) || + old_state != ParameterState::Updated) { + params->state = old_state; + } + + if (buffer_unmapped || in_params.is_new) { + usage_state = UsageState::New; + params->state = ParameterState::Initialized; + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size); + return; + } + } + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void ReverbInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) { + auto in_specific{reinterpret_cast(in_params.specific.data())}; + auto params{reinterpret_cast(parameter.data())}; + + if (IsChannelCountValid(in_specific->channel_count_max)) { + const auto old_state{params->state}; + std::memcpy(params, in_specific, sizeof(ParameterVersion2)); + mix_id = in_params.mix_id; + process_order = in_params.process_order; + enabled = in_params.enabled; + + if (!IsChannelCountValid(in_specific->channel_count)) { + params->channel_count = params->channel_count_max; + } + + if (!IsChannelCountValid(in_specific->channel_count) || + old_state != ParameterState::Updated) { + params->state = old_state; + } + + if (buffer_unmapped || in_params.is_new) { + usage_state = UsageState::New; + params->state = ParameterState::Initialized; + buffer_unmapped = !pool_mapper.TryAttachBuffer( + error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size); + return; + } + } + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void ReverbInfo::UpdateForCommandGeneration() { + if (enabled) { + usage_state = UsageState::Enabled; + } else { + usage_state = UsageState::Disabled; + } + + auto params{reinterpret_cast(parameter.data())}; + params->state = ParameterState::Updated; +} + +void ReverbInfo::InitializeResultState(EffectResultState& result_state) {} + +void ReverbInfo::UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) {} + +CpuAddr ReverbInfo::GetWorkbuffer(s32 index) { + return GetSingleBuffer(index); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/effect/reverb.h b/src/audio_core/renderer/effect/reverb.h new file mode 100644 index 0000000000..b4df9f6eff --- /dev/null +++ b/src/audio_core/renderer/effect/reverb.h @@ -0,0 +1,190 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { + +class ReverbInfo : public EffectInfoBase { +public: + struct ParameterVersion1 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ u16 channel_count_max; + /* 0x0E */ u16 channel_count; + /* 0x10 */ u32 sample_rate; + /* 0x14 */ u32 early_mode; + /* 0x18 */ s32 early_gain; + /* 0x1C */ s32 pre_delay; + /* 0x20 */ s32 late_mode; + /* 0x24 */ s32 late_gain; + /* 0x28 */ s32 decay_time; + /* 0x2C */ s32 high_freq_Decay_ratio; + /* 0x30 */ s32 colouration; + /* 0x34 */ s32 base_gain; + /* 0x38 */ s32 wet_gain; + /* 0x3C */ s32 dry_gain; + /* 0x40 */ ParameterState state; + }; + static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1), + "ReverbInfo::ParameterVersion1 has the wrong size!"); + + struct ParameterVersion2 { + /* 0x00 */ std::array inputs; + /* 0x06 */ std::array outputs; + /* 0x0C */ u16 channel_count_max; + /* 0x0E */ u16 channel_count; + /* 0x10 */ u32 sample_rate; + /* 0x14 */ u32 early_mode; + /* 0x18 */ s32 early_gain; + /* 0x1C */ s32 pre_delay; + /* 0x20 */ s32 late_mode; + /* 0x24 */ s32 late_gain; + /* 0x28 */ s32 decay_time; + /* 0x2C */ s32 high_freq_decay_ratio; + /* 0x30 */ s32 colouration; + /* 0x34 */ s32 base_gain; + /* 0x38 */ s32 wet_gain; + /* 0x3C */ s32 dry_gain; + /* 0x40 */ ParameterState state; + }; + static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2), + "ReverbInfo::ParameterVersion2 has the wrong size!"); + + static constexpr u32 MaxDelayLines = 4; + static constexpr u32 MaxDelayTaps = 10; + static constexpr u32 NumEarlyModes = 5; + static constexpr u32 NumLateModes = 5; + + struct ReverbDelayLine { + void Initialize(const s32 delay_time, const f32 decay_rate) { + buffer.resize(delay_time + 1, 0); + buffer_end = &buffer[delay_time]; + output = &buffer[0]; + decay = decay_rate; + sample_count_max = delay_time; + SetDelay(delay_time); + } + + void SetDelay(const s32 delay_time) { + if (sample_count_max < delay_time) { + return; + } + sample_count = delay_time; + input = &buffer[(output - buffer.data() + sample_count) % (sample_count_max + 1)]; + } + + Common::FixedPoint<50, 14> Tick(const Common::FixedPoint<50, 14> sample) { + Write(sample); + + auto out_sample{Read()}; + + output++; + if (output >= buffer_end) { + output = buffer.data(); + } + + return out_sample; + } + + Common::FixedPoint<50, 14> Read() { + return *output; + } + + void Write(const Common::FixedPoint<50, 14> sample) { + *(input++) = sample; + if (input >= buffer_end) { + input = buffer.data(); + } + } + + Common::FixedPoint<50, 14> TapOut(const s32 index) { + auto out{input - (index + 1)}; + if (out < buffer.data()) { + out += sample_count; + } + return *out; + } + + s32 sample_count{}; + s32 sample_count_max{}; + std::vector> buffer{}; + Common::FixedPoint<50, 14>* buffer_end; + Common::FixedPoint<50, 14>* input{}; + Common::FixedPoint<50, 14>* output{}; + Common::FixedPoint<50, 14> decay{}; + }; + + struct State { + ReverbDelayLine pre_delay_line; + ReverbDelayLine center_delay_line; + std::array early_delay_times; + std::array, MaxDelayTaps> early_gains; + s32 pre_delay_time; + std::array decay_delay_lines; + std::array fdn_delay_lines; + std::array, MaxDelayLines> hf_decay_gain; + std::array, MaxDelayLines> hf_decay_prev_gain; + std::array, MaxDelayLines> prev_feedback_output; + }; + static_assert(sizeof(State) <= sizeof(EffectInfoBase::State), + "ReverbInfo::State is too large!"); + + /** + * Update the info with new parameters, version 1. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info with new parameters, version 2. + * + * @param error_info - Used to write call result code. + * @param in_params - New parameters to update the info with. + * @param pool_mapper - Pool for mapping buffers. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params, + const PoolMapper& pool_mapper) override; + + /** + * Update the info after command generation. Usually only changes its state. + */ + void UpdateForCommandGeneration() override; + + /** + * Initialize a new result state. Version 2 only, unused. + * + * @param result_state - Result state to initialize. + */ + void InitializeResultState(EffectResultState& result_state) override; + + /** + * Update the host-side state with the ADSP-side state. Version 2 only, unused. + * + * @param cpu_state - Host-side result state to update. + * @param dsp_state - AudioRenderer-side result state to update from. + */ + void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override; + + /** + * Get a workbuffer assigned to this effect with the given index. + * + * @param index - Workbuffer index. + * @return Address of the buffer. + */ + CpuAddr GetWorkbuffer(s32 index) override; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/memory/address_info.h b/src/audio_core/renderer/memory/address_info.h new file mode 100644 index 0000000000..4cfefea8ed --- /dev/null +++ b/src/audio_core/renderer/memory/address_info.h @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/memory/memory_pool_info.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +/** + * Represents a region of mapped or unmapped memory. + */ +class AddressInfo { +public: + AddressInfo() = default; + AddressInfo(CpuAddr cpu_address_, u64 size_) : cpu_address{cpu_address_}, size{size_} {} + + /** + * Setup a new AddressInfo. + * + * @param cpu_address - The CPU address of this region. + * @param size - The size of this region. + */ + void Setup(CpuAddr cpu_address_, u64 size_) { + cpu_address = cpu_address_; + size = size_; + memory_pool = nullptr; + dsp_address = 0; + } + + /** + * Get the CPU address. + * + * @return The CpuAddr address + */ + CpuAddr GetCpuAddr() const { + return cpu_address; + } + + /** + * Assign this region to a memory pool. + * + * @param memory_pool_ - Memory pool to assign. + * @return The CpuAddr address of this region. + */ + void SetPool(MemoryPoolInfo* memory_pool_) { + memory_pool = memory_pool_; + } + + /** + * Get the size of this region. + * + * @return The size of this region. + */ + u64 GetSize() const { + return size; + } + + /** + * Get the ADSP address for this region. + * + * @return The ADSP address for this region. + */ + CpuAddr GetForceMappedDspAddr() const { + return dsp_address; + } + + /** + * Set the ADSP address for this region. + * + * @param dsp_addr - The new ADSP address for this region. + */ + void SetForceMappedDspAddr(CpuAddr dsp_addr) { + dsp_address = dsp_addr; + } + + /** + * Check whether this region has an active memory pool. + * + * @return True if this region has a mapped memory pool, otherwise false. + */ + bool HasMappedMemoryPool() const { + return memory_pool != nullptr && memory_pool->GetDspAddress() != 0; + } + + /** + * Check whether this region is mapped to the ADSP. + * + * @return True if this region is mapped, otherwise false. + */ + bool IsMapped() const { + return HasMappedMemoryPool() || dsp_address != 0; + } + + /** + * Get a usable reference to this region of memory. + * + * @param mark_in_use - Whether this region should be marked as being in use. + * @return A valid memory address if valid, otherwise 0. + */ + CpuAddr GetReference(bool mark_in_use) { + if (!HasMappedMemoryPool()) { + return dsp_address; + } + + if (mark_in_use) { + memory_pool->SetUsed(true); + } + + return memory_pool->Translate(cpu_address, size); + } + +private: + /// CPU address of this region + CpuAddr cpu_address; + /// Size of this region + u64 size; + /// The memory this region is mapped to + MemoryPoolInfo* memory_pool; + /// ADSP address of this region + CpuAddr dsp_address; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/memory/memory_pool_info.cpp b/src/audio_core/renderer/memory/memory_pool_info.cpp new file mode 100644 index 0000000000..9b7824af1b --- /dev/null +++ b/src/audio_core/renderer/memory/memory_pool_info.cpp @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/memory/memory_pool_info.h" + +namespace AudioCore::AudioRenderer { + +CpuAddr MemoryPoolInfo::GetCpuAddress() const { + return cpu_address; +} + +CpuAddr MemoryPoolInfo::GetDspAddress() const { + return dsp_address; +} + +u64 MemoryPoolInfo::GetSize() const { + return size; +} + +MemoryPoolInfo::Location MemoryPoolInfo::GetLocation() const { + return location; +} + +void MemoryPoolInfo::SetCpuAddress(const CpuAddr address, const u64 size_) { + cpu_address = address; + size = size_; +} + +void MemoryPoolInfo::SetDspAddress(const CpuAddr address) { + dsp_address = address; +} + +bool MemoryPoolInfo::Contains(const CpuAddr address_, const u64 size_) const { + return cpu_address <= address_ && (address_ + size_) <= (cpu_address + size); +} + +bool MemoryPoolInfo::IsMapped() const { + return dsp_address != 0; +} + +CpuAddr MemoryPoolInfo::Translate(const CpuAddr address, const u64 size_) const { + if (!Contains(address, size_)) { + return 0; + } + + if (!IsMapped()) { + return 0; + } + + return dsp_address + (address - cpu_address); +} + +void MemoryPoolInfo::SetUsed(const bool used) { + in_use = used; +} + +bool MemoryPoolInfo::IsUsed() const { + return in_use; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/memory/memory_pool_info.h b/src/audio_core/renderer/memory/memory_pool_info.h new file mode 100644 index 0000000000..537a466ec7 --- /dev/null +++ b/src/audio_core/renderer/memory/memory_pool_info.h @@ -0,0 +1,170 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * CPU pools are mapped in user memory with the supplied process_handle (see PoolMapper). + */ +class MemoryPoolInfo { +public: + /** + * The location of this pool. + * CPU pools are mapped in user memory with the supplied process_handle (see PoolMapper). + * DSP pools are mapped in the current process sysmodule. + */ + enum class Location { + CPU = 1, + DSP = 2, + }; + + /** + * Current state of the pool + */ + enum class State { + Invalid, + Aquired, + RequestDetach, + Detached, + RequestAttach, + Attached, + Released, + }; + + /** + * Result code for updating the pool (See InfoUpdater::Update) + */ + enum class ResultState { + Success, + BadParam, + MapFailed, + InUse, + }; + + /** + * Input parameters coming from the game which are used to update current pools + * (See InfoUpdater::Update) + */ + struct InParameter { + /* 0x00 */ u64 address; + /* 0x08 */ u64 size; + /* 0x10 */ State state; + /* 0x14 */ bool in_use; + /* 0x18 */ char unk18[0x8]; + }; + static_assert(sizeof(InParameter) == 0x20, "MemoryPoolInfo::InParameter has the wrong size!"); + + /** + * Output status sent back to the game on update (See InfoUpdater::Update) + */ + struct OutStatus { + /* 0x00 */ State state; + /* 0x04 */ char unk04[0xC]; + }; + static_assert(sizeof(OutStatus) == 0x10, "MemoryPoolInfo::OutStatus has the wrong size!"); + + MemoryPoolInfo() = default; + MemoryPoolInfo(Location location_) : location{location_} {} + + /** + * Get the CPU address for this pool. + * + * @return The CPU address of this pool. + */ + CpuAddr GetCpuAddress() const; + + /** + * Get the DSP address for this pool. + * + * @return The DSP address of this pool. + */ + CpuAddr GetDspAddress() const; + + /** + * Get the size of this pool. + * + * @return The size of this pool. + */ + u64 GetSize() const; + + /** + * Get the location of this pool. + * + * @return The location for the pool (see MemoryPoolInfo::Location). + */ + Location GetLocation() const; + + /** + * Set the CPU address for this pool. + * + * @param address - The new CPU address for this pool. + * @param size - The new size for this pool. + */ + void SetCpuAddress(CpuAddr address, u64 size); + + /** + * Set the DSP address for this pool. + * + * @param address - The new DSP address for this pool. + */ + void SetDspAddress(CpuAddr address); + + /** + * Check whether the pool contains a given range. + * + * @param address - The buffer address to look for. + * @param size - The size of the given buffer. + * @return True if the range is within this pool, otherwise false. + */ + bool Contains(CpuAddr address, u64 size) const; + + /** + * Check whether this pool is mapped, which is when the dsp address is set. + * + * @return True if the pool is mapped, otherwise false. + */ + bool IsMapped() const; + + /** + * Translates a given CPU range into a relative offset for the DSP. + * + * @param address - The buffer address to look for. + * @param size - The size of the given buffer. + * @return Pointer to the DSP-mapped memory. + */ + CpuAddr Translate(CpuAddr address, u64 size) const; + + /** + * Set or unset whether this memory pool is in use. + * + * @param used - Use state for this pool. + */ + void SetUsed(bool used); + + /** + * Get whether this pool is in use. + * + * @return True if in use, otherwise false. + */ + bool IsUsed() const; + +private: + /// Base address for the CPU-side memory + CpuAddr cpu_address{}; + /// Base address for the DSP-side memory + CpuAddr dsp_address{}; + /// Size of this pool + u64 size{}; + /// Location of this pool, either CPU or DSP + Location location{Location::DSP}; + /// If this pool is in use + bool in_use{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/memory/pool_mapper.cpp b/src/audio_core/renderer/memory/pool_mapper.cpp new file mode 100644 index 0000000000..2baf2ce085 --- /dev/null +++ b/src/audio_core/renderer/memory/pool_mapper.cpp @@ -0,0 +1,243 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/memory/address_info.h" +#include "audio_core/renderer/memory/pool_mapper.h" +#include "core/hle/kernel/k_process.h" +#include "core/hle/kernel/svc.h" + +namespace AudioCore::AudioRenderer { + +PoolMapper::PoolMapper(u32 process_handle_, bool force_map_) + : process_handle{process_handle_}, force_map{force_map_} {} + +PoolMapper::PoolMapper(u32 process_handle_, std::span pool_infos_, u32 pool_count_, + bool force_map_) + : process_handle{process_handle_}, pool_infos{pool_infos_.data()}, + pool_count{pool_count_}, force_map{force_map_} {} + +void PoolMapper::ClearUseState(std::span pools, const u32 count) { + for (u32 i = 0; i < count; i++) { + pools[i].SetUsed(false); + } +} + +MemoryPoolInfo* PoolMapper::FindMemoryPool(MemoryPoolInfo* pools, const u64 count, + const CpuAddr address, const u64 size) const { + auto pool{pools}; + for (u64 i = 0; i < count; i++, pool++) { + if (pool->Contains(address, size)) { + return pool; + } + } + return nullptr; +} + +MemoryPoolInfo* PoolMapper::FindMemoryPool(const CpuAddr address, const u64 size) const { + auto pool{pool_infos}; + for (u64 i = 0; i < pool_count; i++, pool++) { + if (pool->Contains(address, size)) { + return pool; + } + } + return nullptr; +} + +bool PoolMapper::FillDspAddr(AddressInfo& address_info, MemoryPoolInfo* pools, + const u32 count) const { + if (address_info.GetCpuAddr() == 0) { + address_info.SetPool(nullptr); + return false; + } + + auto found_pool{ + FindMemoryPool(pools, count, address_info.GetCpuAddr(), address_info.GetSize())}; + if (found_pool != nullptr) { + address_info.SetPool(found_pool); + return true; + } + + if (force_map) { + address_info.SetForceMappedDspAddr(address_info.GetCpuAddr()); + } else { + address_info.SetPool(nullptr); + } + + return false; +} + +bool PoolMapper::FillDspAddr(AddressInfo& address_info) const { + if (address_info.GetCpuAddr() == 0) { + address_info.SetPool(nullptr); + return false; + } + + auto found_pool{FindMemoryPool(address_info.GetCpuAddr(), address_info.GetSize())}; + if (found_pool != nullptr) { + address_info.SetPool(found_pool); + return true; + } + + if (force_map) { + address_info.SetForceMappedDspAddr(address_info.GetCpuAddr()); + } else { + address_info.SetPool(nullptr); + } + + return false; +} + +bool PoolMapper::TryAttachBuffer(BehaviorInfo::ErrorInfo& error_info, AddressInfo& address_info, + const CpuAddr address, const u64 size) const { + address_info.Setup(address, size); + + if (!FillDspAddr(address_info)) { + error_info.error_code = Service::Audio::ERR_POOL_MAPPING_FAILED; + error_info.address = address; + return force_map; + } + + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + return true; +} + +bool PoolMapper::IsForceMapEnabled() const { + return force_map; +} + +u32 PoolMapper::GetProcessHandle(const MemoryPoolInfo* pool) const { + switch (pool->GetLocation()) { + case MemoryPoolInfo::Location::CPU: + return process_handle; + case MemoryPoolInfo::Location::DSP: + return Kernel::Svc::CurrentProcess; + } + LOG_WARNING(Service_Audio, "Invalid MemoryPoolInfo location!"); + return Kernel::Svc::CurrentProcess; +} + +bool PoolMapper::Map([[maybe_unused]] const u32 handle, [[maybe_unused]] const CpuAddr cpu_addr, + [[maybe_unused]] const u64 size) const { + // nn::audio::dsp::MapUserPointer(handle, cpu_addr, size); + return true; +} + +bool PoolMapper::Map(MemoryPoolInfo& pool) const { + switch (pool.GetLocation()) { + case MemoryPoolInfo::Location::CPU: + // Map with process_handle + pool.SetDspAddress(pool.GetCpuAddress()); + return true; + case MemoryPoolInfo::Location::DSP: + // Map with Kernel::Svc::CurrentProcess + pool.SetDspAddress(pool.GetCpuAddress()); + return true; + default: + LOG_WARNING(Service_Audio, "Invalid MemoryPoolInfo location={}!", + static_cast(pool.GetLocation())); + return false; + } +} + +bool PoolMapper::Unmap([[maybe_unused]] const u32 handle, [[maybe_unused]] const CpuAddr cpu_addr, + [[maybe_unused]] const u64 size) const { + // nn::audio::dsp::UnmapUserPointer(handle, cpu_addr, size); + return true; +} + +bool PoolMapper::Unmap(MemoryPoolInfo& pool) const { + [[maybe_unused]] u32 handle{0}; + + switch (pool.GetLocation()) { + case MemoryPoolInfo::Location::CPU: + handle = process_handle; + break; + case MemoryPoolInfo::Location::DSP: + handle = Kernel::Svc::CurrentProcess; + break; + } + // nn::audio::dsp::UnmapUserPointer(handle, pool->cpu_address, pool->size); + pool.SetCpuAddress(0, 0); + pool.SetDspAddress(0); + return true; +} + +void PoolMapper::ForceUnmapPointer(const AddressInfo& address_info) const { + if (force_map) { + [[maybe_unused]] auto found_pool{ + FindMemoryPool(address_info.GetCpuAddr(), address_info.GetSize())}; + // nn::audio::dsp::UnmapUserPointer(this->processHandle, address_info.GetCpuAddr(), 0); + } +} + +MemoryPoolInfo::ResultState PoolMapper::Update(MemoryPoolInfo& pool, + const MemoryPoolInfo::InParameter& in_params, + MemoryPoolInfo::OutStatus& out_params) const { + if (in_params.state != MemoryPoolInfo::State::RequestAttach && + in_params.state != MemoryPoolInfo::State::RequestDetach) { + return MemoryPoolInfo::ResultState::Success; + } + + if (in_params.address == 0 || in_params.size == 0 || !Common::Is4KBAligned(in_params.address) || + !Common::Is4KBAligned(in_params.size)) { + return MemoryPoolInfo::ResultState::BadParam; + } + + switch (in_params.state) { + case MemoryPoolInfo::State::RequestAttach: + pool.SetCpuAddress(in_params.address, in_params.size); + + Map(pool); + + if (pool.IsMapped()) { + out_params.state = MemoryPoolInfo::State::Attached; + return MemoryPoolInfo::ResultState::Success; + } + pool.SetCpuAddress(0, 0); + return MemoryPoolInfo::ResultState::MapFailed; + + case MemoryPoolInfo::State::RequestDetach: + if (pool.GetCpuAddress() != in_params.address || pool.GetSize() != in_params.size) { + return MemoryPoolInfo::ResultState::BadParam; + } + + if (pool.IsUsed()) { + return MemoryPoolInfo::ResultState::InUse; + } + + Unmap(pool); + + pool.SetCpuAddress(0, 0); + pool.SetDspAddress(0); + out_params.state = MemoryPoolInfo::State::Detached; + return MemoryPoolInfo::ResultState::Success; + + default: + LOG_ERROR(Service_Audio, "Invalid MemoryPoolInfo::State!"); + break; + } + + return MemoryPoolInfo::ResultState::Success; +} + +bool PoolMapper::InitializeSystemPool(MemoryPoolInfo& pool, const u8* memory, + const u64 size_) const { + switch (pool.GetLocation()) { + case MemoryPoolInfo::Location::CPU: + return false; + case MemoryPoolInfo::Location::DSP: + pool.SetCpuAddress(reinterpret_cast(memory), size_); + if (Map(Kernel::Svc::CurrentProcess, reinterpret_cast(memory), size_)) { + pool.SetDspAddress(pool.GetCpuAddress()); + return true; + } + return false; + default: + LOG_WARNING(Service_Audio, "Invalid MemoryPoolInfo location={}!", + static_cast(pool.GetLocation())); + return false; + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/memory/pool_mapper.h b/src/audio_core/renderer/memory/pool_mapper.h new file mode 100644 index 0000000000..9a691da7a2 --- /dev/null +++ b/src/audio_core/renderer/memory/pool_mapper.h @@ -0,0 +1,179 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/memory/memory_pool_info.h" +#include "common/common_types.h" +#include "core/hle/service/audio/errors.h" + +namespace AudioCore::AudioRenderer { +class AddressInfo; + +/** + * Utility functions for managing MemoryPoolInfos + */ +class PoolMapper { +public: + explicit PoolMapper(u32 process_handle, bool force_map); + explicit PoolMapper(u32 process_handle, std::span pool_infos, u32 pool_count, + bool force_map); + + /** + * Clear the usage state for all given pools. + * + * @param pools - The memory pools to clear. + * @param count - The number of pools. + */ + static void ClearUseState(std::span pools, u32 count); + + /** + * Find the memory pool containing the given address and size from a given list of pools. + * + * @param pools - The memory pools to search within. + * @param count - The number of pools. + * @param address - The address of the region to find. + * @param size - The size of the region to find. + * @return Pointer to the memory pool if found, otherwise nullptr. + */ + MemoryPoolInfo* FindMemoryPool(MemoryPoolInfo* pools, u64 count, CpuAddr address, + u64 size) const; + + /** + * Find the memory pool containing the given address and size from the PoolMapper's memory pool. + * + * @param address - The address of the region to find. + * @param size - The size of the region to find. + * @return Pointer to the memory pool if found, otherwise nullptr. + */ + MemoryPoolInfo* FindMemoryPool(CpuAddr address, u64 size) const; + + /** + * Set the PoolMapper's memory pool to one in the given list of pools, which contains + * address_info. + * + * @param address_info - The expected region to find within pools. + * @param pools - The list of pools to search within. + * @param count - The number of pools given. + * @return True if successfully mapped, otherwise false. + */ + bool FillDspAddr(AddressInfo& address_info, MemoryPoolInfo* pools, u32 count) const; + + /** + * Set the PoolMapper's memory pool to the one containing address_info. + * + * @param address_info - The address to find the memory pool for. + * @return True if successfully mapped, otherwise false. + */ + bool FillDspAddr(AddressInfo& address_info) const; + + /** + * Try to attach a {address, size} region to the given address_info, and map it. Fills in the + * given error_info and address_info. + * + * @param error_info - Output error info. + * @param address_info - Output address info, initialized with the given {address, size} and + * attempted to map. + * @param address - Address of the region to map. + * @param size - Size of the region to map. + * @return True if successfully attached, otherwise false. + */ + bool TryAttachBuffer(BehaviorInfo::ErrorInfo& error_info, AddressInfo& address_info, + CpuAddr address, u64 size) const; + + /** + * Return whether force mapping is enabled. + * + * @return True if force mapping is enabled, otherwise false. + */ + bool IsForceMapEnabled() const; + + /** + * Get the process handle, depending on location. + * + * @param pool - The pool to check the location of. + * @return CurrentProcessHandle if location == DSP, + * the PoolMapper's process_handle if location == CPU + */ + u32 GetProcessHandle(const MemoryPoolInfo* pool) const; + + /** + * Map the given region with the given handle. This is a no-op. + * + * @param handle - The process handle to map to. + * @param cpu_addr - Address to map. + * @param size - Size to map. + * @return True if successfully mapped, otherwise false. + */ + bool Map(u32 handle, CpuAddr cpu_addr, u64 size) const; + + /** + * Map the given memory pool. + * + * @param pool - The pool to map. + * @return True if successfully mapped, otherwise false. + */ + bool Map(MemoryPoolInfo& pool) const; + + /** + * Unmap the given region with the given handle. + * + * @param handle - The process handle to unmap to. + * @param cpu_addr - Address to unmap. + * @param size - Size to unmap. + * @return True if successfully unmapped, otherwise false. + */ + bool Unmap(u32 handle, CpuAddr cpu_addr, u64 size) const; + + /** + * Unmap the given memory pool. + * + * @param pool - The pool to unmap. + * @return True if successfully unmapped, otherwise false. + */ + bool Unmap(MemoryPoolInfo& pool) const; + + /** + * Forcibly unmap the given region. + * + * @param address_info - The region to unmap. + */ + void ForceUnmapPointer(const AddressInfo& address_info) const; + + /** + * Update the given memory pool. + * + * @param pool - Pool to update. + * @param in_params - Input parameters for the update. + * @param out_params - Output parameters for the update. + * @return The result of the update. See MemoryPoolInfo::ResultState + */ + MemoryPoolInfo::ResultState Update(MemoryPoolInfo& pool, + const MemoryPoolInfo::InParameter& in_params, + MemoryPoolInfo::OutStatus& out_params) const; + + /** + * Initialize the PoolMapper's memory pool. + * + * @param pool - Input pool to initialize. + * @param memory - Pointer to the memory region for the pool. + * @param size - Size of the memory region for the pool. + * @return True if initialized successfully, otherwise false. + */ + bool InitializeSystemPool(MemoryPoolInfo& pool, const u8* memory, u64 size) const; + +private: + /// Process handle for this mapper, used when location == CPU + u32 process_handle; + /// List of memory pools assigned to this mapper + MemoryPoolInfo* pool_infos{}; + /// The number of pools + u64 pool_count{}; + /// Is forced mapping enabled + bool force_map; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/mix/mix_context.cpp b/src/audio_core/renderer/mix/mix_context.cpp new file mode 100644 index 0000000000..2427c83ed6 --- /dev/null +++ b/src/audio_core/renderer/mix/mix_context.cpp @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/mix/mix_context.h" +#include "audio_core/renderer/splitter/splitter_context.h" + +namespace AudioCore::AudioRenderer { + +void MixContext::Initialize(std::span sorted_mix_infos_, std::span mix_infos_, + const u32 count_, std::span effect_process_order_buffer_, + const u32 effect_count_, std::span node_states_workbuffer, + const u64 node_buffer_size, std::span edge_matrix_workbuffer, + const u64 edge_matrix_size) { + count = count_; + sorted_mix_infos = sorted_mix_infos_; + mix_infos = mix_infos_; + effect_process_order_buffer = effect_process_order_buffer_; + effect_count = effect_count_; + + if (node_states_workbuffer.size() > 0 && edge_matrix_workbuffer.size() > 0) { + node_states.Initialize(node_states_workbuffer, node_buffer_size, count); + edge_matrix.Initialize(edge_matrix_workbuffer, edge_matrix_size, count); + } + + for (s32 i = 0; i < count; i++) { + sorted_mix_infos[i] = &mix_infos[i]; + } +} + +MixInfo* MixContext::GetSortedInfo(const s32 index) { + return sorted_mix_infos[index]; +} + +void MixContext::SetSortedInfo(const s32 index, MixInfo& mix_info) { + sorted_mix_infos[index] = &mix_info; +} + +MixInfo* MixContext::GetInfo(const s32 index) { + return &mix_infos[index]; +} + +MixInfo* MixContext::GetFinalMixInfo() { + return &mix_infos[0]; +} + +s32 MixContext::GetCount() const { + return count; +} + +void MixContext::UpdateDistancesFromFinalMix() { + for (s32 i = 0; i < count; i++) { + mix_infos[i].distance_from_final_mix = InvalidDistanceFromFinalMix; + } + + for (s32 i = 0; i < count; i++) { + auto& mix_info{mix_infos[i]}; + sorted_mix_infos[i] = &mix_info; + + if (!mix_info.in_use) { + continue; + } + + auto mix_id{mix_info.mix_id}; + auto distance_to_final_mix{FinalMixId}; + + while (distance_to_final_mix < count) { + if (mix_id == FinalMixId) { + break; + } + + if (mix_id == UnusedMixId) { + distance_to_final_mix = InvalidDistanceFromFinalMix; + break; + } + + auto distance_from_final_mix{mix_infos[mix_id].distance_from_final_mix}; + if (distance_from_final_mix != InvalidDistanceFromFinalMix) { + distance_to_final_mix = distance_from_final_mix + 1; + break; + } + + distance_to_final_mix++; + mix_id = mix_infos[mix_id].dst_mix_id; + } + + if (distance_to_final_mix >= count) { + distance_to_final_mix = InvalidDistanceFromFinalMix; + } + mix_info.distance_from_final_mix = distance_to_final_mix; + } +} + +void MixContext::SortInfo() { + UpdateDistancesFromFinalMix(); + + std::ranges::sort(sorted_mix_infos, [](const MixInfo* lhs, const MixInfo* rhs) { + return lhs->distance_from_final_mix > rhs->distance_from_final_mix; + }); + + CalcMixBufferOffset(); +} + +void MixContext::CalcMixBufferOffset() { + s16 offset{0}; + for (s32 i = 0; i < count; i++) { + auto mix_info{sorted_mix_infos[i]}; + if (mix_info->in_use) { + const auto buffer_count{mix_info->buffer_count}; + mix_info->buffer_offset = offset; + offset += buffer_count; + } + } +} + +bool MixContext::TSortInfo(const SplitterContext& splitter_context) { + if (!splitter_context.UsingSplitter()) { + CalcMixBufferOffset(); + return true; + } + + if (!node_states.Tsort(edge_matrix)) { + return false; + } + + std::vector sorted_results{node_states.GetSortedResuls()}; + const auto result_size{std::min(count, static_cast(sorted_results.size()))}; + for (s32 i = 0; i < result_size; i++) { + sorted_mix_infos[i] = &mix_infos[sorted_results[i]]; + } + + CalcMixBufferOffset(); + return true; +} + +EdgeMatrix& MixContext::GetEdgeMatrix() { + return edge_matrix; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/mix/mix_context.h b/src/audio_core/renderer/mix/mix_context.h new file mode 100644 index 0000000000..da3aa28291 --- /dev/null +++ b/src/audio_core/renderer/mix/mix_context.h @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/mix/mix_info.h" +#include "audio_core/renderer/nodes/edge_matrix.h" +#include "audio_core/renderer/nodes/node_states.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +class SplitterContext; + +/* + * Manages mixing states, sorting and building a node graph to describe a mix order. + */ +class MixContext { +public: + /** + * Initialize the mix context. + * + * @param sorted_mix_infos - Buffer for the sorted mix infos. + * @param mix_infos - Buffer for the mix infos. + * @param effect_process_order_buffer - Buffer for the effect process orders. + * @param effect_count - Number of effects in the buffer. + * @param node_states_workbuffer - Buffer for node states. + * @param node_buffer_size - Size of the node states buffer. + * @param edge_matrix_workbuffer - Buffer for edge matrix. + * @param edge_matrix_size - Size of the edge matrix buffer. + */ + void Initialize(std::span sorted_mix_infos, std::span mix_infos, u32 count_, + std::span effect_process_order_buffer, u32 effect_count, + std::span node_states_workbuffer, u64 node_buffer_size, + std::span edge_matrix_workbuffer, u64 edge_matrix_size); + + /** + * Get a sorted mix at the given index. + * + * @param index - Index of sorted mix. + * @return The sorted mix. + */ + MixInfo* GetSortedInfo(s32 index); + + /** + * Set the sorted info at the given index. + * + * @param index - Index of sorted mix. + * @param mix_info - The new mix for this index. + */ + void SetSortedInfo(s32 index, MixInfo& mix_info); + + /** + * Get a mix at the given index. + * + * @param index - Index of mix. + * @return The mix. + */ + MixInfo* GetInfo(s32 index); + + /** + * Get the final mix. + * + * @return The final mix. + */ + MixInfo* GetFinalMixInfo(); + + /** + * Get the current number of mixes. + * + * @return The number of active mixes. + */ + s32 GetCount() const; + + /** + * Update all of the mixes' distance from the final mix. + * Needs to be called after altering the mix graph. + */ + void UpdateDistancesFromFinalMix(); + + /** + * Non-splitter sort, sorts the sorted mixes based on their distance from the final mix. + */ + void SortInfo(); + + /** + * Re-calculate the mix buffer offsets for each mix after altering the mix. + */ + void CalcMixBufferOffset(); + + /** + * Splitter sort, traverse the splitter node graph and sort the sorted mixes from results. + * + * @param splitter_context - Splitter context for the sort. + * @return True if the sort was successful, othewise false. + */ + bool TSortInfo(const SplitterContext& splitter_context); + + /** + * Get the edge matrix used for the mix graph. + * + * @return The edge matrix used. + */ + EdgeMatrix& GetEdgeMatrix(); + +private: + /// Array of sorted mixes + std::span sorted_mix_infos{}; + /// Array of mixes + std::span mix_infos{}; + /// Number of active mixes + s32 count{}; + /// Array of effect process orderings + std::span effect_process_order_buffer{}; + /// Number of effects in the process ordering buffer + u64 effect_count{}; + /// Node states used in splitter sort + NodeStates node_states{}; + /// Edge matrix for connected nodes used in splitter sort + EdgeMatrix edge_matrix{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/mix/mix_info.cpp b/src/audio_core/renderer/mix/mix_info.cpp new file mode 100644 index 0000000000..cc18e57eef --- /dev/null +++ b/src/audio_core/renderer/mix/mix_info.cpp @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/effect/effect_context.h" +#include "audio_core/renderer/mix/mix_info.h" +#include "audio_core/renderer/nodes/edge_matrix.h" +#include "audio_core/renderer/splitter/splitter_context.h" + +namespace AudioCore::AudioRenderer { + +MixInfo::MixInfo(std::span effect_order_buffer_, s32 effect_count_, BehaviorInfo& behavior) + : effect_order_buffer{effect_order_buffer_}, effect_count{effect_count_}, + long_size_pre_delay_supported{behavior.IsLongSizePreDelaySupported()} { + ClearEffectProcessingOrder(); +} + +void MixInfo::Cleanup() { + mix_id = UnusedMixId; + dst_mix_id = UnusedMixId; + dst_splitter_id = UnusedSplitterId; +} + +void MixInfo::ClearEffectProcessingOrder() { + for (s32 i = 0; i < effect_count; i++) { + effect_order_buffer[i] = -1; + } +} + +bool MixInfo::Update(EdgeMatrix& edge_matrix, const InParameter& in_params, + EffectContext& effect_context, SplitterContext& splitter_context, + const BehaviorInfo& behavior) { + volume = in_params.volume; + sample_rate = in_params.sample_rate; + buffer_count = static_cast(in_params.buffer_count); + in_use = in_params.in_use; + mix_id = in_params.mix_id; + node_id = in_params.node_id; + mix_volumes = in_params.mix_volumes; + + bool sort_required{false}; + if (behavior.IsSplitterSupported()) { + sort_required = UpdateConnection(edge_matrix, in_params, splitter_context); + } else { + if (dst_mix_id != in_params.dest_mix_id) { + dst_mix_id = in_params.dest_mix_id; + sort_required = true; + } + dst_splitter_id = UnusedSplitterId; + } + + ClearEffectProcessingOrder(); + + // Check all effects, and set their order if they belong to this mix. + const auto count{effect_context.GetCount()}; + for (u32 i = 0; i < count; i++) { + const auto& info{effect_context.GetInfo(i)}; + if (mix_id == info.GetMixId()) { + const auto processing_order{info.GetProcessingOrder()}; + if (processing_order > effect_count) { + break; + } + effect_order_buffer[processing_order] = i; + } + } + + return sort_required; +} + +bool MixInfo::UpdateConnection(EdgeMatrix& edge_matrix, const InParameter& in_params, + SplitterContext& splitter_context) { + auto has_new_connection{false}; + if (dst_splitter_id != UnusedSplitterId) { + auto& splitter_info{splitter_context.GetInfo(dst_splitter_id)}; + has_new_connection = splitter_info.HasNewConnection(); + } + + // Check if this mix matches the input parameters. + // If everything is the same, don't bother updating. + if (dst_mix_id == in_params.dest_mix_id && dst_splitter_id == in_params.dest_splitter_id && + !has_new_connection) { + return false; + } + + // Reset the mix in the graph, as we're about to update it. + edge_matrix.RemoveEdges(mix_id); + + if (in_params.dest_mix_id == UnusedMixId) { + if (in_params.dest_splitter_id != UnusedSplitterId) { + // If the splitter is used, connect this mix to each active destination. + auto& splitter_info{splitter_context.GetInfo(in_params.dest_splitter_id)}; + auto const destination_count{splitter_info.GetDestinationCount()}; + + for (u32 i = 0; i < destination_count; i++) { + auto destination{ + splitter_context.GetDesintationData(in_params.dest_splitter_id, i)}; + + if (destination) { + const auto destination_id{destination->GetMixId()}; + if (destination_id != UnusedMixId) { + edge_matrix.Connect(mix_id, destination_id); + } + } + } + } + } else { + // If the splitter is not used, only connect this mix to its destination. + edge_matrix.Connect(mix_id, in_params.dest_mix_id); + } + + dst_mix_id = in_params.dest_mix_id; + dst_splitter_id = in_params.dest_splitter_id; + return true; +} + +bool MixInfo::HasAnyConnection() const { + return dst_mix_id != UnusedMixId || dst_splitter_id != UnusedSplitterId; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/mix/mix_info.h b/src/audio_core/renderer/mix/mix_info.h new file mode 100644 index 0000000000..b5fa4c0c7e --- /dev/null +++ b/src/audio_core/renderer/mix/mix_info.h @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +class EdgeMatrix; +class SplitterContext; +class EffectContext; +class BehaviorInfo; + +/** + * A single mix, which may feed through other mixes in a chain until reaching the final output mix. + */ +class MixInfo { +public: + struct InParameter { + /* 0x000 */ f32 volume; + /* 0x004 */ u32 sample_rate; + /* 0x008 */ u32 buffer_count; + /* 0x00C */ bool in_use; + /* 0x00D */ bool is_dirty; + /* 0x010 */ s32 mix_id; + /* 0x014 */ u32 effect_count; + /* 0x018 */ s32 node_id; + /* 0x01C */ char unk01C[0x8]; + /* 0x024 */ std::array, MaxMixBuffers> mix_volumes; + /* 0x924 */ s32 dest_mix_id; + /* 0x928 */ s32 dest_splitter_id; + /* 0x92C */ char unk92C[0x4]; + }; + static_assert(sizeof(InParameter) == 0x930, "MixInfo::InParameter has the wrong size!"); + + struct InDirtyParameter { + /* 0x00 */ u32 magic; + /* 0x04 */ s32 count; + /* 0x08 */ char unk08[0x18]; + }; + static_assert(sizeof(InDirtyParameter) == 0x20, + "MixInfo::InDirtyParameter has the wrong size!"); + + MixInfo(std::span effect_order_buffer, s32 effect_count, BehaviorInfo& behavior); + + /** + * Clean up the mix, resetting it to a default state. + */ + void Cleanup(); + + /** + * Clear the effect process order for all effects in this mix. + */ + void ClearEffectProcessingOrder(); + + /** + * Update the mix according to the given parameters. + * + * @param edge_matrix - Updated with new splitter node connections, if supported. + * @param in_params - Input parameters. + * @param effect_context - Used to update the effect orderings. + * @param splitter_context - Used to update the mix graph if supported. + * @param behavior - Used for checking which features are supported. + * @return True if the mix was updated and a sort is required, otherwise false. + */ + bool Update(EdgeMatrix& edge_matrix, const InParameter& in_params, + EffectContext& effect_context, SplitterContext& splitter_context, + const BehaviorInfo& behavior); + + /** + * Update the mix's connection in the node graph according to the given parameters. + * + * @param edge_matrix - Updated with new splitter node connections, if supported. + * @param in_params - Input parameters. + * @param splitter_context - Used to update the mix graph if supported. + * @return True if the mix was updated and a sort is required, otherwise false. + */ + bool UpdateConnection(EdgeMatrix& edge_matrix, const InParameter& in_params, + SplitterContext& splitter_context); + + /** + * Check if this mix is connected to any other. + * + * @return True if the mix has a connection, otherwise false. + */ + bool HasAnyConnection() const; + + /// Volume of this mix + f32 volume{}; + /// Sample rate of this mix + u32 sample_rate{}; + /// Number of buffers in this mix + s16 buffer_count{}; + /// Is this mix in use? + bool in_use{}; + /// Is this mix enabled? + bool enabled{}; + /// Id of this mix + s32 mix_id{UnusedMixId}; + /// Node id of this mix + s32 node_id{}; + /// Buffer offset for this mix + s16 buffer_offset{}; + /// Distance to the final mix + s32 distance_from_final_mix{InvalidDistanceFromFinalMix}; + /// Array of effect orderings of all effects in this mix + std::span effect_order_buffer; + /// Number of effects in this mix + const s32 effect_count; + /// Id for next mix in the chain + s32 dst_mix_id{UnusedMixId}; + /// Mixing volumes for this mix used when this mix is chained with another + std::array, MaxMixBuffers> mix_volumes{}; + /// Id for next mix in the graph when splitter is used + s32 dst_splitter_id{UnusedSplitterId}; + /// Is a longer pre-delay time supported for the reverb effect? + const bool long_size_pre_delay_supported; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/nodes/bit_array.h b/src/audio_core/renderer/nodes/bit_array.h new file mode 100644 index 0000000000..b0d53cd512 --- /dev/null +++ b/src/audio_core/renderer/nodes/bit_array.h @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Represents an array of bits used for nodes and edges for the mixing graph. + */ +struct BitArray { + void reset() { + buffer.assign(buffer.size(), false); + } + + /// Bits + std::vector buffer{}; + /// Size of the buffer + u32 size{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/nodes/edge_matrix.cpp b/src/audio_core/renderer/nodes/edge_matrix.cpp new file mode 100644 index 0000000000..5573f33b97 --- /dev/null +++ b/src/audio_core/renderer/nodes/edge_matrix.cpp @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/nodes/edge_matrix.h" + +namespace AudioCore::AudioRenderer { + +void EdgeMatrix::Initialize([[maybe_unused]] std::span buffer, + [[maybe_unused]] const u64 node_buffer_size, const u32 count_) { + count = count_; + edges.buffer.resize(count_ * count_); + edges.size = count_ * count_; + edges.reset(); +} + +bool EdgeMatrix::Connected(const u32 id, const u32 destination_id) const { + return edges.buffer[count * id + destination_id]; +} + +void EdgeMatrix::Connect(const u32 id, const u32 destination_id) { + edges.buffer[count * id + destination_id] = true; +} + +void EdgeMatrix::Disconnect(const u32 id, const u32 destination_id) { + edges.buffer[count * id + destination_id] = false; +} + +void EdgeMatrix::RemoveEdges(const u32 id) { + for (u32 dest = 0; dest < count; dest++) { + Disconnect(id, dest); + } +} + +u32 EdgeMatrix::GetNodeCount() const { + return count; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/nodes/edge_matrix.h b/src/audio_core/renderer/nodes/edge_matrix.h new file mode 100644 index 0000000000..27a20e43ec --- /dev/null +++ b/src/audio_core/renderer/nodes/edge_matrix.h @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/nodes/bit_array.h" +#include "common/alignment.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * An edge matrix, holding the connections for each node to every other node in the graph. + */ +class EdgeMatrix { +public: + /** + * Calculate the size required for its workbuffer. + * + * @param count - The number of nodes in the graph. + * @return The required workbuffer size. + */ + static u64 GetWorkBufferSize(u32 count) { + return Common::AlignUp(count * count, 0x40) / sizeof(u64); + } + + /** + * Initialize this edge matrix. + * + * @param buffer - The workbuffer to use. Unused. + * @param node_buffer_size - The size of the workbuffer. Unused. + * @param count - The number of nodes in the graph. + */ + void Initialize(std::span buffer, u64 node_buffer_size, u32 count); + + /** + * Check if a node is connected to another. + * + * @param id - The node id to check. + * @param destination_id - Node id to check connection with. + */ + bool Connected(u32 id, u32 destination_id) const; + + /** + * Connect a node to another. + * + * @param id - The node id to connect. + * @param destination_id - Destination to connect it to. + */ + void Connect(u32 id, u32 destination_id); + + /** + * Disconnect a node from another. + * + * @param id - The node id to disconnect. + * @param destination_id - Destination to disconnect it from. + */ + void Disconnect(u32 id, u32 destination_id); + + /** + * Remove all connections for a given node. + * + * @param id - The node id to disconnect. + */ + void RemoveEdges(u32 id); + + /** + * Get the number of nodes in the graph. + * + * @return Number of nodes. + */ + u32 GetNodeCount() const; + +private: + /// Edges for the current graph + BitArray edges; + /// Number of nodes (not edges) in the graph + u32 count; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/nodes/node_states.cpp b/src/audio_core/renderer/nodes/node_states.cpp new file mode 100644 index 0000000000..1821a51e66 --- /dev/null +++ b/src/audio_core/renderer/nodes/node_states.cpp @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/nodes/node_states.h" +#include "common/logging/log.h" + +namespace AudioCore::AudioRenderer { + +void NodeStates::Initialize(std::span buffer_, [[maybe_unused]] const u64 node_buffer_size, + const u32 count) { + u64 num_blocks{Common::AlignUp(count, 0x40) / sizeof(u64)}; + u64 offset{0}; + + node_count = count; + + nodes_found.buffer.resize(count); + nodes_found.size = count; + nodes_found.reset(); + + offset += num_blocks; + + nodes_complete.buffer.resize(count); + nodes_complete.size = count; + nodes_complete.reset(); + + offset += num_blocks; + + results = {reinterpret_cast(&buffer_[offset]), count}; + + offset += count * sizeof(u32); + + stack.stack = {reinterpret_cast(&buffer_[offset]), count * count}; + stack.size = count * count; + stack.unk_10 = count * count; + + offset += count * count * sizeof(u32); +} + +bool NodeStates::Tsort(const EdgeMatrix& edge_matrix) { + return DepthFirstSearch(edge_matrix, stack); +} + +bool NodeStates::DepthFirstSearch(const EdgeMatrix& edge_matrix, Stack& stack_) { + ResetState(); + + for (u32 node_id = 0; node_id < node_count; node_id++) { + if (GetState(node_id) == SearchState::Unknown) { + stack_.push(node_id); + } + + while (stack_.Count() > 0) { + auto current_node{stack_.top()}; + switch (GetState(current_node)) { + case SearchState::Unknown: + SetState(current_node, SearchState::Found); + break; + case SearchState::Found: + SetState(current_node, SearchState::Complete); + PushTsortResult(current_node); + stack_.pop(); + continue; + case SearchState::Complete: + stack_.pop(); + continue; + } + + const auto edge_count{edge_matrix.GetNodeCount()}; + for (u32 edge_id = 0; edge_id < edge_count; edge_id++) { + if (!edge_matrix.Connected(current_node, edge_id)) { + continue; + } + + switch (GetState(edge_id)) { + case SearchState::Unknown: + stack_.push(edge_id); + break; + case SearchState::Found: + LOG_ERROR(Service_Audio, + "Cycle detected in the node graph, graph is not a DAG! " + "Bailing to avoid an infinite loop"); + ResetState(); + return false; + case SearchState::Complete: + break; + } + } + } + } + + return true; +} + +NodeStates::SearchState NodeStates::GetState(const u32 id) const { + if (nodes_found.buffer[id]) { + return SearchState::Found; + } else if (nodes_complete.buffer[id]) { + return SearchState::Complete; + } + return SearchState::Unknown; +} + +void NodeStates::PushTsortResult(const u32 id) { + results[result_pos++] = id; +} + +void NodeStates::SetState(const u32 id, const SearchState state) { + switch (state) { + case SearchState::Complete: + nodes_found.buffer[id] = false; + nodes_complete.buffer[id] = true; + break; + case SearchState::Found: + nodes_found.buffer[id] = true; + nodes_complete.buffer[id] = false; + break; + case SearchState::Unknown: + nodes_found.buffer[id] = false; + nodes_complete.buffer[id] = false; + break; + default: + LOG_ERROR(Service_Audio, "Unknown node SearchState {}", static_cast(state)); + break; + } +} + +void NodeStates::ResetState() { + nodes_found.reset(); + nodes_complete.reset(); + std::fill(results.begin(), results.end(), -1); + result_pos = 0; +} + +u32 NodeStates::GetNodeCount() const { + return node_count; +} + +std::vector NodeStates::GetSortedResuls() const { + return {results.rbegin(), results.rbegin() + result_pos}; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/nodes/node_states.h b/src/audio_core/renderer/nodes/node_states.h new file mode 100644 index 0000000000..a1e0958a29 --- /dev/null +++ b/src/audio_core/renderer/nodes/node_states.h @@ -0,0 +1,195 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/nodes/edge_matrix.h" +#include "common/alignment.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Graph utility functions for sorting and getting results from the DAG. + */ +class NodeStates { + /** + * State of a node in the depth first search. + */ + enum class SearchState { + Unknown, + Found, + Complete, + }; + + /** + * Stack used for a depth first search. + */ + struct Stack { + /** + * Calculate the workbuffer size required for this stack. + * + * @param count - Maximum number of nodes for the stack. + * @return Required buffer size. + */ + static u32 CalcBufferSize(u32 count) { + return count * sizeof(u32); + } + + /** + * Reset the stack back to default. + * + * @param buffer_ - The new buffer to use. + * @param size_ - The size of the new buffer. + */ + void Reset(u32* buffer_, u32 size_) { + stack = {buffer_, size_}; + size = size_; + pos = 0; + unk_10 = size_; + } + + /** + * Get the current stack position. + * + * @return The current stack position. + */ + u32 Count() { + return pos; + } + + /** + * Push a new node to the stack. + * + * @param data - The node to push. + */ + void push(u32 data) { + stack[pos++] = data; + } + + /** + * Pop a node from the stack. + * + * @return The node on the top of the stack. + */ + u32 pop() { + return stack[--pos]; + } + + /** + * Get the top of the stack without popping. + * + * @return The node on the top of the stack. + */ + u32 top() { + return stack[pos - 1]; + } + + /// Buffer for the stack + std::span stack{}; + /// Size of the stack buffer + u32 size{}; + /// Current stack position + u32 pos{}; + /// Unknown + u32 unk_10{}; + }; + +public: + /** + * Calculate the workbuffer size required for the node states. + * + * @param count - The number of nodes. + * @return The required workbuffer size. + */ + static u64 GetWorkBufferSize(u32 count) { + return (Common::AlignUp(count, 0x40) / sizeof(u64)) * 2 + count * sizeof(BitArray) + + count * Stack::CalcBufferSize(count); + } + + /** + * Initialize the node states. + * + * @param buffer - The workbuffer to use. Unused. + * @param node_buffer_size - The size of the workbuffer. Unused. + * @param count - The number of nodes in the graph. + */ + void Initialize(std::span nodes, u64 node_buffer_size, u32 count); + + /** + * Sort the graph. Only calls DepthFirstSearch. + * + * @param edge_matrix - The edge matrix used to hold the connections between nodes. + * @return True if the sort was successful, otherwise false. + */ + bool Tsort(const EdgeMatrix& edge_matrix); + + /** + * Sort the graph via depth first search. + * + * @param edge_matrix - The edge matrix used to hold the connections between nodes. + * @param stack - The stack used for pushing and popping nodes. + * @return True if the sort was successful, otherwise false. + */ + bool DepthFirstSearch(const EdgeMatrix& edge_matrix, Stack& stack); + + /** + * Get the search state of a given node. + * + * @param id - The node id to check. + * @return The node's search state. See SearchState + */ + SearchState GetState(u32 id) const; + + /** + * Push a node id to the results buffer when found in the DFS. + * + * @param id - The node id to push. + */ + void PushTsortResult(u32 id); + + /** + * Set the state of a node. + * + * @param id - The node id to alter. + * @param state - The new search state. + */ + void SetState(u32 id, SearchState state); + + /** + * Reset the nodes found, complete and the results. + */ + void ResetState(); + + /** + * Get the number of nodes in the graph. + * + * @return The number of nodes. + */ + u32 GetNodeCount() const; + + /** + * Get the sorted results from the DFS. + * + * @return Vector of nodes in reverse order. + */ + std::vector GetSortedResuls() const; + +private: + /// Number of nodes in the graph + u32 node_count{}; + /// Position in results buffer + u32 result_pos{}; + /// List of nodes found + BitArray nodes_found{}; + /// List of nodes completed + BitArray nodes_complete{}; + /// List of results from the depth first search + std::span results{}; + /// Stack used during the depth first search + Stack stack{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/detail_aspect.cpp b/src/audio_core/renderer/performance/detail_aspect.cpp new file mode 100644 index 0000000000..f6405937fd --- /dev/null +++ b/src/audio_core/renderer/performance/detail_aspect.cpp @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/command/command_buffer.h" +#include "audio_core/renderer/command/command_generator.h" +#include "audio_core/renderer/performance/detail_aspect.h" + +namespace AudioCore::AudioRenderer { + +DetailAspect::DetailAspect(CommandGenerator& command_generator_, + const PerformanceEntryType entry_type, const s32 node_id_, + const PerformanceDetailType detail_type) + : command_generator{command_generator_}, node_id{node_id_} { + auto perf_manager{command_generator.GetPerformanceManager()}; + if (perf_manager != nullptr && perf_manager->IsInitialized() && + perf_manager->IsDetailTarget(node_id) && + perf_manager->GetNextEntry(performance_entry_address, detail_type, entry_type, node_id)) { + command_generator.GeneratePerformanceCommand(node_id, PerformanceState::Start, + performance_entry_address); + + initialized = true; + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/detail_aspect.h b/src/audio_core/renderer/performance/detail_aspect.h new file mode 100644 index 0000000000..ee4ac2f766 --- /dev/null +++ b/src/audio_core/renderer/performance/detail_aspect.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/performance/performance_entry_addresses.h" +#include "audio_core/renderer/performance/performance_manager.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +class CommandGenerator; + +/** + * Holds detailed information about performance metrics, filled in by the AudioRenderer during + * Performance commands. + */ +class DetailAspect { +public: + DetailAspect() = default; + DetailAspect(CommandGenerator& command_generator, PerformanceEntryType entry_type, s32 node_id, + PerformanceDetailType detail_type); + + /// Command generator the command will be generated into + CommandGenerator& command_generator; + /// Addresses to be filled by the AudioRenderer + PerformanceEntryAddresses performance_entry_address{}; + /// Is this detail aspect initialized? + bool initialized{}; + /// Node id of this aspect + s32 node_id; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/entry_aspect.cpp b/src/audio_core/renderer/performance/entry_aspect.cpp new file mode 100644 index 0000000000..dd4165803c --- /dev/null +++ b/src/audio_core/renderer/performance/entry_aspect.cpp @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/command/command_buffer.h" +#include "audio_core/renderer/command/command_generator.h" +#include "audio_core/renderer/performance/entry_aspect.h" + +namespace AudioCore::AudioRenderer { + +EntryAspect::EntryAspect(CommandGenerator& command_generator_, const PerformanceEntryType type, + const s32 node_id_) + : command_generator{command_generator_}, node_id{node_id_} { + auto perf_manager{command_generator.GetPerformanceManager()}; + if (perf_manager != nullptr && perf_manager->IsInitialized() && + perf_manager->GetNextEntry(performance_entry_address, type, node_id)) { + command_generator.GeneratePerformanceCommand(node_id, PerformanceState::Start, + performance_entry_address); + + initialized = true; + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/entry_aspect.h b/src/audio_core/renderer/performance/entry_aspect.h new file mode 100644 index 0000000000..01c1eb3f15 --- /dev/null +++ b/src/audio_core/renderer/performance/entry_aspect.h @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/performance/performance_entry_addresses.h" +#include "audio_core/renderer/performance/performance_manager.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +class CommandGenerator; + +/** + * Holds entry information about performance metrics, filled in by the AudioRenderer during + * Performance commands. + */ +class EntryAspect { +public: + EntryAspect() = default; + EntryAspect(CommandGenerator& command_generator, PerformanceEntryType type, s32 node_id); + + /// Command generator the command will be generated into + CommandGenerator& command_generator; + /// Addresses to be filled by the AudioRenderer + PerformanceEntryAddresses performance_entry_address{}; + /// Is this detail aspect initialized? + bool initialized{}; + /// Node id of this aspect + s32 node_id; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/performance_detail.h b/src/audio_core/renderer/performance/performance_detail.h new file mode 100644 index 0000000000..3a4897e606 --- /dev/null +++ b/src/audio_core/renderer/performance/performance_detail.h @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/performance/performance_entry.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +enum class PerformanceDetailType : u8 { + Invalid, + Unk1, + Unk2, + Unk3, + Unk4, + Unk5, + Unk6, + Unk7, + Unk8, + Unk9, + Unk10, + Unk11, + Unk12, + Unk13, +}; + +struct PerformanceDetailVersion1 { + /* 0x00 */ u32 node_id; + /* 0x04 */ u32 start_time; + /* 0x08 */ u32 processed_time; + /* 0x0C */ PerformanceDetailType detail_type; + /* 0x0D */ PerformanceEntryType entry_type; +}; +static_assert(sizeof(PerformanceDetailVersion1) == 0x10, + "PerformanceDetailVersion1 has the worng size!"); + +struct PerformanceDetailVersion2 { + /* 0x00 */ u32 node_id; + /* 0x04 */ u32 start_time; + /* 0x08 */ u32 processed_time; + /* 0x0C */ PerformanceDetailType detail_type; + /* 0x0D */ PerformanceEntryType entry_type; + /* 0x10 */ u32 unk_10; + /* 0x14 */ char unk14[0x4]; +}; +static_assert(sizeof(PerformanceDetailVersion2) == 0x18, + "PerformanceDetailVersion2 has the worng size!"); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/performance_entry.h b/src/audio_core/renderer/performance/performance_entry.h new file mode 100644 index 0000000000..d1b21406b5 --- /dev/null +++ b/src/audio_core/renderer/performance/performance_entry.h @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +enum class PerformanceEntryType : u8 { + Invalid, + Voice, + SubMix, + FinalMix, + Sink, +}; + +struct PerformanceEntryVersion1 { + /* 0x00 */ u32 node_id; + /* 0x04 */ u32 start_time; + /* 0x08 */ u32 processed_time; + /* 0x0C */ PerformanceEntryType entry_type; +}; +static_assert(sizeof(PerformanceEntryVersion1) == 0x10, + "PerformanceEntryVersion1 has the worng size!"); + +struct PerformanceEntryVersion2 { + /* 0x00 */ u32 node_id; + /* 0x04 */ u32 start_time; + /* 0x08 */ u32 processed_time; + /* 0x0C */ PerformanceEntryType entry_type; + /* 0x0D */ char unk0D[0xB]; +}; +static_assert(sizeof(PerformanceEntryVersion2) == 0x18, + "PerformanceEntryVersion2 has the worng size!"); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/performance_entry_addresses.h b/src/audio_core/renderer/performance/performance_entry_addresses.h new file mode 100644 index 0000000000..e381d765ce --- /dev/null +++ b/src/audio_core/renderer/performance/performance_entry_addresses.h @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/common/common.h" + +namespace AudioCore::AudioRenderer { + +struct PerformanceEntryAddresses { + CpuAddr translated_address; + CpuAddr entry_start_time_offset; + CpuAddr header_entry_count_offset; + CpuAddr entry_processed_time_offset; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/performance_frame_header.h b/src/audio_core/renderer/performance/performance_frame_header.h new file mode 100644 index 0000000000..707cc0afb2 --- /dev/null +++ b/src/audio_core/renderer/performance/performance_frame_header.h @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { + +struct PerformanceFrameHeaderVersion1 { + /* 0x00 */ u32 magic; // "PERF" + /* 0x04 */ u32 entry_count; + /* 0x08 */ u32 detail_count; + /* 0x0C */ u32 next_offset; + /* 0x10 */ u32 total_processing_time; + /* 0x14 */ u32 frame_index; +}; +static_assert(sizeof(PerformanceFrameHeaderVersion1) == 0x18, + "PerformanceFrameHeaderVersion1 has the worng size!"); + +struct PerformanceFrameHeaderVersion2 { + /* 0x00 */ u32 magic; // "PERF" + /* 0x04 */ u32 entry_count; + /* 0x08 */ u32 detail_count; + /* 0x0C */ u32 next_offset; + /* 0x10 */ u32 total_processing_time; + /* 0x14 */ u32 voices_dropped; + /* 0x18 */ u64 start_time; + /* 0x20 */ u32 frame_index; + /* 0x24 */ bool render_time_exceeded; + /* 0x25 */ char unk25[0xB]; +}; +static_assert(sizeof(PerformanceFrameHeaderVersion2) == 0x30, + "PerformanceFrameHeaderVersion2 has the worng size!"); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/performance_manager.cpp b/src/audio_core/renderer/performance/performance_manager.cpp new file mode 100644 index 0000000000..fd5873e1ed --- /dev/null +++ b/src/audio_core/renderer/performance/performance_manager.cpp @@ -0,0 +1,645 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/memory/memory_pool_info.h" +#include "audio_core/renderer/performance/performance_manager.h" +#include "common/common_funcs.h" + +namespace AudioCore::AudioRenderer { + +void PerformanceManager::CreateImpl(const size_t version) { + switch (version) { + case 1: + impl = std::make_unique< + PerformanceManagerImpl>(); + break; + case 2: + impl = std::make_unique< + PerformanceManagerImpl>(); + break; + default: + LOG_WARNING(Service_Audio, "Invalid PerformanceMetricsDataFormat {}, creating version 1", + static_cast(version)); + impl = std::make_unique< + PerformanceManagerImpl>(); + } +} + +void PerformanceManager::Initialize(std::span workbuffer, const u64 workbuffer_size, + const AudioRendererParameterInternal& params, + const BehaviorInfo& behavior, + const MemoryPoolInfo& memory_pool) { + CreateImpl(behavior.GetPerformanceMetricsDataFormat()); + impl->Initialize(workbuffer, workbuffer_size, params, behavior, memory_pool); +} + +bool PerformanceManager::IsInitialized() const { + if (impl) { + return impl->IsInitialized(); + } + return false; +} + +u32 PerformanceManager::CopyHistories(u8* out_buffer, u64 out_size) { + if (impl) { + return impl->CopyHistories(out_buffer, out_size); + } + return 0; +} + +bool PerformanceManager::GetNextEntry(PerformanceEntryAddresses& addresses, u32** unk, + const PerformanceSysDetailType sys_detail_type, + const s32 node_id) { + if (impl) { + return impl->GetNextEntry(addresses, unk, sys_detail_type, node_id); + } + return false; +} + +bool PerformanceManager::GetNextEntry(PerformanceEntryAddresses& addresses, + const PerformanceEntryType entry_type, const s32 node_id) { + if (impl) { + return impl->GetNextEntry(addresses, entry_type, node_id); + } + return false; +} + +bool PerformanceManager::GetNextEntry(PerformanceEntryAddresses& addresses, + const PerformanceDetailType detail_type, + const PerformanceEntryType entry_type, const s32 node_id) { + if (impl) { + return impl->GetNextEntry(addresses, detail_type, entry_type, node_id); + } + return false; +} + +void PerformanceManager::TapFrame(const bool dsp_behind, const u32 voices_dropped, + const u64 rendering_start_tick) { + if (impl) { + impl->TapFrame(dsp_behind, voices_dropped, rendering_start_tick); + } +} + +bool PerformanceManager::IsDetailTarget(const u32 target_node_id) const { + if (impl) { + return impl->IsDetailTarget(target_node_id); + } + return false; +} + +void PerformanceManager::SetDetailTarget(const u32 target_node_id) { + if (impl) { + impl->SetDetailTarget(target_node_id); + } +} + +template <> +void PerformanceManagerImpl< + PerformanceVersion::Version1, PerformanceFrameHeaderVersion1, PerformanceEntryVersion1, + PerformanceDetailVersion1>::Initialize(std::span workbuffer_, const u64 workbuffer_size, + const AudioRendererParameterInternal& params, + const BehaviorInfo& behavior, + const MemoryPoolInfo& memory_pool) { + workbuffer = workbuffer_; + entries_per_frame = params.voices + params.effects + params.sinks + params.sub_mixes + 1; + max_detail_count = MaxDetailEntries; + frame_size = GetRequiredBufferSizeForPerformanceMetricsPerFrame(behavior, params); + const auto frame_count{static_cast(workbuffer_size / frame_size)}; + max_frames = frame_count - 1; + translated_buffer = memory_pool.Translate(CpuAddr(workbuffer.data()), workbuffer_size); + + // The first frame is the "current" frame we're writing to. + auto buffer_offset{workbuffer.data()}; + frame_header = reinterpret_cast(buffer_offset); + buffer_offset += sizeof(PerformanceFrameHeaderVersion1); + entry_buffer = {reinterpret_cast(buffer_offset), entries_per_frame}; + buffer_offset += entries_per_frame * sizeof(PerformanceEntryVersion1); + detail_buffer = {reinterpret_cast(buffer_offset), max_detail_count}; + + // After the current, is a ringbuffer of history frames, the current frame will be copied here + // before a new frame is written. + frame_history = std::span(workbuffer.data() + frame_size, workbuffer_size - frame_size); + + // If there's room for any history frames. + if (frame_count >= 2) { + buffer_offset = frame_history.data(); + frame_history_header = reinterpret_cast(buffer_offset); + buffer_offset += sizeof(PerformanceFrameHeaderVersion1); + frame_history_entries = {reinterpret_cast(buffer_offset), + entries_per_frame}; + buffer_offset += entries_per_frame * sizeof(PerformanceEntryVersion1); + frame_history_details = {reinterpret_cast(buffer_offset), + max_detail_count}; + } else { + frame_history_header = {}; + frame_history_entries = {}; + frame_history_details = {}; + } + + target_node_id = 0; + version = PerformanceVersion(behavior.GetPerformanceMetricsDataFormat()); + entry_count = 0; + detail_count = 0; + frame_header->entry_count = 0; + frame_header->detail_count = 0; + output_frame_index = 0; + last_output_frame_index = 0; + is_initialized = true; +} + +template <> +bool PerformanceManagerImpl::IsInitialized() + const { + return is_initialized; +} + +template <> +u32 PerformanceManagerImpl::CopyHistories(u8* out_buffer, u64 out_size) { + if (out_buffer == nullptr || out_size == 0 || !is_initialized) { + return 0; + } + + // Are there any new frames waiting to be output? + if (last_output_frame_index == output_frame_index) { + return 0; + } + + PerformanceFrameHeaderVersion1* out_header{nullptr}; + u32 out_history_size{0}; + + while (last_output_frame_index != output_frame_index) { + PerformanceFrameHeaderVersion1* history_header{nullptr}; + std::span history_entries{}; + std::span history_details{}; + + if (max_frames > 0) { + auto frame_offset{&frame_history[last_output_frame_index * frame_size]}; + history_header = reinterpret_cast(frame_offset); + frame_offset += sizeof(PerformanceFrameHeaderVersion1); + history_entries = {reinterpret_cast(frame_offset), + history_header->entry_count}; + frame_offset += entries_per_frame * sizeof(PerformanceFrameHeaderVersion1); + history_details = {reinterpret_cast(frame_offset), + history_header->detail_count}; + } else { + // Original code does not break here, but will crash when trying to dereference the + // header in the next if, so let's just skip this frame and continue... + // Hopefully this will not happen. + LOG_WARNING(Service_Audio, + "max_frames should not be 0! Skipping frame to avoid a crash"); + last_output_frame_index++; + continue; + } + + if (out_size < history_header->entry_count * sizeof(PerformanceEntryVersion1) + + history_header->detail_count * sizeof(PerformanceDetailVersion1) + + 2 * sizeof(PerformanceFrameHeaderVersion1)) { + break; + } + + u32 out_offset{sizeof(PerformanceFrameHeaderVersion1)}; + auto out_entries{std::span( + reinterpret_cast(out_buffer + out_offset), + history_header->entry_count)}; + u32 out_entry_count{0}; + u32 total_processing_time{0}; + for (auto& history_entry : history_entries) { + if (history_entry.processed_time > 0 || history_entry.start_time > 0) { + out_entries[out_entry_count++] = history_entry; + total_processing_time += history_entry.processed_time; + } + } + + out_offset += static_cast(out_entry_count * sizeof(PerformanceEntryVersion1)); + auto out_details{std::span( + reinterpret_cast(out_buffer + out_offset), + history_header->detail_count)}; + u32 out_detail_count{0}; + for (auto& history_detail : history_details) { + if (history_detail.processed_time > 0 || history_detail.start_time > 0) { + out_details[out_detail_count++] = history_detail; + } + } + + out_offset += static_cast(out_detail_count * sizeof(PerformanceDetailVersion1)); + out_header = reinterpret_cast(out_buffer); + out_header->magic = Common::MakeMagic('P', 'E', 'R', 'F'); + out_header->entry_count = out_entry_count; + out_header->detail_count = out_detail_count; + out_header->next_offset = out_offset; + out_header->total_processing_time = total_processing_time; + out_header->frame_index = history_header->frame_index; + + out_history_size += out_offset; + + out_buffer += out_offset; + out_size -= out_offset; + last_output_frame_index = (last_output_frame_index + 1) % max_frames; + } + + // We're out of frames to output, so if there's enough left in the output buffer for another + // header, and we output at least 1 frame, set the next header to null. + if (out_size > sizeof(PerformanceFrameHeaderVersion1) && out_header != nullptr) { + std::memset(out_buffer, 0, sizeof(PerformanceFrameHeaderVersion1)); + } + + return out_history_size; +} + +template <> +bool PerformanceManagerImpl:: + GetNextEntry([[maybe_unused]] PerformanceEntryAddresses& addresses, [[maybe_unused]] u32** unk, + [[maybe_unused]] PerformanceSysDetailType sys_detail_type, + [[maybe_unused]] s32 node_id) { + return false; +} + +template <> +bool PerformanceManagerImpl< + PerformanceVersion::Version1, PerformanceFrameHeaderVersion1, PerformanceEntryVersion1, + PerformanceDetailVersion1>::GetNextEntry(PerformanceEntryAddresses& addresses, + const PerformanceEntryType entry_type, + const s32 node_id) { + if (!is_initialized) { + return false; + } + + addresses.translated_address = translated_buffer; + addresses.header_entry_count_offset = CpuAddr(frame_header) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceFrameHeaderVersion1, entry_count); + + auto entry{&entry_buffer[entry_count++]}; + addresses.entry_start_time_offset = CpuAddr(entry) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceEntryVersion1, start_time); + addresses.entry_processed_time_offset = CpuAddr(entry) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceEntryVersion1, processed_time); + + std::memset(entry, 0, sizeof(PerformanceEntryVersion1)); + entry->node_id = node_id; + entry->entry_type = entry_type; + return true; +} + +template <> +bool PerformanceManagerImpl< + PerformanceVersion::Version1, PerformanceFrameHeaderVersion1, PerformanceEntryVersion1, + PerformanceDetailVersion1>::GetNextEntry(PerformanceEntryAddresses& addresses, + const PerformanceDetailType detail_type, + const PerformanceEntryType entry_type, + const s32 node_id) { + if (!is_initialized || detail_count > MaxDetailEntries) { + return false; + } + + auto detail{&detail_buffer[detail_count++]}; + + addresses.translated_address = translated_buffer; + addresses.header_entry_count_offset = CpuAddr(frame_header) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceFrameHeaderVersion1, detail_count); + addresses.entry_start_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceDetailVersion1, start_time); + addresses.entry_processed_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceDetailVersion1, processed_time); + + std::memset(detail, 0, sizeof(PerformanceDetailVersion1)); + detail->node_id = node_id; + detail->entry_type = entry_type; + detail->detail_type = detail_type; + return true; +} + +template <> +void PerformanceManagerImpl< + PerformanceVersion::Version1, PerformanceFrameHeaderVersion1, PerformanceEntryVersion1, + PerformanceDetailVersion1>::TapFrame([[maybe_unused]] bool dsp_behind, + [[maybe_unused]] u32 voices_dropped, + [[maybe_unused]] u64 rendering_start_tick) { + if (!is_initialized) { + return; + } + + if (max_frames > 0) { + if (!frame_history.empty() && !workbuffer.empty()) { + auto history_frame = reinterpret_cast( + &frame_history[output_frame_index * frame_size]); + std::memcpy(history_frame, workbuffer.data(), frame_size); + history_frame->frame_index = history_frame_index++; + } + output_frame_index = (output_frame_index + 1) % max_frames; + } + + entry_count = 0; + detail_count = 0; + frame_header->entry_count = 0; + frame_header->detail_count = 0; +} + +template <> +bool PerformanceManagerImpl< + PerformanceVersion::Version1, PerformanceFrameHeaderVersion1, PerformanceEntryVersion1, + PerformanceDetailVersion1>::IsDetailTarget(const u32 target_node_id_) const { + return target_node_id == target_node_id_; +} + +template <> +void PerformanceManagerImpl::SetDetailTarget(const u32 target_node_id_) { + target_node_id = target_node_id_; +} + +template <> +void PerformanceManagerImpl< + PerformanceVersion::Version2, PerformanceFrameHeaderVersion2, PerformanceEntryVersion2, + PerformanceDetailVersion2>::Initialize(std::span workbuffer_, const u64 workbuffer_size, + const AudioRendererParameterInternal& params, + const BehaviorInfo& behavior, + const MemoryPoolInfo& memory_pool) { + workbuffer = workbuffer_; + entries_per_frame = params.voices + params.effects + params.sinks + params.sub_mixes + 1; + max_detail_count = MaxDetailEntries; + frame_size = GetRequiredBufferSizeForPerformanceMetricsPerFrame(behavior, params); + const auto frame_count{static_cast(workbuffer_size / frame_size)}; + max_frames = frame_count - 1; + translated_buffer = memory_pool.Translate(CpuAddr(workbuffer.data()), workbuffer_size); + + // The first frame is the "current" frame we're writing to. + auto buffer_offset{workbuffer.data()}; + frame_header = reinterpret_cast(buffer_offset); + buffer_offset += sizeof(PerformanceFrameHeaderVersion2); + entry_buffer = {reinterpret_cast(buffer_offset), entries_per_frame}; + buffer_offset += entries_per_frame * sizeof(PerformanceEntryVersion2); + detail_buffer = {reinterpret_cast(buffer_offset), max_detail_count}; + + // After the current, is a ringbuffer of history frames, the current frame will be copied here + // before a new frame is written. + frame_history = std::span(workbuffer.data() + frame_size, workbuffer_size - frame_size); + + // If there's room for any history frames. + if (frame_count >= 2) { + buffer_offset = frame_history.data(); + frame_history_header = reinterpret_cast(buffer_offset); + buffer_offset += sizeof(PerformanceFrameHeaderVersion2); + frame_history_entries = {reinterpret_cast(buffer_offset), + entries_per_frame}; + buffer_offset += entries_per_frame * sizeof(PerformanceEntryVersion2); + frame_history_details = {reinterpret_cast(buffer_offset), + max_detail_count}; + } else { + frame_history_header = {}; + frame_history_entries = {}; + frame_history_details = {}; + } + + target_node_id = 0; + version = PerformanceVersion(behavior.GetPerformanceMetricsDataFormat()); + entry_count = 0; + detail_count = 0; + frame_header->entry_count = 0; + frame_header->detail_count = 0; + output_frame_index = 0; + last_output_frame_index = 0; + is_initialized = true; +} + +template <> +bool PerformanceManagerImpl::IsInitialized() + const { + return is_initialized; +} + +template <> +u32 PerformanceManagerImpl::CopyHistories(u8* out_buffer, u64 out_size) { + if (out_buffer == nullptr || out_size == 0 || !is_initialized) { + return 0; + } + + // Are there any new frames waiting to be output? + if (last_output_frame_index == output_frame_index) { + return 0; + } + + PerformanceFrameHeaderVersion2* out_header{nullptr}; + u32 out_history_size{0}; + + while (last_output_frame_index != output_frame_index) { + PerformanceFrameHeaderVersion2* history_header{nullptr}; + std::span history_entries{}; + std::span history_details{}; + + if (max_frames > 0) { + auto frame_offset{&frame_history[last_output_frame_index * frame_size]}; + history_header = reinterpret_cast(frame_offset); + frame_offset += sizeof(PerformanceFrameHeaderVersion2); + history_entries = {reinterpret_cast(frame_offset), + history_header->entry_count}; + frame_offset += entries_per_frame * sizeof(PerformanceFrameHeaderVersion2); + history_details = {reinterpret_cast(frame_offset), + history_header->detail_count}; + } else { + // Original code does not break here, but will crash when trying to dereference the + // header in the next if, so let's just skip this frame and continue... + // Hopefully this will not happen. + LOG_WARNING(Service_Audio, + "max_frames should not be 0! Skipping frame to avoid a crash"); + last_output_frame_index++; + continue; + } + + if (out_size < history_header->entry_count * sizeof(PerformanceEntryVersion2) + + history_header->detail_count * sizeof(PerformanceDetailVersion2) + + 2 * sizeof(PerformanceFrameHeaderVersion2)) { + break; + } + + u32 out_offset{sizeof(PerformanceFrameHeaderVersion2)}; + auto out_entries{std::span( + reinterpret_cast(out_buffer + out_offset), + history_header->entry_count)}; + u32 out_entry_count{0}; + u32 total_processing_time{0}; + for (auto& history_entry : history_entries) { + if (history_entry.processed_time > 0 || history_entry.start_time > 0) { + out_entries[out_entry_count++] = history_entry; + total_processing_time += history_entry.processed_time; + } + } + + out_offset += static_cast(out_entry_count * sizeof(PerformanceEntryVersion2)); + auto out_details{std::span( + reinterpret_cast(out_buffer + out_offset), + history_header->detail_count)}; + u32 out_detail_count{0}; + for (auto& history_detail : history_details) { + if (history_detail.processed_time > 0 || history_detail.start_time > 0) { + out_details[out_detail_count++] = history_detail; + } + } + + out_offset += static_cast(out_detail_count * sizeof(PerformanceDetailVersion2)); + out_header = reinterpret_cast(out_buffer); + out_header->magic = Common::MakeMagic('P', 'E', 'R', 'F'); + out_header->entry_count = out_entry_count; + out_header->detail_count = out_detail_count; + out_header->next_offset = out_offset; + out_header->total_processing_time = total_processing_time; + out_header->voices_dropped = history_header->voices_dropped; + out_header->start_time = history_header->start_time; + out_header->frame_index = history_header->frame_index; + out_header->render_time_exceeded = history_header->render_time_exceeded; + + out_history_size += out_offset; + + out_buffer += out_offset; + out_size -= out_offset; + last_output_frame_index = (last_output_frame_index + 1) % max_frames; + } + + // We're out of frames to output, so if there's enough left in the output buffer for another + // header, and we output at least 1 frame, set the next header to null. + if (out_size > sizeof(PerformanceFrameHeaderVersion2) && out_header != nullptr) { + std::memset(out_buffer, 0, sizeof(PerformanceFrameHeaderVersion2)); + } + + return out_history_size; +} + +template <> +bool PerformanceManagerImpl< + PerformanceVersion::Version2, PerformanceFrameHeaderVersion2, PerformanceEntryVersion2, + PerformanceDetailVersion2>::GetNextEntry(PerformanceEntryAddresses& addresses, u32** unk, + const PerformanceSysDetailType sys_detail_type, + const s32 node_id) { + if (!is_initialized || detail_count > MaxDetailEntries) { + return false; + } + + auto detail{&detail_buffer[detail_count++]}; + + addresses.translated_address = translated_buffer; + addresses.header_entry_count_offset = CpuAddr(frame_header) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceFrameHeaderVersion2, detail_count); + addresses.entry_start_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceDetailVersion2, start_time); + addresses.entry_processed_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceDetailVersion2, processed_time); + + std::memset(detail, 0, sizeof(PerformanceDetailVersion2)); + detail->node_id = node_id; + detail->detail_type = static_cast(sys_detail_type); + + if (unk) { + *unk = &detail->unk_10; + } + return true; +} + +template <> +bool PerformanceManagerImpl< + PerformanceVersion::Version2, PerformanceFrameHeaderVersion2, PerformanceEntryVersion2, + PerformanceDetailVersion2>::GetNextEntry(PerformanceEntryAddresses& addresses, + const PerformanceEntryType entry_type, + const s32 node_id) { + if (!is_initialized) { + return false; + } + + auto entry{&entry_buffer[entry_count++]}; + + addresses.translated_address = translated_buffer; + addresses.header_entry_count_offset = CpuAddr(frame_header) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceFrameHeaderVersion2, entry_count); + addresses.entry_start_time_offset = CpuAddr(entry) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceEntryVersion2, start_time); + addresses.entry_processed_time_offset = CpuAddr(entry) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceEntryVersion2, processed_time); + + std::memset(entry, 0, sizeof(PerformanceEntryVersion2)); + entry->node_id = node_id; + entry->entry_type = entry_type; + return true; +} + +template <> +bool PerformanceManagerImpl< + PerformanceVersion::Version2, PerformanceFrameHeaderVersion2, PerformanceEntryVersion2, + PerformanceDetailVersion2>::GetNextEntry(PerformanceEntryAddresses& addresses, + const PerformanceDetailType detail_type, + const PerformanceEntryType entry_type, + const s32 node_id) { + if (!is_initialized || detail_count > MaxDetailEntries) { + return false; + } + + auto detail{&detail_buffer[detail_count++]}; + + addresses.translated_address = translated_buffer; + addresses.header_entry_count_offset = CpuAddr(frame_header) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceFrameHeaderVersion2, detail_count); + addresses.entry_start_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceDetailVersion2, start_time); + addresses.entry_processed_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) + + offsetof(PerformanceDetailVersion2, processed_time); + + std::memset(detail, 0, sizeof(PerformanceDetailVersion2)); + detail->node_id = node_id; + detail->entry_type = entry_type; + detail->detail_type = detail_type; + return true; +} + +template <> +void PerformanceManagerImpl::TapFrame(const bool dsp_behind, + const u32 voices_dropped, + const u64 rendering_start_tick) { + if (!is_initialized) { + return; + } + + if (max_frames > 0) { + if (!frame_history.empty() && !workbuffer.empty()) { + auto history_frame{reinterpret_cast( + &frame_history[output_frame_index * frame_size])}; + std::memcpy(history_frame, workbuffer.data(), frame_size); + history_frame->render_time_exceeded = dsp_behind; + history_frame->voices_dropped = voices_dropped; + history_frame->start_time = rendering_start_tick; + history_frame->frame_index = history_frame_index++; + } + output_frame_index = (output_frame_index + 1) % max_frames; + } + + entry_count = 0; + detail_count = 0; + frame_header->entry_count = 0; + frame_header->detail_count = 0; +} + +template <> +bool PerformanceManagerImpl< + PerformanceVersion::Version2, PerformanceFrameHeaderVersion2, PerformanceEntryVersion2, + PerformanceDetailVersion2>::IsDetailTarget(const u32 target_node_id_) const { + return target_node_id == target_node_id_; +} + +template <> +void PerformanceManagerImpl::SetDetailTarget(const u32 target_node_id_) { + target_node_id = target_node_id_; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/performance/performance_manager.h b/src/audio_core/renderer/performance/performance_manager.h new file mode 100644 index 0000000000..b82176bef1 --- /dev/null +++ b/src/audio_core/renderer/performance/performance_manager.h @@ -0,0 +1,273 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "audio_core/common/audio_renderer_parameter.h" +#include "audio_core/renderer/performance/performance_detail.h" +#include "audio_core/renderer/performance/performance_entry.h" +#include "audio_core/renderer/performance/performance_entry_addresses.h" +#include "audio_core/renderer/performance/performance_frame_header.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +class BehaviorInfo; +class MemoryPoolInfo; + +enum class PerformanceVersion { + Version1, + Version2, +}; + +enum class PerformanceSysDetailType { + PcmInt16 = 15, + PcmFloat = 16, + Adpcm = 17, + LightLimiter = 37, +}; + +enum class PerformanceState { + Invalid, + Start, + Stop, +}; + +/** + * Manages performance information. + * + * The performance buffer is split into frames, each comprised of: + * Frame header - Information about the number of entries/details and some others + * Entries - Created when starting to generate types of commands, such as voice + * commands, mix commands, sink commands etc. Details - Created for specific commands + * within each group. Up to MaxDetailEntries per frame. + * + * A current frame is written to by the AudioRenderer, and before it processes the next command + * list, the current frame is copied to a ringbuffer of history frames. These frames are then + * output back to the game if it supplies a performance buffer to RequestUpdate. + * + * Two versions currently exist, version 2 adds a few extra fields to the header, and a new + * SysDetail type which is seemingly unused. + */ +class PerformanceManager { +public: + static constexpr size_t MaxDetailEntries = 100; + + struct InParameter { + /* 0x00 */ s32 target_node_id; + /* 0x04 */ char unk04[0xC]; + }; + static_assert(sizeof(InParameter) == 0x10, + "PerformanceManager::InParameter has the wrong size!"); + + struct OutStatus { + /* 0x00 */ s32 history_size; + /* 0x04 */ char unk04[0xC]; + }; + static_assert(sizeof(OutStatus) == 0x10, "PerformanceManager::OutStatus has the wrong size!"); + + /** + * Calculate the required size for the performance workbuffer. + * + * @param behavior - Check which version is supported. + * @param params - Input parameters. + * @return Required workbuffer size. + */ + static u64 GetRequiredBufferSizeForPerformanceMetricsPerFrame( + const BehaviorInfo& behavior, const AudioRendererParameterInternal& params) { + u64 entry_count{params.voices + params.effects + params.sub_mixes + params.sinks + 1}; + switch (behavior.GetPerformanceMetricsDataFormat()) { + case 1: + return sizeof(PerformanceFrameHeaderVersion1) + + PerformanceManager::MaxDetailEntries * sizeof(PerformanceDetailVersion1) + + entry_count * sizeof(PerformanceEntryVersion1); + case 2: + return sizeof(PerformanceFrameHeaderVersion2) + + PerformanceManager::MaxDetailEntries * sizeof(PerformanceDetailVersion2) + + entry_count * sizeof(PerformanceEntryVersion2); + } + + LOG_WARNING(Service_Audio, "Invalid PerformanceMetrics version, assuming version 1"); + return sizeof(PerformanceFrameHeaderVersion1) + + PerformanceManager::MaxDetailEntries * sizeof(PerformanceDetailVersion1) + + entry_count * sizeof(PerformanceEntryVersion1); + } + + virtual ~PerformanceManager() = default; + + /** + * Initialize the performance manager. + * + * @param workbuffer - Workbuffer to use for performance frames. + * @param workbuffer_size - Size of the workbuffer. + * @param params - Input parameters. + * @param behavior - Behaviour to check version and data format. + * @param memory_pool - Used to translate the workbuffer address for the DSP. + */ + virtual void Initialize(std::span workbuffer, u64 workbuffer_size, + const AudioRendererParameterInternal& params, + const BehaviorInfo& behavior, const MemoryPoolInfo& memory_pool); + + /** + * Check if the manager is initialized. + * + * @return True if initialized, otherwise false. + */ + virtual bool IsInitialized() const; + + /** + * Copy the waiting performance frames to the output buffer. + * + * @param out_buffer - Output buffer to store performance frames. + * @param out_size - Size of the output buffer. + * @return Size in bytes that were written to the buffer. + */ + virtual u32 CopyHistories(u8* out_buffer, u64 out_size); + + /** + * Setup a new sys detail in the current frame, filling in addresses with offsets to the + * current workbuffer, to be written by the AudioRenderer. Note: This version is + * unused/incomplete. + * + * @param addresses - Filled with pointers to the new entry, which should be passed to + * the AudioRenderer with Performance commands to be written. + * @param unk - Unknown. + * @param sys_detail_type - Sys detail type. + * @param node_id - Node id for this entry. + * @return True if a new entry was created and the offsets are valid, otherwise false. + */ + virtual bool GetNextEntry(PerformanceEntryAddresses& addresses, u32** unk, + PerformanceSysDetailType sys_detail_type, s32 node_id); + + /** + * Setup a new entry in the current frame, filling in addresses with offsets to the current + * workbuffer, to be written by the AudioRenderer. + * + * @param addresses - Filled with pointers to the new entry, which should be passed to + * the AudioRenderer with Performance commands to be written. + * @param entry_type - The type of this entry. See PerformanceEntryType + * @param node_id - Node id for this entry. + * @return True if a new entry was created and the offsets are valid, otherwise false. + */ + virtual bool GetNextEntry(PerformanceEntryAddresses& addresses, PerformanceEntryType entry_type, + s32 node_id); + + /** + * Setup a new detail in the current frame, filling in addresses with offsets to the current + * workbuffer, to be written by the AudioRenderer. + * + * @param addresses - Filled with pointers to the new detail, which should be passed + * to the AudioRenderer with Performance commands to be written. + * @param entry_type - The type of this detail. See PerformanceEntryType + * @param node_id - Node id for this detail. + * @return True if a new detail was created and the offsets are valid, otherwise false. + */ + virtual bool GetNextEntry(PerformanceEntryAddresses& addresses, + PerformanceDetailType detail_type, PerformanceEntryType entry_type, + s32 node_id); + + /** + * Save the current frame to the ring buffer. + * + * @param dsp_behind - Did the AudioRenderer fall behind and not + * finish processing the command list? + * @param voices_dropped - The number of voices that were dropped. + * @param rendering_start_tick - The tick rendering started. + */ + virtual void TapFrame(bool dsp_behind, u32 voices_dropped, u64 rendering_start_tick); + + /** + * Check if the node id is a detail type. + * + * @return True if the node is a detail type, otherwise false. + */ + virtual bool IsDetailTarget(u32 target_node_id) const; + + /** + * Set the given node to be a detail type. + * + * @param target_node_id - Node to set. + */ + virtual void SetDetailTarget(u32 target_node_id); + +private: + /** + * Create the performance manager. + * + * @param version - Performance version to create. + */ + void CreateImpl(size_t version); + + std::unique_ptr + /// Impl for the performance manager, may be version 1 or 2. + impl; +}; + +template +class PerformanceManagerImpl : public PerformanceManager { +public: + void Initialize(std::span workbuffer, u64 workbuffer_size, + const AudioRendererParameterInternal& params, const BehaviorInfo& behavior, + const MemoryPoolInfo& memory_pool) override; + bool IsInitialized() const override; + u32 CopyHistories(u8* out_buffer, u64 out_size) override; + bool GetNextEntry(PerformanceEntryAddresses& addresses, u32** unk, + PerformanceSysDetailType sys_detail_type, s32 node_id) override; + bool GetNextEntry(PerformanceEntryAddresses& addresses, PerformanceEntryType entry_type, + s32 node_id) override; + bool GetNextEntry(PerformanceEntryAddresses& addresses, PerformanceDetailType detail_type, + PerformanceEntryType entry_type, s32 node_id) override; + void TapFrame(bool dsp_behind, u32 voices_dropped, u64 rendering_start_tick) override; + bool IsDetailTarget(u32 target_node_id) const override; + void SetDetailTarget(u32 target_node_id) override; + +private: + /// Workbuffer used to store the current performance frame + std::span workbuffer{}; + /// DSP address of the workbuffer, used by the AudioRenderer + CpuAddr translated_buffer{}; + /// Current frame index + u32 history_frame_index{}; + /// Current frame header + FrameHeaderVersion* frame_header{}; + /// Current frame entry buffer + std::span entry_buffer{}; + /// Current frame detail buffer + std::span detail_buffer{}; + /// Current frame entry count + u32 entry_count{}; + /// Current frame detail count + u32 detail_count{}; + /// Ringbuffer of previous frames + std::span frame_history{}; + /// Current history frame header + FrameHeaderVersion* frame_history_header{}; + /// Current history entry buffer + std::span frame_history_entries{}; + /// Current history detail buffer + std::span frame_history_details{}; + /// Current history ringbuffer write index + u32 output_frame_index{}; + /// Last history frame index that was written back to the game + u32 last_output_frame_index{}; + /// Maximum number of history frames in the ringbuffer + u32 max_frames{}; + /// Number of entries per frame + u32 entries_per_frame{}; + /// Maximum number of details per frame + u32 max_detail_count{}; + /// Frame size in bytes + u64 frame_size{}; + /// Is the performance manager initialized? + bool is_initialized{}; + /// Target node id + u32 target_node_id{}; + /// Performance version in use + PerformanceVersion version{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/sink/circular_buffer_sink_info.cpp b/src/audio_core/renderer/sink/circular_buffer_sink_info.cpp new file mode 100644 index 0000000000..d91f104025 --- /dev/null +++ b/src/audio_core/renderer/sink/circular_buffer_sink_info.cpp @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/memory/pool_mapper.h" +#include "audio_core/renderer/sink/circular_buffer_sink_info.h" +#include "audio_core/renderer/upsampler/upsampler_manager.h" + +namespace AudioCore::AudioRenderer { + +CircularBufferSinkInfo::CircularBufferSinkInfo() { + state.fill(0); + parameter.fill(0); + type = Type::CircularBufferSink; + + auto state_{reinterpret_cast(state.data())}; + state_->address_info.Setup(0, 0); +} + +void CircularBufferSinkInfo::CleanUp() { + auto state_{reinterpret_cast(state.data())}; + + if (state_->upsampler_info) { + state_->upsampler_info->manager->Free(state_->upsampler_info); + state_->upsampler_info = nullptr; + } + + parameter.fill(0); + type = Type::Invalid; +} + +void CircularBufferSinkInfo::Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status, + const InParameter& in_params, const PoolMapper& pool_mapper) { + const auto buffer_params{ + reinterpret_cast(&in_params.circular_buffer)}; + auto current_params{reinterpret_cast(parameter.data())}; + auto current_state{reinterpret_cast(state.data())}; + + if (in_use == buffer_params->in_use && !buffer_unmapped) { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + out_status.writeOffset = current_state->last_pos2; + return; + } + + node_id = in_params.node_id; + in_use = in_params.in_use; + + if (in_use) { + buffer_unmapped = + !pool_mapper.TryAttachBuffer(error_info, current_state->address_info, + buffer_params->cpu_address, buffer_params->size); + *current_params = *buffer_params; + } else { + *current_params = *buffer_params; + } + out_status.writeOffset = current_state->last_pos2; +} + +void CircularBufferSinkInfo::UpdateForCommandGeneration() { + if (in_use) { + auto params{reinterpret_cast(parameter.data())}; + auto state_{reinterpret_cast(state.data())}; + + const auto pos{state_->current_pos}; + state_->last_pos2 = state_->last_pos; + state_->last_pos = pos; + + state_->current_pos += static_cast(params->input_count * params->sample_count * + GetSampleFormatByteSize(SampleFormat::PcmInt16)); + if (params->size > 0) { + state_->current_pos %= params->size; + } + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/sink/circular_buffer_sink_info.h b/src/audio_core/renderer/sink/circular_buffer_sink_info.h new file mode 100644 index 0000000000..3356213ea7 --- /dev/null +++ b/src/audio_core/renderer/sink/circular_buffer_sink_info.h @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/sink/sink_info_base.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Info for a circular buffer sink. + */ +class CircularBufferSinkInfo : public SinkInfoBase { +public: + CircularBufferSinkInfo(); + + /** + * Clean up for info, resetting it to a default state. + */ + void CleanUp() override; + + /** + * Update the info according to parameters, and write the current state to out_status. + * + * @param error_info - Output error code. + * @param out_status - Output status. + * @param in_params - Input parameters. + * @param pool_mapper - Used to map the circular buffer. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status, + const InParameter& in_params, const PoolMapper& pool_mapper) override; + + /** + * Update the circular buffer on command generation, incrementing its current offsets. + */ + void UpdateForCommandGeneration() override; +}; +static_assert(sizeof(CircularBufferSinkInfo) <= sizeof(SinkInfoBase), + "CircularBufferSinkInfo is too large!"); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/sink/device_sink_info.cpp b/src/audio_core/renderer/sink/device_sink_info.cpp new file mode 100644 index 0000000000..b7b3d6f1db --- /dev/null +++ b/src/audio_core/renderer/sink/device_sink_info.cpp @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/sink/device_sink_info.h" +#include "audio_core/renderer/upsampler/upsampler_manager.h" + +namespace AudioCore::AudioRenderer { + +DeviceSinkInfo::DeviceSinkInfo() { + state.fill(0); + parameter.fill(0); + type = Type::DeviceSink; +} + +void DeviceSinkInfo::CleanUp() { + auto state_{reinterpret_cast(state.data())}; + + if (state_->upsampler_info) { + state_->upsampler_info->manager->Free(state_->upsampler_info); + state_->upsampler_info = nullptr; + } + + parameter.fill(0); + type = Type::Invalid; +} + +void DeviceSinkInfo::Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status, + const InParameter& in_params, + [[maybe_unused]] const PoolMapper& pool_mapper) { + + const auto device_params{reinterpret_cast(&in_params.device)}; + auto current_params{reinterpret_cast(parameter.data())}; + + if (in_use == in_params.in_use) { + current_params->downmix_enabled = device_params->downmix_enabled; + current_params->downmix_coeff = device_params->downmix_coeff; + } else { + type = in_params.type; + in_use = in_params.in_use; + node_id = in_params.node_id; + *current_params = *device_params; + } + + auto current_state{reinterpret_cast(state.data())}; + + for (size_t i = 0; i < current_state->downmix_coeff.size(); i++) { + current_state->downmix_coeff[i] = current_params->downmix_coeff[i]; + } + + std::memset(&out_status, 0, sizeof(OutStatus)); + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void DeviceSinkInfo::UpdateForCommandGeneration() {} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/sink/device_sink_info.h b/src/audio_core/renderer/sink/device_sink_info.h new file mode 100644 index 0000000000..a1c4414547 --- /dev/null +++ b/src/audio_core/renderer/sink/device_sink_info.h @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/sink/sink_info_base.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Info for a device sink. + */ +class DeviceSinkInfo : public SinkInfoBase { +public: + DeviceSinkInfo(); + + /** + * Clean up for info, resetting it to a default state. + */ + void CleanUp() override; + + /** + * Update the info according to parameters, and write the current state to out_status. + * + * @param error_info - Output error code. + * @param out_status - Output status. + * @param in_params - Input parameters. + * @param pool_mapper - Unused. + */ + void Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status, + const InParameter& in_params, const PoolMapper& pool_mapper) override; + + /** + * Update the device sink on command generation, unused. + */ + void UpdateForCommandGeneration() override; +}; +static_assert(sizeof(DeviceSinkInfo) <= sizeof(SinkInfoBase), "DeviceSinkInfo is too large!"); + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/sink/sink_context.cpp b/src/audio_core/renderer/sink/sink_context.cpp new file mode 100644 index 0000000000..634bc1cf95 --- /dev/null +++ b/src/audio_core/renderer/sink/sink_context.cpp @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/sink/sink_context.h" + +namespace AudioCore::AudioRenderer { + +void SinkContext::Initialize(std::span sink_infos_, const u32 sink_count_) { + sink_infos = sink_infos_; + sink_count = sink_count_; +} + +SinkInfoBase* SinkContext::GetInfo(const u32 index) { + return &sink_infos[index]; +} + +u32 SinkContext::GetCount() const { + return sink_count; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/sink/sink_context.h b/src/audio_core/renderer/sink/sink_context.h new file mode 100644 index 0000000000..185572e290 --- /dev/null +++ b/src/audio_core/renderer/sink/sink_context.h @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/sink/sink_info_base.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Manages output sinks. + */ +class SinkContext { +public: + /** + * Initialize the sink context. + * + * @param sink_infos - Workbuffer for the sinks. + * @param sink_count - Number of sinks in the buffer. + */ + void Initialize(std::span sink_infos, u32 sink_count); + + /** + * Get a given index's info. + * + * @param index - Sink index to get. + * @return The sink info base for the given index. + */ + SinkInfoBase* GetInfo(u32 index); + + /** + * Get the current number of sinks. + * + * @return The number of sinks. + */ + u32 GetCount() const; + +private: + /// Buffer of sink infos + std::span sink_infos{}; + /// Number of sinks in the buffer + u32 sink_count{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/sink/sink_info_base.cpp b/src/audio_core/renderer/sink/sink_info_base.cpp new file mode 100644 index 0000000000..4279beaa0e --- /dev/null +++ b/src/audio_core/renderer/sink/sink_info_base.cpp @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/memory/pool_mapper.h" +#include "audio_core/renderer/sink/sink_info_base.h" + +namespace AudioCore::AudioRenderer { + +void SinkInfoBase::CleanUp() { + type = Type::Invalid; +} + +void SinkInfoBase::Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status, + [[maybe_unused]] const InParameter& in_params, + [[maybe_unused]] const PoolMapper& pool_mapper) { + std::memset(&out_status, 0, sizeof(OutStatus)); + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); +} + +void SinkInfoBase::UpdateForCommandGeneration() {} + +SinkInfoBase::DeviceState* SinkInfoBase::GetDeviceState() { + return reinterpret_cast(state.data()); +} + +SinkInfoBase::Type SinkInfoBase::GetType() const { + return type; +} + +bool SinkInfoBase::IsUsed() const { + return in_use; +} + +bool SinkInfoBase::ShouldSkip() const { + return buffer_unmapped; +} + +u32 SinkInfoBase::GetNodeId() const { + return node_id; +} + +u8* SinkInfoBase::GetState() { + return state.data(); +} + +u8* SinkInfoBase::GetParameter() { + return parameter.data(); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/sink/sink_info_base.h b/src/audio_core/renderer/sink/sink_info_base.h new file mode 100644 index 0000000000..a1b855f205 --- /dev/null +++ b/src/audio_core/renderer/sink/sink_info_base.h @@ -0,0 +1,177 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/memory/address_info.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { +struct UpsamplerInfo; +class PoolMapper; + +/** + * Base for the circular buffer and device sinks, holding their states for the AudioRenderer and + * their parametetrs for generating sink commands. + */ +class SinkInfoBase { +public: + enum class Type : u8 { + Invalid, + DeviceSink, + CircularBufferSink, + }; + + struct DeviceInParameter { + /* 0x000 */ char name[0x100]; + /* 0x100 */ u32 input_count; + /* 0x104 */ std::array inputs; + /* 0x10A */ char unk10A[0x1]; + /* 0x10B */ bool downmix_enabled; + /* 0x10C */ std::array downmix_coeff; + }; + static_assert(sizeof(DeviceInParameter) == 0x11C, "DeviceInParameter has the wrong size!"); + + struct DeviceState { + /* 0x00 */ UpsamplerInfo* upsampler_info; + /* 0x08 */ std::array, 4> downmix_coeff; + /* 0x18 */ char unk18[0x18]; + }; + static_assert(sizeof(DeviceState) == 0x30, "DeviceState has the wrong size!"); + + struct CircularBufferInParameter { + /* 0x00 */ u64 cpu_address; + /* 0x08 */ u32 size; + /* 0x0C */ u32 input_count; + /* 0x10 */ u32 sample_count; + /* 0x14 */ u32 previous_pos; + /* 0x18 */ SampleFormat format; + /* 0x1C */ std::array inputs; + /* 0x22 */ bool in_use; + /* 0x23 */ char unk23[0x5]; + }; + static_assert(sizeof(CircularBufferInParameter) == 0x28, + "CircularBufferInParameter has the wrong size!"); + + struct CircularBufferState { + /* 0x00 */ u32 last_pos2; + /* 0x04 */ s32 current_pos; + /* 0x08 */ u32 last_pos; + /* 0x0C */ char unk0C[0x4]; + /* 0x10 */ AddressInfo address_info; + }; + static_assert(sizeof(CircularBufferState) == 0x30, "CircularBufferState has the wrong size!"); + + struct InParameter { + /* 0x000 */ Type type; + /* 0x001 */ bool in_use; + /* 0x004 */ u32 node_id; + /* 0x008 */ char unk08[0x18]; + union { + /* 0x020 */ DeviceInParameter device; + /* 0x020 */ CircularBufferInParameter circular_buffer; + }; + }; + static_assert(sizeof(InParameter) == 0x140, "SinkInfoBase::InParameter has the wrong size!"); + + struct OutStatus { + /* 0x00 */ u32 writeOffset; + /* 0x04 */ char unk04[0x1C]; + }; // size == 0x20 + static_assert(sizeof(OutStatus) == 0x20, "SinkInfoBase::OutStatus has the wrong size!"); + + virtual ~SinkInfoBase() = default; + + /** + * Clean up for info, resetting it to a default state. + */ + virtual void CleanUp(); + + /** + * Update the info according to parameters, and write the current state to out_status. + * + * @param error_info - Output error code. + * @param out_status - Output status. + * @param in_params - Input parameters. + * @param pool_mapper - Used to map the circular buffer. + */ + virtual void Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status, + [[maybe_unused]] const InParameter& in_params, + [[maybe_unused]] const PoolMapper& pool_mapper); + + /** + * Update the circular buffer on command generation, incrementing its current offsets. + */ + virtual void UpdateForCommandGeneration(); + + /** + * Get the state as a device sink. + * + * @return Device state. + */ + DeviceState* GetDeviceState(); + + /** + * Get the type of this sink. + * + * @return Either Device, Circular, or Invalid. + */ + Type GetType() const; + + /** + * Check if this sink is in use. + * + * @return True if used, otherwise false. + */ + bool IsUsed() const; + + /** + * Check if this sink should be skipped for updates. + * + * @return True if it should be skipped, otherwise false. + */ + bool ShouldSkip() const; + + /** + * Get the node if of this sink. + * + * @return Node id for this sink. + */ + u32 GetNodeId() const; + + /** + * Get the state of this sink. + * + * @return Pointer to the state, must be cast to the correct type. + */ + u8* GetState(); + + /** + * Get the parameters of this sink. + * + * @return Pointer to the parameters, must be cast to the correct type. + */ + u8* GetParameter(); + +protected: + /// Type of this sink + Type type{Type::Invalid}; + /// Is this sink in use? + bool in_use{}; + /// Is this sink's buffer unmapped? Circular only + bool buffer_unmapped{}; + /// Node id for this sink + u32 node_id{}; + /// State buffer for this sink + std::array state{}; + /// Parameter buffer for this sink + std::array + parameter{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/splitter/splitter_context.cpp b/src/audio_core/renderer/splitter/splitter_context.cpp new file mode 100644 index 0000000000..7a23ba43f7 --- /dev/null +++ b/src/audio_core/renderer/splitter/splitter_context.cpp @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/common/audio_renderer_parameter.h" +#include "audio_core/common/workbuffer_allocator.h" +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/splitter/splitter_context.h" +#include "common/alignment.h" + +namespace AudioCore::AudioRenderer { + +SplitterDestinationData* SplitterContext::GetDesintationData(const s32 splitter_id, + const s32 destination_id) { + return splitter_infos[splitter_id].GetData(destination_id); +} + +SplitterInfo& SplitterContext::GetInfo(const s32 splitter_id) { + return splitter_infos[splitter_id]; +} + +u32 SplitterContext::GetDataCount() const { + return destinations_count; +} + +u32 SplitterContext::GetInfoCount() const { + return info_count; +} + +SplitterDestinationData& SplitterContext::GetData(const u32 index) { + return splitter_destinations[index]; +} + +void SplitterContext::Setup(std::span splitter_infos_, const u32 splitter_info_count_, + SplitterDestinationData* splitter_destinations_, + const u32 destination_count_, const bool splitter_bug_fixed_) { + splitter_infos = splitter_infos_; + info_count = splitter_info_count_; + splitter_destinations = splitter_destinations_; + destinations_count = destination_count_; + splitter_bug_fixed = splitter_bug_fixed_; +} + +bool SplitterContext::UsingSplitter() const { + return splitter_infos.size() > 0 && info_count > 0 && splitter_destinations != nullptr && + destinations_count > 0; +} + +void SplitterContext::ClearAllNewConnectionFlag() { + for (s32 i = 0; i < info_count; i++) { + splitter_infos[i].SetNewConnectionFlag(); + } +} + +bool SplitterContext::Initialize(const BehaviorInfo& behavior, + const AudioRendererParameterInternal& params, + WorkbufferAllocator& allocator) { + if (behavior.IsSplitterSupported() && params.splitter_infos > 0 && + params.splitter_destinations > 0) { + splitter_infos = allocator.Allocate(params.splitter_infos, 0x10); + + for (u32 i = 0; i < params.splitter_infos; i++) { + std::construct_at(&splitter_infos[i], static_cast(i)); + } + + if (splitter_infos.size() == 0) { + splitter_infos = {}; + return false; + } + + splitter_destinations = + allocator.Allocate(params.splitter_destinations, 0x10).data(); + + for (s32 i = 0; i < params.splitter_destinations; i++) { + std::construct_at(&splitter_destinations[i], i); + } + + if (params.splitter_destinations <= 0) { + splitter_infos = {}; + splitter_destinations = nullptr; + return false; + } + + Setup(splitter_infos, params.splitter_infos, splitter_destinations, + params.splitter_destinations, behavior.IsSplitterBugFixed()); + } + return true; +} + +bool SplitterContext::Update(const u8* input, u32& consumed_size) { + auto in_params{reinterpret_cast(input)}; + + if (destinations_count == 0 || info_count == 0) { + consumed_size = 0; + return true; + } + + if (in_params->magic != GetSplitterInParamHeaderMagic()) { + consumed_size = 0; + return false; + } + + for (auto& splitter_info : splitter_infos) { + splitter_info.ClearNewConnectionFlag(); + } + + u32 offset{sizeof(InParameterHeader)}; + offset = UpdateInfo(input, offset, in_params->info_count); + offset = UpdateData(input, offset, in_params->destination_count); + + consumed_size = Common::AlignUp(offset, 0x10); + return true; +} + +u32 SplitterContext::UpdateInfo(const u8* input, u32 offset, const u32 splitter_count) { + for (u32 i = 0; i < splitter_count; i++) { + auto info_header{reinterpret_cast(input + offset)}; + + if (info_header->magic != GetSplitterInfoMagic()) { + continue; + } + + if (info_header->id < 0 || info_header->id > info_count) { + break; + } + + auto& info{splitter_infos[info_header->id]}; + RecomposeDestination(info, info_header); + + offset += info.Update(info_header); + } + + return offset; +} + +u32 SplitterContext::UpdateData(const u8* input, u32 offset, const u32 count) { + for (u32 i = 0; i < count; i++) { + auto data_header{ + reinterpret_cast(input + offset)}; + + if (data_header->magic != GetSplitterSendDataMagic()) { + continue; + } + + if (data_header->id < 0 || data_header->id > destinations_count) { + continue; + } + + splitter_destinations[data_header->id].Update(*data_header); + offset += sizeof(SplitterDestinationData::InParameter); + } + + return offset; +} + +void SplitterContext::UpdateInternalState() { + for (s32 i = 0; i < info_count; i++) { + splitter_infos[i].UpdateInternalState(); + } +} + +void SplitterContext::RecomposeDestination(SplitterInfo& out_info, + const SplitterInfo::InParameter* info_header) { + auto destination{out_info.GetData(0)}; + while (destination != nullptr) { + auto dest{destination->GetNext()}; + destination->SetNext(nullptr); + destination = dest; + } + out_info.SetDestinations(nullptr); + + auto dest_count{info_header->destination_count}; + if (!splitter_bug_fixed) { + dest_count = std::min(dest_count, GetDestCountPerInfoForCompat()); + } + + if (dest_count == 0) { + return; + } + + std::span destination_ids{reinterpret_cast(&info_header[1]), dest_count}; + + auto head{&splitter_destinations[destination_ids[0]]}; + auto current_destination{head}; + for (u32 i = 1; i < dest_count; i++) { + auto next_destination{&splitter_destinations[destination_ids[i]]}; + current_destination->SetNext(next_destination); + current_destination = next_destination; + } + + out_info.SetDestinations(head); + out_info.SetDestinationCount(dest_count); +} + +u32 SplitterContext::GetDestCountPerInfoForCompat() const { + if (info_count <= 0) { + return 0; + } + return static_cast(destinations_count / info_count); +} + +u64 SplitterContext::CalcWorkBufferSize(const BehaviorInfo& behavior, + const AudioRendererParameterInternal& params) { + u64 size{0}; + if (!behavior.IsSplitterSupported()) { + return size; + } + + size += params.splitter_destinations * sizeof(SplitterDestinationData) + + params.splitter_infos * sizeof(SplitterInfo); + + if (behavior.IsSplitterBugFixed()) { + size += Common::AlignUp(params.splitter_destinations * sizeof(u32), 0x10); + } + return size; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/splitter/splitter_context.h b/src/audio_core/renderer/splitter/splitter_context.h new file mode 100644 index 0000000000..cfd092b4fb --- /dev/null +++ b/src/audio_core/renderer/splitter/splitter_context.h @@ -0,0 +1,189 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/splitter/splitter_destinations_data.h" +#include "audio_core/renderer/splitter/splitter_info.h" +#include "common/common_types.h" + +namespace AudioCore { +struct AudioRendererParameterInternal; +class WorkbufferAllocator; + +namespace AudioRenderer { +class BehaviorInfo; + +/** + * The splitter allows much more control over how sound is mixed together. + * Previously, one mix can only connect to one other, and you may need + * more mixes (and duplicate processing) to achieve the same result. + * With the splitter, many-to-one and one-to-many mixing is possible. + * This was added in revision 2. + * Had a bug with incorrect numbers of destinations, fixed in revision 5. + */ +class SplitterContext { + struct InParameterHeader { + /* 0x00 */ u32 magic; // 'SNDH' + /* 0x04 */ s32 info_count; + /* 0x08 */ s32 destination_count; + /* 0x0C */ char unk0C[0x14]; + }; + static_assert(sizeof(InParameterHeader) == 0x20, + "SplitterContext::InParameterHeader has the wrong size!"); + +public: + /** + * Get a destination mix from the given splitter and destination index. + * + * @param splitter_id - Splitter index to get from. + * @param destination_id - Destination index within the splitter. + * @return Pointer to the found destination. May be nullptr. + */ + SplitterDestinationData* GetDesintationData(s32 splitter_id, s32 destination_id); + + /** + * Get a splitter from the given index. + * + * @param index - Index of the desired splitter. + * @return Splitter requested. + */ + SplitterInfo& GetInfo(s32 index); + + /** + * Get the total number of splitter destinations. + * + * @return Number of destiantions. + */ + u32 GetDataCount() const; + + /** + * Get the total number of splitters. + * + * @return Number of splitters. + */ + u32 GetInfoCount() const; + + /** + * Get a specific global destination. + * + * @param index - Index of the desired destination. + * @return The requested destination. + */ + SplitterDestinationData& GetData(u32 index); + + /** + * Check if the splitter is in use. + * + * @return True if any splitter or destination is in use, otherwise false. + */ + bool UsingSplitter() const; + + /** + * Mark all splitters as having new connections. + */ + void ClearAllNewConnectionFlag(); + + /** + * Initialize the context. + * + * @param behavior - Used to check for splitter support. + * @param params - Input parameters. + * @param allocator - Allocator used to allocate workbuffer memory. + */ + bool Initialize(const BehaviorInfo& behavior, const AudioRendererParameterInternal& params, + WorkbufferAllocator& allocator); + + /** + * Update the context. + * + * @param input - Input buffer with the new info, + * expected to point to a InParameterHeader. + * @param consumed_size - Output with the number of bytes consumed from input. + */ + bool Update(const u8* input, u32& consumed_size); + + /** + * Update the splitters. + * + * @param input - Input buffer with the new info. + * @param offset - Current offset within the input buffer, + * input + offset should point to a SplitterInfo::InParameter. + * @param splitter_count - Number of splitters in the input buffer. + * @return Number of bytes consumed in input. + */ + u32 UpdateInfo(const u8* input, u32 offset, u32 splitter_count); + + /** + * Update the splitters. + * + * @param input - Input buffer with the new info. + * @param offset - Current offset within the input buffer, + * input + offset should point to a + * SplitterDestinationData::InParameter. + * @param destination_count - Number of destinations in the input buffer. + * @return Number of bytes consumed in input. + */ + u32 UpdateData(const u8* input, u32 offset, u32 destination_count); + + /** + * Update the state of all destinations in all splitters. + */ + void UpdateInternalState(); + + /** + * Replace the given splitter's destinations with new ones. + * + * @param out_info - Splitter to recompose. + * @param info_header - Input parameters containing new destination ids. + */ + void RecomposeDestination(SplitterInfo& out_info, const SplitterInfo::InParameter* info_header); + + /** + * Old calculation for destinations, this is the thing the splitter bug fixes. + * Left for compatibility, and now min'd with the actual count to not bug. + * + * @return Number of splitter destinations. + */ + u32 GetDestCountPerInfoForCompat() const; + + /** + * Calculate the size of the required workbuffer for splitters and destinations. + * + * @param behavior - Used to check splitter features. + * @param params - Input parameters with splitter/destination counts. + * @return Required buffer size. + */ + static u64 CalcWorkBufferSize(const BehaviorInfo& behavior, + const AudioRendererParameterInternal& params); + +private: + /** + * Setup the context. + * + * @param splitter_infos - Workbuffer for splitters. + * @param splitter_info_count - Number of splitters in the workbuffer. + * @param splitter_destinations - Workbuffer for splitter destinations. + * @param destination_count - Number of destinations in the workbuffer. + * @param splitter_bug_fixed - Is the splitter bug fixed? + */ + void Setup(std::span splitter_infos, u32 splitter_info_count, + SplitterDestinationData* splitter_destinations, u32 destination_count, + bool splitter_bug_fixed); + + /// Workbuffer for splitters + std::span splitter_infos{}; + /// Number of splitters in buffer + s32 info_count{}; + /// Workbuffer for destinations + SplitterDestinationData* splitter_destinations{}; + /// Number of destinations in buffer + s32 destinations_count{}; + /// Is the splitter bug fixed? + bool splitter_bug_fixed{}; +}; + +} // namespace AudioRenderer +} // namespace AudioCore diff --git a/src/audio_core/renderer/splitter/splitter_destinations_data.cpp b/src/audio_core/renderer/splitter/splitter_destinations_data.cpp new file mode 100644 index 0000000000..b27d44896d --- /dev/null +++ b/src/audio_core/renderer/splitter/splitter_destinations_data.cpp @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/splitter/splitter_destinations_data.h" + +namespace AudioCore::AudioRenderer { + +SplitterDestinationData::SplitterDestinationData(const s32 id_) : id{id_} {} + +void SplitterDestinationData::ClearMixVolume() { + mix_volumes.fill(0.0f); + prev_mix_volumes.fill(0.0f); +} + +s32 SplitterDestinationData::GetId() const { + return id; +} + +bool SplitterDestinationData::IsConfigured() const { + return in_use && destination_id != UnusedMixId; +} + +s32 SplitterDestinationData::GetMixId() const { + return destination_id; +} + +f32 SplitterDestinationData::GetMixVolume(const u32 index) const { + if (index >= mix_volumes.size()) { + LOG_ERROR(Service_Audio, "SplitterDestinationData::GetMixVolume Invalid index {}", index); + return 0.0f; + } + return mix_volumes[index]; +} + +std::span SplitterDestinationData::GetMixVolume() { + return mix_volumes; +} + +f32 SplitterDestinationData::GetMixVolumePrev(const u32 index) const { + if (index >= prev_mix_volumes.size()) { + LOG_ERROR(Service_Audio, "SplitterDestinationData::GetMixVolumePrev Invalid index {}", + index); + return 0.0f; + } + return prev_mix_volumes[index]; +} + +std::span SplitterDestinationData::GetMixVolumePrev() { + return prev_mix_volumes; +} + +void SplitterDestinationData::Update(const InParameter& params) { + if (params.id != id || params.magic != GetSplitterSendDataMagic()) { + return; + } + + destination_id = params.mix_id; + mix_volumes = params.mix_volumes; + + if (!in_use && params.in_use) { + prev_mix_volumes = mix_volumes; + need_update = false; + } + + in_use = params.in_use; +} + +void SplitterDestinationData::MarkAsNeedToUpdateInternalState() { + need_update = true; +} + +void SplitterDestinationData::UpdateInternalState() { + if (in_use && need_update) { + prev_mix_volumes = mix_volumes; + } + need_update = false; +} + +SplitterDestinationData* SplitterDestinationData::GetNext() const { + return next; +} + +void SplitterDestinationData::SetNext(SplitterDestinationData* next_) { + next = next_; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/splitter/splitter_destinations_data.h b/src/audio_core/renderer/splitter/splitter_destinations_data.h new file mode 100644 index 0000000000..bd3d55748a --- /dev/null +++ b/src/audio_core/renderer/splitter/splitter_destinations_data.h @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Represents a mixing node, can be connected to a previous and next destination forming a chain + * that a certain mix buffer will pass through to output. + */ +class SplitterDestinationData { +public: + struct InParameter { + /* 0x00 */ u32 magic; // 'SNDD' + /* 0x04 */ s32 id; + /* 0x08 */ std::array mix_volumes; + /* 0x68 */ u32 mix_id; + /* 0x6C */ bool in_use; + }; + static_assert(sizeof(InParameter) == 0x70, + "SplitterDestinationData::InParameter has the wrong size!"); + + SplitterDestinationData(s32 id); + + /** + * Reset the mix volumes for this destination. + */ + void ClearMixVolume(); + + /** + * Get the id of this destination. + * + * @return Id for this destination. + */ + s32 GetId() const; + + /** + * Check if this destination is correctly configured. + * + * @return True if configured, otherwise false. + */ + bool IsConfigured() const; + + /** + * Get the mix id for this destination. + * + * @return Mix id for this destination. + */ + s32 GetMixId() const; + + /** + * Get the current mix volume of a given index in this destination. + * + * @param index - Mix buffer index to get the volume for. + * @return Current volume of the specified mix. + */ + f32 GetMixVolume(u32 index) const; + + /** + * Get the current mix volumes for all mix buffers in this destination. + * + * @return Span of current mix buffer volumes. + */ + std::span GetMixVolume(); + + /** + * Get the previous mix volume of a given index in this destination. + * + * @param index - Mix buffer index to get the volume for. + * @return Previous volume of the specified mix. + */ + f32 GetMixVolumePrev(u32 index) const; + + /** + * Get the previous mix volumes for all mix buffers in this destination. + * + * @return Span of previous mix buffer volumes. + */ + std::span GetMixVolumePrev(); + + /** + * Update this destination. + * + * @param params - Inpout parameters to update the destination. + */ + void Update(const InParameter& params); + + /** + * Mark this destination as needing its volumes updated. + */ + void MarkAsNeedToUpdateInternalState(); + + /** + * Copy current volumes to previous if an update is required. + */ + void UpdateInternalState(); + + /** + * Get the next destination in the mix chain. + * + * @return The next splitter destination, may be nullptr if this is the last in the chain. + */ + SplitterDestinationData* GetNext() const; + + /** + * Set the next destination in the mix chain. + * + * @param next - Destination this one is to be connected to. + */ + void SetNext(SplitterDestinationData* next); + +private: + /// Id of this destination + const s32 id; + /// Mix id this destination represents + s32 destination_id{UnusedMixId}; + /// Current mix volumes + std::array mix_volumes{0.0f}; + /// Previous mix volumes + std::array prev_mix_volumes{0.0f}; + /// Next destination in the mix chain + SplitterDestinationData* next{}; + /// Is this destiantion in use? + bool in_use{}; + /// Does this destiantion need its volumes updated? + bool need_update{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/splitter/splitter_info.cpp b/src/audio_core/renderer/splitter/splitter_info.cpp new file mode 100644 index 0000000000..1aee6720b7 --- /dev/null +++ b/src/audio_core/renderer/splitter/splitter_info.cpp @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/splitter/splitter_info.h" + +namespace AudioCore::AudioRenderer { + +SplitterInfo::SplitterInfo(const s32 id_) : id{id_} {} + +void SplitterInfo::InitializeInfos(SplitterInfo* splitters, const u32 count) { + if (splitters == nullptr) { + return; + } + + for (u32 i = 0; i < count; i++) { + auto& splitter{splitters[i]}; + splitter.destinations = nullptr; + splitter.destination_count = 0; + splitter.has_new_connection = true; + } +} + +u32 SplitterInfo::Update(const InParameter* params) { + if (params->id != id) { + return 0; + } + sample_rate = params->sample_rate; + has_new_connection = true; + return static_cast((sizeof(InParameter) + 3 * sizeof(s32)) + + params->destination_count * sizeof(s32)); +} + +SplitterDestinationData* SplitterInfo::GetData(const u32 destination_id) { + auto out_destination{destinations}; + u32 i{0}; + while (i < destination_id) { + if (out_destination == nullptr) { + break; + } + out_destination = out_destination->GetNext(); + i++; + } + + return out_destination; +} + +u32 SplitterInfo::GetDestinationCount() const { + return destination_count; +} + +void SplitterInfo::SetDestinationCount(const u32 count) { + destination_count = count; +} + +bool SplitterInfo::HasNewConnection() const { + return has_new_connection; +} + +void SplitterInfo::ClearNewConnectionFlag() { + has_new_connection = false; +} + +void SplitterInfo::SetNewConnectionFlag() { + has_new_connection = true; +} + +void SplitterInfo::UpdateInternalState() { + auto destination{destinations}; + while (destination != nullptr) { + destination->UpdateInternalState(); + destination = destination->GetNext(); + } +} + +void SplitterInfo::SetDestinations(SplitterDestinationData* destinations_) { + destinations = destinations_; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/splitter/splitter_info.h b/src/audio_core/renderer/splitter/splitter_info.h new file mode 100644 index 0000000000..d1d75064cc --- /dev/null +++ b/src/audio_core/renderer/splitter/splitter_info.h @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/renderer/splitter/splitter_destinations_data.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Represents a splitter, wraps multiple output destinations to split an input mix into. + */ +class SplitterInfo { +public: + struct InParameter { + /* 0x00 */ u32 magic; // 'SNDI' + /* 0x04 */ s32 id; + /* 0x08 */ u32 sample_rate; + /* 0x0C */ u32 destination_count; + }; + static_assert(sizeof(InParameter) == 0x10, "SplitterInfo::InParameter has the wrong size!"); + + explicit SplitterInfo(s32 id); + + /** + * Initialize the given splitters. + * + * @param splitters - Splitters to initialize. + * @param count - Number of splitters given. + */ + static void InitializeInfos(SplitterInfo* splitters, u32 count); + + /** + * Update this splitter. + * + * @param params - Input parameters to update with. + * @return The size in bytes of this splitter. + */ + u32 Update(const InParameter* params); + + /** + * Get a destination in this splitter. + * + * @param id - Destination id to get. + * @return Pointer to the destination, may be nullptr. + */ + SplitterDestinationData* GetData(u32 id); + + /** + * Get the number of destinations in this splitter. + * + * @return The number of destiantions. + */ + u32 GetDestinationCount() const; + + /** + * Set the number of destinations in this splitter. + * + * @param count - The new number of destiantions. + */ + void SetDestinationCount(u32 count); + + /** + * Check if the splitter has a new connection. + * + * @return True if there is a new connection, otherwise false. + */ + bool HasNewConnection() const; + + /** + * Reset the new connection flag. + */ + void ClearNewConnectionFlag(); + + /** + * Mark as having a new connection. + */ + void SetNewConnectionFlag(); + + /** + * Update the state of all destinations. + */ + void UpdateInternalState(); + + /** + * Set this splitter's destinations. + * + * @param destinations - The new destination list for this splitter. + */ + void SetDestinations(SplitterDestinationData* destinations); + +private: + /// Id of this splitter + s32 id; + /// Sample rate of this splitter + u32 sample_rate{}; + /// Number of destinations in this splitter + u32 destination_count{}; + /// Does this splitter have a new connection? + bool has_new_connection{true}; + /// Pointer to the destinations of this splitter + SplitterDestinationData* destinations{}; + /// Number of channels this splitter manages + u32 channel_count{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/system.cpp b/src/audio_core/renderer/system.cpp new file mode 100644 index 0000000000..7a217969e9 --- /dev/null +++ b/src/audio_core/renderer/system.cpp @@ -0,0 +1,802 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "audio_core/audio_core.h" +#include "audio_core/common/audio_renderer_parameter.h" +#include "audio_core/common/common.h" +#include "audio_core/common/feature_support.h" +#include "audio_core/common/workbuffer_allocator.h" +#include "audio_core/renderer/adsp/adsp.h" +#include "audio_core/renderer/behavior/info_updater.h" +#include "audio_core/renderer/command/command_buffer.h" +#include "audio_core/renderer/command/command_generator.h" +#include "audio_core/renderer/command/command_list_header.h" +#include "audio_core/renderer/effect/effect_info_base.h" +#include "audio_core/renderer/effect/effect_result_state.h" +#include "audio_core/renderer/memory/memory_pool_info.h" +#include "audio_core/renderer/memory/pool_mapper.h" +#include "audio_core/renderer/mix/mix_info.h" +#include "audio_core/renderer/nodes/edge_matrix.h" +#include "audio_core/renderer/nodes/node_states.h" +#include "audio_core/renderer/sink/sink_info_base.h" +#include "audio_core/renderer/system.h" +#include "audio_core/renderer/upsampler/upsampler_info.h" +#include "audio_core/renderer/voice/voice_channel_resource.h" +#include "audio_core/renderer/voice/voice_info.h" +#include "audio_core/renderer/voice/voice_state.h" +#include "common/alignment.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/hle/kernel/k_event.h" +#include "core/hle/kernel/k_transfer_memory.h" +#include "core/memory.h" + +namespace AudioCore::AudioRenderer { + +u64 System::GetWorkBufferSize(const AudioRendererParameterInternal& params) { + BehaviorInfo behavior; + behavior.SetUserLibRevision(params.revision); + + u64 size{0}; + + size += Common::AlignUp(params.mixes * sizeof(s32), 0x40); + size += params.sub_mixes * MaxEffects * sizeof(s32); + size += (params.sub_mixes + 1) * sizeof(MixInfo); + size += params.voices * (sizeof(VoiceInfo) + sizeof(VoiceChannelResource) + sizeof(VoiceState)); + size += Common::AlignUp((params.sub_mixes + 1) * sizeof(MixInfo*), 0x10); + size += Common::AlignUp(params.voices * sizeof(VoiceInfo*), 0x10); + size += Common::AlignUp(((params.sinks + params.sub_mixes) * TargetSampleCount * sizeof(s32) + + params.sample_count * sizeof(s32)) * + (params.mixes + MaxChannels), + 0x40); + + if (behavior.IsSplitterSupported()) { + const auto node_size{NodeStates::GetWorkBufferSize(params.sub_mixes + 1)}; + const auto edge_size{EdgeMatrix::GetWorkBufferSize(params.sub_mixes + 1)}; + size += Common::AlignUp(node_size + edge_size, 0x10); + } + + size += SplitterContext::CalcWorkBufferSize(behavior, params); + size += (params.effects + params.voices * MaxWaveBuffers) * sizeof(MemoryPoolInfo); + + if (behavior.IsEffectInfoVersion2Supported()) { + size += params.effects * sizeof(EffectResultState); + } + size += 0x50; + + size = Common::AlignUp(size, 0x40); + + size += (params.sinks + params.sub_mixes) * sizeof(UpsamplerInfo); + size += params.effects * sizeof(EffectInfoBase); + size += Common::AlignUp(params.voices * sizeof(VoiceState), 0x40); + size += params.sinks * sizeof(SinkInfoBase); + + if (behavior.IsEffectInfoVersion2Supported()) { + size += params.effects * sizeof(EffectResultState); + } + + if (params.perf_frames > 0) { + auto perf_size{PerformanceManager::GetRequiredBufferSizeForPerformanceMetricsPerFrame( + behavior, params)}; + size += Common::AlignUp(perf_size * (params.perf_frames + 1) + 0xC0, 0x100); + } + + if (behavior.IsVariadicCommandBufferSizeSupported()) { + size += CommandGenerator::CalculateCommandBufferSize(behavior, params) + (0x40 - 1) * 2; + } else { + size += 0x18000 + (0x40 - 1) * 2; + } + + size = Common::AlignUp(size, 0x1000); + return size; +} + +System::System(Core::System& core_, Kernel::KEvent* adsp_rendered_event_) + : core{core_}, adsp{core.AudioCore().GetADSP()}, adsp_rendered_event{adsp_rendered_event_} {} + +Result System::Initialize(const AudioRendererParameterInternal& params, + Kernel::KTransferMemory* transfer_memory, const u64 transfer_memory_size, + const u32 process_handle_, const u64 applet_resource_user_id_, + const s32 session_id_) { + if (!CheckValidRevision(params.revision)) { + return Service::Audio::ERR_INVALID_REVISION; + } + + if (GetWorkBufferSize(params) > transfer_memory_size) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + if (process_handle_ == 0) { + return Service::Audio::ERR_INVALID_PROCESS_HANDLE; + } + + behavior.SetUserLibRevision(params.revision); + + process_handle = process_handle_; + applet_resource_user_id = applet_resource_user_id_; + session_id = session_id_; + + sample_rate = params.sample_rate; + sample_count = params.sample_count; + mix_buffer_count = static_cast(params.mixes); + voice_channels = MaxChannels; + upsampler_count = params.sinks + params.sub_mixes; + memory_pool_count = params.effects + params.voices * MaxWaveBuffers; + render_device = params.rendering_device; + execution_mode = params.execution_mode; + + core.Memory().ZeroBlock(*core.Kernel().CurrentProcess(), transfer_memory->GetSourceAddress(), + transfer_memory_size); + + // Note: We're not actually using the transfer memory because it's a pain to code for. + // Allocate the memory normally instead and hope the game doesn't try to read anything back + workbuffer = std::make_unique(transfer_memory_size); + workbuffer_size = transfer_memory_size; + + PoolMapper pool_mapper(process_handle, false); + pool_mapper.InitializeSystemPool(memory_pool_info, workbuffer.get(), workbuffer_size); + + WorkbufferAllocator allocator({workbuffer.get(), workbuffer_size}, workbuffer_size); + + samples_workbuffer = + allocator.Allocate((voice_channels + mix_buffer_count) * sample_count, 0x10); + if (samples_workbuffer.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + auto upsampler_workbuffer{allocator.Allocate( + (voice_channels + mix_buffer_count) * TargetSampleCount * upsampler_count, 0x10)}; + if (upsampler_workbuffer.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + depop_buffer = + allocator.Allocate(Common::AlignUp(static_cast(mix_buffer_count), 0x40), 0x40); + if (depop_buffer.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + // invalidate samples_workbuffer DSP cache + + auto voice_infos{allocator.Allocate(params.voices, 0x10)}; + for (auto& voice_info : voice_infos) { + std::construct_at(&voice_info); + } + + if (voice_infos.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + auto sorted_voice_infos{allocator.Allocate(params.voices, 0x10)}; + if (sorted_voice_infos.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + std::memset(sorted_voice_infos.data(), 0, sorted_voice_infos.size_bytes()); + + auto voice_channel_resources{allocator.Allocate(params.voices, 0x10)}; + u32 i{0}; + for (auto& voice_channel_resource : voice_channel_resources) { + std::construct_at(&voice_channel_resource, i++); + } + + if (voice_channel_resources.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + auto voice_cpu_states{allocator.Allocate(params.voices, 0x10)}; + if (voice_cpu_states.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + for (auto& voice_state : voice_cpu_states) { + voice_state = {}; + } + + auto mix_infos{allocator.Allocate(params.sub_mixes + 1, 0x10)}; + + if (mix_infos.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + u32 effect_process_order_count{0}; + std::span effect_process_order_buffer{}; + + if (params.effects > 0) { + effect_process_order_count = params.effects * (params.sub_mixes + 1); + effect_process_order_buffer = allocator.Allocate(effect_process_order_count, 0x10); + if (effect_process_order_buffer.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + } + + i = 0; + for (auto& mix_info : mix_infos) { + std::construct_at( + &mix_info, effect_process_order_buffer.subspan(i * params.effects, params.effects), + params.effects, this->behavior); + i++; + } + + auto sorted_mix_infos{allocator.Allocate(params.sub_mixes + 1, 0x10)}; + if (sorted_mix_infos.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + std::memset(sorted_mix_infos.data(), 0, sorted_mix_infos.size_bytes()); + + if (behavior.IsSplitterSupported()) { + u64 node_state_size{NodeStates::GetWorkBufferSize(params.sub_mixes + 1)}; + u64 edge_matrix_size{EdgeMatrix::GetWorkBufferSize(params.sub_mixes + 1)}; + + auto node_states_workbuffer{allocator.Allocate(node_state_size, 1)}; + auto edge_matrix_workbuffer{allocator.Allocate(edge_matrix_size, 1)}; + + if (node_states_workbuffer.empty() || edge_matrix_workbuffer.size() == 0) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + mix_context.Initialize(sorted_mix_infos, mix_infos, params.sub_mixes + 1, + effect_process_order_buffer, effect_process_order_count, + node_states_workbuffer, node_state_size, edge_matrix_workbuffer, + edge_matrix_size); + } else { + mix_context.Initialize(sorted_mix_infos, mix_infos, params.sub_mixes + 1, + effect_process_order_buffer, effect_process_order_count, {}, 0, {}, + 0); + } + + upsampler_manager = allocator.Allocate(1, 0x10).data(); + if (upsampler_manager == nullptr) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + memory_pool_workbuffer = allocator.Allocate(memory_pool_count, 0x10); + for (auto& memory_pool : memory_pool_workbuffer) { + std::construct_at(&memory_pool, MemoryPoolInfo::Location::DSP); + } + + if (memory_pool_workbuffer.empty() && memory_pool_count > 0) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + if (!splitter_context.Initialize(behavior, params, allocator)) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + std::span effect_result_states_cpu{}; + if (behavior.IsEffectInfoVersion2Supported() && params.effects > 0) { + effect_result_states_cpu = allocator.Allocate(params.effects, 0x10); + if (effect_result_states_cpu.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + std::memset(effect_result_states_cpu.data(), 0, effect_result_states_cpu.size_bytes()); + } + + allocator.Align(0x40); + + unk_2B0 = allocator.GetSize() - allocator.GetCurrentOffset(); + unk_2A8 = {&workbuffer[allocator.GetCurrentOffset()], unk_2B0}; + + upsampler_infos = allocator.Allocate(upsampler_count, 0x40); + for (auto& upsampler_info : upsampler_infos) { + std::construct_at(&upsampler_info); + } + + std::construct_at(upsampler_manager, upsampler_count, upsampler_infos, + upsampler_workbuffer); + + if (upsampler_infos.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + auto effect_infos{allocator.Allocate(params.effects, 0x40)}; + for (auto& effect_info : effect_infos) { + std::construct_at(&effect_info); + } + + if (effect_infos.empty() && params.effects > 0) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + std::span effect_result_states_dsp{}; + if (behavior.IsEffectInfoVersion2Supported() && params.effects > 0) { + effect_result_states_dsp = allocator.Allocate(params.effects, 0x40); + if (effect_result_states_dsp.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + std::memset(effect_result_states_dsp.data(), 0, effect_result_states_dsp.size_bytes()); + } + + effect_context.Initialize(effect_infos, params.effects, effect_result_states_cpu, + effect_result_states_dsp, effect_result_states_dsp.size()); + + auto sinks{allocator.Allocate(params.sinks, 0x10)}; + for (auto& sink : sinks) { + std::construct_at(&sink); + } + + if (sinks.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + sink_context.Initialize(sinks, params.sinks); + + auto voice_dsp_states{allocator.Allocate(params.voices, 0x40)}; + if (voice_dsp_states.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + for (auto& voice_state : voice_dsp_states) { + voice_state = {}; + } + + voice_context.Initialize(sorted_voice_infos, voice_infos, voice_channel_resources, + voice_cpu_states, voice_dsp_states, params.voices); + + if (params.perf_frames > 0) { + const auto perf_workbuffer_size{ + PerformanceManager::GetRequiredBufferSizeForPerformanceMetricsPerFrame(behavior, + params) * + (params.perf_frames + 1) + + 0xC}; + performance_workbuffer = allocator.Allocate(perf_workbuffer_size, 0x40); + if (performance_workbuffer.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + std::memset(performance_workbuffer.data(), 0, performance_workbuffer.size_bytes()); + performance_manager.Initialize(performance_workbuffer, performance_workbuffer.size_bytes(), + params, behavior, memory_pool_info); + } + + render_time_limit_percent = 100; + drop_voice = params.voice_drop_enabled && params.execution_mode == ExecutionMode::Auto; + + allocator.Align(0x40); + command_workbuffer_size = allocator.GetRemainingSize(); + command_workbuffer = allocator.Allocate(command_workbuffer_size, 0x40); + if (command_workbuffer.empty()) { + return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE; + } + + command_buffer_size = 0; + reset_command_buffers = true; + + // nn::audio::dsp::FlushDataCache(transferMemory, transferMemorySize); + + if (behavior.IsCommandProcessingTimeEstimatorVersion5Supported()) { + command_processing_time_estimator = + std::make_unique(sample_count, + mix_buffer_count); + } else if (behavior.IsCommandProcessingTimeEstimatorVersion4Supported()) { + command_processing_time_estimator = + std::make_unique(sample_count, + mix_buffer_count); + } else if (behavior.IsCommandProcessingTimeEstimatorVersion3Supported()) { + command_processing_time_estimator = + std::make_unique(sample_count, + mix_buffer_count); + } else if (behavior.IsCommandProcessingTimeEstimatorVersion2Supported()) { + command_processing_time_estimator = + std::make_unique(sample_count, + mix_buffer_count); + } else { + command_processing_time_estimator = + std::make_unique(sample_count, + mix_buffer_count); + } + + initialized = true; + return ResultSuccess; +} + +void System::Finalize() { + if (!initialized) { + return; + } + + if (active) { + Stop(); + } + + applet_resource_user_id = 0; + + PoolMapper pool_mapper(process_handle, false); + pool_mapper.Unmap(memory_pool_info); + + if (process_handle) { + pool_mapper.ClearUseState(memory_pool_workbuffer, memory_pool_count); + for (auto& memory_pool : memory_pool_workbuffer) { + if (memory_pool.IsMapped()) { + pool_mapper.Unmap(memory_pool); + } + } + + // dsp::ProcessCleanup + // close handle + } + initialized = false; +} + +void System::Start() { + std::scoped_lock l{lock}; + frames_elapsed = 0; + state = State::Started; + active = true; +} + +void System::Stop() { + { + std::scoped_lock l{lock}; + state = State::Stopped; + active = false; + } + + if (execution_mode == ExecutionMode::Auto) { + // Should wait for the system to terminate here, but core timing (should have) already + // stopped, so this isn't needed. Find a way to make this definite. + + // terminate_event.Wait(); + } +} + +Result System::Update(std::span input, std::span performance, std::span output) { + std::scoped_lock l{lock}; + + const auto start_time{core.CoreTiming().GetClockTicks()}; + + InfoUpdater info_updater(input, output, process_handle, behavior); + + auto result{info_updater.UpdateBehaviorInfo(behavior)}; + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update BehaviorInfo!"); + return result; + } + + result = info_updater.UpdateMemoryPools(memory_pool_workbuffer, memory_pool_count); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update MemoryPools!"); + return result; + } + + result = info_updater.UpdateVoiceChannelResources(voice_context); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update VoiceChannelResources!"); + return result; + } + + result = info_updater.UpdateVoices(voice_context, memory_pool_workbuffer, memory_pool_count); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update Voices!"); + return result; + } + + result = info_updater.UpdateEffects(effect_context, active, memory_pool_workbuffer, + memory_pool_count); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update Effects!"); + return result; + } + + if (behavior.IsSplitterSupported()) { + result = info_updater.UpdateSplitterInfo(splitter_context); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update SplitterInfo!"); + return result; + } + } + + result = + info_updater.UpdateMixes(mix_context, mix_buffer_count, effect_context, splitter_context); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update Mixes!"); + return result; + } + + result = info_updater.UpdateSinks(sink_context, memory_pool_workbuffer, memory_pool_count); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update Sinks!"); + return result; + } + + PerformanceManager* perf_manager{nullptr}; + if (performance_manager.IsInitialized()) { + perf_manager = &performance_manager; + } + + result = + info_updater.UpdatePerformanceBuffer(performance, performance.size_bytes(), perf_manager); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update PerformanceBuffer!"); + return result; + } + + result = info_updater.UpdateErrorInfo(behavior); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update ErrorInfo!"); + return result; + } + + if (behavior.IsElapsedFrameCountSupported()) { + result = info_updater.UpdateRendererInfo(frames_elapsed); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Failed to update RendererInfo!"); + return result; + } + } + + result = info_updater.CheckConsumedSize(); + if (result.IsError()) { + LOG_ERROR(Service_Audio, "Invalid consume size!"); + return result; + } + + adsp_rendered_event->GetWritableEvent().Clear(); + num_times_updated++; + + const auto end_time{core.CoreTiming().GetClockTicks()}; + ticks_spent_updating += end_time - start_time; + + return ResultSuccess; +} + +u32 System::GetRenderingTimeLimit() const { + return render_time_limit_percent; +} + +void System::SetRenderingTimeLimit(const u32 limit) { + render_time_limit_percent = limit; +} + +u32 System::GetSessionId() const { + return session_id; +} + +u32 System::GetSampleRate() const { + return sample_rate; +} + +u32 System::GetSampleCount() const { + return sample_count; +} + +u32 System::GetMixBufferCount() const { + return mix_buffer_count; +} + +ExecutionMode System::GetExecutionMode() const { + return execution_mode; +} + +u32 System::GetRenderingDevice() const { + return render_device; +} + +bool System::IsActive() const { + return active; +} + +void System::SendCommandToDsp() { + std::scoped_lock l{lock}; + + if (initialized) { + if (active) { + terminate_event.Reset(); + const auto remaining_command_count{adsp.GetRemainCommandCount(session_id)}; + u64 command_size{0}; + + if (remaining_command_count) { + adsp_behind = true; + command_size = command_buffer_size; + } else { + command_size = GenerateCommand(command_workbuffer, command_workbuffer_size); + } + + auto translated_addr{ + memory_pool_info.Translate(CpuAddr(command_workbuffer.data()), command_size)}; + + auto time_limit_percent{70.0f}; + if (behavior.IsAudioRendererProcessingTimeLimit80PercentSupported()) { + time_limit_percent = 80.0f; + } else if (behavior.IsAudioRendererProcessingTimeLimit75PercentSupported()) { + time_limit_percent = 75.0f; + } else { + // result ignored and 70 is used anyway + behavior.IsAudioRendererProcessingTimeLimit70PercentSupported(); + time_limit_percent = 70.0f; + } + + ADSP::CommandBuffer command_buffer{ + .buffer{translated_addr}, + .size{command_size}, + .time_limit{ + static_cast((time_limit_percent / 100) * 2'880'000.0 * + (static_cast(render_time_limit_percent) / 100.0f))}, + .remaining_command_count{remaining_command_count}, + .reset_buffers{reset_command_buffers}, + .applet_resource_user_id{applet_resource_user_id}, + .render_time_taken{adsp.GetRenderTimeTaken(session_id)}, + }; + + adsp.SendCommandBuffer(session_id, command_buffer); + reset_command_buffers = false; + command_buffer_size = command_size; + if (remaining_command_count == 0) { + adsp_rendered_event->GetWritableEvent().Signal(); + } + } else { + adsp.ClearRemainCount(session_id); + terminate_event.Set(); + } + } +} + +u64 System::GenerateCommand(std::span in_command_buffer, + [[maybe_unused]] const u64 command_buffer_size_) { + PoolMapper::ClearUseState(memory_pool_workbuffer, memory_pool_count); + const auto start_time{core.CoreTiming().GetClockTicks()}; + + auto command_list_header{reinterpret_cast(in_command_buffer.data())}; + + command_list_header->buffer_count = static_cast(voice_channels + mix_buffer_count); + command_list_header->sample_count = sample_count; + command_list_header->sample_rate = sample_rate; + command_list_header->samples_buffer = samples_workbuffer; + + const auto performance_initialized{performance_manager.IsInitialized()}; + if (performance_initialized) { + performance_manager.TapFrame(adsp_behind, num_voices_dropped, render_start_tick); + adsp_behind = false; + num_voices_dropped = 0; + render_start_tick = 0; + } + + s8 channel_count{2}; + if (execution_mode == ExecutionMode::Auto) { + const auto& sink{core.AudioCore().GetOutputSink()}; + channel_count = static_cast(sink.GetDeviceChannels()); + } + + AudioRendererSystemContext render_context{ + .session_id{session_id}, + .channels{channel_count}, + .mix_buffer_count{mix_buffer_count}, + .behavior{&behavior}, + .depop_buffer{depop_buffer}, + .upsampler_manager{upsampler_manager}, + .memory_pool_info{&memory_pool_info}, + }; + + CommandBuffer command_buffer{ + .command_list{in_command_buffer}, + .sample_count{sample_count}, + .sample_rate{sample_rate}, + .size{sizeof(CommandListHeader)}, + .count{0}, + .estimated_process_time{0}, + .memory_pool{&memory_pool_info}, + .time_estimator{command_processing_time_estimator.get()}, + .behavior{&behavior}, + }; + + PerformanceManager* perf_manager{nullptr}; + if (performance_initialized) { + perf_manager = &performance_manager; + } + + CommandGenerator command_generator{command_buffer, *command_list_header, render_context, + voice_context, mix_context, effect_context, + sink_context, splitter_context, perf_manager}; + + voice_context.SortInfo(); + + const auto start_estimated_time{command_buffer.estimated_process_time}; + + command_generator.GenerateVoiceCommands(); + command_generator.GenerateSubMixCommands(); + command_generator.GenerateFinalMixCommands(); + command_generator.GenerateSinkCommands(); + + if (drop_voice) { + f32 time_limit_percent{70.0f}; + if (render_context.behavior->IsAudioRendererProcessingTimeLimit80PercentSupported()) { + time_limit_percent = 80.0f; + } else if (render_context.behavior + ->IsAudioRendererProcessingTimeLimit75PercentSupported()) { + time_limit_percent = 75.0f; + } else { + // result is ignored + render_context.behavior->IsAudioRendererProcessingTimeLimit70PercentSupported(); + time_limit_percent = 70.0f; + } + const auto time_limit{static_cast( + static_cast(start_estimated_time - command_buffer.estimated_process_time) + + (((time_limit_percent / 100.0f) * 2'880'000.0) * + (static_cast(render_time_limit_percent) / 100.0f)))}; + num_voices_dropped = DropVoices(command_buffer, start_estimated_time, time_limit); + } + + command_list_header->buffer_size = command_buffer.size; + command_list_header->command_count = command_buffer.count; + + voice_context.UpdateStateByDspShared(); + + if (render_context.behavior->IsEffectInfoVersion2Supported()) { + effect_context.UpdateStateByDspShared(); + } + + const auto end_time{core.CoreTiming().GetClockTicks()}; + total_ticks_elapsed += end_time - start_time; + num_command_lists_generated++; + render_start_tick = adsp.GetRenderingStartTick(session_id); + frames_elapsed++; + + return command_buffer.size; +} + +u32 System::DropVoices(CommandBuffer& command_buffer, const u32 estimated_process_time, + const u32 time_limit) { + u32 i{0}; + auto command_list{command_buffer.command_list.data() + sizeof(CommandListHeader)}; + ICommand* cmd{}; + + for (; i < command_buffer.count; i++) { + cmd = reinterpret_cast(command_list); + if (cmd->type != CommandId::Performance && + cmd->type != CommandId::DataSourcePcmInt16Version1 && + cmd->type != CommandId::DataSourcePcmInt16Version2 && + cmd->type != CommandId::DataSourcePcmFloatVersion1 && + cmd->type != CommandId::DataSourcePcmFloatVersion2 && + cmd->type != CommandId::DataSourceAdpcmVersion1 && + cmd->type != CommandId::DataSourceAdpcmVersion2) { + break; + } + command_list += cmd->size; + } + + if (cmd == nullptr || command_buffer.count == 0 || i >= command_buffer.count) { + return 0; + } + + auto voices_dropped{0}; + while (i < command_buffer.count) { + const auto node_id{cmd->node_id}; + const auto node_id_type{cmd->node_id >> 28}; + const auto node_id_base{cmd->node_id & 0xFFF}; + + if (estimated_process_time <= time_limit) { + break; + } + + if (node_id_type != 1) { + break; + } + + auto& voice_info{voice_context.GetInfo(node_id_base)}; + if (voice_info.priority == HighestVoicePriority) { + break; + } + + voices_dropped++; + voice_info.voice_dropped = true; + + if (i < command_buffer.count) { + while (cmd->node_id == node_id) { + if (cmd->type == CommandId::DepopPrepare) { + cmd->enabled = true; + } else if (cmd->type == CommandId::Performance || !cmd->enabled) { + cmd->enabled = false; + } + i++; + command_list += cmd->size; + cmd = reinterpret_cast(command_list); + } + } + } + return voices_dropped; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/system.h b/src/audio_core/renderer/system.h new file mode 100644 index 0000000000..bcbe65b070 --- /dev/null +++ b/src/audio_core/renderer/system.h @@ -0,0 +1,307 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/command/command_processing_time_estimator.h" +#include "audio_core/renderer/effect/effect_context.h" +#include "audio_core/renderer/memory/memory_pool_info.h" +#include "audio_core/renderer/mix/mix_context.h" +#include "audio_core/renderer/performance/performance_manager.h" +#include "audio_core/renderer/sink/sink_context.h" +#include "audio_core/renderer/splitter/splitter_context.h" +#include "audio_core/renderer/upsampler/upsampler_manager.h" +#include "audio_core/renderer/voice/voice_context.h" +#include "common/thread.h" +#include "core/hle/service/audio/errors.h" + +namespace Core { +namespace Memory { +class Memory; +} +class System; +} // namespace Core + +namespace Kernel { +class KEvent; +class KTransferMemory; +} // namespace Kernel + +namespace AudioCore { +struct AudioRendererParameterInternal; + +namespace AudioRenderer { +class CommandBuffer; +namespace ADSP { +class ADSP; +} + +/** + * Audio Renderer System, the main worker for audio rendering. + */ +class System { + enum class State { + Started = 0, + Stopped = 2, + }; + +public: + explicit System(Core::System& core, Kernel::KEvent* adsp_rendered_event); + + /** + * Calculate the total size required for all audio render workbuffers. + * + * @param params - Input parameters with the numbers of voices/mixes/sinks/etc. + * @return Size (in bytes) required for the audio renderer. + */ + static u64 GetWorkBufferSize(const AudioRendererParameterInternal& params); + + /** + * Initialize the renderer system. + * Allocates workbuffers and initializes everything to a default state, ready to receive a + * RequestUpdate. + * + * @param params - Input parameters to initialize the system with. + * @param transfer_memory - Game-supplied memory for all workbuffers. Unused. + * @param transfer_memory_size - Size of the transfer memory. Unused. + * @param process_handle - Process handle, also used for memory. Unused. + * @param applet_resource_user_id - Applet id for this renderer. Unused. + * @param session_id - Session id of this renderer. + * @return Result code. + */ + Result Initialize(const AudioRendererParameterInternal& params, + Kernel::KTransferMemory* transfer_memory, u64 transfer_memory_size, + u32 process_handle, u64 applet_resource_user_id, s32 session_id); + + /** + * Finalize the system. + */ + void Finalize(); + + /** + * Start the system. + */ + void Start(); + + /** + * Stop the system. + */ + void Stop(); + + /** + * Update the system. + * + * @param input - Inout buffer containing the update data. + * @param performance - Optional buffer for writing back performance metrics. + * @param output - Output information from rendering. + * @return Result code. + */ + Result Update(std::span input, std::span performance, std::span output); + + /** + * Get the time limit (percent) for rendering + * + * @return Time limit as a percent. + */ + u32 GetRenderingTimeLimit() const; + + /** + * Set the time limit (percent) for rendering + * + * @param limit - New time limit. + */ + void SetRenderingTimeLimit(u32 limit); + + /** + * Get the session id for this system. + * + * @return Session id of this system. + */ + u32 GetSessionId() const; + + /** + * Get the sample rate of this system. + * + * @return Sample rate of this system. + */ + u32 GetSampleRate() const; + + /** + * Get the sample count of this system. + * + * @return Sample count of this system. + */ + u32 GetSampleCount() const; + + /** + * Get the number of mix buffers for this system. + * + * @return Number of mix buffers in the system. + */ + u32 GetMixBufferCount() const; + + /** + * Get the execution mode of this system. + * Note: Only Auto is implemented. + * + * @return Execution mode for this system. + */ + ExecutionMode GetExecutionMode() const; + + /** + * Get the rendering deivce for this system. + * This is unused. + * + * @return Rendering device for this system. + */ + u32 GetRenderingDevice() const; + + /** + * Check if this system is currently active. + * + * @return True if active, otherwise false. + */ + bool IsActive() const; + + /** + * Prepare and generate a list of commands for the AudioRenderer based on current state, + * signalling the buffer event when all processed. + */ + void SendCommandToDsp(); + + /** + * Generate a list of commands for the AudioRenderer based on current state. + * + * @param command_buffer - Buffer for commands to be written to. + * @param command_buffer_size - Size of the command_buffer. + * + * @return Number of bytes written. + */ + u64 GenerateCommand(std::span command_buffer, u64 command_buffer_size); + + /** + * Try to drop some voices if the AudioRenderer fell behind. + * + * @param command_buffer - Command buffer to drop voices from. + * @param estimated_process_time - Current estimated processing time of all commands. + * @param time_limit - Time limit for rendering, voices are dropped if estimated + * exceeds this. + * + * @return Number of voices dropped. + */ + u32 DropVoices(CommandBuffer& command_buffer, u32 estimated_process_time, u32 time_limit); + +private: + /// Core system + Core::System& core; + /// Reference to the ADSP for communication + ADSP::ADSP& adsp; + /// Is this system initialized? + bool initialized{}; + /// Is this system currently active? + std::atomic active{}; + /// State of the system + State state{State::Stopped}; + /// Sample rate for the system + u32 sample_rate{}; + /// Sample count of the system + u32 sample_count{}; + /// Number of mix buffers in use by the system + s16 mix_buffer_count{}; + /// Workbuffer for mix buffers, used by the AudioRenderer + std::span samples_workbuffer{}; + /// Depop samples for depopping commands + std::span depop_buffer{}; + /// Number of memory pools in the buffer + u32 memory_pool_count{}; + /// Workbuffer for memory pools + std::span memory_pool_workbuffer{}; + /// System memory pool info + MemoryPoolInfo memory_pool_info{}; + /// Workbuffer that commands will be generated into + std::span command_workbuffer{}; + /// Size of command workbuffer + u64 command_workbuffer_size{}; + /// Numebr of commands in the workbuffer + u64 command_buffer_size{}; + /// Manager for upsamplers + UpsamplerManager* upsampler_manager{}; + /// Upsampler workbuffer + std::span upsampler_infos{}; + /// Number of upsamplers in the workbuffer + u32 upsampler_count{}; + /// Holds and controls all voices + VoiceContext voice_context{}; + /// Holds and controls all mixes + MixContext mix_context{}; + /// Holds and controls all effects + EffectContext effect_context{}; + /// Holds and controls all sinks + SinkContext sink_context{}; + /// Holds and controls all splitters + SplitterContext splitter_context{}; + /// Estimates the time taken for each command + std::unique_ptr command_processing_time_estimator{}; + /// Session id of this system + s32 session_id{}; + /// Number of channels in use by voices + s32 voice_channels{}; + /// Event to be called when the AudioRenderer processes a command list + Kernel::KEvent* adsp_rendered_event{}; + /// Event signalled on system terminate + Common::Event terminate_event{}; + /// Does what locks do + std::mutex lock{}; + /// Handle for the process for this system, unused + u32 process_handle{}; + /// Applet resource id for this system, unused + u64 applet_resource_user_id{}; + /// Controls performance input and output + PerformanceManager performance_manager{}; + /// Workbuffer for performance metrics + std::span performance_workbuffer{}; + /// Main workbuffer, from which all other workbuffers here allocate into + std::unique_ptr workbuffer{}; + /// Size of the main workbuffer + u64 workbuffer_size{}; + /// Unknown buffer/marker + std::span unk_2A8{}; + /// Size of the above unknown buffer/marker + u64 unk_2B0{}; + /// Rendering time limit (percent) + u32 render_time_limit_percent{}; + /// Should any voices be dropped? + bool drop_voice{}; + /// Should the backend stream have its buffers flushed? + bool reset_command_buffers{}; + /// Execution mode of this system, only Auto is supported + ExecutionMode execution_mode{ExecutionMode::Auto}; + /// Render device, unused + u32 render_device{}; + /// Behaviour to check which features are supported by the user revision + BehaviorInfo behavior{}; + /// Total ticks the audio system has been running + u64 total_ticks_elapsed{}; + /// Ticks the system has spent in updates + u64 ticks_spent_updating{}; + /// Number of times a command list was generated + u64 num_command_lists_generated{}; + /// Number of times the system has updated + u64 num_times_updated{}; + /// Number of frames generated, written back to the game + std::atomic frames_elapsed{}; + /// Is the AudioRenderer running too slow? + bool adsp_behind{}; + /// Number of voices dropped + u32 num_voices_dropped{}; + /// Tick that rendering started + u64 render_start_tick{}; +}; + +} // namespace AudioRenderer +} // namespace AudioCore diff --git a/src/audio_core/renderer/system_manager.cpp b/src/audio_core/renderer/system_manager.cpp new file mode 100644 index 0000000000..b326819eda --- /dev/null +++ b/src/audio_core/renderer/system_manager.cpp @@ -0,0 +1,162 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/audio_core.h" +#include "audio_core/renderer/adsp/adsp.h" +#include "audio_core/renderer/system_manager.h" +#include "common/microprofile.h" +#include "common/thread.h" +#include "core/core.h" +#include "core/core_timing.h" + +MICROPROFILE_DEFINE(Audio_RenderSystemManager, "Audio", "Render System Manager", + MP_RGB(60, 19, 97)); + +namespace AudioCore::AudioRenderer { +constexpr std::chrono::nanoseconds BaseRenderTime{5'000'000UL}; +constexpr std::chrono::nanoseconds RenderTimeOffset{400'000UL}; + +SystemManager::SystemManager(Core::System& core_) + : core{core_}, adsp{core.AudioCore().GetADSP()}, mailbox{adsp.GetRenderMailbox()}, + thread_event{Core::Timing::CreateEvent( + "AudioRendererSystemManager", [this](std::uintptr_t, s64 time, std::chrono::nanoseconds) { + return ThreadFunc2(time); + })} { + core.CoreTiming().RegisterPauseCallback([this](bool paused) { PauseCallback(paused); }); +} + +SystemManager::~SystemManager() { + Stop(); +} + +bool SystemManager::InitializeUnsafe() { + if (!active) { + if (adsp.Start()) { + active = true; + thread = std::jthread([this](std::stop_token stop_token) { ThreadFunc(); }); + core.CoreTiming().ScheduleLoopingEvent(std::chrono::nanoseconds(0), + BaseRenderTime - RenderTimeOffset, thread_event); + } + } + + return adsp.GetState() == ADSP::State::Started; +} + +void SystemManager::Stop() { + if (!active) { + return; + } + core.CoreTiming().UnscheduleEvent(thread_event, {}); + active = false; + update.store(true); + update.notify_all(); + thread.join(); + adsp.Stop(); +} + +bool SystemManager::Add(System& system_) { + std::scoped_lock l2{mutex2}; + + if (systems.size() + 1 > MaxRendererSessions) { + LOG_ERROR(Service_Audio, "Maximum AudioRenderer Systems active, cannot add more!"); + return false; + } + + { + std::scoped_lock l{mutex1}; + if (systems.empty()) { + if (!InitializeUnsafe()) { + LOG_ERROR(Service_Audio, "Failed to start the AudioRenderer SystemManager"); + return false; + } + } + } + + systems.push_back(&system_); + return true; +} + +bool SystemManager::Remove(System& system_) { + std::scoped_lock l2{mutex2}; + + { + std::scoped_lock l{mutex1}; + if (systems.remove(&system_) == 0) { + LOG_ERROR(Service_Audio, + "Failed to remove a render system, it was not found in the list!"); + return false; + } + } + + if (systems.empty()) { + Stop(); + } + return true; +} + +void SystemManager::ThreadFunc() { + constexpr char name[]{"yuzu:AudioRenderSystemManager"}; + MicroProfileOnThreadCreate(name); + Common::SetCurrentThreadName(name); + Common::SetCurrentThreadPriority(Common::ThreadPriority::High); + while (active) { + { + std::scoped_lock l{mutex1}; + + MICROPROFILE_SCOPE(Audio_RenderSystemManager); + + for (auto system : systems) { + system->SendCommandToDsp(); + } + } + + adsp.Signal(); + adsp.Wait(); + + update.wait(false); + update.store(false); + } +} + +std::optional SystemManager::ThreadFunc2(s64 time) { + std::optional new_schedule_time{std::nullopt}; + const auto queue_size{core.AudioCore().GetStreamQueue()}; + switch (state) { + case StreamState::Filling: + if (queue_size >= 5) { + new_schedule_time = BaseRenderTime; + state = StreamState::Steady; + } + break; + case StreamState::Steady: + if (queue_size <= 2) { + new_schedule_time = BaseRenderTime - RenderTimeOffset; + state = StreamState::Filling; + } else if (queue_size > 5) { + new_schedule_time = BaseRenderTime + RenderTimeOffset; + state = StreamState::Draining; + } + break; + case StreamState::Draining: + if (queue_size <= 5) { + new_schedule_time = BaseRenderTime; + state = StreamState::Steady; + } + break; + } + + update.store(true); + update.notify_all(); + return new_schedule_time; +} + +void SystemManager::PauseCallback(bool paused) { + if (paused && core.IsPoweredOn() && core.IsShuttingDown()) { + update.store(true); + update.notify_all(); + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/system_manager.h b/src/audio_core/renderer/system_manager.h new file mode 100644 index 0000000000..1291e9e0ef --- /dev/null +++ b/src/audio_core/renderer/system_manager.h @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#include "audio_core/renderer/system.h" + +namespace Core { +namespace Timing { +struct EventType; +} +class System; +} // namespace Core + +namespace AudioCore::AudioRenderer { +namespace ADSP { +class ADSP; +class AudioRenderer_Mailbox; +} // namespace ADSP + +/** + * Manages all audio renderers, responsible for triggering command list generation and signalling + * the ADSP. + */ +class SystemManager { +public: + explicit SystemManager(Core::System& core); + ~SystemManager(); + + /** + * Initialize the system manager, called when any system is registered. + * + * @return True if sucessfully initialized, otherwise false. + */ + bool InitializeUnsafe(); + + /** + * Stop the system manager. + */ + void Stop(); + + /** + * Add an audio render system to the manager. + * The manager does not own the system, so do not free it without calling Remove. + * + * @param system - The system to add. + * @return True if succesfully added, otherwise false. + */ + bool Add(System& system); + + /** + * Remove an audio render system from the manager. + * + * @param system - The system to remove. + * @return True if succesfully removed, otherwise false. + */ + bool Remove(System& system); + +private: + /** + * Main thread responsible for command generation. + */ + void ThreadFunc(); + + /** + * Signalling core timing thread to run ThreadFunc. + */ + std::optional ThreadFunc2(s64 time); + + /** + * Callback from core timing when pausing, used to detect shutdowns and stop ThreadFunc. + * + * @param paused - Are we pausing or resuming? + */ + void PauseCallback(bool paused); + + enum class StreamState { + Filling, + Steady, + Draining, + }; + + /// Core system + Core::System& core; + /// List of pointers to managed systems + std::list systems{}; + /// Main worker thread for generating command lists + std::jthread thread; + /// Mutex for the systems + std::mutex mutex1{}; + /// Mutex for adding/removing systems + std::mutex mutex2{}; + /// Is the system manager thread active? + std::atomic active{}; + /// Reference to the ADSP for communication + ADSP::ADSP& adsp; + /// AudioRenderer mailbox for communication + ADSP::AudioRenderer_Mailbox* mailbox{}; + /// Core timing event to signal main thread + std::shared_ptr thread_event; + /// Atomic for main thread to wait on + std::atomic update{}; + /// Current state of the streams + StreamState state{StreamState::Filling}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/upsampler/upsampler_info.cpp b/src/audio_core/renderer/upsampler/upsampler_info.cpp new file mode 100644 index 0000000000..e3d2f7db06 --- /dev/null +++ b/src/audio_core/renderer/upsampler/upsampler_info.cpp @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/upsampler/upsampler_info.h" + +namespace AudioCore::AudioRenderer {} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/upsampler/upsampler_info.h b/src/audio_core/renderer/upsampler/upsampler_info.h new file mode 100644 index 0000000000..a43c15af31 --- /dev/null +++ b/src/audio_core/renderer/upsampler/upsampler_info.h @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "audio_core/renderer/upsampler/upsampler_state.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +class UpsamplerManager; + +/** + * Manages information needed to upsample a mix buffer. + */ +struct UpsamplerInfo { + /// States used by the AudioRenderer across calls. + std::array states{}; + /// Pointer to the manager + UpsamplerManager* manager{}; + /// Pointer to the samples to be upsampled + CpuAddr samples_pos{}; + /// Target number of samples to upsample to + u32 sample_count{}; + /// Number of channels to upsample + u32 input_count{}; + /// Is this upsampler enabled? + bool enabled{}; + /// Mix buffer indexes to be upsampled + std::array inputs{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/upsampler/upsampler_manager.cpp b/src/audio_core/renderer/upsampler/upsampler_manager.cpp new file mode 100644 index 0000000000..4c76a50660 --- /dev/null +++ b/src/audio_core/renderer/upsampler/upsampler_manager.cpp @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/upsampler/upsampler_manager.h" + +namespace AudioCore::AudioRenderer { + +UpsamplerManager::UpsamplerManager(const u32 count_, std::span infos_, + std::span workbuffer_) + : count{count_}, upsampler_infos{infos_}, workbuffer{workbuffer_} {} + +UpsamplerInfo* UpsamplerManager::Allocate() { + std::scoped_lock l{lock}; + + if (count == 0) { + return nullptr; + } + + u32 free_index{0}; + for (auto& upsampler : upsampler_infos) { + if (!upsampler.enabled) { + break; + } + free_index++; + } + + if (free_index >= count) { + return nullptr; + } + + auto& upsampler{upsampler_infos[free_index]}; + upsampler.manager = this; + upsampler.sample_count = TargetSampleCount; + upsampler.samples_pos = CpuAddr(&workbuffer[upsampler.sample_count * MaxChannels]); + upsampler.enabled = true; + return &upsampler; +} + +void UpsamplerManager::Free(UpsamplerInfo* info) { + std::scoped_lock l{lock}; + info->enabled = false; +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/upsampler/upsampler_manager.h b/src/audio_core/renderer/upsampler/upsampler_manager.h new file mode 100644 index 0000000000..70cd42b08d --- /dev/null +++ b/src/audio_core/renderer/upsampler/upsampler_manager.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/renderer/upsampler/upsampler_info.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Manages and has utility functions for upsampler infos. + */ +class UpsamplerManager { +public: + UpsamplerManager(u32 count, std::span infos, std::span workbuffer); + + /** + * Allocate a new UpsamplerInfo. + * + * @return The allocated upsampler, may be nullptr if alloc failed. + */ + UpsamplerInfo* Allocate(); + + /** + * Free the given upsampler. + * + * @param The upsampler to be freed. + */ + void Free(UpsamplerInfo* info); + +private: + /// Maximum number of upsamplers in the buffer + const u32 count; + /// Upsamplers buffer + std::span upsampler_infos; + /// Workbuffer for upsampling samples + std::span workbuffer; + /// Lock for allocate/free + std::mutex lock{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/upsampler/upsampler_state.h b/src/audio_core/renderer/upsampler/upsampler_state.h new file mode 100644 index 0000000000..28cebe2004 --- /dev/null +++ b/src/audio_core/renderer/upsampler/upsampler_state.h @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { +/** + * Upsampling state used by the AudioRenderer across calls. + */ +struct UpsamplerState { + static constexpr u16 HistorySize = 20; + + /// Source data to target data ratio. E.g 48'000/32'000 = 1.5 + Common::FixedPoint<16, 16> ratio; + /// Sample history + std::array, HistorySize> history; + /// Size of the sinc coefficient window + u16 window_size; + /// Read index for the history + u16 history_output_index; + /// Write index for the history + u16 history_input_index; + /// Start offset within the history, fixed to 0 + u16 history_start_index; + /// Ebd offset within the history, fixed to HistorySize + u16 history_end_index; + /// Is this state initialized? + bool initialized; + /// Index of the current sample. + /// E.g 16K -> 48K has a ratio of 3, so this will be 0-2. + /// See the Upsample command in the AudioRenderer for more information. + u8 sample_index; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/voice/voice_channel_resource.h b/src/audio_core/renderer/voice/voice_channel_resource.h new file mode 100644 index 0000000000..26ab4ccceb --- /dev/null +++ b/src/audio_core/renderer/voice/voice_channel_resource.h @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Represents one channel for mixing a voice. + */ +class VoiceChannelResource { +public: + struct InParameter { + /* 0x00 */ u32 id; + /* 0x04 */ std::array mix_volumes; + /* 0x64 */ bool in_use; + /* 0x65 */ char unk65[0xB]; + }; + static_assert(sizeof(InParameter) == 0x70, + "VoiceChannelResource::InParameter has the wrong size!"); + + explicit VoiceChannelResource(u32 id_) : id{id_} {} + + /// Current volume for each mix buffer + std::array mix_volumes{}; + /// Previous volume for each mix buffer + std::array prev_mix_volumes{}; + /// Id of this resource + const u32 id; + /// Is this resource in use? + bool in_use{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/voice/voice_context.cpp b/src/audio_core/renderer/voice/voice_context.cpp new file mode 100644 index 0000000000..eafb51b011 --- /dev/null +++ b/src/audio_core/renderer/voice/voice_context.cpp @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "audio_core/renderer/voice/voice_context.h" + +namespace AudioCore::AudioRenderer { + +VoiceState& VoiceContext::GetDspSharedState(const u32 index) { + if (index >= dsp_states.size()) { + LOG_ERROR(Service_Audio, "Invalid voice dsp state index {:04X}", index); + } + return dsp_states[index]; +} + +VoiceChannelResource& VoiceContext::GetChannelResource(const u32 index) { + if (index >= channel_resources.size()) { + LOG_ERROR(Service_Audio, "Invalid voice channel resource index {:04X}", index); + } + return channel_resources[index]; +} + +void VoiceContext::Initialize(std::span sorted_voice_infos_, + std::span voice_infos_, + std::span voice_channel_resources_, + std::span cpu_states_, std::span dsp_states_, + const u32 voice_count_) { + sorted_voice_info = sorted_voice_infos_; + voices = voice_infos_; + channel_resources = voice_channel_resources_; + cpu_states = cpu_states_; + dsp_states = dsp_states_; + voice_count = voice_count_; + active_count = 0; +} + +VoiceInfo* VoiceContext::GetSortedInfo(const u32 index) { + if (index >= sorted_voice_info.size()) { + LOG_ERROR(Service_Audio, "Invalid voice sorted info index {:04X}", index); + } + return sorted_voice_info[index]; +} + +VoiceInfo& VoiceContext::GetInfo(const u32 index) { + if (index >= voices.size()) { + LOG_ERROR(Service_Audio, "Invalid voice info index {:04X}", index); + } + return voices[index]; +} + +VoiceState& VoiceContext::GetState(const u32 index) { + if (index >= cpu_states.size()) { + LOG_ERROR(Service_Audio, "Invalid voice cpu state index {:04X}", index); + } + return cpu_states[index]; +} + +u32 VoiceContext::GetCount() const { + return voice_count; +} + +u32 VoiceContext::GetActiveCount() const { + return active_count; +} + +void VoiceContext::SetActiveCount(const u32 active_count_) { + active_count = active_count_; +} + +void VoiceContext::SortInfo() { + for (u32 i = 0; i < voice_count; i++) { + sorted_voice_info[i] = &voices[i]; + } + + std::ranges::sort(sorted_voice_info, [](const VoiceInfo* a, const VoiceInfo* b) { + return a->priority != b->priority ? a->priority < b->priority + : a->sort_order < b->sort_order; + }); +} + +void VoiceContext::UpdateStateByDspShared() { + std::memcpy(cpu_states.data(), dsp_states.data(), voice_count * sizeof(VoiceState)); +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/voice/voice_context.h b/src/audio_core/renderer/voice/voice_context.h new file mode 100644 index 0000000000..43b6771547 --- /dev/null +++ b/src/audio_core/renderer/voice/voice_context.h @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/renderer/voice/voice_channel_resource.h" +#include "audio_core/renderer/voice/voice_info.h" +#include "audio_core/renderer/voice/voice_state.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +/** + * Contains all voices, with utility functions for managing them. + */ +class VoiceContext { +public: + /** + * Get the AudioRenderer state for a given index + * + * @param index - State index to get. + * @return The requested voice state. + */ + VoiceState& GetDspSharedState(u32 index); + + /** + * Get the channel resource for a given index + * + * @param index - Resource index to get. + * @return The requested voice resource. + */ + VoiceChannelResource& GetChannelResource(u32 index); + + /** + * Initialize the voice context. + * + * @param sorted_voice_infos - Workbuffer for the sorted voices. + * @param voice_infos - Workbuffer for the voices. + * @param voice_channel_resources - Workbuffer for the voice channel resources. + * @param cpu_states - Workbuffer for the host-side voice states. + * @param dsp_states - Workbuffer for the AudioRenderer-side voice states. + * @param voice_count - The number of voices in each workbuffer. + */ + void Initialize(std::span sorted_voice_infos, std::span voice_infos, + std::span voice_channel_resources, + std::span cpu_states, std::span dsp_states, + u32 voice_count); + + /** + * Get a sorted voice with the given index. + * + * @param index - The sorted voice index to get. + * @return The sorted voice. + */ + VoiceInfo* GetSortedInfo(u32 index); + + /** + * Get a voice with the given index. + * + * @param index - The voice index to get. + * @return The voice. + */ + VoiceInfo& GetInfo(u32 index); + + /** + * Get a host voice state with the given index. + * + * @param index - The host voice state index to get. + * @return The voice state. + */ + VoiceState& GetState(u32 index); + + /** + * Get the maximum number of voices. + * Not all voices in the buffers may be in use, see GetActiveCount. + * + * @return The maximum number of voices. + */ + u32 GetCount() const; + + /** + * Get the number of active voices. + * Can be less than or equal to the maximum number of voices. + * + * @return The number of active voices. + */ + u32 GetActiveCount() const; + + /** + * Set the number of active voices. + * Can be less than or equal to the maximum number of voices. + * + * @param active_count - The new number of active voices. + */ + void SetActiveCount(u32 active_count); + + /** + * Sort all voices. Results are available via GetSortedInfo. + * Voices are sorted descendingly, according to priority, and then sort order. + */ + void SortInfo(); + + /** + * Update all voice states, copying AudioRenderer-side states to host-side states. + */ + void UpdateStateByDspShared(); + +private: + /// Sorted voices + std::span sorted_voice_info{}; + /// Voices + std::span voices{}; + /// Channel resources + std::span channel_resources{}; + /// Host-side voice states + std::span cpu_states{}; + /// AudioRenderer-side voice states + std::span dsp_states{}; + /// Maximum number of voices + u32 voice_count{}; + /// Number of active voices + u32 active_count{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/voice/voice_info.cpp b/src/audio_core/renderer/voice/voice_info.cpp new file mode 100644 index 0000000000..1849eeb579 --- /dev/null +++ b/src/audio_core/renderer/voice/voice_info.cpp @@ -0,0 +1,408 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "audio_core/renderer/memory/pool_mapper.h" +#include "audio_core/renderer/voice/voice_context.h" +#include "audio_core/renderer/voice/voice_info.h" +#include "audio_core/renderer/voice/voice_state.h" + +namespace AudioCore::AudioRenderer { + +VoiceInfo::VoiceInfo() { + Initialize(); +} + +void VoiceInfo::Initialize() { + in_use = false; + is_new = false; + id = 0; + node_id = 0; + current_play_state = ServerPlayState::Stopped; + src_quality = SrcQuality::Medium; + priority = LowestVoicePriority; + sample_format = SampleFormat::Invalid; + sample_rate = 0; + channel_count = 0; + wave_buffer_count = 0; + wave_buffer_index = 0; + pitch = 0.0f; + volume = 0.0f; + prev_volume = 0.0f; + mix_id = UnusedMixId; + splitter_id = UnusedSplitterId; + biquads = {}; + biquad_initialized = {}; + voice_dropped = false; + data_unmapped = false; + buffer_unmapped = false; + flush_buffer_count = 0; + + data_address.Setup(0, 0); + for (auto& wavebuffer : wavebuffers) { + wavebuffer.Initialize(); + } +} + +bool VoiceInfo::ShouldUpdateParameters(const InParameter& params) const { + return data_address.GetCpuAddr() != params.src_data_address || + data_address.GetSize() != params.src_data_size || data_unmapped; +} + +void VoiceInfo::UpdateParameters(BehaviorInfo::ErrorInfo& error_info, const InParameter& params, + const PoolMapper& pool_mapper, const BehaviorInfo& behavior) { + in_use = params.in_use; + id = params.id; + node_id = params.node_id; + UpdatePlayState(params.play_state); + UpdateSrcQuality(params.src_quality); + priority = params.priority; + sort_order = params.sort_order; + sample_rate = params.sample_rate; + sample_format = params.sample_format; + channel_count = static_cast(params.channel_count); + pitch = params.pitch; + volume = params.volume; + biquads = params.biquads; + wave_buffer_count = params.wave_buffer_count; + wave_buffer_index = params.wave_buffer_index; + + if (behavior.IsFlushVoiceWaveBuffersSupported()) { + flush_buffer_count += params.flush_buffer_count; + } + + mix_id = params.mix_id; + + if (behavior.IsSplitterSupported()) { + splitter_id = params.splitter_id; + } else { + splitter_id = UnusedSplitterId; + } + + channel_resource_ids = params.channel_resource_ids; + + flags &= u16(~0b11); + if (behavior.IsVoicePlayedSampleCountResetAtLoopPointSupported()) { + flags |= u16(params.flags.IsVoicePlayedSampleCountResetAtLoopPointSupported); + } + + if (behavior.IsVoicePitchAndSrcSkippedSupported()) { + flags |= u16(params.flags.IsVoicePitchAndSrcSkippedSupported); + } + + if (params.clear_voice_drop) { + voice_dropped = false; + } + + if (ShouldUpdateParameters(params)) { + data_unmapped = !pool_mapper.TryAttachBuffer(error_info, data_address, + params.src_data_address, params.src_data_size); + } else { + error_info.error_code = ResultSuccess; + error_info.address = CpuAddr(0); + } +} + +void VoiceInfo::UpdatePlayState(const PlayState state) { + last_play_state = current_play_state; + + switch (state) { + case PlayState::Started: + current_play_state = ServerPlayState::Started; + break; + case PlayState::Stopped: + if (current_play_state != ServerPlayState::Stopped) { + current_play_state = ServerPlayState::RequestStop; + } + break; + case PlayState::Paused: + current_play_state = ServerPlayState::Paused; + break; + default: + LOG_ERROR(Service_Audio, "Invalid input play state {}", static_cast(state)); + break; + } +} + +void VoiceInfo::UpdateSrcQuality(const SrcQuality quality) { + switch (quality) { + case SrcQuality::Medium: + src_quality = quality; + break; + case SrcQuality::High: + src_quality = quality; + break; + case SrcQuality::Low: + src_quality = quality; + break; + default: + LOG_ERROR(Service_Audio, "Invalid input src quality {}", static_cast(quality)); + break; + } +} + +void VoiceInfo::UpdateWaveBuffers(std::span> error_infos, + [[maybe_unused]] u32 error_count, const InParameter& params, + std::span voice_states, + const PoolMapper& pool_mapper, const BehaviorInfo& behavior) { + if (params.is_new) { + for (size_t i = 0; i < wavebuffers.size(); i++) { + wavebuffers[i].Initialize(); + } + + for (s8 channel = 0; channel < static_cast(params.channel_count); channel++) { + voice_states[channel]->wave_buffer_valid.fill(false); + } + } + + for (u32 i = 0; i < MaxWaveBuffers; i++) { + UpdateWaveBuffer(error_infos[i], wavebuffers[i], params.wave_buffer_internal[i], + params.sample_format, voice_states[0]->wave_buffer_valid[i], pool_mapper, + behavior); + } +} + +void VoiceInfo::UpdateWaveBuffer(std::span error_info, + WaveBuffer& wave_buffer, + const WaveBufferInternal& wave_buffer_internal, + const SampleFormat sample_format_, const bool valid, + const PoolMapper& pool_mapper, const BehaviorInfo& behavior) { + if (!valid && wave_buffer.sent_to_DSP && wave_buffer.buffer_address.GetCpuAddr() != 0) { + pool_mapper.ForceUnmapPointer(wave_buffer.buffer_address); + wave_buffer.buffer_address.Setup(0, 0); + } + + if (!ShouldUpdateWaveBuffer(wave_buffer_internal)) { + return; + } + + switch (sample_format_) { + case SampleFormat::PcmInt16: { + constexpr auto byte_size{GetSampleFormatByteSize(SampleFormat::PcmInt16)}; + if (wave_buffer_internal.start_offset * byte_size > wave_buffer_internal.size || + wave_buffer_internal.end_offset * byte_size > wave_buffer_internal.size) { + LOG_ERROR(Service_Audio, "Invalid PCM16 start/end wavebuffer sizes!"); + error_info[0].error_code = Service::Audio::ERR_INVALID_UPDATE_DATA; + error_info[0].address = wave_buffer_internal.address; + return; + } + } break; + + case SampleFormat::PcmFloat: { + constexpr auto byte_size{GetSampleFormatByteSize(SampleFormat::PcmFloat)}; + if (wave_buffer_internal.start_offset * byte_size > wave_buffer_internal.size || + wave_buffer_internal.end_offset * byte_size > wave_buffer_internal.size) { + LOG_ERROR(Service_Audio, "Invalid PCMFloat start/end wavebuffer sizes!"); + error_info[0].error_code = Service::Audio::ERR_INVALID_UPDATE_DATA; + error_info[0].address = wave_buffer_internal.address; + return; + } + } break; + + case SampleFormat::Adpcm: { + const auto start_frame{wave_buffer_internal.start_offset / 14}; + auto start_extra{wave_buffer_internal.start_offset % 14 == 0 + ? 0 + : (wave_buffer_internal.start_offset % 14) / 2 + 1 + + ((wave_buffer_internal.start_offset % 14) % 2)}; + const auto start{start_frame * 8 + start_extra}; + + const auto end_frame{wave_buffer_internal.end_offset / 14}; + const auto end_extra{wave_buffer_internal.end_offset % 14 == 0 + ? 0 + : (wave_buffer_internal.end_offset % 14) / 2 + 1 + + ((wave_buffer_internal.end_offset % 14) % 2)}; + const auto end{end_frame * 8 + end_extra}; + + if (start > static_cast(wave_buffer_internal.size) || + end > static_cast(wave_buffer_internal.size)) { + LOG_ERROR(Service_Audio, "Invalid ADPCM start/end wavebuffer sizes!"); + error_info[0].error_code = Service::Audio::ERR_INVALID_UPDATE_DATA; + error_info[0].address = wave_buffer_internal.address; + return; + } + } break; + + default: + break; + } + + if (wave_buffer_internal.start_offset < 0 || wave_buffer_internal.end_offset < 0) { + LOG_ERROR(Service_Audio, "Invalid input start/end wavebuffer sizes!"); + error_info[0].error_code = Service::Audio::ERR_INVALID_UPDATE_DATA; + error_info[0].address = wave_buffer_internal.address; + return; + } + + wave_buffer.start_offset = wave_buffer_internal.start_offset; + wave_buffer.end_offset = wave_buffer_internal.end_offset; + wave_buffer.loop = wave_buffer_internal.loop; + wave_buffer.stream_ended = wave_buffer_internal.stream_ended; + wave_buffer.sent_to_DSP = false; + wave_buffer.loop_start_offset = wave_buffer_internal.loop_start; + wave_buffer.loop_end_offset = wave_buffer_internal.loop_end; + wave_buffer.loop_count = wave_buffer_internal.loop_count; + + buffer_unmapped = + !pool_mapper.TryAttachBuffer(error_info[0], wave_buffer.buffer_address, + wave_buffer_internal.address, wave_buffer_internal.size); + + if (sample_format_ == SampleFormat::Adpcm && behavior.IsAdpcmLoopContextBugFixed() && + wave_buffer_internal.context_address != 0) { + buffer_unmapped = !pool_mapper.TryAttachBuffer(error_info[1], wave_buffer.context_address, + wave_buffer_internal.context_address, + wave_buffer_internal.context_size) || + data_unmapped; + } else { + wave_buffer.context_address.Setup(0, 0); + } +} + +bool VoiceInfo::ShouldUpdateWaveBuffer(const WaveBufferInternal& wave_buffer_internal) const { + return !wave_buffer_internal.sent_to_DSP || buffer_unmapped; +} + +void VoiceInfo::WriteOutStatus(OutStatus& out_status, const InParameter& params, + std::span voice_states) { + if (params.is_new) { + is_new = true; + } + + if (params.is_new || is_new) { + out_status.played_sample_count = 0; + out_status.wave_buffers_consumed = 0; + out_status.voice_dropped = false; + } else { + out_status.played_sample_count = voice_states[0]->played_sample_count; + out_status.wave_buffers_consumed = voice_states[0]->wave_buffers_consumed; + out_status.voice_dropped = voice_dropped; + } +} + +bool VoiceInfo::ShouldSkip() const { + return !in_use || wave_buffer_count == 0 || data_unmapped || buffer_unmapped || voice_dropped; +} + +bool VoiceInfo::HasAnyConnection() const { + return mix_id != UnusedMixId || splitter_id != UnusedSplitterId; +} + +void VoiceInfo::FlushWaveBuffers(const u32 flush_count, std::span voice_states, + const s8 channel_count_) { + auto wave_index{wave_buffer_index}; + + for (size_t i = 0; i < flush_count; i++) { + wavebuffers[wave_index].sent_to_DSP = true; + + for (s8 j = 0; j < channel_count_; j++) { + auto voice_state{voice_states[j]}; + if (voice_state->wave_buffer_index == wave_index) { + voice_state->wave_buffer_index = + (voice_state->wave_buffer_index + 1) % MaxWaveBuffers; + voice_state->wave_buffers_consumed++; + } + voice_state->wave_buffer_valid[wave_index] = false; + } + + wave_index = (wave_index + 1) % MaxWaveBuffers; + } +} + +bool VoiceInfo::UpdateParametersForCommandGeneration(std::span voice_states) { + if (flush_buffer_count > 0) { + FlushWaveBuffers(flush_buffer_count, voice_states, channel_count); + flush_buffer_count = 0; + } + + switch (current_play_state) { + case ServerPlayState::Started: + for (u32 i = 0; i < MaxWaveBuffers; i++) { + if (!wavebuffers[i].sent_to_DSP) { + for (s8 channel = 0; channel < channel_count; channel++) { + voice_states[channel]->wave_buffer_valid[i] = true; + } + wavebuffers[i].sent_to_DSP = true; + } + } + + was_playing = false; + + for (u32 i = 0; i < MaxWaveBuffers; i++) { + if (voice_states[0]->wave_buffer_valid[i]) { + return true; + } + } + break; + + case ServerPlayState::Stopped: + case ServerPlayState::Paused: + for (auto& wavebuffer : wavebuffers) { + if (!wavebuffer.sent_to_DSP) { + wavebuffer.buffer_address.GetReference(true); + wavebuffer.context_address.GetReference(true); + } + } + + if (sample_format == SampleFormat::Adpcm && data_address.GetCpuAddr() != 0) { + data_address.GetReference(true); + } + + was_playing = last_play_state == ServerPlayState::Started; + break; + + case ServerPlayState::RequestStop: + for (u32 i = 0; i < MaxWaveBuffers; i++) { + wavebuffers[i].sent_to_DSP = true; + + for (s8 channel = 0; channel < channel_count; channel++) { + if (voice_states[channel]->wave_buffer_valid[i]) { + voice_states[channel]->wave_buffer_index = + (voice_states[channel]->wave_buffer_index + 1) % MaxWaveBuffers; + voice_states[channel]->wave_buffers_consumed++; + } + voice_states[channel]->wave_buffer_valid[i] = false; + } + } + + for (s8 channel = 0; channel < channel_count; channel++) { + voice_states[channel]->offset = 0; + voice_states[channel]->played_sample_count = 0; + voice_states[channel]->adpcm_context = {}; + voice_states[channel]->sample_history.fill(0); + voice_states[channel]->fraction = 0; + } + + current_play_state = ServerPlayState::Stopped; + was_playing = last_play_state == ServerPlayState::Started; + break; + } + + return was_playing; +} + +bool VoiceInfo::UpdateForCommandGeneration(VoiceContext& voice_context) { + std::array voice_states{}; + + if (is_new) { + ResetResources(voice_context); + prev_volume = volume; + is_new = false; + } + + for (s8 channel = 0; channel < channel_count; channel++) { + voice_states[channel] = &voice_context.GetDspSharedState(channel_resource_ids[channel]); + } + + return UpdateParametersForCommandGeneration(voice_states); +} + +void VoiceInfo::ResetResources(VoiceContext& voice_context) const { + for (s8 channel = 0; channel < channel_count; channel++) { + auto& state{voice_context.GetDspSharedState(channel_resource_ids[channel])}; + state = {}; + + auto& channel_resource{voice_context.GetChannelResource(channel_resource_ids[channel])}; + channel_resource.prev_mix_volumes = channel_resource.mix_volumes; + } +} + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/voice/voice_info.h b/src/audio_core/renderer/voice/voice_info.h new file mode 100644 index 0000000000..896723e0c7 --- /dev/null +++ b/src/audio_core/renderer/voice/voice_info.h @@ -0,0 +1,378 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/common/common.h" +#include "audio_core/common/wave_buffer.h" +#include "audio_core/renderer/behavior/behavior_info.h" +#include "audio_core/renderer/memory/address_info.h" +#include "common/common_types.h" + +namespace AudioCore::AudioRenderer { +class PoolMapper; +class VoiceContext; +struct VoiceState; + +/** + * Represents one voice. Voices are essentially noises, and they can be further mixed and have + * effects applied to them, but voices are the basis of all sounds. + */ +class VoiceInfo { +public: + enum class ServerPlayState { + Started, + Stopped, + RequestStop, + Paused, + }; + + struct Flags { + u8 IsVoicePlayedSampleCountResetAtLoopPointSupported : 1; + u8 IsVoicePitchAndSrcSkippedSupported : 1; + }; + + /** + * A wavebuffer contains information on the data source buffers. + */ + struct WaveBuffer { + void Copy(WaveBufferVersion1& other) { + other.buffer = buffer_address.GetReference(true); + other.buffer_size = buffer_address.GetSize(); + other.start_offset = start_offset; + other.end_offset = end_offset; + other.loop = loop; + other.stream_ended = stream_ended; + + if (context_address.GetCpuAddr()) { + other.context = context_address.GetReference(true); + other.context_size = context_address.GetSize(); + } else { + other.context = CpuAddr(0); + other.context_size = 0; + } + } + + void Copy(WaveBufferVersion2& other) { + other.buffer = buffer_address.GetReference(true); + other.buffer_size = buffer_address.GetSize(); + other.start_offset = start_offset; + other.end_offset = end_offset; + other.loop_start_offset = loop_start_offset; + other.loop_end_offset = loop_end_offset; + other.loop = loop; + other.loop_count = loop_count; + other.stream_ended = stream_ended; + + if (context_address.GetCpuAddr()) { + other.context = context_address.GetReference(true); + other.context_size = context_address.GetSize(); + } else { + other.context = CpuAddr(0); + other.context_size = 0; + } + } + + void Initialize() { + buffer_address.Setup(0, 0); + context_address.Setup(0, 0); + start_offset = 0; + end_offset = 0; + loop = false; + stream_ended = false; + sent_to_DSP = true; + loop_start_offset = 0; + loop_end_offset = 0; + loop_count = 0; + } + /// Game memory address of the wavebuffer data + AddressInfo buffer_address{0, 0}; + /// Context for decoding, used for ADPCM + AddressInfo context_address{0, 0}; + /// Starting offset for the wavebuffer + u32 start_offset{}; + /// Ending offset the wavebuffer + u32 end_offset{}; + /// Should this wavebuffer loop? + bool loop{}; + /// Has this wavebuffer ended? + bool stream_ended{}; + /// Has this wavebuffer been sent to the AudioRenderer? + bool sent_to_DSP{true}; + /// Starting offset when looping, can differ from start_offset + u32 loop_start_offset{}; + /// Ending offset when looping, can differ from end_offset + u32 loop_end_offset{}; + /// Number of times to loop this wavebuffer + s32 loop_count{}; + }; + + struct WaveBufferInternal { + /* 0x00 */ CpuAddr address; + /* 0x08 */ u64 size; + /* 0x10 */ s32 start_offset; + /* 0x14 */ s32 end_offset; + /* 0x18 */ bool loop; + /* 0x19 */ bool stream_ended; + /* 0x1A */ bool sent_to_DSP; + /* 0x1C */ s32 loop_count; + /* 0x20 */ CpuAddr context_address; + /* 0x28 */ u64 context_size; + /* 0x30 */ u32 loop_start; + /* 0x34 */ u32 loop_end; + }; + static_assert(sizeof(WaveBufferInternal) == 0x38, + "VoiceInfo::WaveBufferInternal has the wrong size!"); + + struct BiquadFilterParameter { + /* 0x00 */ bool enabled; + /* 0x02 */ std::array b; + /* 0x08 */ std::array a; + }; + static_assert(sizeof(BiquadFilterParameter) == 0xC, + "VoiceInfo::BiquadFilterParameter has the wrong size!"); + + struct InParameter { + /* 0x000 */ u32 id; + /* 0x004 */ u32 node_id; + /* 0x008 */ bool is_new; + /* 0x009 */ bool in_use; + /* 0x00A */ PlayState play_state; + /* 0x00B */ SampleFormat sample_format; + /* 0x00C */ u32 sample_rate; + /* 0x010 */ s32 priority; + /* 0x014 */ s32 sort_order; + /* 0x018 */ u32 channel_count; + /* 0x01C */ f32 pitch; + /* 0x020 */ f32 volume; + /* 0x024 */ std::array biquads; + /* 0x03C */ u32 wave_buffer_count; + /* 0x040 */ u16 wave_buffer_index; + /* 0x042 */ char unk042[0x6]; + /* 0x048 */ CpuAddr src_data_address; + /* 0x050 */ u64 src_data_size; + /* 0x058 */ u32 mix_id; + /* 0x05C */ u32 splitter_id; + /* 0x060 */ std::array wave_buffer_internal; + /* 0x140 */ std::array channel_resource_ids; + /* 0x158 */ bool clear_voice_drop; + /* 0x159 */ u8 flush_buffer_count; + /* 0x15A */ char unk15A[0x2]; + /* 0x15C */ Flags flags; + /* 0x15D */ char unk15D[0x1]; + /* 0x15E */ SrcQuality src_quality; + /* 0x15F */ char unk15F[0x11]; + }; + static_assert(sizeof(InParameter) == 0x170, "VoiceInfo::InParameter has the wrong size!"); + + struct OutStatus { + /* 0x00 */ u64 played_sample_count; + /* 0x08 */ u32 wave_buffers_consumed; + /* 0x0C */ bool voice_dropped; + }; + static_assert(sizeof(OutStatus) == 0x10, "OutStatus::InParameter has the wrong size!"); + + VoiceInfo(); + + /** + * Initialize this voice. + */ + void Initialize(); + + /** + * Does this voice ned an update? + * + * @param params - Input parametetrs to check matching. + * @return True if this voice needs an update, otherwise false. + */ + bool ShouldUpdateParameters(const InParameter& params) const; + + /** + * Update the parameters of this voice. + * + * @param error_info - Output error code. + * @param params - Input parametters to udpate from. + * @param pool_mapper - Used to map buffers. + * @param behavior - behavior to check supported features. + */ + void UpdateParameters(BehaviorInfo::ErrorInfo& error_info, const InParameter& params, + const PoolMapper& pool_mapper, const BehaviorInfo& behavior); + + /** + * Update the current play state. + * + * @param state - New play state for this voice. + */ + void UpdatePlayState(PlayState state); + + /** + * Update the current sample rate conversion quality. + * + * @param quality - New quality. + */ + void UpdateSrcQuality(SrcQuality quality); + + /** + * Update all wavebuffers. + * + * @param error_infos - Output 2D array of errors, 2 per wavebuffer. + * @param error_count - Number of errors provided. Unused. + * @param params - Input parametters to be used for the update. + * @param voice_states - The voice states for each channel in this voice to be updated. + * @param pool_mapper - Used to map the wavebuffers. + * @param behavior - Used to check for supported features. + */ + void UpdateWaveBuffers(std::span> error_infos, + u32 error_count, const InParameter& params, + std::span voice_states, const PoolMapper& pool_mapper, + const BehaviorInfo& behavior); + + /** + * Update a wavebuffer. + * + * @param error_infos - Output array of errors. + * @param wave_buffer - The wavebuffer to be updated. + * @param wave_buffer_internal - Input parametters to be used for the update. + * @param sample_format - Sample format of the wavebuffer. + * @param valid - Is this wavebuffer valid? + * @param pool_mapper - Used to map the wavebuffers. + * @param behavior - Used to check for supported features. + */ + void UpdateWaveBuffer(std::span error_info, WaveBuffer& wave_buffer, + const WaveBufferInternal& wave_buffer_internal, + SampleFormat sample_format, bool valid, const PoolMapper& pool_mapper, + const BehaviorInfo& behavior); + + /** + * Check if the input wavebuffer needs an update. + * + * @param wave_buffer_internal - Input wavebuffer parameters to check. + * @return True if the given wavebuffer needs an update, otherwise false. + */ + bool ShouldUpdateWaveBuffer(const WaveBufferInternal& wave_buffer_internal) const; + + /** + * Write the number of played samples, number of consumed wavebuffers and if this voice was + * dropped, to the given out_status. + * + * @param out_status - Output status to be written to. + * @param in_params - Input parameters to check if the wavebuffer is new. + * @param voice_states - Current host voice states for this voice, source of the output. + */ + void WriteOutStatus(OutStatus& out_status, const InParameter& in_params, + std::span voice_states); + + /** + * Check if this voice should be skipped for command generation. + * Checks various things such as usage state, whether data is mapped etc. + * + * @return True if this voice should not be generated, otherwise false. + */ + bool ShouldSkip() const; + + /** + * Check if this voice has any mixing connections. + * + * @return True if this voice participes in mixing, otherwise false. + */ + bool HasAnyConnection() const; + + /** + * Flush flush_count wavebuffers, marking them as consumed. + * + * @param flush_count - Number of wavebuffers to flush. + * @param voice_states - Voice states for these wavebuffers. + * @param channel_count - Number of active channels. + */ + void FlushWaveBuffers(u32 flush_count, std::span voice_states, s8 channel_count); + + /** + * Update this voice's parameters on command generation, + * updating voice states and flushing if needed. + * + * @param voice_states - Voice states for these wavebuffers. + * @return True if this voice should be generated, otherwise false. + */ + bool UpdateParametersForCommandGeneration(std::span voice_states); + + /** + * Update this voice on command generation. + * + * @param voice_states - Voice states for these wavebuffers. + * @return True if this voice should be generated, otherwise false. + */ + bool UpdateForCommandGeneration(VoiceContext& voice_context); + + /** + * Reset the AudioRenderer-side voice states, and the channel resources for this voice. + * + * @param voice_context - Context from which to get the resources. + */ + void ResetResources(VoiceContext& voice_context) const; + + /// Is this voice in use? + bool in_use{}; + /// Is this voice new? + bool is_new{}; + /// Was this voice last playing? Used for depopping + bool was_playing{}; + /// Sample format of the wavebuffers in this voice + SampleFormat sample_format{}; + /// Sample rate of the wavebuffers in this voice + u32 sample_rate{}; + /// Number of channels in this voice + s8 channel_count{}; + /// Id of this voice + u32 id{}; + /// Node id of this voice + u32 node_id{}; + /// Mix id this voice is mixed to + u32 mix_id{}; + /// Play state of this voice + ServerPlayState current_play_state{ServerPlayState::Stopped}; + /// Last play state of this voice + ServerPlayState last_play_state{ServerPlayState::Started}; + /// Priority of this voice, lower is higher + s32 priority{}; + /// Sort order of this voice, used when same priority + s32 sort_order{}; + /// Pitch of this voice (for sample rate conversion) + f32 pitch{}; + /// Current volume of this voice + f32 volume{}; + /// Previous volume of this voice + f32 prev_volume{}; + /// Biquad filters for generating filter commands on this voice + std::array biquads{}; + /// Number of active wavebuffers + u32 wave_buffer_count{}; + /// Current playing wavebuffer index + u16 wave_buffer_index{}; + /// Flags controlling decode behavior + u16 flags{}; + /// Game memory for ADPCM coefficients + AddressInfo data_address{0, 0}; + /// Wavebuffers + std::array wavebuffers{}; + /// Channel resources for this voice + std::array channel_resource_ids{}; + /// Splitter id this voice is connected with + s32 splitter_id{UnusedSplitterId}; + /// Sample rate conversion quality + SrcQuality src_quality{SrcQuality::Medium}; + /// Was this voice dropped due to limited time? + bool voice_dropped{}; + /// Is this voice's coefficient (data_address) unmapped? + bool data_unmapped{}; + /// Is this voice's buffers (wavebuffer data and ADPCM context) unmapped? + bool buffer_unmapped{}; + /// Initialisation state of the biquads + std::array biquad_initialized{}; + /// Number of wavebuffers to flush + u8 flush_buffer_count{}; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/voice/voice_state.h b/src/audio_core/renderer/voice/voice_state.h new file mode 100644 index 0000000000..d5497e2fb6 --- /dev/null +++ b/src/audio_core/renderer/voice/voice_state.h @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" +#include "common/fixed_point.h" + +namespace AudioCore::AudioRenderer { +/** + * Holds a state for a voice. One is kept host-side, and one is used by the AudioRenderer, + * host-side is updated on the next iteration. + */ +struct VoiceState { + /** + * State of the voice's biquad filter. + */ + struct BiquadFilterState { + Common::FixedPoint<50, 14> s0; + Common::FixedPoint<50, 14> s1; + Common::FixedPoint<50, 14> s2; + Common::FixedPoint<50, 14> s3; + }; + + /** + * Context for ADPCM decoding. + */ + struct AdpcmContext { + u16 header; + s16 yn0; + s16 yn1; + }; + + /// Number of samples played + u64 played_sample_count; + /// Current offset from the starting offset + u32 offset; + /// Currently active wavebuffer index + u32 wave_buffer_index; + /// Array of which wavebuffers are currently valid + + std::array wave_buffer_valid; + /// Number of wavebuffers consumed, given back to the game + u32 wave_buffers_consumed; + /// History of samples, used for rate conversion + + std::array sample_history; + /// Current read fraction, used for resampling + Common::FixedPoint<49, 15> fraction; + /// Current adpcm context + AdpcmContext adpcm_context; + /// Current biquad states, used when filtering + + std::array, MaxBiquadFilters> biquad_states; + /// Previous samples + std::array previous_samples; + /// Unused + u32 external_context_size; + /// Unused + bool external_context_enabled; + /// Was this voice dropped? + bool voice_dropped; + /// Number of times the wavebuffer has looped + s32 loop_count; +}; + +} // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/sdl2_sink.cpp b/src/audio_core/sdl2_sink.cpp deleted file mode 100644 index a10ba40441..0000000000 --- a/src/audio_core/sdl2_sink.cpp +++ /dev/null @@ -1,160 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include -#include -#include -#include "audio_core/sdl2_sink.h" -#include "audio_core/stream.h" -#include "common/assert.h" -#include "common/logging/log.h" -//#include "common/settings.h" - -// Ignore -Wimplicit-fallthrough due to https://github.com/libsdl-org/SDL/issues/4307 -#ifdef __clang__ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wimplicit-fallthrough" -#endif -#include -#ifdef __clang__ -#pragma clang diagnostic pop -#endif - -namespace AudioCore { - -class SDLSinkStream final : public SinkStream { -public: - SDLSinkStream(u32 sample_rate, u32 num_channels_, const std::string& output_device) - : num_channels{std::min(num_channels_, 6u)} { - - SDL_AudioSpec spec; - spec.freq = sample_rate; - spec.channels = static_cast(num_channels); - spec.format = AUDIO_S16SYS; - spec.samples = 4096; - spec.callback = nullptr; - - SDL_AudioSpec obtained; - if (output_device.empty()) { - dev = SDL_OpenAudioDevice(nullptr, 0, &spec, &obtained, 0); - } else { - dev = SDL_OpenAudioDevice(output_device.c_str(), 0, &spec, &obtained, 0); - } - - if (dev == 0) { - LOG_CRITICAL(Audio_Sink, "Error opening sdl audio device: {}", SDL_GetError()); - return; - } - - SDL_PauseAudioDevice(dev, 0); - } - - ~SDLSinkStream() override { - if (dev == 0) { - return; - } - - SDL_CloseAudioDevice(dev); - } - - void EnqueueSamples(u32 source_num_channels, const std::vector& samples) override { - if (source_num_channels > num_channels) { - // Downsample 6 channels to 2 - ASSERT_MSG(source_num_channels == 6, "Channel count must be 6"); - - std::vector buf; - buf.reserve(samples.size() * num_channels / source_num_channels); - for (std::size_t i = 0; i < samples.size(); i += source_num_channels) { - // Downmixing implementation taken from the ATSC standard - const s16 left{samples[i + 0]}; - const s16 right{samples[i + 1]}; - const s16 center{samples[i + 2]}; - const s16 surround_left{samples[i + 4]}; - const s16 surround_right{samples[i + 5]}; - // Not used in the ATSC reference implementation - [[maybe_unused]] const s16 low_frequency_effects{samples[i + 3]}; - - constexpr s32 clev{707}; // center mixing level coefficient - constexpr s32 slev{707}; // surround mixing level coefficient - - buf.push_back(static_cast(left + (clev * center / 1000) + - (slev * surround_left / 1000))); - buf.push_back(static_cast(right + (clev * center / 1000) + - (slev * surround_right / 1000))); - } - int ret = SDL_QueueAudio(dev, static_cast(buf.data()), - static_cast(buf.size() * sizeof(s16))); - if (ret < 0) - LOG_WARNING(Audio_Sink, "Could not queue audio buffer: {}", SDL_GetError()); - return; - } - - int ret = SDL_QueueAudio(dev, static_cast(samples.data()), - static_cast(samples.size() * sizeof(s16))); - if (ret < 0) - LOG_WARNING(Audio_Sink, "Could not queue audio buffer: {}", SDL_GetError()); - } - - std::size_t SamplesInQueue(u32 channel_count) const override { - if (dev == 0) - return 0; - - return SDL_GetQueuedAudioSize(dev) / (channel_count * sizeof(s16)); - } - - void Flush() override { - should_flush = true; - } - - u32 GetNumChannels() const { - return num_channels; - } - -private: - SDL_AudioDeviceID dev = 0; - u32 num_channels{}; - std::atomic should_flush{}; -}; - -SDLSink::SDLSink(std::string_view target_device_name) { - if (!SDL_WasInit(SDL_INIT_AUDIO)) { - if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { - LOG_CRITICAL(Audio_Sink, "SDL_InitSubSystem audio failed: {}", SDL_GetError()); - return; - } - } - - if (target_device_name != auto_device_name && !target_device_name.empty()) { - output_device = target_device_name; - } else { - output_device.clear(); - } -} - -SDLSink::~SDLSink() = default; - -SinkStream& SDLSink::AcquireSinkStream(u32 sample_rate, u32 num_channels, const std::string&) { - sink_streams.push_back( - std::make_unique(sample_rate, num_channels, output_device)); - return *sink_streams.back(); -} - -std::vector ListSDLSinkDevices() { - std::vector device_list; - - if (!SDL_WasInit(SDL_INIT_AUDIO)) { - if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { - LOG_CRITICAL(Audio_Sink, "SDL_InitSubSystem audio failed: {}", SDL_GetError()); - return {}; - } - } - - const int device_count = SDL_GetNumAudioDevices(0); - for (int i = 0; i < device_count; ++i) { - device_list.emplace_back(SDL_GetAudioDeviceName(i, 0)); - } - - return device_list; -} - -} // namespace AudioCore diff --git a/src/audio_core/sdl2_sink.h b/src/audio_core/sdl2_sink.h deleted file mode 100644 index f1dd1d677c..0000000000 --- a/src/audio_core/sdl2_sink.h +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include - -#include "audio_core/sink.h" - -namespace AudioCore { - -class SDLSink final : public Sink { -public: - explicit SDLSink(std::string_view device_id); - ~SDLSink() override; - - SinkStream& AcquireSinkStream(u32 sample_rate, u32 num_channels, - const std::string& name) override; - -private: - std::string output_device; - std::vector sink_streams; -}; - -std::vector ListSDLSinkDevices(); - -} // namespace AudioCore diff --git a/src/audio_core/sink.h b/src/audio_core/sink.h deleted file mode 100644 index 3c03554fa3..0000000000 --- a/src/audio_core/sink.h +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include - -#include "audio_core/sink_stream.h" -#include "common/common_types.h" - -namespace AudioCore { - -constexpr char auto_device_name[] = "auto"; - -/** - * This class is an interface for an audio sink. An audio sink accepts samples in stereo signed - * PCM16 format to be output. Sinks *do not* handle resampling and expect the correct sample rate. - * They are dumb outputs. - */ -class Sink { -public: - virtual ~Sink() = default; - virtual SinkStream& AcquireSinkStream(u32 sample_rate, u32 num_channels, - const std::string& name) = 0; -}; - -using SinkPtr = std::unique_ptr; - -} // namespace AudioCore diff --git a/src/audio_core/sink/cubeb_sink.cpp b/src/audio_core/sink/cubeb_sink.cpp new file mode 100644 index 0000000000..a4e28de6de --- /dev/null +++ b/src/audio_core/sink/cubeb_sink.cpp @@ -0,0 +1,651 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include "audio_core/audio_core.h" +#include "audio_core/audio_event.h" +#include "audio_core/audio_manager.h" +#include "audio_core/sink/cubeb_sink.h" +#include "audio_core/sink/sink_stream.h" +#include "common/assert.h" +#include "common/fixed_point.h" +#include "common/logging/log.h" +#include "common/reader_writer_queue.h" +#include "common/ring_buffer.h" +#include "common/settings.h" +#include "core/core.h" + +#ifdef _WIN32 +#include +#undef CreateEvent +#endif + +namespace AudioCore::Sink { +/** + * Cubeb sink stream, responsible for sinking samples to hardware. + */ +class CubebSinkStream final : public SinkStream { +public: + /** + * Create a new sink stream. + * + * @param ctx_ - Cubeb context to create this stream with. + * @param device_channels_ - Number of channels supported by the hardware. + * @param system_channels_ - Number of channels the audio systems expect. + * @param output_device - Cubeb output device id. + * @param input_device - Cubeb input device id. + * @param name_ - Name of this stream. + * @param type_ - Type of this stream. + * @param system_ - Core system. + * @param event - Event used only for audio renderer, signalled on buffer consume. + */ + CubebSinkStream(cubeb* ctx_, const u32 device_channels_, const u32 system_channels_, + cubeb_devid output_device, cubeb_devid input_device, const std::string& name_, + const StreamType type_, Core::System& system_) + : ctx{ctx_}, type{type_}, system{system_} { +#ifdef _WIN32 + CoInitializeEx(nullptr, COINIT_MULTITHREADED); +#endif + name = name_; + device_channels = device_channels_; + system_channels = system_channels_; + + cubeb_stream_params params{}; + params.rate = TargetSampleRate; + params.channels = device_channels; + params.format = CUBEB_SAMPLE_S16LE; + params.prefs = CUBEB_STREAM_PREF_NONE; + switch (params.channels) { + case 1: + params.layout = CUBEB_LAYOUT_MONO; + break; + case 2: + params.layout = CUBEB_LAYOUT_STEREO; + break; + case 6: + params.layout = CUBEB_LAYOUT_3F2_LFE; + break; + } + + u32 minimum_latency{0}; + const auto latency_error = cubeb_get_min_latency(ctx, ¶ms, &minimum_latency); + if (latency_error != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error getting minimum latency, error: {}", latency_error); + minimum_latency = 256U; + } + + minimum_latency = std::max(minimum_latency, 256u); + + playing_buffer.consumed = true; + + LOG_DEBUG(Service_Audio, + "Opening cubeb stream {} type {} with: rate {} channels {} (system channels {}) " + "latency {}", + name, type, params.rate, params.channels, system_channels, minimum_latency); + + auto init_error{0}; + if (type == StreamType::In) { + init_error = cubeb_stream_init(ctx, &stream_backend, name.c_str(), input_device, + ¶ms, output_device, nullptr, minimum_latency, + &CubebSinkStream::DataCallback, + &CubebSinkStream::StateCallback, this); + } else { + init_error = cubeb_stream_init(ctx, &stream_backend, name.c_str(), input_device, + nullptr, output_device, ¶ms, minimum_latency, + &CubebSinkStream::DataCallback, + &CubebSinkStream::StateCallback, this); + } + + if (init_error != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error initializing cubeb stream, error: {}", init_error); + return; + } + } + + /** + * Destroy the sink stream. + */ + ~CubebSinkStream() override { + LOG_DEBUG(Service_Audio, "Destructing cubeb stream {}", name); + + if (!ctx) { + return; + } + + Finalize(); + +#ifdef _WIN32 + CoUninitialize(); +#endif + } + + /** + * Finalize the sink stream. + */ + void Finalize() override { + Stop(); + cubeb_stream_destroy(stream_backend); + } + + /** + * Start the sink stream. + * + * @param resume - Set to true if this is resuming the stream a previously-active stream. + * Default false. + */ + void Start(const bool resume = false) override { + if (!ctx) { + return; + } + + if (resume && was_playing) { + if (cubeb_stream_start(stream_backend) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream"); + } + paused = false; + } else if (!resume) { + if (cubeb_stream_start(stream_backend) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream"); + } + paused = false; + } + } + + /** + * Stop the sink stream. + */ + void Stop() override { + if (!ctx) { + return; + } + + if (cubeb_stream_stop(stream_backend) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error stopping cubeb stream"); + } + + was_playing.store(!paused); + paused = true; + } + + /** + * Append a new buffer and its samples to a waiting queue to play. + * + * @param buffer - Audio buffer information to be queued. + * @param samples - The s16 samples to be queue for playback. + */ + void AppendBuffer(::AudioCore::Sink::SinkBuffer& buffer, std::vector& samples) override { + if (type == StreamType::In) { + queue.enqueue(buffer); + queued_buffers++; + } else { + constexpr s32 min{std::numeric_limits::min()}; + constexpr s32 max{std::numeric_limits::max()}; + + auto yuzu_volume{Settings::Volume()}; + auto volume{system_volume * device_volume * yuzu_volume}; + + if (system_channels == 6 && device_channels == 2) { + // We're given 6 channels, but our device only outputs 2, so downmix. + constexpr std::array down_mix_coeff{1.0f, 0.707f, 0.251f, 0.707f}; + + for (u32 read_index = 0, write_index = 0; read_index < samples.size(); + read_index += system_channels, write_index += device_channels) { + const auto left_sample{ + ((Common::FixedPoint<49, 15>( + samples[read_index + static_cast(Channels::FrontLeft)]) * + down_mix_coeff[0] + + samples[read_index + static_cast(Channels::Center)] * + down_mix_coeff[1] + + samples[read_index + static_cast(Channels::LFE)] * + down_mix_coeff[2] + + samples[read_index + static_cast(Channels::BackLeft)] * + down_mix_coeff[3]) * + volume) + .to_int()}; + + const auto right_sample{ + ((Common::FixedPoint<49, 15>( + samples[read_index + static_cast(Channels::FrontRight)]) * + down_mix_coeff[0] + + samples[read_index + static_cast(Channels::Center)] * + down_mix_coeff[1] + + samples[read_index + static_cast(Channels::LFE)] * + down_mix_coeff[2] + + samples[read_index + static_cast(Channels::BackRight)] * + down_mix_coeff[3]) * + volume) + .to_int()}; + + samples[write_index + static_cast(Channels::FrontLeft)] = + static_cast(std::clamp(left_sample, min, max)); + samples[write_index + static_cast(Channels::FrontRight)] = + static_cast(std::clamp(right_sample, min, max)); + } + + samples.resize(samples.size() / system_channels * device_channels); + + } else if (system_channels == 2 && device_channels == 6) { + // We need moar samples! Not all games will provide 6 channel audio. + // TODO: Implement some upmixing here. Currently just passthrough, with other + // channels left as silence. + std::vector new_samples(samples.size() / system_channels * device_channels, 0); + + for (u32 read_index = 0, write_index = 0; read_index < samples.size(); + read_index += system_channels, write_index += device_channels) { + const auto left_sample{static_cast(std::clamp( + static_cast( + static_cast( + samples[read_index + static_cast(Channels::FrontLeft)]) * + volume), + min, max))}; + + new_samples[write_index + static_cast(Channels::FrontLeft)] = left_sample; + + const auto right_sample{static_cast(std::clamp( + static_cast( + static_cast( + samples[read_index + static_cast(Channels::FrontRight)]) * + volume), + min, max))}; + + new_samples[write_index + static_cast(Channels::FrontRight)] = + right_sample; + } + samples = std::move(new_samples); + + } else if (volume != 1.0f) { + for (u32 i = 0; i < samples.size(); i++) { + samples[i] = static_cast(std::clamp( + static_cast(static_cast(samples[i]) * volume), min, max)); + } + } + + samples_buffer.Push(samples); + queue.enqueue(buffer); + queued_buffers++; + } + } + + /** + * Release a buffer. Audio In only, will fill a buffer with recorded samples. + * + * @param num_samples - Maximum number of samples to receive. + * @return Vector of recorded samples. May have fewer than num_samples. + */ + std::vector ReleaseBuffer(const u64 num_samples) override { + static constexpr s32 min = std::numeric_limits::min(); + static constexpr s32 max = std::numeric_limits::max(); + + auto samples{samples_buffer.Pop(num_samples)}; + + // TODO: Up-mix to 6 channels if the game expects it. + // For audio input this is unlikely to ever be the case though. + + // Incoming mic volume seems to always be very quiet, so multiply by an additional 8 here. + // TODO: Play with this and find something that works better. + auto volume{system_volume * device_volume * 8}; + for (u32 i = 0; i < samples.size(); i++) { + samples[i] = static_cast( + std::clamp(static_cast(static_cast(samples[i]) * volume), min, max)); + } + + if (samples.size() < num_samples) { + samples.resize(num_samples, 0); + } + return samples; + } + + /** + * Check if a certain buffer has been consumed (fully played). + * + * @param tag - Unique tag of a buffer to check for. + * @return True if the buffer has been played, otherwise false. + */ + bool IsBufferConsumed(const u64 tag) override { + if (released_buffer.tag == 0) { + if (!released_buffers.try_dequeue(released_buffer)) { + return false; + } + } + + if (released_buffer.tag == tag) { + released_buffer.tag = 0; + return true; + } + return false; + } + + /** + * Empty out the buffer queue. + */ + void ClearQueue() override { + samples_buffer.Pop(); + while (queue.pop()) { + } + while (released_buffers.pop()) { + } + queued_buffers = 0; + released_buffer = {}; + playing_buffer = {}; + playing_buffer.consumed = true; + } + +private: + /** + * Signal events back to the audio system that a buffer was played/can be filled. + * + * @param buffer - Consumed audio buffer to be released. + */ + void SignalEvent(const ::AudioCore::Sink::SinkBuffer& buffer) { + auto& manager{system.AudioCore().GetAudioManager()}; + switch (type) { + case StreamType::Out: + released_buffers.enqueue(buffer); + manager.SetEvent(Event::Type::AudioOutManager, true); + break; + case StreamType::In: + released_buffers.enqueue(buffer); + manager.SetEvent(Event::Type::AudioInManager, true); + break; + case StreamType::Render: + break; + } + } + + /** + * Main callback from Cubeb. Either expects samples from us (audio render/audio out), or will + * provide samples to be copied (audio in). + * + * @param stream - Cubeb-specific data about the stream. + * @param user_data - Custom data pointer passed along, points to a CubebSinkStream. + * @param in_buff - Input buffer to be used if the stream is an input type. + * @param out_buff - Output buffer to be used if the stream is an output type. + * @param num_frames_ - Number of frames of audio in the buffers. Note: Not number of samples. + */ + static long DataCallback([[maybe_unused]] cubeb_stream* stream, void* user_data, + [[maybe_unused]] const void* in_buff, void* out_buff, + long num_frames_) { + auto* impl = static_cast(user_data); + if (!impl) { + return -1; + } + + const std::size_t num_channels = impl->GetDeviceChannels(); + const std::size_t frame_size = num_channels; + const std::size_t frame_size_bytes = frame_size * sizeof(s16); + const std::size_t num_frames{static_cast(num_frames_)}; + size_t frames_written{0}; + [[maybe_unused]] bool underrun{false}; + + if (impl->type == StreamType::In) { + // INPUT + std::span input_buffer{reinterpret_cast(in_buff), + num_frames * frame_size}; + + while (frames_written < num_frames) { + auto& playing_buffer{impl->playing_buffer}; + + // If the playing buffer has been consumed or has no frames, we need a new one + if (playing_buffer.consumed || playing_buffer.frames == 0) { + if (!impl->queue.try_dequeue(impl->playing_buffer)) { + // If no buffer was available we've underrun, just push the samples and + // continue. + underrun = true; + impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], + (num_frames - frames_written) * frame_size); + frames_written = num_frames; + continue; + } else { + // Successfully got a new buffer, mark the old one as consumed and signal. + impl->queued_buffers--; + impl->SignalEvent(impl->playing_buffer); + } + } + + // Get the minimum frames available between the currently playing buffer, and the + // amount we have left to fill + size_t frames_available{ + std::min(playing_buffer.frames - playing_buffer.frames_played, + num_frames - frames_written)}; + + impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], + frames_available * frame_size); + + frames_written += frames_available; + playing_buffer.frames_played += frames_available; + + // If that's all the frames in the current buffer, add its samples and mark it as + // consumed + if (playing_buffer.frames_played >= playing_buffer.frames) { + impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); + impl->playing_buffer.consumed = true; + } + } + + std::memcpy(&impl->last_frame[0], &input_buffer[(frames_written - 1) * frame_size], + frame_size_bytes); + } else { + // OUTPUT + std::span output_buffer{reinterpret_cast(out_buff), num_frames * frame_size}; + + while (frames_written < num_frames) { + auto& playing_buffer{impl->playing_buffer}; + + // If the playing buffer has been consumed or has no frames, we need a new one + if (playing_buffer.consumed || playing_buffer.frames == 0) { + if (!impl->queue.try_dequeue(impl->playing_buffer)) { + // If no buffer was available we've underrun, fill the remaining buffer with + // the last written frame and continue. + underrun = true; + for (size_t i = frames_written; i < num_frames; i++) { + std::memcpy(&output_buffer[i * frame_size], &impl->last_frame[0], + frame_size_bytes); + } + frames_written = num_frames; + continue; + } else { + // Successfully got a new buffer, mark the old one as consumed and signal. + impl->queued_buffers--; + impl->SignalEvent(impl->playing_buffer); + } + } + + // Get the minimum frames available between the currently playing buffer, and the + // amount we have left to fill + size_t frames_available{ + std::min(playing_buffer.frames - playing_buffer.frames_played, + num_frames - frames_written)}; + + impl->samples_buffer.Pop(&output_buffer[frames_written * frame_size], + frames_available * frame_size); + + frames_written += frames_available; + playing_buffer.frames_played += frames_available; + + // If that's all the frames in the current buffer, add its samples and mark it as + // consumed + if (playing_buffer.frames_played >= playing_buffer.frames) { + impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); + impl->playing_buffer.consumed = true; + } + } + + std::memcpy(&impl->last_frame[0], &output_buffer[(frames_written - 1) * frame_size], + frame_size_bytes); + } + + return num_frames_; + } + + /** + * Cubeb callback for if a device state changes. Unused currently. + * + * @param stream - Cubeb-specific data about the stream. + * @param user_data - Custom data pointer passed along, points to a CubebSinkStream. + * @param state - New state of the device. + */ + static void StateCallback([[maybe_unused]] cubeb_stream* stream, + [[maybe_unused]] void* user_data, + [[maybe_unused]] cubeb_state state) {} + + /// Main Cubeb context + cubeb* ctx{}; + /// Cubeb stream backend + cubeb_stream* stream_backend{}; + /// Name of this stream + std::string name{}; + /// Type of this stream + StreamType type; + /// Core system + Core::System& system; + /// Ring buffer of the samples waiting to be played or consumed + Common::RingBuffer samples_buffer; + /// Audio buffers queued and waiting to play + Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> queue; + /// The currently-playing audio buffer + ::AudioCore::Sink::SinkBuffer playing_buffer{}; + /// Audio buffers which have been played and are in queue to be released by the audio system + Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> released_buffers{}; + /// Currently released buffer waiting to be taken by the audio system + ::AudioCore::Sink::SinkBuffer released_buffer{}; + /// The last played (or received) frame of audio, used when the callback underruns + std::array last_frame{}; +}; + +CubebSink::CubebSink(std::string_view target_device_name) { + // Cubeb requires COM to be initialized on the thread calling cubeb_init on Windows +#ifdef _WIN32 + com_init_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED); +#endif + + if (cubeb_init(&ctx, "yuzu", nullptr) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "cubeb_init failed"); + return; + } + + if (target_device_name != auto_device_name && !target_device_name.empty()) { + cubeb_device_collection collection; + if (cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection) != CUBEB_OK) { + LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported"); + } else { + const auto collection_end{collection.device + collection.count}; + const auto device{ + std::find_if(collection.device, collection_end, [&](const cubeb_device_info& info) { + return info.friendly_name != nullptr && + target_device_name == std::string(info.friendly_name); + })}; + if (device != collection_end) { + output_device = device->devid; + } + cubeb_device_collection_destroy(ctx, &collection); + } + } + + cubeb_get_max_channel_count(ctx, &device_channels); + device_channels = device_channels >= 6U ? 6U : 2U; +} + +CubebSink::~CubebSink() { + if (!ctx) { + return; + } + + for (auto& sink_stream : sink_streams) { + sink_stream.reset(); + } + + cubeb_destroy(ctx); + +#ifdef _WIN32 + if (SUCCEEDED(com_init_result)) { + CoUninitialize(); + } +#endif +} + +SinkStream* CubebSink::AcquireSinkStream(Core::System& system, const u32 system_channels, + const std::string& name, const StreamType type) { + SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique( + ctx, device_channels, system_channels, output_device, input_device, name, type, system)); + + return stream.get(); +} + +void CubebSink::CloseStream(const SinkStream* stream) { + for (size_t i = 0; i < sink_streams.size(); i++) { + if (sink_streams[i].get() == stream) { + sink_streams[i].reset(); + sink_streams.erase(sink_streams.begin() + i); + break; + } + } +} + +void CubebSink::CloseStreams() { + sink_streams.clear(); +} + +void CubebSink::PauseStreams() { + for (auto& stream : sink_streams) { + stream->Stop(); + } +} + +void CubebSink::UnpauseStreams() { + for (auto& stream : sink_streams) { + stream->Start(true); + } +} + +f32 CubebSink::GetDeviceVolume() const { + if (sink_streams.empty()) { + return 1.0f; + } + + return sink_streams[0]->GetDeviceVolume(); +} + +void CubebSink::SetDeviceVolume(const f32 volume) { + for (auto& stream : sink_streams) { + stream->SetDeviceVolume(volume); + } +} + +void CubebSink::SetSystemVolume(const f32 volume) { + for (auto& stream : sink_streams) { + stream->SetSystemVolume(volume); + } +} + +std::vector ListCubebSinkDevices(const bool capture) { + std::vector device_list; + cubeb* ctx; + + if (cubeb_init(&ctx, "yuzu Device Enumerator", nullptr) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "cubeb_init failed"); + return {}; + } + + auto type{capture ? CUBEB_DEVICE_TYPE_INPUT : CUBEB_DEVICE_TYPE_OUTPUT}; + cubeb_device_collection collection; + if (cubeb_enumerate_devices(ctx, type, &collection) != CUBEB_OK) { + LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported"); + } else { + for (std::size_t i = 0; i < collection.count; i++) { + const cubeb_device_info& device = collection.device[i]; + if (device.friendly_name && device.friendly_name[0] != '\0' && + device.state == CUBEB_DEVICE_STATE_ENABLED) { + device_list.emplace_back(device.friendly_name); + } + } + cubeb_device_collection_destroy(ctx, &collection); + } + + cubeb_destroy(ctx); + return device_list; +} + +} // namespace AudioCore::Sink diff --git a/src/audio_core/sink/cubeb_sink.h b/src/audio_core/sink/cubeb_sink.h new file mode 100644 index 0000000000..f0f43dfa12 --- /dev/null +++ b/src/audio_core/sink/cubeb_sink.h @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include + +#include "audio_core/sink/sink.h" + +namespace Core { +class System; +} + +namespace AudioCore::Sink { +class SinkStream; + +/** + * Cubeb backend sink, holds multiple output streams and is responsible for sinking samples to + * hardware. Used by Audio Render, Audio In and Audio Out. + */ +class CubebSink final : public Sink { +public: + explicit CubebSink(std::string_view device_id); + ~CubebSink() override; + + /** + * Create a new sink stream. + * + * @param system - Core system. + * @param system_channels - Number of channels the audio system expects. + * May differ from the device's channel count. + * @param name - Name of this stream. + * @param type - Type of this stream, render/in/out. + * @param event - Audio render only, a signal used to prevent the renderer running too + * fast. + * @return A pointer to the created SinkStream + */ + SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels, + const std::string& name, StreamType type) override; + + /** + * Close a given stream. + * + * @param stream - The stream to close. + */ + void CloseStream(const SinkStream* stream) override; + + /** + * Close all streams. + */ + void CloseStreams() override; + + /** + * Pause all streams. + */ + void PauseStreams() override; + + /** + * Unpause all streams. + */ + void UnpauseStreams() override; + + /** + * Get the device volume. Set from calls to the IAudioDevice service. + * + * @return Volume of the device. + */ + f32 GetDeviceVolume() const override; + + /** + * Set the device volume. Set from calls to the IAudioDevice service. + * + * @param volume - New volume of the device. + */ + void SetDeviceVolume(f32 volume) override; + + /** + * Set the system volume. Comes from the audio system using this stream. + * + * @param volume - New volume of the system. + */ + void SetSystemVolume(f32 volume) override; + +private: + /// Backend Cubeb context + cubeb* ctx{}; + /// Cubeb id of the actual hardware output device + cubeb_devid output_device{}; + /// Cubeb id of the actual hardware input device + cubeb_devid input_device{}; + /// Vector of streams managed by this sink + std::vector sink_streams{}; + +#ifdef _WIN32 + /// Cubeb required COM to be initialized multi-threaded on Windows + u32 com_init_result = 0; +#endif +}; + +/** + * Get a list of conencted devices from Cubeb. + * + * @param capture - Return input (capture) devices if true, otherwise output devices. + */ +std::vector ListCubebSinkDevices(bool capture); + +} // namespace AudioCore::Sink diff --git a/src/audio_core/sink/null_sink.h b/src/audio_core/sink/null_sink.h new file mode 100644 index 0000000000..47a3421719 --- /dev/null +++ b/src/audio_core/sink/null_sink.h @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "audio_core/sink/sink.h" +#include "audio_core/sink/sink_stream.h" + +namespace AudioCore::Sink { +/** + * A no-op sink for when no audio out is wanted. + */ +class NullSink final : public Sink { +public: + explicit NullSink(std::string_view) {} + ~NullSink() override = default; + + SinkStream* AcquireSinkStream([[maybe_unused]] Core::System& system, + [[maybe_unused]] u32 system_channels, + [[maybe_unused]] const std::string& name, + [[maybe_unused]] StreamType type) override { + return &null_sink_stream; + } + + void CloseStream([[maybe_unused]] const SinkStream* stream) override {} + void CloseStreams() override {} + void PauseStreams() override {} + void UnpauseStreams() override {} + f32 GetDeviceVolume() const override { + return 1.0f; + } + void SetDeviceVolume(f32 volume) override {} + void SetSystemVolume(f32 volume) override {} + +private: + struct NullSinkStreamImpl final : SinkStream { + void Finalize() override {} + void Start(bool resume = false) override {} + void Stop() override {} + void AppendBuffer([[maybe_unused]] ::AudioCore::Sink::SinkBuffer& buffer, + [[maybe_unused]] std::vector& samples) override {} + std::vector ReleaseBuffer([[maybe_unused]] u64 num_samples) override { + return {}; + } + bool IsBufferConsumed([[maybe_unused]] const u64 tag) { + return true; + } + void ClearQueue() override {} + } null_sink_stream; +}; + +} // namespace AudioCore::Sink diff --git a/src/audio_core/sink/sdl2_sink.cpp b/src/audio_core/sink/sdl2_sink.cpp new file mode 100644 index 0000000000..d6c9ec90dd --- /dev/null +++ b/src/audio_core/sink/sdl2_sink.cpp @@ -0,0 +1,556 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "audio_core/audio_core.h" +#include "audio_core/audio_event.h" +#include "audio_core/audio_manager.h" +#include "audio_core/sink/sdl2_sink.h" +#include "audio_core/sink/sink_stream.h" +#include "common/assert.h" +#include "common/fixed_point.h" +#include "common/logging/log.h" +#include "common/reader_writer_queue.h" +#include "common/ring_buffer.h" +#include "common/settings.h" +#include "core/core.h" + +// Ignore -Wimplicit-fallthrough due to https://github.com/libsdl-org/SDL/issues/4307 +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wimplicit-fallthrough" +#endif +#include +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +namespace AudioCore::Sink { +/** + * SDL sink stream, responsible for sinking samples to hardware. + */ +class SDLSinkStream final : public SinkStream { +public: + /** + * Create a new sink stream. + * + * @param device_channels_ - Number of channels supported by the hardware. + * @param system_channels_ - Number of channels the audio systems expect. + * @param output_device - Name of the output device to use for this stream. + * @param input_device - Name of the input device to use for this stream. + * @param type_ - Type of this stream. + * @param system_ - Core system. + * @param event - Event used only for audio renderer, signalled on buffer consume. + */ + SDLSinkStream(u32 device_channels_, const u32 system_channels_, + const std::string& output_device, const std::string& input_device, + const StreamType type_, Core::System& system_) + : type{type_}, system{system_} { + system_channels = system_channels_; + device_channels = device_channels_; + + SDL_AudioSpec spec; + spec.freq = TargetSampleRate; + spec.channels = static_cast(device_channels); + spec.format = AUDIO_S16SYS; + if (type == StreamType::Render) { + spec.samples = TargetSampleCount; + } else { + spec.samples = 1024; + } + spec.callback = &SDLSinkStream::DataCallback; + spec.userdata = this; + + playing_buffer.consumed = true; + + std::string device_name{output_device}; + bool capture{false}; + if (type == StreamType::In) { + device_name = input_device; + capture = true; + } + + SDL_AudioSpec obtained; + if (device_name.empty()) { + device = SDL_OpenAudioDevice(nullptr, capture, &spec, &obtained, false); + } else { + device = SDL_OpenAudioDevice(device_name.c_str(), capture, &spec, &obtained, false); + } + + if (device == 0) { + LOG_CRITICAL(Audio_Sink, "Error opening SDL audio device: {}", SDL_GetError()); + return; + } + + LOG_DEBUG(Service_Audio, + "Opening sdl stream {} with: rate {} channels {} (system channels {}) " + " samples {}", + device, obtained.freq, obtained.channels, system_channels, obtained.samples); + } + + /** + * Destroy the sink stream. + */ + ~SDLSinkStream() override { + if (device == 0) { + return; + } + + SDL_CloseAudioDevice(device); + } + + /** + * Finalize the sink stream. + */ + void Finalize() override { + if (device == 0) { + return; + } + + SDL_CloseAudioDevice(device); + } + + /** + * Start the sink stream. + * + * @param resume - Set to true if this is resuming the stream a previously-active stream. + * Default false. + */ + void Start(const bool resume = false) override { + if (device == 0) { + return; + } + + if (resume && was_playing) { + SDL_PauseAudioDevice(device, 0); + paused = false; + } else if (!resume) { + SDL_PauseAudioDevice(device, 0); + paused = false; + } + } + + /** + * Stop the sink stream. + */ + void Stop() { + if (device == 0) { + return; + } + SDL_PauseAudioDevice(device, 1); + paused = true; + } + + /** + * Append a new buffer and its samples to a waiting queue to play. + * + * @param buffer - Audio buffer information to be queued. + * @param samples - The s16 samples to be queue for playback. + */ + void AppendBuffer(::AudioCore::Sink::SinkBuffer& buffer, std::vector& samples) override { + if (type == StreamType::In) { + queue.enqueue(buffer); + queued_buffers++; + } else { + constexpr s32 min = std::numeric_limits::min(); + constexpr s32 max = std::numeric_limits::max(); + + auto yuzu_volume{Settings::Volume()}; + auto volume{system_volume * device_volume * yuzu_volume}; + + if (system_channels == 6 && device_channels == 2) { + // We're given 6 channels, but our device only outputs 2, so downmix. + constexpr std::array down_mix_coeff{1.0f, 0.707f, 0.251f, 0.707f}; + + for (u32 read_index = 0, write_index = 0; read_index < samples.size(); + read_index += system_channels, write_index += device_channels) { + const auto left_sample{ + ((Common::FixedPoint<49, 15>( + samples[read_index + static_cast(Channels::FrontLeft)]) * + down_mix_coeff[0] + + samples[read_index + static_cast(Channels::Center)] * + down_mix_coeff[1] + + samples[read_index + static_cast(Channels::LFE)] * + down_mix_coeff[2] + + samples[read_index + static_cast(Channels::BackLeft)] * + down_mix_coeff[3]) * + volume) + .to_int()}; + + const auto right_sample{ + ((Common::FixedPoint<49, 15>( + samples[read_index + static_cast(Channels::FrontRight)]) * + down_mix_coeff[0] + + samples[read_index + static_cast(Channels::Center)] * + down_mix_coeff[1] + + samples[read_index + static_cast(Channels::LFE)] * + down_mix_coeff[2] + + samples[read_index + static_cast(Channels::BackRight)] * + down_mix_coeff[3]) * + volume) + .to_int()}; + + samples[write_index + static_cast(Channels::FrontLeft)] = + static_cast(std::clamp(left_sample, min, max)); + samples[write_index + static_cast(Channels::FrontRight)] = + static_cast(std::clamp(right_sample, min, max)); + } + + samples.resize(samples.size() / system_channels * device_channels); + + } else if (system_channels == 2 && device_channels == 6) { + // We need moar samples! Not all games will provide 6 channel audio. + // TODO: Implement some upmixing here. Currently just passthrough, with other + // channels left as silence. + std::vector new_samples(samples.size() / system_channels * device_channels, 0); + + for (u32 read_index = 0, write_index = 0; read_index < samples.size(); + read_index += system_channels, write_index += device_channels) { + const auto left_sample{static_cast(std::clamp( + static_cast( + static_cast( + samples[read_index + static_cast(Channels::FrontLeft)]) * + volume), + min, max))}; + + new_samples[write_index + static_cast(Channels::FrontLeft)] = left_sample; + + const auto right_sample{static_cast(std::clamp( + static_cast( + static_cast( + samples[read_index + static_cast(Channels::FrontRight)]) * + volume), + min, max))}; + + new_samples[write_index + static_cast(Channels::FrontRight)] = + right_sample; + } + samples = std::move(new_samples); + + } else if (volume != 1.0f) { + for (u32 i = 0; i < samples.size(); i++) { + samples[i] = static_cast(std::clamp( + static_cast(static_cast(samples[i]) * volume), min, max)); + } + } + + samples_buffer.Push(samples); + queue.enqueue(buffer); + queued_buffers++; + } + } + + /** + * Release a buffer. Audio In only, will fill a buffer with recorded samples. + * + * @param num_samples - Maximum number of samples to receive. + * @return Vector of recorded samples. May have fewer than num_samples. + */ + std::vector ReleaseBuffer(const u64 num_samples) override { + static constexpr s32 min = std::numeric_limits::min(); + static constexpr s32 max = std::numeric_limits::max(); + + auto samples{samples_buffer.Pop(num_samples)}; + + // TODO: Up-mix to 6 channels if the game expects it. + // For audio input this is unlikely to ever be the case though. + + // Incoming mic volume seems to always be very quiet, so multiply by an additional 8 here. + // TODO: Play with this and find something that works better. + auto volume{system_volume * device_volume * 8}; + for (u32 i = 0; i < samples.size(); i++) { + samples[i] = static_cast( + std::clamp(static_cast(static_cast(samples[i]) * volume), min, max)); + } + + if (samples.size() < num_samples) { + samples.resize(num_samples, 0); + } + return samples; + } + + /** + * Check if a certain buffer has been consumed (fully played). + * + * @param tag - Unique tag of a buffer to check for. + * @return True if the buffer has been played, otherwise false. + */ + bool IsBufferConsumed(const u64 tag) override { + if (released_buffer.tag == 0) { + if (!released_buffers.try_dequeue(released_buffer)) { + return false; + } + } + + if (released_buffer.tag == tag) { + released_buffer.tag = 0; + return true; + } + return false; + } + + /** + * Empty out the buffer queue. + */ + void ClearQueue() override { + samples_buffer.Pop(); + while (queue.pop()) { + } + while (released_buffers.pop()) { + } + released_buffer = {}; + playing_buffer = {}; + playing_buffer.consumed = true; + queued_buffers = 0; + } + +private: + /** + * Signal events back to the audio system that a buffer was played/can be filled. + * + * @param buffer - Consumed audio buffer to be released. + */ + void SignalEvent(const ::AudioCore::Sink::SinkBuffer& buffer) { + auto& manager{system.AudioCore().GetAudioManager()}; + switch (type) { + case StreamType::Out: + released_buffers.enqueue(buffer); + manager.SetEvent(Event::Type::AudioOutManager, true); + break; + case StreamType::In: + released_buffers.enqueue(buffer); + manager.SetEvent(Event::Type::AudioInManager, true); + break; + case StreamType::Render: + break; + } + } + + /** + * Main callback from SDL. Either expects samples from us (audio render/audio out), or will + * provide samples to be copied (audio in). + * + * @param userdata - Custom data pointer passed along, points to a SDLSinkStream. + * @param stream - Buffer of samples to be filled or read. + * @param len - Length of the stream in bytes. + */ + static void DataCallback(void* userdata, Uint8* stream, int len) { + auto* impl = static_cast(userdata); + + if (!impl) { + return; + } + + const std::size_t num_channels = impl->GetDeviceChannels(); + const std::size_t frame_size = num_channels; + const std::size_t frame_size_bytes = frame_size * sizeof(s16); + const std::size_t num_frames{len / num_channels / sizeof(s16)}; + size_t frames_written{0}; + [[maybe_unused]] bool underrun{false}; + + if (impl->type == StreamType::In) { + std::span input_buffer{reinterpret_cast(stream), num_frames * frame_size}; + + while (frames_written < num_frames) { + auto& playing_buffer{impl->playing_buffer}; + + // If the playing buffer has been consumed or has no frames, we need a new one + if (playing_buffer.consumed || playing_buffer.frames == 0) { + if (!impl->queue.try_dequeue(impl->playing_buffer)) { + // If no buffer was available we've underrun, just push the samples and + // continue. + underrun = true; + impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], + (num_frames - frames_written) * frame_size); + frames_written = num_frames; + continue; + } else { + impl->queued_buffers--; + impl->SignalEvent(impl->playing_buffer); + } + } + + // Get the minimum frames available between the currently playing buffer, and the + // amount we have left to fill + size_t frames_available{ + std::min(playing_buffer.frames - playing_buffer.frames_played, + num_frames - frames_written)}; + + impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], + frames_available * frame_size); + + frames_written += frames_available; + playing_buffer.frames_played += frames_available; + + // If that's all the frames in the current buffer, add its samples and mark it as + // consumed + if (playing_buffer.frames_played >= playing_buffer.frames) { + impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); + impl->playing_buffer.consumed = true; + } + } + + std::memcpy(&impl->last_frame[0], &input_buffer[(frames_written - 1) * frame_size], + frame_size_bytes); + } else { + std::span output_buffer{reinterpret_cast(stream), num_frames * frame_size}; + + while (frames_written < num_frames) { + auto& playing_buffer{impl->playing_buffer}; + + // If the playing buffer has been consumed or has no frames, we need a new one + if (playing_buffer.consumed || playing_buffer.frames == 0) { + if (!impl->queue.try_dequeue(impl->playing_buffer)) { + // If no buffer was available we've underrun, fill the remaining buffer with + // the last written frame and continue. + underrun = true; + for (size_t i = frames_written; i < num_frames; i++) { + std::memcpy(&output_buffer[i * frame_size], &impl->last_frame[0], + frame_size_bytes); + } + frames_written = num_frames; + continue; + } else { + impl->queued_buffers--; + impl->SignalEvent(impl->playing_buffer); + } + } + + // Get the minimum frames available between the currently playing buffer, and the + // amount we have left to fill + size_t frames_available{ + std::min(playing_buffer.frames - playing_buffer.frames_played, + num_frames - frames_written)}; + + impl->samples_buffer.Pop(&output_buffer[frames_written * frame_size], + frames_available * frame_size); + + frames_written += frames_available; + playing_buffer.frames_played += frames_available; + + // If that's all the frames in the current buffer, add its samples and mark it as + // consumed + if (playing_buffer.frames_played >= playing_buffer.frames) { + impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); + impl->playing_buffer.consumed = true; + } + } + + std::memcpy(&impl->last_frame[0], &output_buffer[(frames_written - 1) * frame_size], + frame_size_bytes); + } + } + + /// SDL device id of the opened input/output device + SDL_AudioDeviceID device{}; + /// Type of this stream + StreamType type; + /// Core system + Core::System& system; + /// Ring buffer of the samples waiting to be played or consumed + Common::RingBuffer samples_buffer; + /// Audio buffers queued and waiting to play + Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> queue; + /// The currently-playing audio buffer + ::AudioCore::Sink::SinkBuffer playing_buffer{}; + /// Audio buffers which have been played and are in queue to be released by the audio system + Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> released_buffers{}; + /// Currently released buffer waiting to be taken by the audio system + ::AudioCore::Sink::SinkBuffer released_buffer{}; + /// The last played (or received) frame of audio, used when the callback underruns + std::array last_frame{}; +}; + +SDLSink::SDLSink(std::string_view target_device_name) { + if (!SDL_WasInit(SDL_INIT_AUDIO)) { + if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + LOG_CRITICAL(Audio_Sink, "SDL_InitSubSystem audio failed: {}", SDL_GetError()); + return; + } + } + + if (target_device_name != auto_device_name && !target_device_name.empty()) { + output_device = target_device_name; + } else { + output_device.clear(); + } + + device_channels = 2; +} + +SDLSink::~SDLSink() = default; + +SinkStream* SDLSink::AcquireSinkStream(Core::System& system, const u32 system_channels, + const std::string&, const StreamType type) { + SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique( + device_channels, system_channels, output_device, input_device, type, system)); + return stream.get(); +} + +void SDLSink::CloseStream(const SinkStream* stream) { + for (size_t i = 0; i < sink_streams.size(); i++) { + if (sink_streams[i].get() == stream) { + sink_streams[i].reset(); + sink_streams.erase(sink_streams.begin() + i); + break; + } + } +} + +void SDLSink::CloseStreams() { + sink_streams.clear(); +} + +void SDLSink::PauseStreams() { + for (auto& stream : sink_streams) { + stream->Stop(); + } +} + +void SDLSink::UnpauseStreams() { + for (auto& stream : sink_streams) { + stream->Start(); + } +} + +f32 SDLSink::GetDeviceVolume() const { + if (sink_streams.empty()) { + return 1.0f; + } + + return sink_streams[0]->GetDeviceVolume(); +} + +void SDLSink::SetDeviceVolume(const f32 volume) { + for (auto& stream : sink_streams) { + stream->SetDeviceVolume(volume); + } +} + +void SDLSink::SetSystemVolume(const f32 volume) { + for (auto& stream : sink_streams) { + stream->SetSystemVolume(volume); + } +} + +std::vector ListSDLSinkDevices(const bool capture) { + std::vector device_list; + + if (!SDL_WasInit(SDL_INIT_AUDIO)) { + if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + LOG_CRITICAL(Audio_Sink, "SDL_InitSubSystem audio failed: {}", SDL_GetError()); + return {}; + } + } + + const int device_count = SDL_GetNumAudioDevices(capture); + for (int i = 0; i < device_count; ++i) { + device_list.emplace_back(SDL_GetAudioDeviceName(i, 0)); + } + + return device_list; +} + +} // namespace AudioCore::Sink diff --git a/src/audio_core/sink/sdl2_sink.h b/src/audio_core/sink/sdl2_sink.h new file mode 100644 index 0000000000..186bc2fa3a --- /dev/null +++ b/src/audio_core/sink/sdl2_sink.h @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/sink/sink.h" + +namespace Core { +class System; +} + +namespace AudioCore::Sink { +class SinkStream; + +/** + * SDL backend sink, holds multiple output streams and is responsible for sinking samples to + * hardware. Used by Audio Render, Audio In and Audio Out. + */ +class SDLSink final : public Sink { +public: + explicit SDLSink(std::string_view device_id); + ~SDLSink() override; + + /** + * Create a new sink stream. + * + * @param system - Core system. + * @param system_channels - Number of channels the audio system expects. + * May differ from the device's channel count. + * @param name - Name of this stream. + * @param type - Type of this stream, render/in/out. + * @param event - Audio render only, a signal used to prevent the renderer running too + * fast. + * @return A pointer to the created SinkStream + */ + SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels, + const std::string& name, StreamType type) override; + + /** + * Close a given stream. + * + * @param stream - The stream to close. + */ + void CloseStream(const SinkStream* stream) override; + + /** + * Close all streams. + */ + void CloseStreams() override; + + /** + * Pause all streams. + */ + void PauseStreams() override; + + /** + * Unpause all streams. + */ + void UnpauseStreams() override; + + /** + * Get the device volume. Set from calls to the IAudioDevice service. + * + * @return Volume of the device. + */ + f32 GetDeviceVolume() const override; + + /** + * Set the device volume. Set from calls to the IAudioDevice service. + * + * @param volume - New volume of the device. + */ + void SetDeviceVolume(f32 volume) override; + + /** + * Set the system volume. Comes from the audio system using this stream. + * + * @param volume - New volume of the system. + */ + void SetSystemVolume(f32 volume) override; + +private: + /// Name of the output device used by streams + std::string output_device; + /// Name of the input device used by streams + std::string input_device; + /// Vector of streams managed by this sink + std::vector sink_streams; +}; + +/** + * Get a list of conencted devices from Cubeb. + * + * @param capture - Return input (capture) devices if true, otherwise output devices. + */ +std::vector ListSDLSinkDevices(bool capture); + +} // namespace AudioCore::Sink diff --git a/src/audio_core/sink/sink.h b/src/audio_core/sink/sink.h new file mode 100644 index 0000000000..91fe455e48 --- /dev/null +++ b/src/audio_core/sink/sink.h @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "audio_core/sink/sink_stream.h" +#include "common/common_types.h" + +namespace Common { +class Event; +} +namespace Core { +class System; +} + +namespace AudioCore::Sink { + +constexpr char auto_device_name[] = "auto"; + +/** + * This class is an interface for an audio sink, holds multiple output streams and is responsible + * for sinking samples to hardware. Used by Audio Render, Audio In and Audio Out. + */ +class Sink { +public: + virtual ~Sink() = default; + /** + * Close a given stream. + * + * @param stream - The stream to close. + */ + virtual void CloseStream(const SinkStream* stream) = 0; + + /** + * Close all streams. + */ + virtual void CloseStreams() = 0; + + /** + * Pause all streams. + */ + virtual void PauseStreams() = 0; + + /** + * Unpause all streams. + */ + virtual void UnpauseStreams() = 0; + + /** + * Create a new sink stream, kept within this sink, with a pointer returned for use. + * Do not free the returned pointer. When done with the stream, call CloseStream on the sink. + * + * @param system - Core system. + * @param system_channels - Number of channels the audio system expects. + * May differ from the device's channel count. + * @param name - Name of this stream. + * @param type - Type of this stream, render/in/out. + * @param event - Audio render only, a signal used to prevent the renderer running too + * fast. + * @return A pointer to the created SinkStream + */ + virtual SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels, + const std::string& name, StreamType type) = 0; + + /** + * Get the number of channels the hardware device supports. + * Either 2 or 6. + * + * @return Number of device channels. + */ + u32 GetDeviceChannels() const { + return device_channels; + } + + /** + * Get the device volume. Set from calls to the IAudioDevice service. + * + * @return Volume of the device. + */ + virtual f32 GetDeviceVolume() const = 0; + + /** + * Set the device volume. Set from calls to the IAudioDevice service. + * + * @param volume - New volume of the device. + */ + virtual void SetDeviceVolume(f32 volume) = 0; + + /** + * Set the system volume. Comes from the audio system using this stream. + * + * @param volume - New volume of the system. + */ + virtual void SetSystemVolume(f32 volume) = 0; + +protected: + /// Number of device channels supported by the hardware + u32 device_channels{2}; +}; + +using SinkPtr = std::unique_ptr; + +} // namespace AudioCore::Sink diff --git a/src/audio_core/sink_details.cpp b/src/audio_core/sink/sink_details.cpp similarity index 76% rename from src/audio_core/sink_details.cpp rename to src/audio_core/sink/sink_details.cpp index c4cc661119..253c0fd1e5 100644 --- a/src/audio_core/sink_details.cpp +++ b/src/audio_core/sink/sink_details.cpp @@ -5,21 +5,21 @@ #include #include #include -#include "audio_core/null_sink.h" -#include "audio_core/sink_details.h" +#include "audio_core/sink/null_sink.h" +#include "audio_core/sink/sink_details.h" #ifdef HAVE_CUBEB -#include "audio_core/cubeb_sink.h" +#include "audio_core/sink/cubeb_sink.h" #endif #ifdef HAVE_SDL2 -#include "audio_core/sdl2_sink.h" +#include "audio_core/sink/sdl2_sink.h" #endif #include "common/logging/log.h" -namespace AudioCore { +namespace AudioCore::Sink { namespace { struct SinkDetails { using FactoryFn = std::unique_ptr (*)(std::string_view); - using ListDevicesFn = std::vector (*)(); + using ListDevicesFn = std::vector (*)(bool); /// Name for this sink. const char* id; @@ -49,17 +49,18 @@ constexpr SinkDetails sink_details[] = { [](std::string_view device_id) -> std::unique_ptr { return std::make_unique(device_id); }, - [] { return std::vector{"null"}; }}, + [](bool capture) { return std::vector{"null"}; }}, }; -const SinkDetails& GetSinkDetails(std::string_view sink_id) { +const SinkDetails& GetOutputSinkDetails(std::string_view sink_id) { auto iter = std::find_if(std::begin(sink_details), std::end(sink_details), [sink_id](const auto& sink_detail) { return sink_detail.id == sink_id; }); if (sink_id == "auto" || iter == std::end(sink_details)) { if (sink_id != "auto") { - LOG_ERROR(Audio, "AudioCore::SelectSink given invalid sink_id {}", sink_id); + LOG_ERROR(Audio, "AudioCore::Sink::GetOutputSinkDetails given invalid sink_id {}", + sink_id); } // Auto-select. // sink_details is ordered in terms of desirability, with the best choice at the front. @@ -79,12 +80,12 @@ std::vector GetSinkIDs() { return sink_ids; } -std::vector GetDeviceListForSink(std::string_view sink_id) { - return GetSinkDetails(sink_id).list_devices(); +std::vector GetDeviceListForSink(std::string_view sink_id, bool capture) { + return GetOutputSinkDetails(sink_id).list_devices(capture); } std::unique_ptr CreateSinkFromID(std::string_view sink_id, std::string_view device_id) { - return GetSinkDetails(sink_id).factory(device_id); + return GetOutputSinkDetails(sink_id).factory(device_id); } -} // namespace AudioCore +} // namespace AudioCore::Sink diff --git a/src/audio_core/sink/sink_details.h b/src/audio_core/sink/sink_details.h new file mode 100644 index 0000000000..3ebdb1e305 --- /dev/null +++ b/src/audio_core/sink/sink_details.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +namespace AudioCore { +class AudioManager; + +namespace Sink { + +class Sink; + +/** + * Retrieves the IDs for all available audio sinks. + * + * @return Vector of available sink names. + */ +std::vector GetSinkIDs(); + +/** + * Gets the list of devices for a particular sink identified by the given ID. + * + * @param sink_id - Id of the sink to get devices from. + * @param capture - Get capture (input) devices, or output devices? + * @return Vector of device names. + */ +std::vector GetDeviceListForSink(std::string_view sink_id, bool capture); + +/** + * Creates an audio sink identified by the given device ID. + * + * @param sink_id - Id of the sink to create. + * @param device_id - Name of the device to create. + * @return Pointer to the created sink. + */ +std::unique_ptr CreateSinkFromID(std::string_view sink_id, std::string_view device_id); + +} // namespace Sink +} // namespace AudioCore diff --git a/src/audio_core/sink/sink_stream.h b/src/audio_core/sink/sink_stream.h new file mode 100644 index 0000000000..17ed6593fb --- /dev/null +++ b/src/audio_core/sink/sink_stream.h @@ -0,0 +1,224 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "audio_core/common/common.h" +#include "common/common_types.h" + +namespace AudioCore::Sink { + +enum class StreamType { + Render, + Out, + In, +}; + +struct SinkBuffer { + u64 frames; + u64 frames_played; + u64 tag; + bool consumed; +}; + +/** + * Contains a real backend stream for outputting samples to hardware, + * created only via a Sink (See Sink::AcquireSinkStream). + * + * Accepts a SinkBuffer and samples in PCM16 format to be output (see AppendBuffer). + * Appended buffers act as a FIFO queue, and will be held until played. + * You should regularly call IsBufferConsumed with the unique SinkBuffer tag to check if the buffer + * has been consumed. + * + * Since these are a FIFO queue, always check IsBufferConsumed in the same order you appended the + * buffers, skipping a buffer will result in all following buffers to never release. + * + * If the buffers appear to be stuck, you can stop and re-open an IAudioIn/IAudioOut service (this + * is what games do), or call ClearQueue to flush all of the buffers without a full restart. + */ +class SinkStream { +public: + virtual ~SinkStream() = default; + + /** + * Finalize the sink stream. + */ + virtual void Finalize() = 0; + + /** + * Start the sink stream. + * + * @param resume - Set to true if this is resuming the stream a previously-active stream. + * Default false. + */ + virtual void Start(bool resume = false) = 0; + + /** + * Stop the sink stream. + */ + virtual void Stop() = 0; + + /** + * Append a new buffer and its samples to a waiting queue to play. + * + * @param buffer - Audio buffer information to be queued. + * @param samples - The s16 samples to be queue for playback. + */ + virtual void AppendBuffer(SinkBuffer& buffer, std::vector& samples) = 0; + + /** + * Release a buffer. Audio In only, will fill a buffer with recorded samples. + * + * @param num_samples - Maximum number of samples to receive. + * @return Vector of recorded samples. May have fewer than num_samples. + */ + virtual std::vector ReleaseBuffer(u64 num_samples) = 0; + + /** + * Check if a certain buffer has been consumed (fully played). + * + * @param tag - Unique tag of a buffer to check for. + * @return True if the buffer has been played, otherwise false. + */ + virtual bool IsBufferConsumed(u64 tag) = 0; + + /** + * Empty out the buffer queue. + */ + virtual void ClearQueue() = 0; + + /** + * Check if the stream is paused. + * + * @return True if paused, otherwise false. + */ + bool IsPaused() { + return paused; + } + + /** + * Get the number of system channels in this stream. + * + * @return Number of system channels. + */ + u32 GetSystemChannels() const { + return system_channels; + } + + /** + * Set the number of channels the system expects. + * + * @param channels - New number of system channels. + */ + void SetSystemChannels(u32 channels) { + system_channels = channels; + } + + /** + * Get the number of channels the hardware supports. + * + * @return Number of channels supported. + */ + u32 GetDeviceChannels() const { + return device_channels; + } + + /** + * Get the total number of samples played by this stream. + * + * @return Number of samples played. + */ + u64 GetPlayedSampleCount() const { + return played_sample_count; + } + + /** + * Set the number of samples played. + * This is started and stopped on system start/stop. + * + * @param played_sample_count_ - Number of samples to set. + */ + void SetPlayedSampleCount(u64 played_sample_count_) { + played_sample_count = played_sample_count_; + } + + /** + * Add to the played sample count. + * + * @param num_samples - Number of samples to add. + */ + void AddPlayedSampleCount(u64 num_samples) { + played_sample_count += num_samples; + } + + /** + * Get the system volume. + * + * @return The current system volume. + */ + f32 GetSystemVolume() const { + return system_volume; + } + + /** + * Get the device volume. + * + * @return The current device volume. + */ + f32 GetDeviceVolume() const { + return device_volume; + } + + /** + * Set the system volume. + * + * @param volume_ - The new system volume. + */ + void SetSystemVolume(f32 volume_) { + system_volume = volume_; + } + + /** + * Set the device volume. + * + * @param volume_ - The new device volume. + */ + void SetDeviceVolume(f32 volume_) { + device_volume = volume_; + } + + /** + * Get the number of queued audio buffers. + * + * @return The number of queued buffers. + */ + u32 GetQueueSize() { + return queued_buffers.load(); + } + +protected: + /// Number of buffers waiting to be played + std::atomic queued_buffers{}; + /// Total samples played by this stream + std::atomic played_sample_count{}; + /// Set by the audio render/in/out system which uses this stream + f32 system_volume{1.0f}; + /// Set via IAudioDevice service calls + f32 device_volume{1.0f}; + /// Set by the audio render/in/out systen which uses this stream + u32 system_channels{2}; + /// Channels supported by hardware + u32 device_channels{2}; + /// Is this stream currently paused? + std::atomic paused{true}; + /// Was this stream previously playing? + std::atomic was_playing{false}; +}; + +using SinkStreamPtr = std::unique_ptr; + +} // namespace AudioCore::Sink diff --git a/src/audio_core/sink_context.cpp b/src/audio_core/sink_context.cpp deleted file mode 100644 index 835e12f67a..0000000000 --- a/src/audio_core/sink_context.cpp +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "audio_core/sink_context.h" - -namespace AudioCore { -SinkContext::SinkContext(std::size_t sink_count_) : sink_count{sink_count_} {} -SinkContext::~SinkContext() = default; - -std::size_t SinkContext::GetCount() const { - return sink_count; -} - -void SinkContext::UpdateMainSink(const SinkInfo::InParams& in) { - ASSERT(in.type == SinkTypes::Device); - - if (in.device.down_matrix_enabled) { - downmix_coefficients = in.device.down_matrix_coef; - } else { - downmix_coefficients = { - 1.0f, // front - 0.707f, // center - 0.0f, // lfe - 0.707f, // back - }; - } - - in_use = in.in_use; - use_count = in.device.input_count; - buffers = in.device.input; -} - -bool SinkContext::InUse() const { - return in_use; -} - -std::vector SinkContext::OutputBuffers() const { - std::vector buffer_ret(use_count); - std::memcpy(buffer_ret.data(), buffers.data(), use_count); - return buffer_ret; -} - -const DownmixCoefficients& SinkContext::GetDownmixCoefficients() const { - return downmix_coefficients; -} - -} // namespace AudioCore diff --git a/src/audio_core/sink_context.h b/src/audio_core/sink_context.h deleted file mode 100644 index cc5a90d804..0000000000 --- a/src/audio_core/sink_context.h +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include "audio_core/common.h" -#include "common/common_funcs.h" -#include "common/common_types.h" -#include "common/swap.h" - -namespace AudioCore { - -using DownmixCoefficients = std::array; - -enum class SinkTypes : u8 { - Invalid = 0, - Device = 1, - Circular = 2, -}; - -enum class SinkSampleFormat : u32_le { - None = 0, - Pcm8 = 1, - Pcm16 = 2, - Pcm24 = 3, - Pcm32 = 4, - PcmFloat = 5, - Adpcm = 6, -}; - -class SinkInfo { -public: - struct CircularBufferIn { - u64_le address; - u32_le size; - u32_le input_count; - u32_le sample_count; - u32_le previous_position; - SinkSampleFormat sample_format; - std::array input; - bool in_use; - INSERT_PADDING_BYTES_NOINIT(5); - }; - static_assert(sizeof(CircularBufferIn) == 0x28, - "SinkInfo::CircularBufferIn is in invalid size"); - - struct DeviceIn { - std::array device_name; - INSERT_PADDING_BYTES_NOINIT(1); - s32_le input_count; - std::array input; - INSERT_PADDING_BYTES_NOINIT(1); - bool down_matrix_enabled; - DownmixCoefficients down_matrix_coef; - }; - static_assert(sizeof(DeviceIn) == 0x11c, "SinkInfo::DeviceIn is an invalid size"); - - struct InParams { - SinkTypes type{}; - bool in_use{}; - INSERT_PADDING_BYTES(2); - u32_le node_id{}; - INSERT_PADDING_WORDS(6); - union { - // std::array raw{}; - DeviceIn device; - CircularBufferIn circular_buffer; - }; - }; - static_assert(sizeof(InParams) == 0x140, "SinkInfo::InParams are an invalid size!"); -}; - -class SinkContext { -public: - explicit SinkContext(std::size_t sink_count_); - ~SinkContext(); - - [[nodiscard]] std::size_t GetCount() const; - - void UpdateMainSink(const SinkInfo::InParams& in); - [[nodiscard]] bool InUse() const; - [[nodiscard]] std::vector OutputBuffers() const; - - [[nodiscard]] const DownmixCoefficients& GetDownmixCoefficients() const; - -private: - bool in_use{false}; - s32 use_count{}; - std::array buffers{}; - std::size_t sink_count{}; - DownmixCoefficients downmix_coefficients{}; -}; -} // namespace AudioCore diff --git a/src/audio_core/sink_details.h b/src/audio_core/sink_details.h deleted file mode 100644 index 0427663582..0000000000 --- a/src/audio_core/sink_details.h +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include - -namespace AudioCore { - -class Sink; - -/// Retrieves the IDs for all available audio sinks. -std::vector GetSinkIDs(); - -/// Gets the list of devices for a particular sink identified by the given ID. -std::vector GetDeviceListForSink(std::string_view sink_id); - -/// Creates an audio sink identified by the given device ID. -std::unique_ptr CreateSinkFromID(std::string_view sink_id, std::string_view device_id); - -} // namespace AudioCore diff --git a/src/audio_core/sink_stream.h b/src/audio_core/sink_stream.h deleted file mode 100644 index 0449b90afa..0000000000 --- a/src/audio_core/sink_stream.h +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include - -#include "common/common_types.h" - -namespace AudioCore { - -/** - * Accepts samples in stereo signed PCM16 format to be output. Sinks *do not* handle resampling and - * expect the correct sample rate. They are dumb outputs. - */ -class SinkStream { -public: - virtual ~SinkStream() = default; - - /** - * Feed stereo samples to sink. - * @param num_channels Number of channels used. - * @param samples Samples in interleaved stereo PCM16 format. - */ - virtual void EnqueueSamples(u32 num_channels, const std::vector& samples) = 0; - - virtual std::size_t SamplesInQueue(u32 num_channels) const = 0; - - virtual void Flush() = 0; -}; - -using SinkStreamPtr = std::unique_ptr; - -} // namespace AudioCore diff --git a/src/audio_core/splitter_context.cpp b/src/audio_core/splitter_context.cpp deleted file mode 100644 index 10646dc059..0000000000 --- a/src/audio_core/splitter_context.cpp +++ /dev/null @@ -1,616 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "audio_core/behavior_info.h" -#include "audio_core/splitter_context.h" -#include "common/alignment.h" -#include "common/assert.h" -#include "common/logging/log.h" - -namespace AudioCore { - -ServerSplitterDestinationData::ServerSplitterDestinationData(s32 id_) : id{id_} {} -ServerSplitterDestinationData::~ServerSplitterDestinationData() = default; - -void ServerSplitterDestinationData::Update(SplitterInfo::InDestinationParams& header) { - // Log error as these are not actually failure states - if (header.magic != SplitterMagic::DataHeader) { - LOG_ERROR(Audio, "Splitter destination header is invalid!"); - return; - } - - // Incorrect splitter id - if (header.splitter_id != id) { - LOG_ERROR(Audio, "Splitter destination ids do not match!"); - return; - } - - mix_id = header.mix_id; - // Copy our mix volumes - std::copy(header.mix_volumes.begin(), header.mix_volumes.end(), current_mix_volumes.begin()); - if (!in_use && header.in_use) { - // Update mix volumes - std::copy(current_mix_volumes.begin(), current_mix_volumes.end(), last_mix_volumes.begin()); - needs_update = false; - } - in_use = header.in_use; -} - -ServerSplitterDestinationData* ServerSplitterDestinationData::GetNextDestination() { - return next; -} - -const ServerSplitterDestinationData* ServerSplitterDestinationData::GetNextDestination() const { - return next; -} - -void ServerSplitterDestinationData::SetNextDestination(ServerSplitterDestinationData* dest) { - next = dest; -} - -bool ServerSplitterDestinationData::ValidMixId() const { - return GetMixId() != AudioCommon::NO_MIX; -} - -s32 ServerSplitterDestinationData::GetMixId() const { - return mix_id; -} - -bool ServerSplitterDestinationData::IsConfigured() const { - return in_use && ValidMixId(); -} - -float ServerSplitterDestinationData::GetMixVolume(std::size_t i) const { - ASSERT(i < AudioCommon::MAX_MIX_BUFFERS); - return current_mix_volumes.at(i); -} - -const std::array& -ServerSplitterDestinationData::CurrentMixVolumes() const { - return current_mix_volumes; -} - -const std::array& -ServerSplitterDestinationData::LastMixVolumes() const { - return last_mix_volumes; -} - -void ServerSplitterDestinationData::MarkDirty() { - needs_update = true; -} - -void ServerSplitterDestinationData::UpdateInternalState() { - if (in_use && needs_update) { - std::copy(current_mix_volumes.begin(), current_mix_volumes.end(), last_mix_volumes.begin()); - } - needs_update = false; -} - -ServerSplitterInfo::ServerSplitterInfo(s32 id_) : id(id_) {} -ServerSplitterInfo::~ServerSplitterInfo() = default; - -void ServerSplitterInfo::InitializeInfos() { - send_length = 0; - head = nullptr; - new_connection = true; -} - -void ServerSplitterInfo::ClearNewConnectionFlag() { - new_connection = false; -} - -std::size_t ServerSplitterInfo::Update(SplitterInfo::InInfoPrams& header) { - if (header.send_id != id) { - return 0; - } - - sample_rate = header.sample_rate; - new_connection = true; - // We need to update the size here due to the splitter bug being present and providing an - // incorrect size. We're suppose to also update the header here but we just ignore and continue - return (sizeof(s32_le) * (header.length - 1)) + (sizeof(s32_le) * 3); -} - -ServerSplitterDestinationData* ServerSplitterInfo::GetHead() { - return head; -} - -const ServerSplitterDestinationData* ServerSplitterInfo::GetHead() const { - return head; -} - -ServerSplitterDestinationData* ServerSplitterInfo::GetData(std::size_t depth) { - auto* current_head = head; - for (std::size_t i = 0; i < depth; i++) { - if (current_head == nullptr) { - return nullptr; - } - current_head = current_head->GetNextDestination(); - } - return current_head; -} - -const ServerSplitterDestinationData* ServerSplitterInfo::GetData(std::size_t depth) const { - auto* current_head = head; - for (std::size_t i = 0; i < depth; i++) { - if (current_head == nullptr) { - return nullptr; - } - current_head = current_head->GetNextDestination(); - } - return current_head; -} - -bool ServerSplitterInfo::HasNewConnection() const { - return new_connection; -} - -s32 ServerSplitterInfo::GetLength() const { - return send_length; -} - -void ServerSplitterInfo::SetHead(ServerSplitterDestinationData* new_head) { - head = new_head; -} - -void ServerSplitterInfo::SetHeadDepth(s32 length) { - send_length = length; -} - -SplitterContext::SplitterContext() = default; -SplitterContext::~SplitterContext() = default; - -void SplitterContext::Initialize(BehaviorInfo& behavior_info, std::size_t _info_count, - std::size_t _data_count) { - if (!behavior_info.IsSplitterSupported() || _data_count == 0 || _info_count == 0) { - Setup(0, 0, false); - return; - } - // Only initialize if we're using splitters - Setup(_info_count, _data_count, behavior_info.IsSplitterBugFixed()); -} - -bool SplitterContext::Update(const std::vector& input, std::size_t& input_offset, - std::size_t& bytes_read) { - const auto UpdateOffsets = [&](std::size_t read) { - input_offset += read; - bytes_read += read; - }; - - if (info_count == 0 || data_count == 0) { - bytes_read = 0; - return true; - } - - if (!AudioCommon::CanConsumeBuffer(input.size(), input_offset, - sizeof(SplitterInfo::InHeader))) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return false; - } - SplitterInfo::InHeader header{}; - std::memcpy(&header, input.data() + input_offset, sizeof(SplitterInfo::InHeader)); - UpdateOffsets(sizeof(SplitterInfo::InHeader)); - - if (header.magic != SplitterMagic::SplitterHeader) { - LOG_ERROR(Audio, "Invalid header magic! Expecting {:X} but got {:X}", - SplitterMagic::SplitterHeader, header.magic); - return false; - } - - // Clear all connections - for (auto& info : infos) { - info.ClearNewConnectionFlag(); - } - - UpdateInfo(input, input_offset, bytes_read, header.info_count); - UpdateData(input, input_offset, bytes_read, header.data_count); - const auto aligned_bytes_read = Common::AlignUp(bytes_read, 16); - input_offset += aligned_bytes_read - bytes_read; - bytes_read = aligned_bytes_read; - return true; -} - -bool SplitterContext::UsingSplitter() const { - return info_count > 0 && data_count > 0; -} - -ServerSplitterInfo& SplitterContext::GetInfo(std::size_t i) { - ASSERT(i < info_count); - return infos.at(i); -} - -const ServerSplitterInfo& SplitterContext::GetInfo(std::size_t i) const { - ASSERT(i < info_count); - return infos.at(i); -} - -ServerSplitterDestinationData& SplitterContext::GetData(std::size_t i) { - ASSERT(i < data_count); - return datas.at(i); -} - -const ServerSplitterDestinationData& SplitterContext::GetData(std::size_t i) const { - ASSERT(i < data_count); - return datas.at(i); -} - -ServerSplitterDestinationData* SplitterContext::GetDestinationData(std::size_t info, - std::size_t data) { - ASSERT(info < info_count); - auto& cur_info = GetInfo(info); - return cur_info.GetData(data); -} - -const ServerSplitterDestinationData* SplitterContext::GetDestinationData(std::size_t info, - std::size_t data) const { - ASSERT(info < info_count); - const auto& cur_info = GetInfo(info); - return cur_info.GetData(data); -} - -void SplitterContext::UpdateInternalState() { - if (data_count == 0) { - return; - } - - for (auto& data : datas) { - data.UpdateInternalState(); - } -} - -std::size_t SplitterContext::GetInfoCount() const { - return info_count; -} - -std::size_t SplitterContext::GetDataCount() const { - return data_count; -} - -void SplitterContext::Setup(std::size_t info_count_, std::size_t data_count_, - bool is_splitter_bug_fixed) { - - info_count = info_count_; - data_count = data_count_; - - for (std::size_t i = 0; i < info_count; i++) { - auto& splitter = infos.emplace_back(static_cast(i)); - splitter.InitializeInfos(); - } - for (std::size_t i = 0; i < data_count; i++) { - datas.emplace_back(static_cast(i)); - } - - bug_fixed = is_splitter_bug_fixed; -} - -bool SplitterContext::UpdateInfo(const std::vector& input, std::size_t& input_offset, - std::size_t& bytes_read, s32 in_splitter_count) { - const auto UpdateOffsets = [&](std::size_t read) { - input_offset += read; - bytes_read += read; - }; - - for (s32 i = 0; i < in_splitter_count; i++) { - if (!AudioCommon::CanConsumeBuffer(input.size(), input_offset, - sizeof(SplitterInfo::InInfoPrams))) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return false; - } - SplitterInfo::InInfoPrams header{}; - std::memcpy(&header, input.data() + input_offset, sizeof(SplitterInfo::InInfoPrams)); - - // Logged as warning as these don't actually cause a bailout for some reason - if (header.magic != SplitterMagic::InfoHeader) { - LOG_ERROR(Audio, "Bad splitter data header"); - break; - } - - if (header.send_id < 0 || static_cast(header.send_id) > info_count) { - LOG_ERROR(Audio, "Bad splitter data id"); - break; - } - - UpdateOffsets(sizeof(SplitterInfo::InInfoPrams)); - auto& info = GetInfo(header.send_id); - if (!RecomposeDestination(info, header, input, input_offset)) { - LOG_ERROR(Audio, "Failed to recompose destination for splitter!"); - return false; - } - const std::size_t read = info.Update(header); - bytes_read += read; - input_offset += read; - } - return true; -} - -bool SplitterContext::UpdateData(const std::vector& input, std::size_t& input_offset, - std::size_t& bytes_read, s32 in_data_count) { - const auto UpdateOffsets = [&](std::size_t read) { - input_offset += read; - bytes_read += read; - }; - - for (s32 i = 0; i < in_data_count; i++) { - if (!AudioCommon::CanConsumeBuffer(input.size(), input_offset, - sizeof(SplitterInfo::InDestinationParams))) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return false; - } - SplitterInfo::InDestinationParams header{}; - std::memcpy(&header, input.data() + input_offset, - sizeof(SplitterInfo::InDestinationParams)); - UpdateOffsets(sizeof(SplitterInfo::InDestinationParams)); - - // Logged as warning as these don't actually cause a bailout for some reason - if (header.magic != SplitterMagic::DataHeader) { - LOG_ERROR(Audio, "Bad splitter data header"); - break; - } - - if (header.splitter_id < 0 || static_cast(header.splitter_id) > data_count) { - LOG_ERROR(Audio, "Bad splitter data id"); - break; - } - GetData(header.splitter_id).Update(header); - } - return true; -} - -bool SplitterContext::RecomposeDestination(ServerSplitterInfo& info, - SplitterInfo::InInfoPrams& header, - const std::vector& input, - const std::size_t& input_offset) { - // Clear our current destinations - auto* current_head = info.GetHead(); - while (current_head != nullptr) { - auto* next_head = current_head->GetNextDestination(); - current_head->SetNextDestination(nullptr); - current_head = next_head; - } - info.SetHead(nullptr); - - s32 size = header.length; - // If the splitter bug is present, calculate fixed size - if (!bug_fixed) { - if (info_count > 0) { - const auto factor = data_count / info_count; - size = std::min(header.length, static_cast(factor)); - } else { - size = 0; - } - } - - if (size < 1) { - LOG_ERROR(Audio, "Invalid splitter info size! size={:X}", size); - return true; - } - - auto* start_head = &GetData(header.resource_id_base); - current_head = start_head; - std::vector resource_ids(size - 1); - if (!AudioCommon::CanConsumeBuffer(input.size(), input_offset, - resource_ids.size() * sizeof(s32_le))) { - LOG_ERROR(Audio, "Buffer is an invalid size!"); - return false; - } - std::memcpy(resource_ids.data(), input.data() + input_offset, - resource_ids.size() * sizeof(s32_le)); - - for (auto resource_id : resource_ids) { - auto* head = &GetData(resource_id); - current_head->SetNextDestination(head); - current_head = head; - } - - info.SetHead(start_head); - info.SetHeadDepth(size); - - return true; -} - -NodeStates::NodeStates() = default; -NodeStates::~NodeStates() = default; - -void NodeStates::Initialize(std::size_t node_count_) { - // Setup our work parameters - node_count = node_count_; - was_node_found.resize(node_count); - was_node_completed.resize(node_count); - index_list.resize(node_count); - index_stack.Reset(node_count * node_count); -} - -bool NodeStates::Tsort(EdgeMatrix& edge_matrix) { - return DepthFirstSearch(edge_matrix); -} - -std::size_t NodeStates::GetIndexPos() const { - return index_pos; -} - -const std::vector& NodeStates::GetIndexList() const { - return index_list; -} - -void NodeStates::PushTsortResult(s32 index) { - ASSERT(index < static_cast(node_count)); - index_list[index_pos++] = index; -} - -bool NodeStates::DepthFirstSearch(EdgeMatrix& edge_matrix) { - ResetState(); - for (std::size_t i = 0; i < node_count; i++) { - const auto node_id = static_cast(i); - - // If we don't have a state, send to our index stack for work - if (GetState(i) == NodeStates::State::NoState) { - index_stack.push(node_id); - } - - // While we have work to do in our stack - while (index_stack.Count() > 0) { - // Get the current node - const auto current_stack_index = index_stack.top(); - // Check if we've seen the node yet - const auto index_state = GetState(current_stack_index); - if (index_state == NodeStates::State::NoState) { - // Mark the node as seen - UpdateState(NodeStates::State::InFound, current_stack_index); - } else if (index_state == NodeStates::State::InFound) { - // We've seen this node before, mark it as completed - UpdateState(NodeStates::State::InCompleted, current_stack_index); - // Update our index list - PushTsortResult(current_stack_index); - // Pop the stack - index_stack.pop(); - continue; - } else if (index_state == NodeStates::State::InCompleted) { - // If our node is already sorted, clear it - index_stack.pop(); - continue; - } - - const auto edge_node_count = edge_matrix.GetNodeCount(); - for (s32 j = 0; j < static_cast(edge_node_count); j++) { - // Check if our node is connected to our edge matrix - if (!edge_matrix.Connected(current_stack_index, j)) { - continue; - } - - // Check if our node exists - const auto node_state = GetState(j); - if (node_state == NodeStates::State::NoState) { - // Add more work - index_stack.push(j); - } else if (node_state == NodeStates::State::InFound) { - ASSERT_MSG(false, "Node start marked as found"); - ResetState(); - return false; - } - } - } - } - return true; -} - -void NodeStates::ResetState() { - // Reset to the start of our index stack - index_pos = 0; - for (std::size_t i = 0; i < node_count; i++) { - // Mark all nodes as not found - was_node_found[i] = false; - // Mark all nodes as uncompleted - was_node_completed[i] = false; - // Mark all indexes as invalid - index_list[i] = -1; - } -} - -void NodeStates::UpdateState(NodeStates::State state, std::size_t i) { - switch (state) { - case NodeStates::State::NoState: - was_node_found[i] = false; - was_node_completed[i] = false; - break; - case NodeStates::State::InFound: - was_node_found[i] = true; - was_node_completed[i] = false; - break; - case NodeStates::State::InCompleted: - was_node_found[i] = false; - was_node_completed[i] = true; - break; - } -} - -NodeStates::State NodeStates::GetState(std::size_t i) { - ASSERT(i < node_count); - if (was_node_found[i]) { - // If our node exists in our found list - return NodeStates::State::InFound; - } else if (was_node_completed[i]) { - // If node is in the completed list - return NodeStates::State::InCompleted; - } else { - // If in neither - return NodeStates::State::NoState; - } -} - -NodeStates::Stack::Stack() = default; -NodeStates::Stack::~Stack() = default; - -void NodeStates::Stack::Reset(std::size_t size) { - // Mark our stack as empty - stack.resize(size); - stack_size = size; - stack_pos = 0; - std::fill(stack.begin(), stack.end(), 0); -} - -void NodeStates::Stack::push(s32 val) { - ASSERT(stack_pos < stack_size); - stack[stack_pos++] = val; -} - -std::size_t NodeStates::Stack::Count() const { - return stack_pos; -} - -s32 NodeStates::Stack::top() const { - ASSERT(stack_pos > 0); - return stack[stack_pos - 1]; -} - -s32 NodeStates::Stack::pop() { - ASSERT(stack_pos > 0); - stack_pos--; - return stack[stack_pos]; -} - -EdgeMatrix::EdgeMatrix() = default; -EdgeMatrix::~EdgeMatrix() = default; - -void EdgeMatrix::Initialize(std::size_t _node_count) { - node_count = _node_count; - edge_matrix.resize(node_count * node_count); -} - -bool EdgeMatrix::Connected(s32 a, s32 b) { - return GetState(a, b); -} - -void EdgeMatrix::Connect(s32 a, s32 b) { - SetState(a, b, true); -} - -void EdgeMatrix::Disconnect(s32 a, s32 b) { - SetState(a, b, false); -} - -void EdgeMatrix::RemoveEdges(s32 edge) { - for (std::size_t i = 0; i < node_count; i++) { - SetState(edge, static_cast(i), false); - } -} - -std::size_t EdgeMatrix::GetNodeCount() const { - return node_count; -} - -void EdgeMatrix::SetState(s32 a, s32 b, bool state) { - ASSERT(InRange(a, b)); - edge_matrix.at(a * node_count + b) = state; -} - -bool EdgeMatrix::GetState(s32 a, s32 b) { - ASSERT(InRange(a, b)); - return edge_matrix.at(a * node_count + b); -} - -bool EdgeMatrix::InRange(s32 a, s32 b) const { - const std::size_t pos = a * node_count + b; - return pos < (node_count * node_count); -} - -} // namespace AudioCore diff --git a/src/audio_core/splitter_context.h b/src/audio_core/splitter_context.h deleted file mode 100644 index 3a4b055eb5..0000000000 --- a/src/audio_core/splitter_context.h +++ /dev/null @@ -1,218 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include "audio_core/common.h" -#include "common/common_funcs.h" -#include "common/common_types.h" -#include "common/swap.h" - -namespace AudioCore { -class BehaviorInfo; - -class EdgeMatrix { -public: - EdgeMatrix(); - ~EdgeMatrix(); - - void Initialize(std::size_t _node_count); - bool Connected(s32 a, s32 b); - void Connect(s32 a, s32 b); - void Disconnect(s32 a, s32 b); - void RemoveEdges(s32 edge); - std::size_t GetNodeCount() const; - -private: - void SetState(s32 a, s32 b, bool state); - bool GetState(s32 a, s32 b); - - bool InRange(s32 a, s32 b) const; - std::vector edge_matrix{}; - std::size_t node_count{}; -}; - -class NodeStates { -public: - enum class State { - NoState = 0, - InFound = 1, - InCompleted = 2, - }; - - // Looks to be a fixed size stack. Placed within the NodeStates class based on symbols - class Stack { - public: - Stack(); - ~Stack(); - - void Reset(std::size_t size); - void push(s32 val); - std::size_t Count() const; - s32 top() const; - s32 pop(); - - private: - std::vector stack{}; - std::size_t stack_size{}; - std::size_t stack_pos{}; - }; - NodeStates(); - ~NodeStates(); - - void Initialize(std::size_t node_count_); - bool Tsort(EdgeMatrix& edge_matrix); - std::size_t GetIndexPos() const; - const std::vector& GetIndexList() const; - -private: - void PushTsortResult(s32 index); - bool DepthFirstSearch(EdgeMatrix& edge_matrix); - void ResetState(); - void UpdateState(State state, std::size_t i); - State GetState(std::size_t i); - - std::size_t node_count{}; - std::vector was_node_found{}; - std::vector was_node_completed{}; - std::size_t index_pos{}; - std::vector index_list{}; - Stack index_stack{}; -}; - -enum class SplitterMagic : u32_le { - SplitterHeader = Common::MakeMagic('S', 'N', 'D', 'H'), - DataHeader = Common::MakeMagic('S', 'N', 'D', 'D'), - InfoHeader = Common::MakeMagic('S', 'N', 'D', 'I'), -}; - -class SplitterInfo { -public: - struct InHeader { - SplitterMagic magic{}; - s32_le info_count{}; - s32_le data_count{}; - INSERT_PADDING_WORDS(5); - }; - static_assert(sizeof(InHeader) == 0x20, "SplitterInfo::InHeader is an invalid size"); - - struct InInfoPrams { - SplitterMagic magic{}; - s32_le send_id{}; - s32_le sample_rate{}; - s32_le length{}; - s32_le resource_id_base{}; - }; - static_assert(sizeof(InInfoPrams) == 0x14, "SplitterInfo::InInfoPrams is an invalid size"); - - struct InDestinationParams { - SplitterMagic magic{}; - s32_le splitter_id{}; - std::array mix_volumes{}; - s32_le mix_id{}; - bool in_use{}; - INSERT_PADDING_BYTES(3); - }; - static_assert(sizeof(InDestinationParams) == 0x70, - "SplitterInfo::InDestinationParams is an invalid size"); -}; - -class ServerSplitterDestinationData { -public: - explicit ServerSplitterDestinationData(s32 id_); - ~ServerSplitterDestinationData(); - - void Update(SplitterInfo::InDestinationParams& header); - - ServerSplitterDestinationData* GetNextDestination(); - const ServerSplitterDestinationData* GetNextDestination() const; - void SetNextDestination(ServerSplitterDestinationData* dest); - bool ValidMixId() const; - s32 GetMixId() const; - bool IsConfigured() const; - float GetMixVolume(std::size_t i) const; - const std::array& CurrentMixVolumes() const; - const std::array& LastMixVolumes() const; - void MarkDirty(); - void UpdateInternalState(); - -private: - bool needs_update{}; - bool in_use{}; - s32 id{}; - s32 mix_id{}; - std::array current_mix_volumes{}; - std::array last_mix_volumes{}; - ServerSplitterDestinationData* next = nullptr; -}; - -class ServerSplitterInfo { -public: - explicit ServerSplitterInfo(s32 id_); - ~ServerSplitterInfo(); - - void InitializeInfos(); - void ClearNewConnectionFlag(); - std::size_t Update(SplitterInfo::InInfoPrams& header); - - ServerSplitterDestinationData* GetHead(); - const ServerSplitterDestinationData* GetHead() const; - ServerSplitterDestinationData* GetData(std::size_t depth); - const ServerSplitterDestinationData* GetData(std::size_t depth) const; - - bool HasNewConnection() const; - s32 GetLength() const; - - void SetHead(ServerSplitterDestinationData* new_head); - void SetHeadDepth(s32 length); - -private: - s32 sample_rate{}; - s32 id{}; - s32 send_length{}; - ServerSplitterDestinationData* head = nullptr; - bool new_connection{}; -}; - -class SplitterContext { -public: - SplitterContext(); - ~SplitterContext(); - - void Initialize(BehaviorInfo& behavior_info, std::size_t splitter_count, - std::size_t data_count); - - bool Update(const std::vector& input, std::size_t& input_offset, std::size_t& bytes_read); - bool UsingSplitter() const; - - ServerSplitterInfo& GetInfo(std::size_t i); - const ServerSplitterInfo& GetInfo(std::size_t i) const; - ServerSplitterDestinationData& GetData(std::size_t i); - const ServerSplitterDestinationData& GetData(std::size_t i) const; - ServerSplitterDestinationData* GetDestinationData(std::size_t info, std::size_t data); - const ServerSplitterDestinationData* GetDestinationData(std::size_t info, - std::size_t data) const; - void UpdateInternalState(); - - std::size_t GetInfoCount() const; - std::size_t GetDataCount() const; - -private: - void Setup(std::size_t info_count, std::size_t data_count, bool is_splitter_bug_fixed); - bool UpdateInfo(const std::vector& input, std::size_t& input_offset, - std::size_t& bytes_read, s32 in_splitter_count); - bool UpdateData(const std::vector& input, std::size_t& input_offset, - std::size_t& bytes_read, s32 in_data_count); - bool RecomposeDestination(ServerSplitterInfo& info, SplitterInfo::InInfoPrams& header, - const std::vector& input, const std::size_t& input_offset); - - std::vector infos{}; - std::vector datas{}; - - std::size_t info_count{}; - std::size_t data_count{}; - bool bug_fixed{}; -}; -} // namespace AudioCore diff --git a/src/audio_core/stream.cpp b/src/audio_core/stream.cpp deleted file mode 100644 index cf3d94c537..0000000000 --- a/src/audio_core/stream.cpp +++ /dev/null @@ -1,175 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include -#include - -#include "audio_core/sink.h" -#include "audio_core/sink_details.h" -#include "audio_core/sink_stream.h" -#include "audio_core/stream.h" -#include "common/assert.h" -#include "common/logging/log.h" -#include "common/settings.h" -#include "core/core_timing.h" - -namespace AudioCore { - -constexpr std::size_t MaxAudioBufferCount{32}; - -u32 Stream::GetNumChannels() const { - switch (format) { - case Format::Mono16: - return 1; - case Format::Stereo16: - return 2; - case Format::Multi51Channel16: - return 6; - } - UNIMPLEMENTED_MSG("Unimplemented format={}", static_cast(format)); - return {}; -} - -Stream::Stream(Core::Timing::CoreTiming& core_timing_, u32 sample_rate_, Format format_, - ReleaseCallback&& release_callback_, SinkStream& sink_stream_, std::string&& name_) - : sample_rate{sample_rate_}, format{format_}, release_callback{std::move(release_callback_)}, - sink_stream{sink_stream_}, core_timing{core_timing_}, name{std::move(name_)} { - release_event = Core::Timing::CreateEvent( - name, [this](std::uintptr_t, s64 time, std::chrono::nanoseconds ns_late) { - ReleaseActiveBuffer(ns_late); - return std::nullopt; - }); -} - -void Stream::Play() { - state = State::Playing; - PlayNextBuffer(); -} - -void Stream::Stop() { - state = State::Stopped; - UNIMPLEMENTED(); -} - -bool Stream::Flush() { - const bool had_buffers = !queued_buffers.empty(); - while (!queued_buffers.empty()) { - queued_buffers.pop(); - } - return had_buffers; -} - -void Stream::SetVolume(float volume) { - game_volume = volume; -} - -Stream::State Stream::GetState() const { - return state; -} - -std::chrono::nanoseconds Stream::GetBufferReleaseNS(const Buffer& buffer) const { - const std::size_t num_samples{buffer.GetSamples().size() / GetNumChannels()}; - return std::chrono::nanoseconds((static_cast(num_samples) * 1000000000ULL) / sample_rate); -} - -static void VolumeAdjustSamples(std::vector& samples, float game_volume) { - const float volume{std::clamp(Settings::Volume() - (1.0f - game_volume), 0.0f, 1.0f)}; - - if (volume == 1.0f) { - return; - } - - // Perceived volume is not the same as the volume level - const float volume_scale_factor = (0.85f * ((volume * volume) - volume)) + volume; - for (auto& sample : samples) { - sample = static_cast(sample * volume_scale_factor); - } -} - -void Stream::PlayNextBuffer(std::chrono::nanoseconds ns_late) { - if (!IsPlaying()) { - // Ensure we are in playing state before playing the next buffer - sink_stream.Flush(); - return; - } - - if (active_buffer) { - // Do not queue a new buffer if we are already playing a buffer - return; - } - - if (queued_buffers.empty()) { - // No queued buffers - we are effectively paused - sink_stream.Flush(); - return; - } - - active_buffer = queued_buffers.front(); - queued_buffers.pop(); - - auto& samples = active_buffer->GetSamples(); - - VolumeAdjustSamples(samples, game_volume); - - sink_stream.EnqueueSamples(GetNumChannels(), samples); - played_samples += samples.size(); - - const auto buffer_release_ns = GetBufferReleaseNS(*active_buffer); - - // If ns_late is higher than the update rate ignore the delay - if (ns_late > buffer_release_ns) { - ns_late = {}; - } - - core_timing.ScheduleEvent(buffer_release_ns - ns_late, release_event, {}); -} - -void Stream::ReleaseActiveBuffer(std::chrono::nanoseconds ns_late) { - ASSERT(active_buffer); - released_buffers.push(std::move(active_buffer)); - release_callback(); - PlayNextBuffer(ns_late); -} - -bool Stream::QueueBuffer(BufferPtr&& buffer) { - if (queued_buffers.size() < MaxAudioBufferCount) { - queued_buffers.push(std::move(buffer)); - PlayNextBuffer(); - return true; - } - return false; -} - -bool Stream::ContainsBuffer([[maybe_unused]] Buffer::Tag tag) const { - UNIMPLEMENTED(); - return {}; -} - -std::vector Stream::GetTagsAndReleaseBuffers(std::size_t max_count) { - std::vector tags; - for (std::size_t count = 0; count < max_count && !released_buffers.empty(); ++count) { - if (released_buffers.front()) { - tags.push_back(released_buffers.front()->GetTag()); - } else { - ASSERT_MSG(false, "Invalid tag in released_buffers!"); - } - released_buffers.pop(); - } - return tags; -} - -std::vector Stream::GetTagsAndReleaseBuffers() { - std::vector tags; - tags.reserve(released_buffers.size()); - while (!released_buffers.empty()) { - if (released_buffers.front()) { - tags.push_back(released_buffers.front()->GetTag()); - } else { - ASSERT_MSG(false, "Invalid tag in released_buffers!"); - } - released_buffers.pop(); - } - return tags; -} - -} // namespace AudioCore diff --git a/src/audio_core/stream.h b/src/audio_core/stream.h deleted file mode 100644 index f5de703962..0000000000 --- a/src/audio_core/stream.h +++ /dev/null @@ -1,130 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "audio_core/buffer.h" -#include "common/common_types.h" - -namespace Core::Timing { -class CoreTiming; -struct EventType; -} // namespace Core::Timing - -namespace AudioCore { - -class SinkStream; - -/** - * Represents an audio stream, which is a sequence of queued buffers, to be outputed by AudioOut - */ -class Stream { -public: - /// Audio format of the stream - enum class Format { - Mono16, - Stereo16, - Multi51Channel16, - }; - - /// Current state of the stream - enum class State { - Stopped, - Playing, - }; - - /// Callback function type, used to change guest state on a buffer being released - using ReleaseCallback = std::function; - - Stream(Core::Timing::CoreTiming& core_timing_, u32 sample_rate_, Format format_, - ReleaseCallback&& release_callback_, SinkStream& sink_stream_, std::string&& name_); - - /// Plays the audio stream - void Play(); - - /// Stops the audio stream - void Stop(); - - /// Queues a buffer into the audio stream, returns true on success - bool QueueBuffer(BufferPtr&& buffer); - - /// Flush audio buffers - bool Flush(); - - /// Returns true if the audio stream contains a buffer with the specified tag - [[nodiscard]] bool ContainsBuffer(Buffer::Tag tag) const; - - /// Returns a vector of recently released buffers specified by tag - [[nodiscard]] std::vector GetTagsAndReleaseBuffers(std::size_t max_count); - - /// Returns a vector of all recently released buffers specified by tag - [[nodiscard]] std::vector GetTagsAndReleaseBuffers(); - - void SetVolume(float volume); - - [[nodiscard]] float GetVolume() const { - return game_volume; - } - - /// Returns true if the stream is currently playing - [[nodiscard]] bool IsPlaying() const { - return state == State::Playing; - } - - /// Returns the number of queued buffers - [[nodiscard]] std::size_t GetQueueSize() const { - return queued_buffers.size(); - } - - /// Gets the sample rate - [[nodiscard]] u32 GetSampleRate() const { - return sample_rate; - } - - /// Gets the number of samples played so far - [[nodiscard]] u64 GetPlayedSampleCount() const { - return played_samples; - } - - /// Gets the number of channels - [[nodiscard]] u32 GetNumChannels() const; - - /// Get the state - [[nodiscard]] State GetState() const; - -private: - /// Plays the next queued buffer in the audio stream, starting playback if necessary - void PlayNextBuffer(std::chrono::nanoseconds ns_late = {}); - - /// Releases the actively playing buffer, signalling that it has been completed - void ReleaseActiveBuffer(std::chrono::nanoseconds ns_late = {}); - - /// Gets the number of core cycles when the specified buffer will be released - [[nodiscard]] std::chrono::nanoseconds GetBufferReleaseNS(const Buffer& buffer) const; - - u32 sample_rate; ///< Sample rate of the stream - u64 played_samples{}; ///< The current played sample count - Format format; ///< Format of the stream - float game_volume = 1.0f; ///< The volume the game currently has set - ReleaseCallback release_callback; ///< Buffer release callback for the stream - State state{State::Stopped}; ///< Playback state of the stream - std::shared_ptr - release_event; ///< Core timing release event for the stream - BufferPtr active_buffer; ///< Actively playing buffer in the stream - std::queue queued_buffers; ///< Buffers queued to be played in the stream - std::queue released_buffers; ///< Buffers recently released from the stream - SinkStream& sink_stream; ///< Output sink for the stream - Core::Timing::CoreTiming& core_timing; ///< Core timing instance. - std::string name; ///< Name of the stream, must be unique -}; - -using StreamPtr = std::shared_ptr; - -} // namespace AudioCore diff --git a/src/audio_core/voice_context.cpp b/src/audio_core/voice_context.cpp deleted file mode 100644 index f58a5c7545..0000000000 --- a/src/audio_core/voice_context.cpp +++ /dev/null @@ -1,579 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include - -#include "audio_core/behavior_info.h" -#include "audio_core/voice_context.h" -#include "core/memory.h" - -namespace AudioCore { - -ServerVoiceChannelResource::ServerVoiceChannelResource(s32 id_) : id(id_) {} -ServerVoiceChannelResource::~ServerVoiceChannelResource() = default; - -bool ServerVoiceChannelResource::InUse() const { - return in_use; -} - -float ServerVoiceChannelResource::GetCurrentMixVolumeAt(std::size_t i) const { - ASSERT(i < AudioCommon::MAX_MIX_BUFFERS); - return mix_volume.at(i); -} - -float ServerVoiceChannelResource::GetLastMixVolumeAt(std::size_t i) const { - ASSERT(i < AudioCommon::MAX_MIX_BUFFERS); - return last_mix_volume.at(i); -} - -void ServerVoiceChannelResource::Update(VoiceChannelResource::InParams& in_params) { - in_use = in_params.in_use; - // Update our mix volumes only if it's in use - if (in_params.in_use) { - mix_volume = in_params.mix_volume; - } -} - -void ServerVoiceChannelResource::UpdateLastMixVolumes() { - last_mix_volume = mix_volume; -} - -const std::array& -ServerVoiceChannelResource::GetCurrentMixVolume() const { - return mix_volume; -} - -const std::array& -ServerVoiceChannelResource::GetLastMixVolume() const { - return last_mix_volume; -} - -ServerVoiceInfo::ServerVoiceInfo() { - Initialize(); -} -ServerVoiceInfo::~ServerVoiceInfo() = default; - -void ServerVoiceInfo::Initialize() { - in_params.in_use = false; - in_params.node_id = 0; - in_params.id = 0; - in_params.current_playstate = ServerPlayState::Stop; - in_params.priority = 255; - in_params.sample_rate = 0; - in_params.sample_format = SampleFormat::Invalid; - in_params.channel_count = 0; - in_params.pitch = 0.0f; - in_params.volume = 0.0f; - in_params.last_volume = 0.0f; - in_params.biquad_filter.fill({}); - in_params.wave_buffer_count = 0; - in_params.wave_buffer_head = 0; - in_params.mix_id = AudioCommon::NO_MIX; - in_params.splitter_info_id = AudioCommon::NO_SPLITTER; - in_params.additional_params_address = 0; - in_params.additional_params_size = 0; - in_params.is_new = false; - out_params.played_sample_count = 0; - out_params.wave_buffer_consumed = 0; - in_params.voice_drop_flag = false; - in_params.buffer_mapped = true; - in_params.wave_buffer_flush_request_count = 0; - in_params.was_biquad_filter_enabled.fill(false); - - for (auto& wave_buffer : in_params.wave_buffer) { - wave_buffer.start_sample_offset = 0; - wave_buffer.end_sample_offset = 0; - wave_buffer.is_looping = false; - wave_buffer.end_of_stream = false; - wave_buffer.buffer_address = 0; - wave_buffer.buffer_size = 0; - wave_buffer.context_address = 0; - wave_buffer.context_size = 0; - wave_buffer.sent_to_dsp = true; - } - - stored_samples.clear(); -} - -void ServerVoiceInfo::UpdateParameters(const VoiceInfo::InParams& voice_in, - BehaviorInfo& behavior_info) { - in_params.in_use = voice_in.is_in_use; - in_params.id = voice_in.id; - in_params.node_id = voice_in.node_id; - in_params.last_playstate = in_params.current_playstate; - switch (voice_in.play_state) { - case PlayState::Paused: - in_params.current_playstate = ServerPlayState::Paused; - break; - case PlayState::Stopped: - if (in_params.current_playstate != ServerPlayState::Stop) { - in_params.current_playstate = ServerPlayState::RequestStop; - } - break; - case PlayState::Started: - in_params.current_playstate = ServerPlayState::Play; - break; - default: - ASSERT_MSG(false, "Unknown playstate {}", voice_in.play_state); - break; - } - - in_params.priority = voice_in.priority; - in_params.sorting_order = voice_in.sorting_order; - in_params.sample_rate = voice_in.sample_rate; - in_params.sample_format = voice_in.sample_format; - in_params.channel_count = voice_in.channel_count; - in_params.pitch = voice_in.pitch; - in_params.volume = voice_in.volume; - in_params.biquad_filter = voice_in.biquad_filter; - in_params.wave_buffer_count = voice_in.wave_buffer_count; - in_params.wave_buffer_head = voice_in.wave_buffer_head; - if (behavior_info.IsFlushVoiceWaveBuffersSupported()) { - const auto in_request_count = in_params.wave_buffer_flush_request_count; - const auto voice_request_count = voice_in.wave_buffer_flush_request_count; - in_params.wave_buffer_flush_request_count = - static_cast(in_request_count + voice_request_count); - } - in_params.mix_id = voice_in.mix_id; - if (behavior_info.IsSplitterSupported()) { - in_params.splitter_info_id = voice_in.splitter_info_id; - } else { - in_params.splitter_info_id = AudioCommon::NO_SPLITTER; - } - - std::memcpy(in_params.voice_channel_resource_id.data(), - voice_in.voice_channel_resource_ids.data(), - sizeof(s32) * in_params.voice_channel_resource_id.size()); - - if (behavior_info.IsVoicePlayedSampleCountResetAtLoopPointSupported()) { - in_params.behavior_flags.is_played_samples_reset_at_loop_point = - voice_in.behavior_flags.is_played_samples_reset_at_loop_point; - } else { - in_params.behavior_flags.is_played_samples_reset_at_loop_point.Assign(0); - } - if (behavior_info.IsVoicePitchAndSrcSkippedSupported()) { - in_params.behavior_flags.is_pitch_and_src_skipped = - voice_in.behavior_flags.is_pitch_and_src_skipped; - } else { - in_params.behavior_flags.is_pitch_and_src_skipped.Assign(0); - } - - if (voice_in.is_voice_drop_flag_clear_requested) { - in_params.voice_drop_flag = false; - } - - if (in_params.additional_params_address != voice_in.additional_params_address || - in_params.additional_params_size != voice_in.additional_params_size) { - in_params.additional_params_address = voice_in.additional_params_address; - in_params.additional_params_size = voice_in.additional_params_size; - // TODO(ogniK): Reattach buffer, do we actually need to? Maybe just signal to the DSP that - // our context is new - } -} - -void ServerVoiceInfo::UpdateWaveBuffers( - const VoiceInfo::InParams& voice_in, - std::array& voice_states, - BehaviorInfo& behavior_info) { - if (voice_in.is_new) { - // Initialize our wave buffers - for (auto& wave_buffer : in_params.wave_buffer) { - wave_buffer.start_sample_offset = 0; - wave_buffer.end_sample_offset = 0; - wave_buffer.is_looping = false; - wave_buffer.end_of_stream = false; - wave_buffer.buffer_address = 0; - wave_buffer.buffer_size = 0; - wave_buffer.context_address = 0; - wave_buffer.context_size = 0; - wave_buffer.loop_start_sample = 0; - wave_buffer.loop_end_sample = 0; - wave_buffer.sent_to_dsp = true; - } - - // Mark all our wave buffers as invalid - for (std::size_t channel = 0; channel < static_cast(in_params.channel_count); - channel++) { - for (std::size_t i = 0; i < AudioCommon::MAX_WAVE_BUFFERS; ++i) { - voice_states[channel]->is_wave_buffer_valid[i] = false; - } - } - } - - // Update our wave buffers - for (std::size_t i = 0; i < AudioCommon::MAX_WAVE_BUFFERS; i++) { - // Assume that we have at least 1 channel voice state - const auto have_valid_wave_buffer = voice_states[0]->is_wave_buffer_valid[i]; - - UpdateWaveBuffer(in_params.wave_buffer[i], voice_in.wave_buffer[i], in_params.sample_format, - have_valid_wave_buffer, behavior_info); - } -} - -void ServerVoiceInfo::UpdateWaveBuffer(ServerWaveBuffer& out_wavebuffer, - const WaveBuffer& in_wave_buffer, SampleFormat sample_format, - bool is_buffer_valid, - [[maybe_unused]] BehaviorInfo& behavior_info) { - if (!is_buffer_valid && out_wavebuffer.sent_to_dsp && out_wavebuffer.buffer_address != 0) { - out_wavebuffer.buffer_address = 0; - out_wavebuffer.buffer_size = 0; - } - - if (!in_wave_buffer.sent_to_server || !in_params.buffer_mapped) { - // Validate sample offset sizings - if (sample_format == SampleFormat::Pcm16) { - const s64 buffer_size = static_cast(in_wave_buffer.buffer_size); - const s64 start = sizeof(s16) * in_wave_buffer.start_sample_offset; - const s64 end = sizeof(s16) * in_wave_buffer.end_sample_offset; - if (0 > start || start > buffer_size || 0 > end || end > buffer_size) { - // TODO(ogniK): Write error info - LOG_ERROR(Audio, - "PCM16 wavebuffer has an invalid size. Buffer has size 0x{:08X}, but " - "offsets were " - "{:08X} - 0x{:08X}", - buffer_size, sizeof(s16) * in_wave_buffer.start_sample_offset, - sizeof(s16) * in_wave_buffer.end_sample_offset); - return; - } - } else if (sample_format == SampleFormat::Adpcm) { - const s64 buffer_size = static_cast(in_wave_buffer.buffer_size); - const s64 start_frames = in_wave_buffer.start_sample_offset / 14; - const s64 start_extra = in_wave_buffer.start_sample_offset % 14 == 0 - ? 0 - : (in_wave_buffer.start_sample_offset % 14) / 2 + 1 + - (in_wave_buffer.start_sample_offset % 2); - const s64 start = start_frames * 8 + start_extra; - const s64 end_frames = in_wave_buffer.end_sample_offset / 14; - const s64 end_extra = in_wave_buffer.end_sample_offset % 14 == 0 - ? 0 - : (in_wave_buffer.end_sample_offset % 14) / 2 + 1 + - (in_wave_buffer.end_sample_offset % 2); - const s64 end = end_frames * 8 + end_extra; - if (in_wave_buffer.start_sample_offset < 0 || start > buffer_size || - in_wave_buffer.end_sample_offset < 0 || end > buffer_size) { - LOG_ERROR(Audio, - "ADPMC wavebuffer has an invalid size. Buffer has size 0x{:08X}, but " - "offsets were " - "{:08X} - 0x{:08X}", - in_wave_buffer.buffer_size, start, end); - return; - } - } - // TODO(ogniK): ADPCM Size error - - out_wavebuffer.sent_to_dsp = false; - out_wavebuffer.start_sample_offset = in_wave_buffer.start_sample_offset; - out_wavebuffer.end_sample_offset = in_wave_buffer.end_sample_offset; - out_wavebuffer.is_looping = in_wave_buffer.is_looping; - out_wavebuffer.end_of_stream = in_wave_buffer.end_of_stream; - - out_wavebuffer.buffer_address = in_wave_buffer.buffer_address; - out_wavebuffer.buffer_size = in_wave_buffer.buffer_size; - out_wavebuffer.context_address = in_wave_buffer.context_address; - out_wavebuffer.context_size = in_wave_buffer.context_size; - out_wavebuffer.loop_start_sample = in_wave_buffer.loop_start_sample; - out_wavebuffer.loop_end_sample = in_wave_buffer.loop_end_sample; - in_params.buffer_mapped = - in_wave_buffer.buffer_address != 0 && in_wave_buffer.buffer_size != 0; - // TODO(ogniK): Pool mapper attachment - // TODO(ogniK): IsAdpcmLoopContextBugFixed - if (sample_format == SampleFormat::Adpcm && in_wave_buffer.context_address != 0 && - in_wave_buffer.context_size != 0 && behavior_info.IsAdpcmLoopContextBugFixed()) { - } else { - out_wavebuffer.context_address = 0; - out_wavebuffer.context_size = 0; - } - } -} - -void ServerVoiceInfo::WriteOutStatus( - VoiceInfo::OutParams& voice_out, VoiceInfo::InParams& voice_in, - std::array& voice_states) { - if (voice_in.is_new || in_params.is_new) { - in_params.is_new = true; - voice_out.wave_buffer_consumed = 0; - voice_out.played_sample_count = 0; - voice_out.voice_dropped = false; - } else { - const auto& state = voice_states[0]; - voice_out.wave_buffer_consumed = state->wave_buffer_consumed; - voice_out.played_sample_count = state->played_sample_count; - voice_out.voice_dropped = state->voice_dropped; - } -} - -const ServerVoiceInfo::InParams& ServerVoiceInfo::GetInParams() const { - return in_params; -} - -ServerVoiceInfo::InParams& ServerVoiceInfo::GetInParams() { - return in_params; -} - -const ServerVoiceInfo::OutParams& ServerVoiceInfo::GetOutParams() const { - return out_params; -} - -ServerVoiceInfo::OutParams& ServerVoiceInfo::GetOutParams() { - return out_params; -} - -bool ServerVoiceInfo::ShouldSkip() const { - // TODO(ogniK): Handle unmapped wave buffers or parameters - return !in_params.in_use || in_params.wave_buffer_count == 0 || !in_params.buffer_mapped || - in_params.voice_drop_flag; -} - -bool ServerVoiceInfo::UpdateForCommandGeneration(VoiceContext& voice_context) { - std::array dsp_voice_states{}; - if (in_params.is_new) { - ResetResources(voice_context); - in_params.last_volume = in_params.volume; - in_params.is_new = false; - } - - const s32 channel_count = in_params.channel_count; - for (s32 i = 0; i < channel_count; i++) { - const auto channel_resource = in_params.voice_channel_resource_id[i]; - dsp_voice_states[i] = - &voice_context.GetDspSharedState(static_cast(channel_resource)); - } - return UpdateParametersForCommandGeneration(dsp_voice_states); -} - -void ServerVoiceInfo::ResetResources(VoiceContext& voice_context) { - const s32 channel_count = in_params.channel_count; - for (s32 i = 0; i < channel_count; i++) { - const auto channel_resource = in_params.voice_channel_resource_id[i]; - auto& dsp_state = - voice_context.GetDspSharedState(static_cast(channel_resource)); - dsp_state = {}; - voice_context.GetChannelResource(static_cast(channel_resource)) - .UpdateLastMixVolumes(); - } -} - -bool ServerVoiceInfo::UpdateParametersForCommandGeneration( - std::array& dsp_voice_states) { - const s32 channel_count = in_params.channel_count; - if (in_params.wave_buffer_flush_request_count > 0) { - FlushWaveBuffers(in_params.wave_buffer_flush_request_count, dsp_voice_states, - channel_count); - in_params.wave_buffer_flush_request_count = 0; - } - - switch (in_params.current_playstate) { - case ServerPlayState::Play: { - for (std::size_t i = 0; i < AudioCommon::MAX_WAVE_BUFFERS; i++) { - if (!in_params.wave_buffer[i].sent_to_dsp) { - for (s32 channel = 0; channel < channel_count; channel++) { - dsp_voice_states[channel]->is_wave_buffer_valid[i] = true; - } - in_params.wave_buffer[i].sent_to_dsp = true; - } - } - in_params.should_depop = false; - return HasValidWaveBuffer(dsp_voice_states[0]); - } - case ServerPlayState::Paused: - case ServerPlayState::Stop: { - in_params.should_depop = in_params.last_playstate == ServerPlayState::Play; - return in_params.should_depop; - } - case ServerPlayState::RequestStop: { - for (std::size_t i = 0; i < AudioCommon::MAX_WAVE_BUFFERS; i++) { - in_params.wave_buffer[i].sent_to_dsp = true; - for (s32 channel = 0; channel < channel_count; channel++) { - auto* dsp_state = dsp_voice_states[channel]; - - if (dsp_state->is_wave_buffer_valid[i]) { - dsp_state->wave_buffer_index = - (dsp_state->wave_buffer_index + 1) % AudioCommon::MAX_WAVE_BUFFERS; - dsp_state->wave_buffer_consumed++; - } - - dsp_state->is_wave_buffer_valid[i] = false; - } - } - - for (s32 channel = 0; channel < channel_count; channel++) { - auto* dsp_state = dsp_voice_states[channel]; - dsp_state->offset = 0; - dsp_state->played_sample_count = 0; - dsp_state->fraction = 0; - dsp_state->sample_history.fill(0); - dsp_state->context = {}; - } - - in_params.current_playstate = ServerPlayState::Stop; - in_params.should_depop = in_params.last_playstate == ServerPlayState::Play; - return in_params.should_depop; - } - default: - ASSERT_MSG(false, "Invalid playstate {}", in_params.current_playstate); - } - - return false; -} - -void ServerVoiceInfo::FlushWaveBuffers( - u8 flush_count, std::array& dsp_voice_states, - s32 channel_count) { - auto wave_head = in_params.wave_buffer_head; - - for (u8 i = 0; i < flush_count; i++) { - in_params.wave_buffer[wave_head].sent_to_dsp = true; - for (s32 channel = 0; channel < channel_count; channel++) { - auto* dsp_state = dsp_voice_states[channel]; - dsp_state->wave_buffer_consumed++; - dsp_state->is_wave_buffer_valid[wave_head] = false; - dsp_state->wave_buffer_index = - (dsp_state->wave_buffer_index + 1) % AudioCommon::MAX_WAVE_BUFFERS; - } - wave_head = (wave_head + 1) % AudioCommon::MAX_WAVE_BUFFERS; - } -} - -bool ServerVoiceInfo::HasValidWaveBuffer(const VoiceState* state) const { - const auto& valid_wb = state->is_wave_buffer_valid; - return std::find(valid_wb.begin(), valid_wb.end(), true) != valid_wb.end(); -} - -void ServerVoiceInfo::SetWaveBufferCompleted(VoiceState& dsp_state, - const ServerWaveBuffer& wave_buffer) { - dsp_state.is_wave_buffer_valid[dsp_state.wave_buffer_index] = false; - dsp_state.wave_buffer_consumed++; - dsp_state.wave_buffer_index = (dsp_state.wave_buffer_index + 1) % AudioCommon::MAX_WAVE_BUFFERS; - dsp_state.loop_count = 0; - if (wave_buffer.end_of_stream) { - dsp_state.played_sample_count = 0; - } -} - -VoiceContext::VoiceContext(std::size_t voice_count_) : voice_count{voice_count_} { - for (std::size_t i = 0; i < voice_count; i++) { - voice_channel_resources.emplace_back(static_cast(i)); - sorted_voice_info.push_back(&voice_info.emplace_back()); - voice_states.emplace_back(); - dsp_voice_states.emplace_back(); - } -} - -VoiceContext::~VoiceContext() { - sorted_voice_info.clear(); -} - -std::size_t VoiceContext::GetVoiceCount() const { - return voice_count; -} - -ServerVoiceChannelResource& VoiceContext::GetChannelResource(std::size_t i) { - ASSERT(i < voice_count); - return voice_channel_resources.at(i); -} - -const ServerVoiceChannelResource& VoiceContext::GetChannelResource(std::size_t i) const { - ASSERT(i < voice_count); - return voice_channel_resources.at(i); -} - -VoiceState& VoiceContext::GetState(std::size_t i) { - ASSERT(i < voice_count); - return voice_states.at(i); -} - -const VoiceState& VoiceContext::GetState(std::size_t i) const { - ASSERT(i < voice_count); - return voice_states.at(i); -} - -VoiceState& VoiceContext::GetDspSharedState(std::size_t i) { - ASSERT(i < voice_count); - return dsp_voice_states.at(i); -} - -const VoiceState& VoiceContext::GetDspSharedState(std::size_t i) const { - ASSERT(i < voice_count); - return dsp_voice_states.at(i); -} - -ServerVoiceInfo& VoiceContext::GetInfo(std::size_t i) { - ASSERT(i < voice_count); - return voice_info.at(i); -} - -const ServerVoiceInfo& VoiceContext::GetInfo(std::size_t i) const { - ASSERT(i < voice_count); - return voice_info.at(i); -} - -ServerVoiceInfo& VoiceContext::GetSortedInfo(std::size_t i) { - ASSERT(i < voice_count); - return *sorted_voice_info.at(i); -} - -const ServerVoiceInfo& VoiceContext::GetSortedInfo(std::size_t i) const { - ASSERT(i < voice_count); - return *sorted_voice_info.at(i); -} - -s32 VoiceContext::DecodePcm16(s32* output_buffer, ServerWaveBuffer* wave_buffer, s32 channel, - s32 channel_count, s32 buffer_offset, s32 sample_count, - Core::Memory::Memory& memory) { - if (wave_buffer->buffer_address == 0) { - return 0; - } - if (wave_buffer->buffer_size == 0) { - return 0; - } - if (wave_buffer->end_sample_offset < wave_buffer->start_sample_offset) { - return 0; - } - - const auto samples_remaining = - (wave_buffer->end_sample_offset - wave_buffer->start_sample_offset) - buffer_offset; - const auto start_offset = (wave_buffer->start_sample_offset + buffer_offset) * channel_count; - const auto buffer_pos = wave_buffer->buffer_address + start_offset; - - s16* buffer_data = reinterpret_cast(memory.GetPointer(buffer_pos)); - - const auto samples_processed = std::min(sample_count, samples_remaining); - - // Fast path - if (channel_count == 1) { - for (std::ptrdiff_t i = 0; i < samples_processed; i++) { - output_buffer[i] = buffer_data[i]; - } - } else { - for (std::ptrdiff_t i = 0; i < samples_processed; i++) { - output_buffer[i] = buffer_data[i * channel_count + channel]; - } - } - - return samples_processed; -} - -void VoiceContext::SortInfo() { - for (std::size_t i = 0; i < voice_count; i++) { - sorted_voice_info[i] = &voice_info[i]; - } - - std::sort(sorted_voice_info.begin(), sorted_voice_info.end(), - [](const ServerVoiceInfo* lhs, const ServerVoiceInfo* rhs) { - const auto& lhs_in = lhs->GetInParams(); - const auto& rhs_in = rhs->GetInParams(); - // Sort by priority - if (lhs_in.priority != rhs_in.priority) { - return lhs_in.priority > rhs_in.priority; - } else { - // If the priorities match, sort by sorting order - return lhs_in.sorting_order > rhs_in.sorting_order; - } - }); -} - -void VoiceContext::UpdateStateByDspShared() { - voice_states = dsp_voice_states; -} - -} // namespace AudioCore diff --git a/src/audio_core/voice_context.h b/src/audio_core/voice_context.h deleted file mode 100644 index 259220dc76..0000000000 --- a/src/audio_core/voice_context.h +++ /dev/null @@ -1,302 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include "audio_core/algorithm/interpolate.h" -#include "audio_core/codec.h" -#include "audio_core/common.h" -#include "common/bit_field.h" -#include "common/common_funcs.h" -#include "common/common_types.h" - -namespace Core::Memory { -class Memory; -} - -namespace AudioCore { - -class BehaviorInfo; -class VoiceContext; - -enum class SampleFormat : u8 { - Invalid = 0, - Pcm8 = 1, - Pcm16 = 2, - Pcm24 = 3, - Pcm32 = 4, - PcmFloat = 5, - Adpcm = 6, -}; - -enum class PlayState : u8 { - Started = 0, - Stopped = 1, - Paused = 2, -}; - -enum class ServerPlayState { - Play = 0, - Stop = 1, - RequestStop = 2, - Paused = 3, -}; - -struct BiquadFilterParameter { - bool enabled{}; - INSERT_PADDING_BYTES(1); - std::array numerator{}; - std::array denominator{}; -}; -static_assert(sizeof(BiquadFilterParameter) == 0xc, "BiquadFilterParameter is an invalid size"); - -struct WaveBuffer { - u64_le buffer_address{}; - u64_le buffer_size{}; - s32_le start_sample_offset{}; - s32_le end_sample_offset{}; - u8 is_looping{}; - u8 end_of_stream{}; - u8 sent_to_server{}; - INSERT_PADDING_BYTES(1); - s32 loop_count{}; - u64 context_address{}; - u64 context_size{}; - u32 loop_start_sample{}; - u32 loop_end_sample{}; -}; -static_assert(sizeof(WaveBuffer) == 0x38, "WaveBuffer is an invalid size"); - -struct ServerWaveBuffer { - VAddr buffer_address{}; - std::size_t buffer_size{}; - s32 start_sample_offset{}; - s32 end_sample_offset{}; - bool is_looping{}; - bool end_of_stream{}; - VAddr context_address{}; - std::size_t context_size{}; - s32 loop_count{}; - u32 loop_start_sample{}; - u32 loop_end_sample{}; - bool sent_to_dsp{true}; -}; - -struct BehaviorFlags { - BitField<0, 1, u16> is_played_samples_reset_at_loop_point; - BitField<1, 1, u16> is_pitch_and_src_skipped; -}; -static_assert(sizeof(BehaviorFlags) == 0x4, "BehaviorFlags is an invalid size"); - -struct ADPCMContext { - u16 header; - s16 yn1; - s16 yn2; -}; -static_assert(sizeof(ADPCMContext) == 0x6, "ADPCMContext is an invalid size"); - -struct VoiceState { - s64 played_sample_count; - s32 offset; - s32 wave_buffer_index; - std::array is_wave_buffer_valid; - s32 wave_buffer_consumed; - std::array sample_history; - s32 fraction; - VAddr context_address; - Codec::ADPCM_Coeff coeff; - ADPCMContext context; - std::array biquad_filter_state; - std::array previous_samples; - u32 external_context_size; - bool is_external_context_used; - bool voice_dropped; - s32 loop_count; -}; - -class VoiceChannelResource { -public: - struct InParams { - s32_le id{}; - std::array mix_volume{}; - bool in_use{}; - INSERT_PADDING_BYTES(11); - }; - static_assert(sizeof(InParams) == 0x70, "InParams is an invalid size"); -}; - -class ServerVoiceChannelResource { -public: - explicit ServerVoiceChannelResource(s32 id_); - ~ServerVoiceChannelResource(); - - bool InUse() const; - float GetCurrentMixVolumeAt(std::size_t i) const; - float GetLastMixVolumeAt(std::size_t i) const; - void Update(VoiceChannelResource::InParams& in_params); - void UpdateLastMixVolumes(); - - const std::array& GetCurrentMixVolume() const; - const std::array& GetLastMixVolume() const; - -private: - s32 id{}; - std::array mix_volume{}; - std::array last_mix_volume{}; - bool in_use{}; -}; - -class VoiceInfo { -public: - struct InParams { - s32_le id{}; - u32_le node_id{}; - u8 is_new{}; - u8 is_in_use{}; - PlayState play_state{}; - SampleFormat sample_format{}; - s32_le sample_rate{}; - s32_le priority{}; - s32_le sorting_order{}; - s32_le channel_count{}; - float_le pitch{}; - float_le volume{}; - std::array biquad_filter{}; - s32_le wave_buffer_count{}; - s16_le wave_buffer_head{}; - INSERT_PADDING_BYTES(6); - u64_le additional_params_address{}; - u64_le additional_params_size{}; - s32_le mix_id{}; - s32_le splitter_info_id{}; - std::array wave_buffer{}; - std::array voice_channel_resource_ids{}; - // TODO(ogniK): Remaining flags - u8 is_voice_drop_flag_clear_requested{}; - u8 wave_buffer_flush_request_count{}; - INSERT_PADDING_BYTES(2); - BehaviorFlags behavior_flags{}; - INSERT_PADDING_BYTES(16); - }; - static_assert(sizeof(InParams) == 0x170, "InParams is an invalid size"); - - struct OutParams { - u64_le played_sample_count{}; - u32_le wave_buffer_consumed{}; - u8 voice_dropped{}; - INSERT_PADDING_BYTES(3); - }; - static_assert(sizeof(OutParams) == 0x10, "OutParams is an invalid size"); -}; - -class ServerVoiceInfo { -public: - struct InParams { - bool in_use{}; - bool is_new{}; - bool should_depop{}; - SampleFormat sample_format{}; - s32 sample_rate{}; - s32 channel_count{}; - s32 id{}; - s32 node_id{}; - s32 mix_id{}; - ServerPlayState current_playstate{}; - ServerPlayState last_playstate{}; - s32 priority{}; - s32 sorting_order{}; - float pitch{}; - float volume{}; - float last_volume{}; - std::array biquad_filter{}; - s32 wave_buffer_count{}; - s16 wave_buffer_head{}; - INSERT_PADDING_BYTES(2); - BehaviorFlags behavior_flags{}; - VAddr additional_params_address{}; - std::size_t additional_params_size{}; - std::array wave_buffer{}; - std::array voice_channel_resource_id{}; - s32 splitter_info_id{}; - u8 wave_buffer_flush_request_count{}; - bool voice_drop_flag{}; - bool buffer_mapped{}; - std::array was_biquad_filter_enabled{}; - }; - - struct OutParams { - s64 played_sample_count{}; - s32 wave_buffer_consumed{}; - }; - - ServerVoiceInfo(); - ~ServerVoiceInfo(); - void Initialize(); - void UpdateParameters(const VoiceInfo::InParams& voice_in, BehaviorInfo& behavior_info); - void UpdateWaveBuffers(const VoiceInfo::InParams& voice_in, - std::array& voice_states, - BehaviorInfo& behavior_info); - void UpdateWaveBuffer(ServerWaveBuffer& out_wavebuffer, const WaveBuffer& in_wave_buffer, - SampleFormat sample_format, bool is_buffer_valid, - BehaviorInfo& behavior_info); - void WriteOutStatus(VoiceInfo::OutParams& voice_out, VoiceInfo::InParams& voice_in, - std::array& voice_states); - - const InParams& GetInParams() const; - InParams& GetInParams(); - - const OutParams& GetOutParams() const; - OutParams& GetOutParams(); - - bool ShouldSkip() const; - bool UpdateForCommandGeneration(VoiceContext& voice_context); - void ResetResources(VoiceContext& voice_context); - bool UpdateParametersForCommandGeneration( - std::array& dsp_voice_states); - void FlushWaveBuffers(u8 flush_count, - std::array& dsp_voice_states, - s32 channel_count); - void SetWaveBufferCompleted(VoiceState& dsp_state, const ServerWaveBuffer& wave_buffer); - -private: - std::vector stored_samples; - InParams in_params{}; - OutParams out_params{}; - - bool HasValidWaveBuffer(const VoiceState* state) const; -}; - -class VoiceContext { -public: - explicit VoiceContext(std::size_t voice_count_); - ~VoiceContext(); - - std::size_t GetVoiceCount() const; - ServerVoiceChannelResource& GetChannelResource(std::size_t i); - const ServerVoiceChannelResource& GetChannelResource(std::size_t i) const; - VoiceState& GetState(std::size_t i); - const VoiceState& GetState(std::size_t i) const; - VoiceState& GetDspSharedState(std::size_t i); - const VoiceState& GetDspSharedState(std::size_t i) const; - ServerVoiceInfo& GetInfo(std::size_t i); - const ServerVoiceInfo& GetInfo(std::size_t i) const; - ServerVoiceInfo& GetSortedInfo(std::size_t i); - const ServerVoiceInfo& GetSortedInfo(std::size_t i) const; - - s32 DecodePcm16(s32* output_buffer, ServerWaveBuffer* wave_buffer, s32 channel, - s32 channel_count, s32 buffer_offset, s32 sample_count, - Core::Memory::Memory& memory); - void SortInfo(); - void UpdateStateByDspShared(); - -private: - std::size_t voice_count{}; - std::vector voice_channel_resources{}; - std::vector voice_states{}; - std::vector dsp_voice_states{}; - std::vector voice_info{}; - std::vector sorted_voice_info{}; -}; - -} // namespace AudioCore diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 73bf626d4c..64bb753e64 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -43,6 +43,7 @@ add_library(common STATIC alignment.h assert.cpp assert.h + atomic_helpers.h atomic_ops.h detached_tasks.cpp detached_tasks.h @@ -64,6 +65,7 @@ add_library(common STATIC expected.h fiber.cpp fiber.h + fixed_point.h fs/file.cpp fs/file.h fs/fs.cpp @@ -109,6 +111,7 @@ add_library(common STATIC parent_of_member.h point.h quaternion.h + reader_writer_queue.h ring_buffer.h scm_rev.cpp scm_rev.h diff --git a/src/common/atomic_helpers.h b/src/common/atomic_helpers.h new file mode 100644 index 0000000000..6d912b52e1 --- /dev/null +++ b/src/common/atomic_helpers.h @@ -0,0 +1,772 @@ +// ©2013-2016 Cameron Desrochers. +// Distributed under the simplified BSD license (see the license file that +// should have come with this header). +// Uses Jeff Preshing's semaphore implementation (under the terms of its +// separate zlib license, embedded below). + +#pragma once + +// Provides portable (VC++2010+, Intel ICC 13, GCC 4.7+, and anything C++11 compliant) +// implementation of low-level memory barriers, plus a few semi-portable utility macros (for +// inlining and alignment). Also has a basic atomic type (limited to hardware-supported atomics with +// no memory ordering guarantees). Uses the AE_* prefix for macros (historical reasons), and the +// "moodycamel" namespace for symbols. + +#include +#include +#include +#include +#include + +// Platform detection +#if defined(__INTEL_COMPILER) +#define AE_ICC +#elif defined(_MSC_VER) +#define AE_VCPP +#elif defined(__GNUC__) +#define AE_GCC +#endif + +#if defined(_M_IA64) || defined(__ia64__) +#define AE_ARCH_IA64 +#elif defined(_WIN64) || defined(__amd64__) || defined(_M_X64) || defined(__x86_64__) +#define AE_ARCH_X64 +#elif defined(_M_IX86) || defined(__i386__) +#define AE_ARCH_X86 +#elif defined(_M_PPC) || defined(__powerpc__) +#define AE_ARCH_PPC +#else +#define AE_ARCH_UNKNOWN +#endif + +// AE_UNUSED +#define AE_UNUSED(x) ((void)x) + +// AE_NO_TSAN/AE_TSAN_ANNOTATE_* +#if defined(__has_feature) +#if __has_feature(thread_sanitizer) +#if __cplusplus >= 201703L // inline variables require C++17 +namespace Common { +inline int ae_tsan_global; +} +#define AE_TSAN_ANNOTATE_RELEASE() \ + AnnotateHappensBefore(__FILE__, __LINE__, (void*)(&::moodycamel::ae_tsan_global)) +#define AE_TSAN_ANNOTATE_ACQUIRE() \ + AnnotateHappensAfter(__FILE__, __LINE__, (void*)(&::moodycamel::ae_tsan_global)) +extern "C" void AnnotateHappensBefore(const char*, int, void*); +extern "C" void AnnotateHappensAfter(const char*, int, void*); +#else // when we can't work with tsan, attempt to disable its warnings +#define AE_NO_TSAN __attribute__((no_sanitize("thread"))) +#endif +#endif +#endif +#ifndef AE_NO_TSAN +#define AE_NO_TSAN +#endif +#ifndef AE_TSAN_ANNOTATE_RELEASE +#define AE_TSAN_ANNOTATE_RELEASE() +#define AE_TSAN_ANNOTATE_ACQUIRE() +#endif + +// AE_FORCEINLINE +#if defined(AE_VCPP) || defined(AE_ICC) +#define AE_FORCEINLINE __forceinline +#elif defined(AE_GCC) +//#define AE_FORCEINLINE __attribute__((always_inline)) +#define AE_FORCEINLINE inline +#else +#define AE_FORCEINLINE inline +#endif + +// AE_ALIGN +#if defined(AE_VCPP) || defined(AE_ICC) +#define AE_ALIGN(x) __declspec(align(x)) +#elif defined(AE_GCC) +#define AE_ALIGN(x) __attribute__((aligned(x))) +#else +// Assume GCC compliant syntax... +#define AE_ALIGN(x) __attribute__((aligned(x))) +#endif + +// Portable atomic fences implemented below: + +namespace Common { + +enum memory_order { + memory_order_relaxed, + memory_order_acquire, + memory_order_release, + memory_order_acq_rel, + memory_order_seq_cst, + + // memory_order_sync: Forces a full sync: + // #LoadLoad, #LoadStore, #StoreStore, and most significantly, #StoreLoad + memory_order_sync = memory_order_seq_cst +}; + +} // namespace Common + +#if (defined(AE_VCPP) && (_MSC_VER < 1700 || defined(__cplusplus_cli))) || \ + (defined(AE_ICC) && __INTEL_COMPILER < 1600) +// VS2010 and ICC13 don't support std::atomic_*_fence, implement our own fences + +#include + +#if defined(AE_ARCH_X64) || defined(AE_ARCH_X86) +#define AeFullSync _mm_mfence +#define AeLiteSync _mm_mfence +#elif defined(AE_ARCH_IA64) +#define AeFullSync __mf +#define AeLiteSync __mf +#elif defined(AE_ARCH_PPC) +#include +#define AeFullSync __sync +#define AeLiteSync __lwsync +#endif + +#ifdef AE_VCPP +#pragma warning(push) +#pragma warning(disable : 4365) // Disable erroneous 'conversion from long to unsigned int, + // signed/unsigned mismatch' error when using `assert` +#ifdef __cplusplus_cli +#pragma managed(push, off) +#endif +#endif + +namespace Common { + +AE_FORCEINLINE void compiler_fence(memory_order order) AE_NO_TSAN { + switch (order) { + case memory_order_relaxed: + break; + case memory_order_acquire: + _ReadBarrier(); + break; + case memory_order_release: + _WriteBarrier(); + break; + case memory_order_acq_rel: + _ReadWriteBarrier(); + break; + case memory_order_seq_cst: + _ReadWriteBarrier(); + break; + default: + assert(false); + } +} + +// x86/x64 have a strong memory model -- all loads and stores have +// acquire and release semantics automatically (so only need compiler +// barriers for those). +#if defined(AE_ARCH_X86) || defined(AE_ARCH_X64) +AE_FORCEINLINE void fence(memory_order order) AE_NO_TSAN { + switch (order) { + case memory_order_relaxed: + break; + case memory_order_acquire: + _ReadBarrier(); + break; + case memory_order_release: + _WriteBarrier(); + break; + case memory_order_acq_rel: + _ReadWriteBarrier(); + break; + case memory_order_seq_cst: + _ReadWriteBarrier(); + AeFullSync(); + _ReadWriteBarrier(); + break; + default: + assert(false); + } +} +#else +AE_FORCEINLINE void fence(memory_order order) AE_NO_TSAN { + // Non-specialized arch, use heavier memory barriers everywhere just in case :-( + switch (order) { + case memory_order_relaxed: + break; + case memory_order_acquire: + _ReadBarrier(); + AeLiteSync(); + _ReadBarrier(); + break; + case memory_order_release: + _WriteBarrier(); + AeLiteSync(); + _WriteBarrier(); + break; + case memory_order_acq_rel: + _ReadWriteBarrier(); + AeLiteSync(); + _ReadWriteBarrier(); + break; + case memory_order_seq_cst: + _ReadWriteBarrier(); + AeFullSync(); + _ReadWriteBarrier(); + break; + default: + assert(false); + } +} +#endif +} // namespace Common +#else +// Use standard library of atomics +#include + +namespace Common { + +AE_FORCEINLINE void compiler_fence(memory_order order) AE_NO_TSAN { + switch (order) { + case memory_order_relaxed: + break; + case memory_order_acquire: + std::atomic_signal_fence(std::memory_order_acquire); + break; + case memory_order_release: + std::atomic_signal_fence(std::memory_order_release); + break; + case memory_order_acq_rel: + std::atomic_signal_fence(std::memory_order_acq_rel); + break; + case memory_order_seq_cst: + std::atomic_signal_fence(std::memory_order_seq_cst); + break; + default: + assert(false); + } +} + +AE_FORCEINLINE void fence(memory_order order) AE_NO_TSAN { + switch (order) { + case memory_order_relaxed: + break; + case memory_order_acquire: + AE_TSAN_ANNOTATE_ACQUIRE(); + std::atomic_thread_fence(std::memory_order_acquire); + break; + case memory_order_release: + AE_TSAN_ANNOTATE_RELEASE(); + std::atomic_thread_fence(std::memory_order_release); + break; + case memory_order_acq_rel: + AE_TSAN_ANNOTATE_ACQUIRE(); + AE_TSAN_ANNOTATE_RELEASE(); + std::atomic_thread_fence(std::memory_order_acq_rel); + break; + case memory_order_seq_cst: + AE_TSAN_ANNOTATE_ACQUIRE(); + AE_TSAN_ANNOTATE_RELEASE(); + std::atomic_thread_fence(std::memory_order_seq_cst); + break; + default: + assert(false); + } +} + +} // namespace Common + +#endif + +#if !defined(AE_VCPP) || (_MSC_VER >= 1700 && !defined(__cplusplus_cli)) +#define AE_USE_STD_ATOMIC_FOR_WEAK_ATOMIC +#endif + +#ifdef AE_USE_STD_ATOMIC_FOR_WEAK_ATOMIC +#include +#endif +#include + +// WARNING: *NOT* A REPLACEMENT FOR std::atomic. READ CAREFULLY: +// Provides basic support for atomic variables -- no memory ordering guarantees are provided. +// The guarantee of atomicity is only made for types that already have atomic load and store +// guarantees at the hardware level -- on most platforms this generally means aligned pointers and +// integers (only). +namespace Common { +template +class weak_atomic { +public: + AE_NO_TSAN weak_atomic() : value() {} +#ifdef AE_VCPP +#pragma warning(push) +#pragma warning(disable : 4100) // Get rid of (erroneous) 'unreferenced formal parameter' warning +#endif + template + AE_NO_TSAN weak_atomic(U&& x) : value(std::forward(x)) {} +#ifdef __cplusplus_cli + // Work around bug with universal reference/nullptr combination that only appears when /clr is + // on + AE_NO_TSAN weak_atomic(nullptr_t) : value(nullptr) {} +#endif + AE_NO_TSAN weak_atomic(weak_atomic const& other) : value(other.load()) {} + AE_NO_TSAN weak_atomic(weak_atomic&& other) : value(std::move(other.load())) {} +#ifdef AE_VCPP +#pragma warning(pop) +#endif + + AE_FORCEINLINE operator T() const AE_NO_TSAN { + return load(); + } + +#ifndef AE_USE_STD_ATOMIC_FOR_WEAK_ATOMIC + template + AE_FORCEINLINE weak_atomic const& operator=(U&& x) AE_NO_TSAN { + value = std::forward(x); + return *this; + } + AE_FORCEINLINE weak_atomic const& operator=(weak_atomic const& other) AE_NO_TSAN { + value = other.value; + return *this; + } + + AE_FORCEINLINE T load() const AE_NO_TSAN { + return value; + } + + AE_FORCEINLINE T fetch_add_acquire(T increment) AE_NO_TSAN { +#if defined(AE_ARCH_X64) || defined(AE_ARCH_X86) + if (sizeof(T) == 4) + return _InterlockedExchangeAdd((long volatile*)&value, (long)increment); +#if defined(_M_AMD64) + else if (sizeof(T) == 8) + return _InterlockedExchangeAdd64((long long volatile*)&value, (long long)increment); +#endif +#else +#error Unsupported platform +#endif + assert(false && "T must be either a 32 or 64 bit type"); + return value; + } + + AE_FORCEINLINE T fetch_add_release(T increment) AE_NO_TSAN { +#if defined(AE_ARCH_X64) || defined(AE_ARCH_X86) + if (sizeof(T) == 4) + return _InterlockedExchangeAdd((long volatile*)&value, (long)increment); +#if defined(_M_AMD64) + else if (sizeof(T) == 8) + return _InterlockedExchangeAdd64((long long volatile*)&value, (long long)increment); +#endif +#else +#error Unsupported platform +#endif + assert(false && "T must be either a 32 or 64 bit type"); + return value; + } +#else + template + AE_FORCEINLINE weak_atomic const& operator=(U&& x) AE_NO_TSAN { + value.store(std::forward(x), std::memory_order_relaxed); + return *this; + } + + AE_FORCEINLINE weak_atomic const& operator=(weak_atomic const& other) AE_NO_TSAN { + value.store(other.value.load(std::memory_order_relaxed), std::memory_order_relaxed); + return *this; + } + + AE_FORCEINLINE T load() const AE_NO_TSAN { + return value.load(std::memory_order_relaxed); + } + + AE_FORCEINLINE T fetch_add_acquire(T increment) AE_NO_TSAN { + return value.fetch_add(increment, std::memory_order_acquire); + } + + AE_FORCEINLINE T fetch_add_release(T increment) AE_NO_TSAN { + return value.fetch_add(increment, std::memory_order_release); + } +#endif + +private: +#ifndef AE_USE_STD_ATOMIC_FOR_WEAK_ATOMIC + // No std::atomic support, but still need to circumvent compiler optimizations. + // `volatile` will make memory access slow, but is guaranteed to be reliable. + volatile T value; +#else + std::atomic value; +#endif +}; + +} // namespace Common + +// Portable single-producer, single-consumer semaphore below: + +#if defined(_WIN32) +// Avoid including windows.h in a header; we only need a handful of +// items, so we'll redeclare them here (this is relatively safe since +// the API generally has to remain stable between Windows versions). +// I know this is an ugly hack but it still beats polluting the global +// namespace with thousands of generic names or adding a .cpp for nothing. +extern "C" { +struct _SECURITY_ATTRIBUTES; +__declspec(dllimport) void* __stdcall CreateSemaphoreW(_SECURITY_ATTRIBUTES* lpSemaphoreAttributes, + long lInitialCount, long lMaximumCount, + const wchar_t* lpName); +__declspec(dllimport) int __stdcall CloseHandle(void* hObject); +__declspec(dllimport) unsigned long __stdcall WaitForSingleObject(void* hHandle, + unsigned long dwMilliseconds); +__declspec(dllimport) int __stdcall ReleaseSemaphore(void* hSemaphore, long lReleaseCount, + long* lpPreviousCount); +} +#elif defined(__MACH__) +#include +#elif defined(__unix__) +#include +#elif defined(FREERTOS) +#include +#include +#include +#endif + +namespace Common { +// Code in the spsc_sema namespace below is an adaptation of Jeff Preshing's +// portable + lightweight semaphore implementations, originally from +// https://github.com/preshing/cpp11-on-multicore/blob/master/common/sema.h +// LICENSE: +// Copyright (c) 2015 Jeff Preshing +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgement in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +namespace spsc_sema { +#if defined(_WIN32) +class Semaphore { +private: + void* m_hSema; + + Semaphore(const Semaphore& other); + Semaphore& operator=(const Semaphore& other); + +public: + AE_NO_TSAN Semaphore(int initialCount = 0) : m_hSema() { + assert(initialCount >= 0); + const long maxLong = 0x7fffffff; + m_hSema = CreateSemaphoreW(nullptr, initialCount, maxLong, nullptr); + assert(m_hSema); + } + + AE_NO_TSAN ~Semaphore() { + CloseHandle(m_hSema); + } + + bool wait() AE_NO_TSAN { + const unsigned long infinite = 0xffffffff; + return WaitForSingleObject(m_hSema, infinite) == 0; + } + + bool try_wait() AE_NO_TSAN { + return WaitForSingleObject(m_hSema, 0) == 0; + } + + bool timed_wait(std::uint64_t usecs) AE_NO_TSAN { + return WaitForSingleObject(m_hSema, (unsigned long)(usecs / 1000)) == 0; + } + + void signal(int count = 1) AE_NO_TSAN { + while (!ReleaseSemaphore(m_hSema, count, nullptr)) + ; + } +}; +#elif defined(__MACH__) +//--------------------------------------------------------- +// Semaphore (Apple iOS and OSX) +// Can't use POSIX semaphores due to +// http://lists.apple.com/archives/darwin-kernel/2009/Apr/msg00010.html +//--------------------------------------------------------- +class Semaphore { +private: + semaphore_t m_sema; + + Semaphore(const Semaphore& other); + Semaphore& operator=(const Semaphore& other); + +public: + AE_NO_TSAN Semaphore(int initialCount = 0) : m_sema() { + assert(initialCount >= 0); + kern_return_t rc = + semaphore_create(mach_task_self(), &m_sema, SYNC_POLICY_FIFO, initialCount); + assert(rc == KERN_SUCCESS); + AE_UNUSED(rc); + } + + AE_NO_TSAN ~Semaphore() { + semaphore_destroy(mach_task_self(), m_sema); + } + + bool wait() AE_NO_TSAN { + return semaphore_wait(m_sema) == KERN_SUCCESS; + } + + bool try_wait() AE_NO_TSAN { + return timed_wait(0); + } + + bool timed_wait(std::uint64_t timeout_usecs) AE_NO_TSAN { + mach_timespec_t ts; + ts.tv_sec = static_cast(timeout_usecs / 1000000); + ts.tv_nsec = static_cast((timeout_usecs % 1000000) * 1000); + + // added in OSX 10.10: + // https://developer.apple.com/library/prerelease/mac/documentation/General/Reference/APIDiffsMacOSX10_10SeedDiff/modules/Darwin.html + kern_return_t rc = semaphore_timedwait(m_sema, ts); + return rc == KERN_SUCCESS; + } + + void signal() AE_NO_TSAN { + while (semaphore_signal(m_sema) != KERN_SUCCESS) + ; + } + + void signal(int count) AE_NO_TSAN { + while (count-- > 0) { + while (semaphore_signal(m_sema) != KERN_SUCCESS) + ; + } + } +}; +#elif defined(__unix__) +//--------------------------------------------------------- +// Semaphore (POSIX, Linux) +//--------------------------------------------------------- +class Semaphore { +private: + sem_t m_sema; + + Semaphore(const Semaphore& other); + Semaphore& operator=(const Semaphore& other); + +public: + AE_NO_TSAN Semaphore(int initialCount = 0) : m_sema() { + assert(initialCount >= 0); + int rc = sem_init(&m_sema, 0, static_cast(initialCount)); + assert(rc == 0); + AE_UNUSED(rc); + } + + AE_NO_TSAN ~Semaphore() { + sem_destroy(&m_sema); + } + + bool wait() AE_NO_TSAN { + // http://stackoverflow.com/questions/2013181/gdb-causes-sem-wait-to-fail-with-eintr-error + int rc; + do { + rc = sem_wait(&m_sema); + } while (rc == -1 && errno == EINTR); + return rc == 0; + } + + bool try_wait() AE_NO_TSAN { + int rc; + do { + rc = sem_trywait(&m_sema); + } while (rc == -1 && errno == EINTR); + return rc == 0; + } + + bool timed_wait(std::uint64_t usecs) AE_NO_TSAN { + struct timespec ts; + const int usecs_in_1_sec = 1000000; + const int nsecs_in_1_sec = 1000000000; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += static_cast(usecs / usecs_in_1_sec); + ts.tv_nsec += static_cast(usecs % usecs_in_1_sec) * 1000; + // sem_timedwait bombs if you have more than 1e9 in tv_nsec + // so we have to clean things up before passing it in + if (ts.tv_nsec >= nsecs_in_1_sec) { + ts.tv_nsec -= nsecs_in_1_sec; + ++ts.tv_sec; + } + + int rc; + do { + rc = sem_timedwait(&m_sema, &ts); + } while (rc == -1 && errno == EINTR); + return rc == 0; + } + + void signal() AE_NO_TSAN { + while (sem_post(&m_sema) == -1) + ; + } + + void signal(int count) AE_NO_TSAN { + while (count-- > 0) { + while (sem_post(&m_sema) == -1) + ; + } + } +}; +#elif defined(FREERTOS) +//--------------------------------------------------------- +// Semaphore (FreeRTOS) +//--------------------------------------------------------- +class Semaphore { +private: + SemaphoreHandle_t m_sema; + + Semaphore(const Semaphore& other); + Semaphore& operator=(const Semaphore& other); + +public: + AE_NO_TSAN Semaphore(int initialCount = 0) : m_sema() { + assert(initialCount >= 0); + m_sema = xSemaphoreCreateCounting(static_cast(~0ull), + static_cast(initialCount)); + assert(m_sema); + } + + AE_NO_TSAN ~Semaphore() { + vSemaphoreDelete(m_sema); + } + + bool wait() AE_NO_TSAN { + return xSemaphoreTake(m_sema, portMAX_DELAY) == pdTRUE; + } + + bool try_wait() AE_NO_TSAN { + // Note: In an ISR context, if this causes a task to unblock, + // the caller won't know about it + if (xPortIsInsideInterrupt()) + return xSemaphoreTakeFromISR(m_sema, NULL) == pdTRUE; + return xSemaphoreTake(m_sema, 0) == pdTRUE; + } + + bool timed_wait(std::uint64_t usecs) AE_NO_TSAN { + std::uint64_t msecs = usecs / 1000; + TickType_t ticks = static_cast(msecs / portTICK_PERIOD_MS); + if (ticks == 0) + return try_wait(); + return xSemaphoreTake(m_sema, ticks) == pdTRUE; + } + + void signal() AE_NO_TSAN { + // Note: In an ISR context, if this causes a task to unblock, + // the caller won't know about it + BaseType_t rc; + if (xPortIsInsideInterrupt()) + rc = xSemaphoreGiveFromISR(m_sema, NULL); + else + rc = xSemaphoreGive(m_sema); + assert(rc == pdTRUE); + AE_UNUSED(rc); + } + + void signal(int count) AE_NO_TSAN { + while (count-- > 0) + signal(); + } +}; +#else +#error Unsupported platform! (No semaphore wrapper available) +#endif + +//--------------------------------------------------------- +// LightweightSemaphore +//--------------------------------------------------------- +class LightweightSemaphore { +public: + typedef std::make_signed::type ssize_t; + +private: + weak_atomic m_count; + Semaphore m_sema; + + bool waitWithPartialSpinning(std::int64_t timeout_usecs = -1) AE_NO_TSAN { + ssize_t oldCount; + // Is there a better way to set the initial spin count? + // If we lower it to 1000, testBenaphore becomes 15x slower on my Core i7-5930K Windows PC, + // as threads start hitting the kernel semaphore. + int spin = 1024; + while (--spin >= 0) { + if (m_count.load() > 0) { + m_count.fetch_add_acquire(-1); + return true; + } + compiler_fence(memory_order_acquire); // Prevent the compiler from collapsing the loop. + } + oldCount = m_count.fetch_add_acquire(-1); + if (oldCount > 0) + return true; + if (timeout_usecs < 0) { + if (m_sema.wait()) + return true; + } + if (timeout_usecs > 0 && m_sema.timed_wait(static_cast(timeout_usecs))) + return true; + // At this point, we've timed out waiting for the semaphore, but the + // count is still decremented indicating we may still be waiting on + // it. So we have to re-adjust the count, but only if the semaphore + // wasn't signaled enough times for us too since then. If it was, we + // need to release the semaphore too. + while (true) { + oldCount = m_count.fetch_add_release(1); + if (oldCount < 0) + return false; // successfully restored things to the way they were + // Oh, the producer thread just signaled the semaphore after all. Try again: + oldCount = m_count.fetch_add_acquire(-1); + if (oldCount > 0 && m_sema.try_wait()) + return true; + } + } + +public: + AE_NO_TSAN LightweightSemaphore(ssize_t initialCount = 0) : m_count(initialCount), m_sema() { + assert(initialCount >= 0); + } + + bool tryWait() AE_NO_TSAN { + if (m_count.load() > 0) { + m_count.fetch_add_acquire(-1); + return true; + } + return false; + } + + bool wait() AE_NO_TSAN { + return tryWait() || waitWithPartialSpinning(); + } + + bool wait(std::int64_t timeout_usecs) AE_NO_TSAN { + return tryWait() || waitWithPartialSpinning(timeout_usecs); + } + + void signal(ssize_t count = 1) AE_NO_TSAN { + assert(count >= 0); + ssize_t oldCount = m_count.fetch_add_release(count); + assert(oldCount >= -1); + if (oldCount < 0) { + m_sema.signal(1); + } + } + + std::size_t availableApprox() const AE_NO_TSAN { + ssize_t count = m_count.load(); + return count > 0 ? static_cast(count) : 0; + } +}; +} // namespace spsc_sema +} // namespace Common + +#if defined(AE_VCPP) && (_MSC_VER < 1700 || defined(__cplusplus_cli)) +#pragma warning(pop) +#ifdef __cplusplus_cli +#pragma managed(pop) +#endif +#endif diff --git a/src/common/fixed_point.h b/src/common/fixed_point.h new file mode 100644 index 0000000000..1d45e51b32 --- /dev/null +++ b/src/common/fixed_point.h @@ -0,0 +1,726 @@ +// From: https://github.com/eteran/cpp-utilities/blob/master/fixed/include/cpp-utilities/fixed.h +// See also: http://stackoverflow.com/questions/79677/whats-the-best-way-to-do-fixed-point-math +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 Evan Teran + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef FIXED_H_ +#define FIXED_H_ + +#if __cplusplus >= 201402L +#define CONSTEXPR14 constexpr +#else +#define CONSTEXPR14 +#endif + +#include // for size_t +#include +#include +#include +#include + +namespace Common { + +template +class FixedPoint; + +namespace detail { + +// helper templates to make magic with types :) +// these allow us to determine resonable types from +// a desired size, they also let us infer the next largest type +// from a type which is nice for the division op +template +struct type_from_size { + using value_type = void; + using unsigned_type = void; + using signed_type = void; + static constexpr bool is_specialized = false; +}; + +#if defined(__GNUC__) && defined(__x86_64__) && !defined(__STRICT_ANSI__) +template <> +struct type_from_size<128> { + static constexpr bool is_specialized = true; + static constexpr size_t size = 128; + + using value_type = __int128; + using unsigned_type = unsigned __int128; + using signed_type = __int128; + using next_size = type_from_size<256>; +}; +#endif + +template <> +struct type_from_size<64> { + static constexpr bool is_specialized = true; + static constexpr size_t size = 64; + + using value_type = int64_t; + using unsigned_type = std::make_unsigned::type; + using signed_type = std::make_signed::type; + using next_size = type_from_size<128>; +}; + +template <> +struct type_from_size<32> { + static constexpr bool is_specialized = true; + static constexpr size_t size = 32; + + using value_type = int32_t; + using unsigned_type = std::make_unsigned::type; + using signed_type = std::make_signed::type; + using next_size = type_from_size<64>; +}; + +template <> +struct type_from_size<16> { + static constexpr bool is_specialized = true; + static constexpr size_t size = 16; + + using value_type = int16_t; + using unsigned_type = std::make_unsigned::type; + using signed_type = std::make_signed::type; + using next_size = type_from_size<32>; +}; + +template <> +struct type_from_size<8> { + static constexpr bool is_specialized = true; + static constexpr size_t size = 8; + + using value_type = int8_t; + using unsigned_type = std::make_unsigned::type; + using signed_type = std::make_signed::type; + using next_size = type_from_size<16>; +}; + +// this is to assist in adding support for non-native base +// types (for adding big-int support), this should be fine +// unless your bit-int class doesn't nicely support casting +template +constexpr B next_to_base(N rhs) { + return static_cast(rhs); +} + +struct divide_by_zero : std::exception {}; + +template +CONSTEXPR14 FixedPoint divide( + FixedPoint numerator, FixedPoint denominator, FixedPoint& remainder, + typename std::enable_if::next_size::is_specialized>::type* = nullptr) { + + using next_type = typename FixedPoint::next_type; + using base_type = typename FixedPoint::base_type; + constexpr size_t fractional_bits = FixedPoint::fractional_bits; + + next_type t(numerator.to_raw()); + t <<= fractional_bits; + + FixedPoint quotient; + + quotient = FixedPoint::from_base(next_to_base(t / denominator.to_raw())); + remainder = FixedPoint::from_base(next_to_base(t % denominator.to_raw())); + + return quotient; +} + +template +CONSTEXPR14 FixedPoint divide( + FixedPoint numerator, FixedPoint denominator, FixedPoint& remainder, + typename std::enable_if::next_size::is_specialized>::type* = nullptr) { + + using unsigned_type = typename FixedPoint::unsigned_type; + + constexpr int bits = FixedPoint::total_bits; + + if (denominator == 0) { + throw divide_by_zero(); + } else { + + int sign = 0; + + FixedPoint quotient; + + if (numerator < 0) { + sign ^= 1; + numerator = -numerator; + } + + if (denominator < 0) { + sign ^= 1; + denominator = -denominator; + } + + unsigned_type n = numerator.to_raw(); + unsigned_type d = denominator.to_raw(); + unsigned_type x = 1; + unsigned_type answer = 0; + + // egyptian division algorithm + while ((n >= d) && (((d >> (bits - 1)) & 1) == 0)) { + x <<= 1; + d <<= 1; + } + + while (x != 0) { + if (n >= d) { + n -= d; + answer += x; + } + + x >>= 1; + d >>= 1; + } + + unsigned_type l1 = n; + unsigned_type l2 = denominator.to_raw(); + + // calculate the lower bits (needs to be unsigned) + while (l1 >> (bits - F) > 0) { + l1 >>= 1; + l2 >>= 1; + } + const unsigned_type lo = (l1 << F) / l2; + + quotient = FixedPoint::from_base((answer << F) | lo); + remainder = n; + + if (sign) { + quotient = -quotient; + } + + return quotient; + } +} + +// this is the usual implementation of multiplication +template +CONSTEXPR14 FixedPoint multiply( + FixedPoint lhs, FixedPoint rhs, + typename std::enable_if::next_size::is_specialized>::type* = nullptr) { + + using next_type = typename FixedPoint::next_type; + using base_type = typename FixedPoint::base_type; + + constexpr size_t fractional_bits = FixedPoint::fractional_bits; + + next_type t(static_cast(lhs.to_raw()) * static_cast(rhs.to_raw())); + t >>= fractional_bits; + + return FixedPoint::from_base(next_to_base(t)); +} + +// this is the fall back version we use when we don't have a next size +// it is slightly slower, but is more robust since it doesn't +// require and upgraded type +template +CONSTEXPR14 FixedPoint multiply( + FixedPoint lhs, FixedPoint rhs, + typename std::enable_if::next_size::is_specialized>::type* = nullptr) { + + using base_type = typename FixedPoint::base_type; + + constexpr size_t fractional_bits = FixedPoint::fractional_bits; + constexpr base_type integer_mask = FixedPoint::integer_mask; + constexpr base_type fractional_mask = FixedPoint::fractional_mask; + + // more costly but doesn't need a larger type + const base_type a_hi = (lhs.to_raw() & integer_mask) >> fractional_bits; + const base_type b_hi = (rhs.to_raw() & integer_mask) >> fractional_bits; + const base_type a_lo = (lhs.to_raw() & fractional_mask); + const base_type b_lo = (rhs.to_raw() & fractional_mask); + + const base_type x1 = a_hi * b_hi; + const base_type x2 = a_hi * b_lo; + const base_type x3 = a_lo * b_hi; + const base_type x4 = a_lo * b_lo; + + return FixedPoint::from_base((x1 << fractional_bits) + (x3 + x2) + + (x4 >> fractional_bits)); +} +} // namespace detail + +template +class FixedPoint { + static_assert(detail::type_from_size::is_specialized, "invalid combination of sizes"); + +public: + static constexpr size_t fractional_bits = F; + static constexpr size_t integer_bits = I; + static constexpr size_t total_bits = I + F; + + using base_type_info = detail::type_from_size; + + using base_type = typename base_type_info::value_type; + using next_type = typename base_type_info::next_size::value_type; + using unsigned_type = typename base_type_info::unsigned_type; + +public: +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Woverflow" +#endif + static constexpr base_type fractional_mask = + ~(static_cast(~base_type(0)) << fractional_bits); + static constexpr base_type integer_mask = ~fractional_mask; +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +public: + static constexpr base_type one = base_type(1) << fractional_bits; + +public: // constructors + FixedPoint() = default; + FixedPoint(const FixedPoint&) = default; + FixedPoint(FixedPoint&&) = default; + FixedPoint& operator=(const FixedPoint&) = default; + + template + constexpr FixedPoint( + Number n, typename std::enable_if::value>::type* = nullptr) + : data_(static_cast(n * one)) {} + +public: // conversion + template + CONSTEXPR14 explicit FixedPoint(FixedPoint other) { + static_assert(I2 <= I && F2 <= F, "Scaling conversion can only upgrade types"); + using T = FixedPoint; + + const base_type fractional = (other.data_ & T::fractional_mask); + const base_type integer = (other.data_ & T::integer_mask) >> T::fractional_bits; + data_ = + (integer << fractional_bits) | (fractional << (fractional_bits - T::fractional_bits)); + } + +private: + // this makes it simpler to create a FixedPoint point object from + // a native type without scaling + // use "FixedPoint::from_base" in order to perform this. + struct NoScale {}; + + constexpr FixedPoint(base_type n, const NoScale&) : data_(n) {} + +public: + static constexpr FixedPoint from_base(base_type n) { + return FixedPoint(n, NoScale()); + } + +public: // comparison operators + constexpr bool operator==(FixedPoint rhs) const { + return data_ == rhs.data_; + } + + constexpr bool operator!=(FixedPoint rhs) const { + return data_ != rhs.data_; + } + + constexpr bool operator<(FixedPoint rhs) const { + return data_ < rhs.data_; + } + + constexpr bool operator>(FixedPoint rhs) const { + return data_ > rhs.data_; + } + + constexpr bool operator<=(FixedPoint rhs) const { + return data_ <= rhs.data_; + } + + constexpr bool operator>=(FixedPoint rhs) const { + return data_ >= rhs.data_; + } + +public: // unary operators + constexpr bool operator!() const { + return !data_; + } + + constexpr FixedPoint operator~() const { + // NOTE(eteran): this will often appear to "just negate" the value + // that is not an error, it is because -x == (~x+1) + // and that "+1" is adding an infinitesimally small fraction to the + // complimented value + return FixedPoint::from_base(~data_); + } + + constexpr FixedPoint operator-() const { + return FixedPoint::from_base(-data_); + } + + constexpr FixedPoint operator+() const { + return FixedPoint::from_base(+data_); + } + + CONSTEXPR14 FixedPoint& operator++() { + data_ += one; + return *this; + } + + CONSTEXPR14 FixedPoint& operator--() { + data_ -= one; + return *this; + } + + CONSTEXPR14 FixedPoint operator++(int) { + FixedPoint tmp(*this); + data_ += one; + return tmp; + } + + CONSTEXPR14 FixedPoint operator--(int) { + FixedPoint tmp(*this); + data_ -= one; + return tmp; + } + +public: // basic math operators + CONSTEXPR14 FixedPoint& operator+=(FixedPoint n) { + data_ += n.data_; + return *this; + } + + CONSTEXPR14 FixedPoint& operator-=(FixedPoint n) { + data_ -= n.data_; + return *this; + } + + CONSTEXPR14 FixedPoint& operator*=(FixedPoint n) { + return assign(detail::multiply(*this, n)); + } + + CONSTEXPR14 FixedPoint& operator/=(FixedPoint n) { + FixedPoint temp; + return assign(detail::divide(*this, n, temp)); + } + +private: + CONSTEXPR14 FixedPoint& assign(FixedPoint rhs) { + data_ = rhs.data_; + return *this; + } + +public: // binary math operators, effects underlying bit pattern since these + // don't really typically make sense for non-integer values + CONSTEXPR14 FixedPoint& operator&=(FixedPoint n) { + data_ &= n.data_; + return *this; + } + + CONSTEXPR14 FixedPoint& operator|=(FixedPoint n) { + data_ |= n.data_; + return *this; + } + + CONSTEXPR14 FixedPoint& operator^=(FixedPoint n) { + data_ ^= n.data_; + return *this; + } + + template ::value>::type> + CONSTEXPR14 FixedPoint& operator>>=(Integer n) { + data_ >>= n; + return *this; + } + + template ::value>::type> + CONSTEXPR14 FixedPoint& operator<<=(Integer n) { + data_ <<= n; + return *this; + } + +public: // conversion to basic types + constexpr void round_up() { + data_ += (data_ & fractional_mask) >> 1; + } + + constexpr int to_int() { + round_up(); + return static_cast((data_ & integer_mask) >> fractional_bits); + } + + constexpr unsigned int to_uint() const { + round_up(); + return static_cast((data_ & integer_mask) >> fractional_bits); + } + + constexpr int64_t to_long() { + round_up(); + return static_cast((data_ & integer_mask) >> fractional_bits); + } + + constexpr int to_int_floor() const { + return static_cast((data_ & integer_mask) >> fractional_bits); + } + + constexpr int64_t to_long_floor() { + return static_cast((data_ & integer_mask) >> fractional_bits); + } + + constexpr unsigned int to_uint_floor() const { + return static_cast((data_ & integer_mask) >> fractional_bits); + } + + constexpr float to_float() const { + return static_cast(data_) / FixedPoint::one; + } + + constexpr double to_double() const { + return static_cast(data_) / FixedPoint::one; + } + + constexpr base_type to_raw() const { + return data_; + } + + constexpr void clear_int() { + data_ &= fractional_mask; + } + + constexpr base_type get_frac() const { + return data_ & fractional_mask; + } + +public: + CONSTEXPR14 void swap(FixedPoint& rhs) { + using std::swap; + swap(data_, rhs.data_); + } + +public: + base_type data_; +}; + +// if we have the same fractional portion, but differing integer portions, we trivially upgrade the +// smaller type +template +CONSTEXPR14 typename std::conditional= I2, FixedPoint, FixedPoint>::type +operator+(FixedPoint lhs, FixedPoint rhs) { + + using T = typename std::conditional= I2, FixedPoint, FixedPoint>::type; + + const T l = T::from_base(lhs.to_raw()); + const T r = T::from_base(rhs.to_raw()); + return l + r; +} + +template +CONSTEXPR14 typename std::conditional= I2, FixedPoint, FixedPoint>::type +operator-(FixedPoint lhs, FixedPoint rhs) { + + using T = typename std::conditional= I2, FixedPoint, FixedPoint>::type; + + const T l = T::from_base(lhs.to_raw()); + const T r = T::from_base(rhs.to_raw()); + return l - r; +} + +template +CONSTEXPR14 typename std::conditional= I2, FixedPoint, FixedPoint>::type +operator*(FixedPoint lhs, FixedPoint rhs) { + + using T = typename std::conditional= I2, FixedPoint, FixedPoint>::type; + + const T l = T::from_base(lhs.to_raw()); + const T r = T::from_base(rhs.to_raw()); + return l * r; +} + +template +CONSTEXPR14 typename std::conditional= I2, FixedPoint, FixedPoint>::type +operator/(FixedPoint lhs, FixedPoint rhs) { + + using T = typename std::conditional= I2, FixedPoint, FixedPoint>::type; + + const T l = T::from_base(lhs.to_raw()); + const T r = T::from_base(rhs.to_raw()); + return l / r; +} + +template +std::ostream& operator<<(std::ostream& os, FixedPoint f) { + os << f.to_double(); + return os; +} + +// basic math operators +template +CONSTEXPR14 FixedPoint operator+(FixedPoint lhs, FixedPoint rhs) { + lhs += rhs; + return lhs; +} +template +CONSTEXPR14 FixedPoint operator-(FixedPoint lhs, FixedPoint rhs) { + lhs -= rhs; + return lhs; +} +template +CONSTEXPR14 FixedPoint operator*(FixedPoint lhs, FixedPoint rhs) { + lhs *= rhs; + return lhs; +} +template +CONSTEXPR14 FixedPoint operator/(FixedPoint lhs, FixedPoint rhs) { + lhs /= rhs; + return lhs; +} + +template ::value>::type> +CONSTEXPR14 FixedPoint operator+(FixedPoint lhs, Number rhs) { + lhs += FixedPoint(rhs); + return lhs; +} +template ::value>::type> +CONSTEXPR14 FixedPoint operator-(FixedPoint lhs, Number rhs) { + lhs -= FixedPoint(rhs); + return lhs; +} +template ::value>::type> +CONSTEXPR14 FixedPoint operator*(FixedPoint lhs, Number rhs) { + lhs *= FixedPoint(rhs); + return lhs; +} +template ::value>::type> +CONSTEXPR14 FixedPoint operator/(FixedPoint lhs, Number rhs) { + lhs /= FixedPoint(rhs); + return lhs; +} + +template ::value>::type> +CONSTEXPR14 FixedPoint operator+(Number lhs, FixedPoint rhs) { + FixedPoint tmp(lhs); + tmp += rhs; + return tmp; +} +template ::value>::type> +CONSTEXPR14 FixedPoint operator-(Number lhs, FixedPoint rhs) { + FixedPoint tmp(lhs); + tmp -= rhs; + return tmp; +} +template ::value>::type> +CONSTEXPR14 FixedPoint operator*(Number lhs, FixedPoint rhs) { + FixedPoint tmp(lhs); + tmp *= rhs; + return tmp; +} +template ::value>::type> +CONSTEXPR14 FixedPoint operator/(Number lhs, FixedPoint rhs) { + FixedPoint tmp(lhs); + tmp /= rhs; + return tmp; +} + +// shift operators +template ::value>::type> +CONSTEXPR14 FixedPoint operator<<(FixedPoint lhs, Integer rhs) { + lhs <<= rhs; + return lhs; +} +template ::value>::type> +CONSTEXPR14 FixedPoint operator>>(FixedPoint lhs, Integer rhs) { + lhs >>= rhs; + return lhs; +} + +// comparison operators +template ::value>::type> +constexpr bool operator>(FixedPoint lhs, Number rhs) { + return lhs > FixedPoint(rhs); +} +template ::value>::type> +constexpr bool operator<(FixedPoint lhs, Number rhs) { + return lhs < FixedPoint(rhs); +} +template ::value>::type> +constexpr bool operator>=(FixedPoint lhs, Number rhs) { + return lhs >= FixedPoint(rhs); +} +template ::value>::type> +constexpr bool operator<=(FixedPoint lhs, Number rhs) { + return lhs <= FixedPoint(rhs); +} +template ::value>::type> +constexpr bool operator==(FixedPoint lhs, Number rhs) { + return lhs == FixedPoint(rhs); +} +template ::value>::type> +constexpr bool operator!=(FixedPoint lhs, Number rhs) { + return lhs != FixedPoint(rhs); +} + +template ::value>::type> +constexpr bool operator>(Number lhs, FixedPoint rhs) { + return FixedPoint(lhs) > rhs; +} +template ::value>::type> +constexpr bool operator<(Number lhs, FixedPoint rhs) { + return FixedPoint(lhs) < rhs; +} +template ::value>::type> +constexpr bool operator>=(Number lhs, FixedPoint rhs) { + return FixedPoint(lhs) >= rhs; +} +template ::value>::type> +constexpr bool operator<=(Number lhs, FixedPoint rhs) { + return FixedPoint(lhs) <= rhs; +} +template ::value>::type> +constexpr bool operator==(Number lhs, FixedPoint rhs) { + return FixedPoint(lhs) == rhs; +} +template ::value>::type> +constexpr bool operator!=(Number lhs, FixedPoint rhs) { + return FixedPoint(lhs) != rhs; +} + +} // namespace Common + +#undef CONSTEXPR14 + +#endif diff --git a/src/common/reader_writer_queue.h b/src/common/reader_writer_queue.h new file mode 100644 index 0000000000..8d2c9408cb --- /dev/null +++ b/src/common/reader_writer_queue.h @@ -0,0 +1,941 @@ +// ©2013-2020 Cameron Desrochers. +// Distributed under the simplified BSD license (see the license file that +// should have come with this header). + +#pragma once + +#include +#include +#include // For malloc/free/abort & size_t +#include +#include +#include +#include +#include + +#include "common/atomic_helpers.h" + +#if __cplusplus > 199711L || _MSC_VER >= 1700 // C++11 or VS2012 +#include +#endif + +// A lock-free queue for a single-consumer, single-producer architecture. +// The queue is also wait-free in the common path (except if more memory +// needs to be allocated, in which case malloc is called). +// Allocates memory sparingly, and only once if the original maximum size +// estimate is never exceeded. +// Tested on x86/x64 processors, but semantics should be correct for all +// architectures (given the right implementations in atomicops.h), provided +// that aligned integer and pointer accesses are naturally atomic. +// Note that there should only be one consumer thread and producer thread; +// Switching roles of the threads, or using multiple consecutive threads for +// one role, is not safe unless properly synchronized. +// Using the queue exclusively from one thread is fine, though a bit silly. + +#ifndef MOODYCAMEL_CACHE_LINE_SIZE +#define MOODYCAMEL_CACHE_LINE_SIZE 64 +#endif + +#ifndef MOODYCAMEL_EXCEPTIONS_ENABLED +#if (defined(_MSC_VER) && defined(_CPPUNWIND)) || (defined(__GNUC__) && defined(__EXCEPTIONS)) || \ + (!defined(_MSC_VER) && !defined(__GNUC__)) +#define MOODYCAMEL_EXCEPTIONS_ENABLED +#endif +#endif + +#ifndef MOODYCAMEL_HAS_EMPLACE +#if !defined(_MSC_VER) || \ + _MSC_VER >= 1800 // variadic templates: either a non-MS compiler or VS >= 2013 +#define MOODYCAMEL_HAS_EMPLACE 1 +#endif +#endif + +#ifndef MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE +#if defined(__APPLE__) && defined(__MACH__) && __cplusplus >= 201703L +// This is required to find out what deployment target we are using +#include +#if !defined(MAC_OS_X_VERSION_MIN_REQUIRED) || \ + MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_14 +// C++17 new(size_t, align_val_t) is not backwards-compatible with older versions of macOS, so we +// can't support over-alignment in this case +#define MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE +#endif +#endif +#endif + +#ifndef MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE +#define MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE AE_ALIGN(MOODYCAMEL_CACHE_LINE_SIZE) +#endif + +#ifdef AE_VCPP +#pragma warning(push) +#pragma warning(disable : 4324) // structure was padded due to __declspec(align()) +#pragma warning(disable : 4820) // padding was added +#pragma warning(disable : 4127) // conditional expression is constant +#endif + +namespace Common { + +template +class MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE ReaderWriterQueue { + // Design: Based on a queue-of-queues. The low-level queues are just + // circular buffers with front and tail indices indicating where the + // next element to dequeue is and where the next element can be enqueued, + // respectively. Each low-level queue is called a "block". Each block + // wastes exactly one element's worth of space to keep the design simple + // (if front == tail then the queue is empty, and can't be full). + // The high-level queue is a circular linked list of blocks; again there + // is a front and tail, but this time they are pointers to the blocks. + // The front block is where the next element to be dequeued is, provided + // the block is not empty. The back block is where elements are to be + // enqueued, provided the block is not full. + // The producer thread owns all the tail indices/pointers. The consumer + // thread owns all the front indices/pointers. Both threads read each + // other's variables, but only the owning thread updates them. E.g. After + // the consumer reads the producer's tail, the tail may change before the + // consumer is done dequeuing an object, but the consumer knows the tail + // will never go backwards, only forwards. + // If there is no room to enqueue an object, an additional block (of + // equal size to the last block) is added. Blocks are never removed. + +public: + typedef T value_type; + + // Constructs a queue that can hold at least `size` elements without further + // allocations. If more than MAX_BLOCK_SIZE elements are requested, + // then several blocks of MAX_BLOCK_SIZE each are reserved (including + // at least one extra buffer block). + AE_NO_TSAN explicit ReaderWriterQueue(size_t size = 15) +#ifndef NDEBUG + : enqueuing(false), dequeuing(false) +#endif + { + assert(MAX_BLOCK_SIZE == ceilToPow2(MAX_BLOCK_SIZE) && + "MAX_BLOCK_SIZE must be a power of 2"); + assert(MAX_BLOCK_SIZE >= 2 && "MAX_BLOCK_SIZE must be at least 2"); + + Block* firstBlock = nullptr; + + largestBlockSize = + ceilToPow2(size + 1); // We need a spare slot to fit size elements in the block + if (largestBlockSize > MAX_BLOCK_SIZE * 2) { + // We need a spare block in case the producer is writing to a different block the + // consumer is reading from, and wants to enqueue the maximum number of elements. We + // also need a spare element in each block to avoid the ambiguity between front == tail + // meaning "empty" and "full". So the effective number of slots that are guaranteed to + // be usable at any time is the block size - 1 times the number of blocks - 1. Solving + // for size and applying a ceiling to the division gives us (after simplifying): + size_t initialBlockCount = (size + MAX_BLOCK_SIZE * 2 - 3) / (MAX_BLOCK_SIZE - 1); + largestBlockSize = MAX_BLOCK_SIZE; + Block* lastBlock = nullptr; + for (size_t i = 0; i != initialBlockCount; ++i) { + auto block = make_block(largestBlockSize); + if (block == nullptr) { +#ifdef MOODYCAMEL_EXCEPTIONS_ENABLED + throw std::bad_alloc(); +#else + abort(); +#endif + } + if (firstBlock == nullptr) { + firstBlock = block; + } else { + lastBlock->next = block; + } + lastBlock = block; + block->next = firstBlock; + } + } else { + firstBlock = make_block(largestBlockSize); + if (firstBlock == nullptr) { +#ifdef MOODYCAMEL_EXCEPTIONS_ENABLED + throw std::bad_alloc(); +#else + abort(); +#endif + } + firstBlock->next = firstBlock; + } + frontBlock = firstBlock; + tailBlock = firstBlock; + + // Make sure the reader/writer threads will have the initialized memory setup above: + fence(memory_order_sync); + } + + // Note: The queue should not be accessed concurrently while it's + // being moved. It's up to the user to synchronize this. + AE_NO_TSAN ReaderWriterQueue(ReaderWriterQueue&& other) + : frontBlock(other.frontBlock.load()), tailBlock(other.tailBlock.load()), + largestBlockSize(other.largestBlockSize) +#ifndef NDEBUG + , + enqueuing(false), dequeuing(false) +#endif + { + other.largestBlockSize = 32; + Block* b = other.make_block(other.largestBlockSize); + if (b == nullptr) { +#ifdef MOODYCAMEL_EXCEPTIONS_ENABLED + throw std::bad_alloc(); +#else + abort(); +#endif + } + b->next = b; + other.frontBlock = b; + other.tailBlock = b; + } + + // Note: The queue should not be accessed concurrently while it's + // being moved. It's up to the user to synchronize this. + ReaderWriterQueue& operator=(ReaderWriterQueue&& other) AE_NO_TSAN { + Block* b = frontBlock.load(); + frontBlock = other.frontBlock.load(); + other.frontBlock = b; + b = tailBlock.load(); + tailBlock = other.tailBlock.load(); + other.tailBlock = b; + std::swap(largestBlockSize, other.largestBlockSize); + return *this; + } + + // Note: The queue should not be accessed concurrently while it's + // being deleted. It's up to the user to synchronize this. + AE_NO_TSAN ~ReaderWriterQueue() { + // Make sure we get the latest version of all variables from other CPUs: + fence(memory_order_sync); + + // Destroy any remaining objects in queue and free memory + Block* frontBlock_ = frontBlock; + Block* block = frontBlock_; + do { + Block* nextBlock = block->next; + size_t blockFront = block->front; + size_t blockTail = block->tail; + + for (size_t i = blockFront; i != blockTail; i = (i + 1) & block->sizeMask) { + auto element = reinterpret_cast(block->data + i * sizeof(T)); + element->~T(); + (void)element; + } + + auto rawBlock = block->rawThis; + block->~Block(); + std::free(rawBlock); + block = nextBlock; + } while (block != frontBlock_); + } + + // Enqueues a copy of element if there is room in the queue. + // Returns true if the element was enqueued, false otherwise. + // Does not allocate memory. + AE_FORCEINLINE bool try_enqueue(T const& element) AE_NO_TSAN { + return inner_enqueue(element); + } + + // Enqueues a moved copy of element if there is room in the queue. + // Returns true if the element was enqueued, false otherwise. + // Does not allocate memory. + AE_FORCEINLINE bool try_enqueue(T&& element) AE_NO_TSAN { + return inner_enqueue(std::forward(element)); + } + +#if MOODYCAMEL_HAS_EMPLACE + // Like try_enqueue() but with emplace semantics (i.e. construct-in-place). + template + AE_FORCEINLINE bool try_emplace(Args&&... args) AE_NO_TSAN { + return inner_enqueue(std::forward(args)...); + } +#endif + + // Enqueues a copy of element on the queue. + // Allocates an additional block of memory if needed. + // Only fails (returns false) if memory allocation fails. + AE_FORCEINLINE bool enqueue(T const& element) AE_NO_TSAN { + return inner_enqueue(element); + } + + // Enqueues a moved copy of element on the queue. + // Allocates an additional block of memory if needed. + // Only fails (returns false) if memory allocation fails. + AE_FORCEINLINE bool enqueue(T&& element) AE_NO_TSAN { + return inner_enqueue(std::forward(element)); + } + +#if MOODYCAMEL_HAS_EMPLACE + // Like enqueue() but with emplace semantics (i.e. construct-in-place). + template + AE_FORCEINLINE bool emplace(Args&&... args) AE_NO_TSAN { + return inner_enqueue(std::forward(args)...); + } +#endif + + // Attempts to dequeue an element; if the queue is empty, + // returns false instead. If the queue has at least one element, + // moves front to result using operator=, then returns true. + template + bool try_dequeue(U& result) AE_NO_TSAN { +#ifndef NDEBUG + ReentrantGuard guard(this->dequeuing); +#endif + + // High-level pseudocode: + // Remember where the tail block is + // If the front block has an element in it, dequeue it + // Else + // If front block was the tail block when we entered the function, return false + // Else advance to next block and dequeue the item there + + // Note that we have to use the value of the tail block from before we check if the front + // block is full or not, in case the front block is empty and then, before we check if the + // tail block is at the front block or not, the producer fills up the front block *and + // moves on*, which would make us skip a filled block. Seems unlikely, but was consistently + // reproducible in practice. + // In order to avoid overhead in the common case, though, we do a double-checked pattern + // where we have the fast path if the front block is not empty, then read the tail block, + // then re-read the front block and check if it's not empty again, then check if the tail + // block has advanced. + + Block* frontBlock_ = frontBlock.load(); + size_t blockTail = frontBlock_->localTail; + size_t blockFront = frontBlock_->front.load(); + + if (blockFront != blockTail || + blockFront != (frontBlock_->localTail = frontBlock_->tail.load())) { + fence(memory_order_acquire); + + non_empty_front_block: + // Front block not empty, dequeue from here + auto element = reinterpret_cast(frontBlock_->data + blockFront * sizeof(T)); + result = std::move(*element); + element->~T(); + + blockFront = (blockFront + 1) & frontBlock_->sizeMask; + + fence(memory_order_release); + frontBlock_->front = blockFront; + } else if (frontBlock_ != tailBlock.load()) { + fence(memory_order_acquire); + + frontBlock_ = frontBlock.load(); + blockTail = frontBlock_->localTail = frontBlock_->tail.load(); + blockFront = frontBlock_->front.load(); + fence(memory_order_acquire); + + if (blockFront != blockTail) { + // Oh look, the front block isn't empty after all + goto non_empty_front_block; + } + + // Front block is empty but there's another block ahead, advance to it + Block* nextBlock = frontBlock_->next; + // Don't need an acquire fence here since next can only ever be set on the tailBlock, + // and we're not the tailBlock, and we did an acquire earlier after reading tailBlock + // which ensures next is up-to-date on this CPU in case we recently were at tailBlock. + + size_t nextBlockFront = nextBlock->front.load(); + size_t nextBlockTail = nextBlock->localTail = nextBlock->tail.load(); + fence(memory_order_acquire); + + // Since the tailBlock is only ever advanced after being written to, + // we know there's for sure an element to dequeue on it + assert(nextBlockFront != nextBlockTail); + AE_UNUSED(nextBlockTail); + + // We're done with this block, let the producer use it if it needs + fence(memory_order_release); // Expose possibly pending changes to frontBlock->front + // from last dequeue + frontBlock = frontBlock_ = nextBlock; + + compiler_fence(memory_order_release); // Not strictly needed + + auto element = reinterpret_cast(frontBlock_->data + nextBlockFront * sizeof(T)); + + result = std::move(*element); + element->~T(); + + nextBlockFront = (nextBlockFront + 1) & frontBlock_->sizeMask; + + fence(memory_order_release); + frontBlock_->front = nextBlockFront; + } else { + // No elements in current block and no other block to advance to + return false; + } + + return true; + } + + // Returns a pointer to the front element in the queue (the one that + // would be removed next by a call to `try_dequeue` or `pop`). If the + // queue appears empty at the time the method is called, nullptr is + // returned instead. + // Must be called only from the consumer thread. + T* peek() const AE_NO_TSAN { +#ifndef NDEBUG + ReentrantGuard guard(this->dequeuing); +#endif + // See try_dequeue() for reasoning + + Block* frontBlock_ = frontBlock.load(); + size_t blockTail = frontBlock_->localTail; + size_t blockFront = frontBlock_->front.load(); + + if (blockFront != blockTail || + blockFront != (frontBlock_->localTail = frontBlock_->tail.load())) { + fence(memory_order_acquire); + non_empty_front_block: + return reinterpret_cast(frontBlock_->data + blockFront * sizeof(T)); + } else if (frontBlock_ != tailBlock.load()) { + fence(memory_order_acquire); + frontBlock_ = frontBlock.load(); + blockTail = frontBlock_->localTail = frontBlock_->tail.load(); + blockFront = frontBlock_->front.load(); + fence(memory_order_acquire); + + if (blockFront != blockTail) { + goto non_empty_front_block; + } + + Block* nextBlock = frontBlock_->next; + + size_t nextBlockFront = nextBlock->front.load(); + fence(memory_order_acquire); + + assert(nextBlockFront != nextBlock->tail.load()); + return reinterpret_cast(nextBlock->data + nextBlockFront * sizeof(T)); + } + + return nullptr; + } + + // Removes the front element from the queue, if any, without returning it. + // Returns true on success, or false if the queue appeared empty at the time + // `pop` was called. + bool pop() AE_NO_TSAN { +#ifndef NDEBUG + ReentrantGuard guard(this->dequeuing); +#endif + // See try_dequeue() for reasoning + + Block* frontBlock_ = frontBlock.load(); + size_t blockTail = frontBlock_->localTail; + size_t blockFront = frontBlock_->front.load(); + + if (blockFront != blockTail || + blockFront != (frontBlock_->localTail = frontBlock_->tail.load())) { + fence(memory_order_acquire); + + non_empty_front_block: + auto element = reinterpret_cast(frontBlock_->data + blockFront * sizeof(T)); + element->~T(); + + blockFront = (blockFront + 1) & frontBlock_->sizeMask; + + fence(memory_order_release); + frontBlock_->front = blockFront; + } else if (frontBlock_ != tailBlock.load()) { + fence(memory_order_acquire); + frontBlock_ = frontBlock.load(); + blockTail = frontBlock_->localTail = frontBlock_->tail.load(); + blockFront = frontBlock_->front.load(); + fence(memory_order_acquire); + + if (blockFront != blockTail) { + goto non_empty_front_block; + } + + // Front block is empty but there's another block ahead, advance to it + Block* nextBlock = frontBlock_->next; + + size_t nextBlockFront = nextBlock->front.load(); + size_t nextBlockTail = nextBlock->localTail = nextBlock->tail.load(); + fence(memory_order_acquire); + + assert(nextBlockFront != nextBlockTail); + AE_UNUSED(nextBlockTail); + + fence(memory_order_release); + frontBlock = frontBlock_ = nextBlock; + + compiler_fence(memory_order_release); + + auto element = reinterpret_cast(frontBlock_->data + nextBlockFront * sizeof(T)); + element->~T(); + + nextBlockFront = (nextBlockFront + 1) & frontBlock_->sizeMask; + + fence(memory_order_release); + frontBlock_->front = nextBlockFront; + } else { + // No elements in current block and no other block to advance to + return false; + } + + return true; + } + + // Returns the approximate number of items currently in the queue. + // Safe to call from both the producer and consumer threads. + inline size_t size_approx() const AE_NO_TSAN { + size_t result = 0; + Block* frontBlock_ = frontBlock.load(); + Block* block = frontBlock_; + do { + fence(memory_order_acquire); + size_t blockFront = block->front.load(); + size_t blockTail = block->tail.load(); + result += (blockTail - blockFront) & block->sizeMask; + block = block->next.load(); + } while (block != frontBlock_); + return result; + } + + // Returns the total number of items that could be enqueued without incurring + // an allocation when this queue is empty. + // Safe to call from both the producer and consumer threads. + // + // NOTE: The actual capacity during usage may be different depending on the consumer. + // If the consumer is removing elements concurrently, the producer cannot add to + // the block the consumer is removing from until it's completely empty, except in + // the case where the producer was writing to the same block the consumer was + // reading from the whole time. + inline size_t max_capacity() const { + size_t result = 0; + Block* frontBlock_ = frontBlock.load(); + Block* block = frontBlock_; + do { + fence(memory_order_acquire); + result += block->sizeMask; + block = block->next.load(); + } while (block != frontBlock_); + return result; + } + +private: + enum AllocationMode { CanAlloc, CannotAlloc }; + +#if MOODYCAMEL_HAS_EMPLACE + template + bool inner_enqueue(Args&&... args) AE_NO_TSAN +#else + template + bool inner_enqueue(U&& element) AE_NO_TSAN +#endif + { +#ifndef NDEBUG + ReentrantGuard guard(this->enqueuing); +#endif + + // High-level pseudocode (assuming we're allowed to alloc a new block): + // If room in tail block, add to tail + // Else check next block + // If next block is not the head block, enqueue on next block + // Else create a new block and enqueue there + // Advance tail to the block we just enqueued to + + Block* tailBlock_ = tailBlock.load(); + size_t blockFront = tailBlock_->localFront; + size_t blockTail = tailBlock_->tail.load(); + + size_t nextBlockTail = (blockTail + 1) & tailBlock_->sizeMask; + if (nextBlockTail != blockFront || + nextBlockTail != (tailBlock_->localFront = tailBlock_->front.load())) { + fence(memory_order_acquire); + // This block has room for at least one more element + char* location = tailBlock_->data + blockTail * sizeof(T); +#if MOODYCAMEL_HAS_EMPLACE + new (location) T(std::forward(args)...); +#else + new (location) T(std::forward(element)); +#endif + + fence(memory_order_release); + tailBlock_->tail = nextBlockTail; + } else { + fence(memory_order_acquire); + if (tailBlock_->next.load() != frontBlock) { + // Note that the reason we can't advance to the frontBlock and start adding new + // entries there is because if we did, then dequeue would stay in that block, + // eventually reading the new values, instead of advancing to the next full block + // (whose values were enqueued first and so should be consumed first). + + fence(memory_order_acquire); // Ensure we get latest writes if we got the latest + // frontBlock + + // tailBlock is full, but there's a free block ahead, use it + Block* tailBlockNext = tailBlock_->next.load(); + size_t nextBlockFront = tailBlockNext->localFront = tailBlockNext->front.load(); + nextBlockTail = tailBlockNext->tail.load(); + fence(memory_order_acquire); + + // This block must be empty since it's not the head block and we + // go through the blocks in a circle + assert(nextBlockFront == nextBlockTail); + tailBlockNext->localFront = nextBlockFront; + + char* location = tailBlockNext->data + nextBlockTail * sizeof(T); +#if MOODYCAMEL_HAS_EMPLACE + new (location) T(std::forward(args)...); +#else + new (location) T(std::forward(element)); +#endif + + tailBlockNext->tail = (nextBlockTail + 1) & tailBlockNext->sizeMask; + + fence(memory_order_release); + tailBlock = tailBlockNext; + } else if (canAlloc == CanAlloc) { + // tailBlock is full and there's no free block ahead; create a new block + auto newBlockSize = + largestBlockSize >= MAX_BLOCK_SIZE ? largestBlockSize : largestBlockSize * 2; + auto newBlock = make_block(newBlockSize); + if (newBlock == nullptr) { + // Could not allocate a block! + return false; + } + largestBlockSize = newBlockSize; + +#if MOODYCAMEL_HAS_EMPLACE + new (newBlock->data) T(std::forward(args)...); +#else + new (newBlock->data) T(std::forward(element)); +#endif + assert(newBlock->front == 0); + newBlock->tail = newBlock->localTail = 1; + + newBlock->next = tailBlock_->next.load(); + tailBlock_->next = newBlock; + + // Might be possible for the dequeue thread to see the new tailBlock->next + // *without* seeing the new tailBlock value, but this is OK since it can't + // advance to the next block until tailBlock is set anyway (because the only + // case where it could try to read the next is if it's already at the tailBlock, + // and it won't advance past tailBlock in any circumstance). + + fence(memory_order_release); + tailBlock = newBlock; + } else if (canAlloc == CannotAlloc) { + // Would have had to allocate a new block to enqueue, but not allowed + return false; + } else { + assert(false && "Should be unreachable code"); + return false; + } + } + + return true; + } + + // Disable copying + ReaderWriterQueue(ReaderWriterQueue const&) {} + + // Disable assignment + ReaderWriterQueue& operator=(ReaderWriterQueue const&) {} + + AE_FORCEINLINE static size_t ceilToPow2(size_t x) { + // From http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + --x; + x |= x >> 1; + x |= x >> 2; + x |= x >> 4; + for (size_t i = 1; i < sizeof(size_t); i <<= 1) { + x |= x >> (i << 3); + } + ++x; + return x; + } + + template + static AE_FORCEINLINE char* align_for(char* ptr) AE_NO_TSAN { + const std::size_t alignment = std::alignment_of::value; + return ptr + (alignment - (reinterpret_cast(ptr) % alignment)) % alignment; + } + +private: +#ifndef NDEBUG + struct ReentrantGuard { + AE_NO_TSAN ReentrantGuard(weak_atomic& _inSection) : inSection(_inSection) { + assert(!inSection && + "Concurrent (or re-entrant) enqueue or dequeue operation detected (only one " + "thread at a time may hold the producer or consumer role)"); + inSection = true; + } + + AE_NO_TSAN ~ReentrantGuard() { + inSection = false; + } + + private: + ReentrantGuard& operator=(ReentrantGuard const&); + + private: + weak_atomic& inSection; + }; +#endif + + struct Block { + // Avoid false-sharing by putting highly contended variables on their own cache lines + weak_atomic front; // (Atomic) Elements are read from here + size_t localTail; // An uncontended shadow copy of tail, owned by the consumer + + char cachelineFiller0[MOODYCAMEL_CACHE_LINE_SIZE - sizeof(weak_atomic) - + sizeof(size_t)]; + weak_atomic tail; // (Atomic) Elements are enqueued here + size_t localFront; + + char cachelineFiller1[MOODYCAMEL_CACHE_LINE_SIZE - sizeof(weak_atomic) - + sizeof(size_t)]; // next isn't very contended, but we don't want it on + // the same cache line as tail (which is) + weak_atomic next; // (Atomic) + + char* data; // Contents (on heap) are aligned to T's alignment + + const size_t sizeMask; + + // size must be a power of two (and greater than 0) + AE_NO_TSAN Block(size_t const& _size, char* _rawThis, char* _data) + : front(0UL), localTail(0), tail(0UL), localFront(0), next(nullptr), data(_data), + sizeMask(_size - 1), rawThis(_rawThis) {} + + private: + // C4512 - Assignment operator could not be generated + Block& operator=(Block const&); + + public: + char* rawThis; + }; + + static Block* make_block(size_t capacity) AE_NO_TSAN { + // Allocate enough memory for the block itself, as well as all the elements it will contain + auto size = sizeof(Block) + std::alignment_of::value - 1; + size += sizeof(T) * capacity + std::alignment_of::value - 1; + auto newBlockRaw = static_cast(std::malloc(size)); + if (newBlockRaw == nullptr) { + return nullptr; + } + + auto newBlockAligned = align_for(newBlockRaw); + auto newBlockData = align_for(newBlockAligned + sizeof(Block)); + return new (newBlockAligned) Block(capacity, newBlockRaw, newBlockData); + } + +private: + weak_atomic frontBlock; // (Atomic) Elements are dequeued from this block + + char cachelineFiller[MOODYCAMEL_CACHE_LINE_SIZE - sizeof(weak_atomic)]; + weak_atomic tailBlock; // (Atomic) Elements are enqueued to this block + + size_t largestBlockSize; + +#ifndef NDEBUG + weak_atomic enqueuing; + mutable weak_atomic dequeuing; +#endif +}; + +// Like ReaderWriterQueue, but also providees blocking operations +template +class BlockingReaderWriterQueue { +private: + typedef ::Common::ReaderWriterQueue ReaderWriterQueue; + +public: + explicit BlockingReaderWriterQueue(size_t size = 15) AE_NO_TSAN + : inner(size), + sema(new spsc_sema::LightweightSemaphore()) {} + + BlockingReaderWriterQueue(BlockingReaderWriterQueue&& other) AE_NO_TSAN + : inner(std::move(other.inner)), + sema(std::move(other.sema)) {} + + BlockingReaderWriterQueue& operator=(BlockingReaderWriterQueue&& other) AE_NO_TSAN { + std::swap(sema, other.sema); + std::swap(inner, other.inner); + return *this; + } + + // Enqueues a copy of element if there is room in the queue. + // Returns true if the element was enqueued, false otherwise. + // Does not allocate memory. + AE_FORCEINLINE bool try_enqueue(T const& element) AE_NO_TSAN { + if (inner.try_enqueue(element)) { + sema->signal(); + return true; + } + return false; + } + + // Enqueues a moved copy of element if there is room in the queue. + // Returns true if the element was enqueued, false otherwise. + // Does not allocate memory. + AE_FORCEINLINE bool try_enqueue(T&& element) AE_NO_TSAN { + if (inner.try_enqueue(std::forward(element))) { + sema->signal(); + return true; + } + return false; + } + +#if MOODYCAMEL_HAS_EMPLACE + // Like try_enqueue() but with emplace semantics (i.e. construct-in-place). + template + AE_FORCEINLINE bool try_emplace(Args&&... args) AE_NO_TSAN { + if (inner.try_emplace(std::forward(args)...)) { + sema->signal(); + return true; + } + return false; + } +#endif + + // Enqueues a copy of element on the queue. + // Allocates an additional block of memory if needed. + // Only fails (returns false) if memory allocation fails. + AE_FORCEINLINE bool enqueue(T const& element) AE_NO_TSAN { + if (inner.enqueue(element)) { + sema->signal(); + return true; + } + return false; + } + + // Enqueues a moved copy of element on the queue. + // Allocates an additional block of memory if needed. + // Only fails (returns false) if memory allocation fails. + AE_FORCEINLINE bool enqueue(T&& element) AE_NO_TSAN { + if (inner.enqueue(std::forward(element))) { + sema->signal(); + return true; + } + return false; + } + +#if MOODYCAMEL_HAS_EMPLACE + // Like enqueue() but with emplace semantics (i.e. construct-in-place). + template + AE_FORCEINLINE bool emplace(Args&&... args) AE_NO_TSAN { + if (inner.emplace(std::forward(args)...)) { + sema->signal(); + return true; + } + return false; + } +#endif + + // Attempts to dequeue an element; if the queue is empty, + // returns false instead. If the queue has at least one element, + // moves front to result using operator=, then returns true. + template + bool try_dequeue(U& result) AE_NO_TSAN { + if (sema->tryWait()) { + bool success = inner.try_dequeue(result); + assert(success); + AE_UNUSED(success); + return true; + } + return false; + } + + // Attempts to dequeue an element; if the queue is empty, + // waits until an element is available, then dequeues it. + template + void wait_dequeue(U& result) AE_NO_TSAN { + while (!sema->wait()) + ; + bool success = inner.try_dequeue(result); + AE_UNUSED(result); + assert(success); + AE_UNUSED(success); + } + + // Attempts to dequeue an element; if the queue is empty, + // waits until an element is available up to the specified timeout, + // then dequeues it and returns true, or returns false if the timeout + // expires before an element can be dequeued. + // Using a negative timeout indicates an indefinite timeout, + // and is thus functionally equivalent to calling wait_dequeue. + template + bool wait_dequeue_timed(U& result, std::int64_t timeout_usecs) AE_NO_TSAN { + if (!sema->wait(timeout_usecs)) { + return false; + } + bool success = inner.try_dequeue(result); + AE_UNUSED(result); + assert(success); + AE_UNUSED(success); + return true; + } + +#if __cplusplus > 199711L || _MSC_VER >= 1700 + // Attempts to dequeue an element; if the queue is empty, + // waits until an element is available up to the specified timeout, + // then dequeues it and returns true, or returns false if the timeout + // expires before an element can be dequeued. + // Using a negative timeout indicates an indefinite timeout, + // and is thus functionally equivalent to calling wait_dequeue. + template + inline bool wait_dequeue_timed(U& result, + std::chrono::duration const& timeout) AE_NO_TSAN { + return wait_dequeue_timed( + result, std::chrono::duration_cast(timeout).count()); + } +#endif + + // Returns a pointer to the front element in the queue (the one that + // would be removed next by a call to `try_dequeue` or `pop`). If the + // queue appears empty at the time the method is called, nullptr is + // returned instead. + // Must be called only from the consumer thread. + AE_FORCEINLINE T* peek() const AE_NO_TSAN { + return inner.peek(); + } + + // Removes the front element from the queue, if any, without returning it. + // Returns true on success, or false if the queue appeared empty at the time + // `pop` was called. + AE_FORCEINLINE bool pop() AE_NO_TSAN { + if (sema->tryWait()) { + bool result = inner.pop(); + assert(result); + AE_UNUSED(result); + return true; + } + return false; + } + + // Returns the approximate number of items currently in the queue. + // Safe to call from both the producer and consumer threads. + AE_FORCEINLINE size_t size_approx() const AE_NO_TSAN { + return sema->availableApprox(); + } + + // Returns the total number of items that could be enqueued without incurring + // an allocation when this queue is empty. + // Safe to call from both the producer and consumer threads. + // + // NOTE: The actual capacity during usage may be different depending on the consumer. + // If the consumer is removing elements concurrently, the producer cannot add to + // the block the consumer is removing from until it's completely empty, except in + // the case where the producer was writing to the same block the consumer was + // reading from the whole time. + AE_FORCEINLINE size_t max_capacity() const { + return inner.max_capacity(); + } + +private: + // Disable copying & assignment + BlockingReaderWriterQueue(BlockingReaderWriterQueue const&) {} + BlockingReaderWriterQueue& operator=(BlockingReaderWriterQueue const&) {} + +private: + ReaderWriterQueue inner; + std::unique_ptr sema; +}; + +} // namespace Common + +#ifdef AE_VCPP +#pragma warning(pop) +#endif diff --git a/src/common/settings.cpp b/src/common/settings.cpp index d4c52989a9..1c7b6dfae6 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -62,7 +62,8 @@ void LogSettings() { log_setting("Renderer_UseAsynchronousShaders", values.use_asynchronous_shaders.GetValue()); log_setting("Renderer_AnisotropicFilteringLevel", values.max_anisotropy.GetValue()); log_setting("Audio_OutputEngine", values.sink_id.GetValue()); - log_setting("Audio_OutputDevice", values.audio_device_id.GetValue()); + log_setting("Audio_OutputDevice", values.audio_output_device_id.GetValue()); + log_setting("Audio_InputDevice", values.audio_input_device_id.GetValue()); log_setting("DataStorage_UseVirtualSd", values.use_virtual_sd.GetValue()); log_path("DataStorage_CacheDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir)); log_path("DataStorage_ConfigDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir)); diff --git a/src/common/settings.h b/src/common/settings.h index 2bccb8642c..06d72c8bff 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -370,10 +370,12 @@ struct TouchFromButtonMap { struct Values { // Audio - Setting audio_device_id{"auto", "output_device"}; Setting sink_id{"auto", "output_engine"}; + Setting audio_output_device_id{"auto", "output_device"}; + Setting audio_input_device_id{"auto", "input_device"}; Setting audio_muted{false, "audio_muted"}; SwitchableSetting volume{100, 0, 100, "volume"}; + Setting dump_audio_commands{false, "dump_audio_commands"}; // Core SwitchableSetting use_multi_core{true, "use_multi_core"}; diff --git a/src/core/core.cpp b/src/core/core.cpp index 7723d9782a..0ede0d85c0 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -8,6 +8,7 @@ #include #include +#include "audio_core/audio_core.h" #include "common/fs/fs.h" #include "common/logging/log.h" #include "common/microprofile.h" @@ -140,6 +141,8 @@ struct System::Impl { core_timing.SyncPause(false); is_paused = false; + audio_core->PauseSinks(false); + return status; } @@ -147,6 +150,8 @@ struct System::Impl { std::unique_lock lk(suspend_guard); status = SystemResultStatus::Success; + audio_core->PauseSinks(true); + core_timing.SyncPause(true); kernel.Suspend(true); is_paused = true; @@ -154,6 +159,11 @@ struct System::Impl { return status; } + bool IsPaused() const { + std::unique_lock lk(suspend_guard); + return is_paused; + } + std::unique_lock StallProcesses() { std::unique_lock lk(suspend_guard); kernel.Suspend(true); @@ -214,6 +224,8 @@ struct System::Impl { return SystemResultStatus::ErrorVideoCore; } + audio_core = std::make_unique(system); + service_manager = std::make_shared(kernel); services = std::make_unique(service_manager, system); interrupt_manager = std::make_unique(system); @@ -290,7 +302,7 @@ struct System::Impl { if (Settings::values.gamecard_current_game) { fs_controller.SetGameCard(GetGameFileFromPath(virtual_filesystem, filepath)); } else if (!Settings::values.gamecard_path.GetValue().empty()) { - const auto gamecard_path = Settings::values.gamecard_path.GetValue(); + const auto& gamecard_path = Settings::values.gamecard_path.GetValue(); fs_controller.SetGameCard(GetGameFileFromPath(virtual_filesystem, gamecard_path)); } } @@ -308,6 +320,8 @@ struct System::Impl { } void Shutdown() { + SetShuttingDown(true); + // Log last frame performance stats if game was loded if (perf_stats) { const auto perf_results = GetAndResetPerfStats(); @@ -333,14 +347,15 @@ struct System::Impl { kernel.ShutdownCores(); cpu_manager.Shutdown(); debugger.reset(); + kernel.CloseServices(); services.reset(); service_manager.reset(); cheat_engine.reset(); telemetry_session.reset(); - cpu_manager.Shutdown(); time_manager.Shutdown(); core_timing.Shutdown(); app_loader.reset(); + audio_core.reset(); gpu_core.reset(); perf_stats.reset(); kernel.Shutdown(); @@ -350,6 +365,14 @@ struct System::Impl { LOG_DEBUG(Core, "Shutdown OK"); } + bool IsShuttingDown() const { + return is_shutting_down; + } + + void SetShuttingDown(bool shutting_down) { + is_shutting_down = shutting_down; + } + Loader::ResultStatus GetGameName(std::string& out) const { if (app_loader == nullptr) return Loader::ResultStatus::ErrorNotInitialized; @@ -392,8 +415,9 @@ struct System::Impl { return perf_stats->GetAndResetStats(core_timing.GetGlobalTimeUs()); } - std::mutex suspend_guard; + mutable std::mutex suspend_guard; bool is_paused{}; + std::atomic is_shutting_down{}; Timing::CoreTiming core_timing; Kernel::KernelCore kernel; @@ -407,6 +431,7 @@ struct System::Impl { std::unique_ptr gpu_core; std::unique_ptr interrupt_manager; std::unique_ptr device_memory; + std::unique_ptr audio_core; Core::Memory::Memory memory; Core::HID::HIDCore hid_core; CpuManager cpu_manager; @@ -479,6 +504,10 @@ SystemResultStatus System::Pause() { return impl->Pause(); } +bool System::IsPaused() const { + return impl->IsPaused(); +} + void System::InvalidateCpuInstructionCaches() { impl->kernel.InvalidateAllInstructionCaches(); } @@ -491,6 +520,14 @@ void System::Shutdown() { impl->Shutdown(); } +bool System::IsShuttingDown() const { + return impl->IsShuttingDown(); +} + +void System::SetShuttingDown(bool shutting_down) { + impl->SetShuttingDown(shutting_down); +} + void System::DetachDebugger() { if (impl->debugger) { impl->debugger->NotifyShutdown(); @@ -640,6 +677,14 @@ const HID::HIDCore& System::HIDCore() const { return impl->hid_core; } +AudioCore::AudioCore& System::AudioCore() { + return *impl->audio_core; +} + +const AudioCore::AudioCore& System::AudioCore() const { + return *impl->audio_core; +} + Timing::CoreTiming& System::CoreTiming() { return impl->core_timing; } diff --git a/src/core/core.h b/src/core/core.h index 60efe44103..a49d1214b7 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -81,6 +81,10 @@ namespace VideoCore { class RendererBase; } // namespace VideoCore +namespace AudioCore { +class AudioCore; +} // namespace AudioCore + namespace Core::Timing { class CoreTiming; } @@ -148,6 +152,9 @@ public: */ [[nodiscard]] SystemResultStatus Pause(); + /// Check if the core is currently paused. + [[nodiscard]] bool IsPaused() const; + /** * Invalidate the CPU instruction caches * This function should only be used by GDB Stub to support breakpoints, memory updates and @@ -160,6 +167,12 @@ public: /// Shutdown the emulated system. void Shutdown(); + /// Check if the core is shutting down. + [[nodiscard]] bool IsShuttingDown() const; + + /// Set the shutting down state. + void SetShuttingDown(bool shutting_down); + /// Forcibly detach the debugger if it is running. void DetachDebugger(); @@ -250,6 +263,12 @@ public: /// Gets an immutable reference to the renderer. [[nodiscard]] const VideoCore::RendererBase& Renderer() const; + /// Gets a mutable reference to the audio interface + [[nodiscard]] AudioCore::AudioCore& AudioCore(); + + /// Gets an immutable reference to the audio interface. + [[nodiscard]] const AudioCore::AudioCore& AudioCore() const; + /// Gets the global scheduler [[nodiscard]] Kernel::GlobalSchedulerContext& GlobalSchedulerContext(); diff --git a/src/core/hle/kernel/hle_ipc.cpp b/src/core/hle/kernel/hle_ipc.cpp index 45135a07ff..5b3feec662 100644 --- a/src/core/hle/kernel/hle_ipc.cpp +++ b/src/core/hle/kernel/hle_ipc.cpp @@ -287,18 +287,52 @@ std::size_t HLERequestContext::WriteBuffer(const void* buffer, std::size_t size, BufferDescriptorB().size() > buffer_index && BufferDescriptorB()[buffer_index].Size() >= size, { return 0; }, "BufferDescriptorB is invalid, index={}, size={}", buffer_index, size); - memory.WriteBlock(BufferDescriptorB()[buffer_index].Address(), buffer, size); + WriteBufferB(buffer, size, buffer_index); } else { ASSERT_OR_EXECUTE_MSG( BufferDescriptorC().size() > buffer_index && BufferDescriptorC()[buffer_index].Size() >= size, { return 0; }, "BufferDescriptorC is invalid, index={}, size={}", buffer_index, size); - memory.WriteBlock(BufferDescriptorC()[buffer_index].Address(), buffer, size); + WriteBufferC(buffer, size, buffer_index); } return size; } +std::size_t HLERequestContext::WriteBufferB(const void* buffer, std::size_t size, + std::size_t buffer_index) const { + if (buffer_index >= BufferDescriptorB().size() || size == 0) { + return 0; + } + + const auto buffer_size{BufferDescriptorB()[buffer_index].Size()}; + if (size > buffer_size) { + LOG_CRITICAL(Core, "size ({:016X}) is greater than buffer_size ({:016X})", size, + buffer_size); + size = buffer_size; // TODO(bunnei): This needs to be HW tested + } + + memory.WriteBlock(BufferDescriptorB()[buffer_index].Address(), buffer, size); + return size; +} + +std::size_t HLERequestContext::WriteBufferC(const void* buffer, std::size_t size, + std::size_t buffer_index) const { + if (buffer_index >= BufferDescriptorC().size() || size == 0) { + return 0; + } + + const auto buffer_size{BufferDescriptorC()[buffer_index].Size()}; + if (size > buffer_size) { + LOG_CRITICAL(Core, "size ({:016X}) is greater than buffer_size ({:016X})", size, + buffer_size); + size = buffer_size; // TODO(bunnei): This needs to be HW tested + } + + memory.WriteBlock(BufferDescriptorC()[buffer_index].Address(), buffer, size); + return size; +} + std::size_t HLERequestContext::GetReadBufferSize(std::size_t buffer_index) const { const bool is_buffer_a{BufferDescriptorA().size() > buffer_index && BufferDescriptorA()[buffer_index].Size()}; diff --git a/src/core/hle/kernel/hle_ipc.h b/src/core/hle/kernel/hle_ipc.h index d3abeee85b..99265ce90a 100644 --- a/src/core/hle/kernel/hle_ipc.h +++ b/src/core/hle/kernel/hle_ipc.h @@ -277,6 +277,14 @@ public: std::size_t WriteBuffer(const void* buffer, std::size_t size, std::size_t buffer_index = 0) const; + /// Helper function to write buffer B + std::size_t WriteBufferB(const void* buffer, std::size_t size, + std::size_t buffer_index = 0) const; + + /// Helper function to write buffer C + std::size_t WriteBufferC(const void* buffer, std::size_t size, + std::size_t buffer_index = 0) const; + /* Helper function to write a buffer using the appropriate buffer descriptor * * @tparam T an arbitrary container that satisfies the diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp index 7307cf262b..f23c629dc3 100644 --- a/src/core/hle/kernel/kernel.cpp +++ b/src/core/hle/kernel/kernel.cpp @@ -95,19 +95,7 @@ struct KernelCore::Impl { process_list.clear(); - // Close all open server sessions and ports. - std::unordered_set server_objects_; - { - std::scoped_lock lk(server_objects_lock); - server_objects_ = server_objects; - server_objects.clear(); - } - for (auto* server_object : server_objects_) { - server_object->Close(); - } - - // Ensures all service threads gracefully shutdown. - ClearServiceThreads(); + CloseServices(); next_object_id = 0; next_kernel_process_id = KProcess::InitialKIPIDMin; @@ -191,6 +179,22 @@ struct KernelCore::Impl { global_object_list_container.reset(); } + void CloseServices() { + // Close all open server sessions and ports. + std::unordered_set server_objects_; + { + std::scoped_lock lk(server_objects_lock); + server_objects_ = server_objects; + server_objects.clear(); + } + for (auto* server_object : server_objects_) { + server_object->Close(); + } + + // Ensures all service threads gracefully shutdown. + ClearServiceThreads(); + } + void InitializePhysicalCores() { exclusive_monitor = Core::MakeExclusiveMonitor(system.Memory(), Core::Hardware::NUM_CPU_CORES); @@ -813,6 +817,10 @@ void KernelCore::Shutdown() { impl->Shutdown(); } +void KernelCore::CloseServices() { + impl->CloseServices(); +} + const KResourceLimit* KernelCore::GetSystemResourceLimit() const { return impl->system_resource_limit; } diff --git a/src/core/hle/kernel/kernel.h b/src/core/hle/kernel/kernel.h index aa0ebaa02c..6c7cf6af2e 100644 --- a/src/core/hle/kernel/kernel.h +++ b/src/core/hle/kernel/kernel.h @@ -109,6 +109,9 @@ public: /// Clears all resources in use by the kernel instance. void Shutdown(); + /// Close all active services in use by the kernel instance. + void CloseServices(); + /// Retrieves a shared pointer to the system resource limit instance. const KResourceLimit* GetSystemResourceLimit() const; diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index 9116dd77c9..118f226e41 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -5,7 +5,6 @@ #include #include #include -#include "audio_core/audio_renderer.h" #include "common/settings.h" #include "core/core.h" #include "core/file_sys/control_metadata.h" @@ -286,7 +285,7 @@ ISelfController::ISelfController(Core::System& system_, NVFlinger::NVFlinger& nv {62, &ISelfController::SetIdleTimeDetectionExtension, "SetIdleTimeDetectionExtension"}, {63, &ISelfController::GetIdleTimeDetectionExtension, "GetIdleTimeDetectionExtension"}, {64, nullptr, "SetInputDetectionSourceSet"}, - {65, nullptr, "ReportUserIsActive"}, + {65, &ISelfController::ReportUserIsActive, "ReportUserIsActive"}, {66, nullptr, "GetCurrentIlluminance"}, {67, nullptr, "IsIlluminanceAvailable"}, {68, &ISelfController::SetAutoSleepDisabled, "SetAutoSleepDisabled"}, @@ -518,6 +517,13 @@ void ISelfController::GetIdleTimeDetectionExtension(Kernel::HLERequestContext& c rb.Push(idle_time_detection_extension); } +void ISelfController::ReportUserIsActive(Kernel::HLERequestContext& ctx) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); +} + void ISelfController::SetAutoSleepDisabled(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; is_auto_sleep_disabled = rp.Pop(); diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index 53144427b4..bb75c62817 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -175,6 +175,7 @@ private: void SetHandlesRequestToDisplay(Kernel::HLERequestContext& ctx); void SetIdleTimeDetectionExtension(Kernel::HLERequestContext& ctx); void GetIdleTimeDetectionExtension(Kernel::HLERequestContext& ctx); + void ReportUserIsActive(Kernel::HLERequestContext& ctx); void SetAutoSleepDisabled(Kernel::HLERequestContext& ctx); void IsAutoSleepDisabled(Kernel::HLERequestContext& ctx); void GetAccumulatedSuspendedTickValue(Kernel::HLERequestContext& ctx); diff --git a/src/core/hle/service/audio/audin_u.cpp b/src/core/hle/service/audio/audin_u.cpp index 18d3ae6829..48a9a73a0e 100644 --- a/src/core/hle/service/audio/audin_u.cpp +++ b/src/core/hle/service/audio/audin_u.cpp @@ -1,68 +1,211 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "audio_core/in/audio_in_system.h" +#include "audio_core/renderer/audio_device.h" +#include "common/common_funcs.h" #include "common/logging/log.h" +#include "common/string_util.h" #include "core/core.h" #include "core/hle/ipc_helpers.h" #include "core/hle/kernel/k_event.h" #include "core/hle/service/audio/audin_u.h" namespace Service::Audio { +using namespace AudioCore::AudioIn; -IAudioIn::IAudioIn(Core::System& system_) - : ServiceFramework{system_, "IAudioIn"}, service_context{system_, "IAudioIn"} { - // clang-format off - static const FunctionInfo functions[] = { - {0, nullptr, "GetAudioInState"}, - {1, &IAudioIn::Start, "Start"}, - {2, nullptr, "Stop"}, - {3, nullptr, "AppendAudioInBuffer"}, - {4, &IAudioIn::RegisterBufferEvent, "RegisterBufferEvent"}, - {5, nullptr, "GetReleasedAudioInBuffer"}, - {6, nullptr, "ContainsAudioInBuffer"}, - {7, nullptr, "AppendUacInBuffer"}, - {8, &IAudioIn::AppendAudioInBufferAuto, "AppendAudioInBufferAuto"}, - {9, nullptr, "GetReleasedAudioInBuffersAuto"}, - {10, nullptr, "AppendUacInBufferAuto"}, - {11, nullptr, "GetAudioInBufferCount"}, - {12, nullptr, "SetDeviceGain"}, - {13, nullptr, "GetDeviceGain"}, - {14, nullptr, "FlushAudioInBuffers"}, - }; - // clang-format on +class IAudioIn final : public ServiceFramework { +public: + explicit IAudioIn(Core::System& system_, Manager& manager, size_t session_id, + std::string& device_name, const AudioInParameter& in_params, u32 handle, + u64 applet_resource_user_id) + : ServiceFramework{system_, "IAudioIn"}, + service_context{system_, "IAudioIn"}, event{service_context.CreateEvent("AudioInEvent")}, + impl{std::make_shared(system_, manager, event, session_id)} { + // clang-format off + static const FunctionInfo functions[] = { + {0, &IAudioIn::GetAudioInState, "GetAudioInState"}, + {1, &IAudioIn::Start, "Start"}, + {2, &IAudioIn::Stop, "Stop"}, + {3, &IAudioIn::AppendAudioInBuffer, "AppendAudioInBuffer"}, + {4, &IAudioIn::RegisterBufferEvent, "RegisterBufferEvent"}, + {5, &IAudioIn::GetReleasedAudioInBuffer, "GetReleasedAudioInBuffer"}, + {6, &IAudioIn::ContainsAudioInBuffer, "ContainsAudioInBuffer"}, + {7, &IAudioIn::AppendAudioInBuffer, "AppendUacInBuffer"}, + {8, &IAudioIn::AppendAudioInBuffer, "AppendAudioInBufferAuto"}, + {9, &IAudioIn::GetReleasedAudioInBuffer, "GetReleasedAudioInBuffersAuto"}, + {10, &IAudioIn::AppendAudioInBuffer, "AppendUacInBufferAuto"}, + {11, &IAudioIn::GetAudioInBufferCount, "GetAudioInBufferCount"}, + {12, &IAudioIn::SetDeviceGain, "SetDeviceGain"}, + {13, &IAudioIn::GetDeviceGain, "GetDeviceGain"}, + {14, &IAudioIn::FlushAudioInBuffers, "FlushAudioInBuffers"}, + }; + // clang-format on - RegisterHandlers(functions); + RegisterHandlers(functions); - buffer_event = service_context.CreateEvent("IAudioIn:BufferEvent"); -} + if (impl->GetSystem() + .Initialize(device_name, in_params, handle, applet_resource_user_id) + .IsError()) { + LOG_ERROR(Service_Audio, "Failed to initialize the AudioIn System!"); + } + } -IAudioIn::~IAudioIn() { - service_context.CloseEvent(buffer_event); -} + ~IAudioIn() override { + impl->Free(); + service_context.CloseEvent(event); + } -void IAudioIn::Start(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + [[nodiscard]] std::shared_ptr GetImpl() { + return impl; + } - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); -} +private: + void GetAudioInState(Kernel::HLERequestContext& ctx) { + const auto state = static_cast(impl->GetState()); -void IAudioIn::RegisterBufferEvent(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + LOG_DEBUG(Service_Audio, "called. State={}", state); - IPC::ResponseBuilder rb{ctx, 2, 1}; - rb.Push(ResultSuccess); - rb.PushCopyObjects(buffer_event->GetReadableEvent()); -} + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.Push(state); + } -void IAudioIn::AppendAudioInBufferAuto(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + void Start(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_Audio, "called"); - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); -} + auto result = impl->StartSystem(); -AudInU::AudInU(Core::System& system_) : ServiceFramework{system_, "audin:u"} { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + } + + void Stop(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_Audio, "called"); + + auto result = impl->StopSystem(); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + } + + void AppendAudioInBuffer(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + u64 tag = rp.PopRaw(); + + const auto in_buffer_size{ctx.GetReadBufferSize()}; + if (in_buffer_size < sizeof(AudioInBuffer)) { + LOG_ERROR(Service_Audio, "Input buffer is too small for an AudioInBuffer!"); + } + + const auto& in_buffer = ctx.ReadBuffer(); + AudioInBuffer buffer{}; + std::memcpy(&buffer, in_buffer.data(), sizeof(AudioInBuffer)); + + [[maybe_unused]] auto sessionid{impl->GetSystem().GetSessionId()}; + LOG_TRACE(Service_Audio, "called. Session {} Appending buffer {:08X}", sessionid, tag); + + auto result = impl->AppendBuffer(buffer, tag); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + } + + void RegisterBufferEvent(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_Audio, "called"); + + auto& buffer_event = impl->GetBufferEvent(); + + IPC::ResponseBuilder rb{ctx, 2, 1}; + rb.Push(ResultSuccess); + rb.PushCopyObjects(buffer_event); + } + + void GetReleasedAudioInBuffer(Kernel::HLERequestContext& ctx) { + auto write_buffer_size = ctx.GetWriteBufferSize() / sizeof(u64); + std::vector released_buffers(write_buffer_size, 0); + + auto count = impl->GetReleasedBuffers(released_buffers); + + [[maybe_unused]] std::string tags{}; + for (u32 i = 0; i < count; i++) { + tags += fmt::format("{:08X}, ", released_buffers[i]); + } + [[maybe_unused]] auto sessionid{impl->GetSystem().GetSessionId()}; + LOG_TRACE(Service_Audio, "called. Session {} released {} buffers: {}", sessionid, count, + tags); + + ctx.WriteBuffer(released_buffers); + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.Push(count); + } + + void ContainsAudioInBuffer(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + + const u64 tag{rp.Pop()}; + const auto buffer_queued{impl->ContainsAudioBuffer(tag)}; + + LOG_DEBUG(Service_Audio, "called. Is buffer {:08X} registered? {}", tag, buffer_queued); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.Push(buffer_queued); + } + + void GetAudioInBufferCount(Kernel::HLERequestContext& ctx) { + const auto buffer_count = impl->GetBufferCount(); + + LOG_DEBUG(Service_Audio, "called. Buffer count={}", buffer_count); + + IPC::ResponseBuilder rb{ctx, 3}; + + rb.Push(ResultSuccess); + rb.Push(buffer_count); + } + + void SetDeviceGain(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + + const auto volume{rp.Pop()}; + LOG_DEBUG(Service_Audio, "called. Gain {}", volume); + + impl->SetVolume(volume); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); + } + + void GetDeviceGain(Kernel::HLERequestContext& ctx) { + auto volume{impl->GetVolume()}; + + LOG_DEBUG(Service_Audio, "called. Gain {}", volume); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.Push(volume); + } + + void FlushAudioInBuffers(Kernel::HLERequestContext& ctx) { + bool flushed{impl->FlushAudioInBuffers()}; + + LOG_DEBUG(Service_Audio, "called. Were any buffers flushed? {}", flushed); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.Push(flushed); + } + + KernelHelpers::ServiceContext service_context; + Kernel::KEvent* event; + std::shared_ptr impl; +}; + +AudInU::AudInU(Core::System& system_) + : ServiceFramework{system_, "audin:u", ServiceThreadType::CreateNew}, + service_context{system_, "AudInU"}, impl{std::make_unique( + system_)} { // clang-format off static const FunctionInfo functions[] = { {0, &AudInU::ListAudioIns, "ListAudioIns"}, @@ -80,59 +223,152 @@ AudInU::AudInU(Core::System& system_) : ServiceFramework{system_, "audin:u"} { AudInU::~AudInU() = default; void AudInU::ListAudioIns(Kernel::HLERequestContext& ctx) { + using namespace AudioCore::AudioRenderer; + LOG_DEBUG(Service_Audio, "called"); - const std::size_t count = ctx.GetWriteBufferSize() / sizeof(AudioInDeviceName); - const std::size_t device_count = std::min(count, audio_device_names.size()); - std::vector device_names; - device_names.reserve(device_count); + const auto write_count = + static_cast(ctx.GetWriteBufferSize() / sizeof(AudioDevice::AudioDeviceName)); + std::vector device_names{}; - for (std::size_t i = 0; i < device_count; i++) { - const auto& device_name = audio_device_names[i]; - auto& entry = device_names.emplace_back(); - device_name.copy(entry.data(), device_name.size()); + u32 out_count{0}; + if (write_count > 0) { + out_count = impl->GetDeviceNames(device_names, write_count, false); + ctx.WriteBuffer(device_names); } - ctx.WriteBuffer(device_names); - IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(static_cast(device_names.size())); + rb.Push(out_count); } void AudInU::ListAudioInsAutoFiltered(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); - constexpr u32 device_count = 0; + using namespace AudioCore::AudioRenderer; - // Since we don't actually use any other audio input devices, we return 0 devices. Filtered - // device listing just omits the default input device + LOG_DEBUG(Service_Audio, "called"); + + const auto write_count = + static_cast(ctx.GetWriteBufferSize() / sizeof(AudioDevice::AudioDeviceName)); + std::vector device_names{}; + + u32 out_count{0}; + if (write_count > 0) { + out_count = impl->GetDeviceNames(device_names, write_count, true); + ctx.WriteBuffer(device_names); + } IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(static_cast(device_count)); -} - -void AudInU::OpenInOutImpl(Kernel::HLERequestContext& ctx) { - AudInOutParams params{}; - params.channel_count = 2; - params.sample_format = SampleFormat::PCM16; - params.sample_rate = 48000; - params.state = State::Started; - - IPC::ResponseBuilder rb{ctx, 6, 0, 1}; - rb.Push(ResultSuccess); - rb.PushRaw(params); - rb.PushIpcInterface(system); + rb.Push(out_count); } void AudInU::OpenAudioIn(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); - OpenInOutImpl(ctx); + IPC::RequestParser rp{ctx}; + auto in_params{rp.PopRaw()}; + auto applet_resource_user_id{rp.PopRaw()}; + const auto device_name_data{ctx.ReadBuffer()}; + auto device_name = Common::StringFromBuffer(device_name_data); + auto handle{ctx.GetCopyHandle(0)}; + + std::scoped_lock l{impl->mutex}; + auto link{impl->LinkToManager()}; + if (link.IsError()) { + LOG_ERROR(Service_Audio, "Failed to link Audio In to Audio Manager"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(link); + return; + } + + size_t new_session_id{}; + auto result{impl->AcquireSessionId(new_session_id)}; + if (result.IsError()) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + return; + } + + LOG_DEBUG(Service_Audio, "Opening new AudioIn, sessionid={}, free sessions={}", new_session_id, + impl->num_free_sessions); + + auto audio_in = std::make_shared(system, *impl, new_session_id, device_name, + in_params, handle, applet_resource_user_id); + impl->sessions[new_session_id] = audio_in->GetImpl(); + impl->applet_resource_user_ids[new_session_id] = applet_resource_user_id; + + auto& out_system = impl->sessions[new_session_id]->GetSystem(); + AudioInParameterInternal out_params{.sample_rate = out_system.GetSampleRate(), + .channel_count = out_system.GetChannelCount(), + .sample_format = + static_cast(out_system.GetSampleFormat()), + .state = static_cast(out_system.GetState())}; + + IPC::ResponseBuilder rb{ctx, 6, 0, 1}; + + std::string out_name{out_system.GetName()}; + ctx.WriteBuffer(out_name); + + rb.Push(ResultSuccess); + rb.PushRaw(out_params); + rb.PushIpcInterface(audio_in); } void AudInU::OpenAudioInProtocolSpecified(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); - OpenInOutImpl(ctx); + IPC::RequestParser rp{ctx}; + auto protocol_specified{rp.PopRaw()}; + auto in_params{rp.PopRaw()}; + auto applet_resource_user_id{rp.PopRaw()}; + const auto device_name_data{ctx.ReadBuffer()}; + auto device_name = Common::StringFromBuffer(device_name_data); + auto handle{ctx.GetCopyHandle(0)}; + + std::scoped_lock l{impl->mutex}; + auto link{impl->LinkToManager()}; + if (link.IsError()) { + LOG_ERROR(Service_Audio, "Failed to link Audio In to Audio Manager"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(link); + return; + } + + size_t new_session_id{}; + auto result{impl->AcquireSessionId(new_session_id)}; + if (result.IsError()) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + return; + } + + LOG_DEBUG(Service_Audio, "Opening new AudioIn, sessionid={}, free sessions={}", new_session_id, + impl->num_free_sessions); + + auto audio_in = std::make_shared(system, *impl, new_session_id, device_name, + in_params, handle, applet_resource_user_id); + impl->sessions[new_session_id] = audio_in->GetImpl(); + impl->applet_resource_user_ids[new_session_id] = applet_resource_user_id; + + auto& out_system = impl->sessions[new_session_id]->GetSystem(); + AudioInParameterInternal out_params{.sample_rate = out_system.GetSampleRate(), + .channel_count = out_system.GetChannelCount(), + .sample_format = + static_cast(out_system.GetSampleFormat()), + .state = static_cast(out_system.GetState())}; + + IPC::ResponseBuilder rb{ctx, 6, 0, 1}; + + std::string out_name{out_system.GetName()}; + if (protocol_specified == 0) { + if (out_system.IsUac()) { + out_name = "UacIn"; + } else { + out_name = "DeviceIn"; + } + } + + ctx.WriteBuffer(out_name); + + rb.Push(ResultSuccess); + rb.PushRaw(out_params); + rb.PushIpcInterface(audio_in); } } // namespace Service::Audio diff --git a/src/core/hle/service/audio/audin_u.h b/src/core/hle/service/audio/audin_u.h index 2bfa38eccb..b45fda78ac 100644 --- a/src/core/hle/service/audio/audin_u.h +++ b/src/core/hle/service/audio/audin_u.h @@ -3,6 +3,8 @@ #pragma once +#include "audio_core/audio_in_manager.h" +#include "audio_core/in/audio_in.h" #include "core/hle/service/kernel_helpers.h" #include "core/hle/service/service.h" @@ -14,56 +16,27 @@ namespace Kernel { class HLERequestContext; } +namespace AudioCore::AudioOut { +class Manager; +class In; +} // namespace AudioCore::AudioOut + namespace Service::Audio { -class IAudioIn final : public ServiceFramework { -public: - explicit IAudioIn(Core::System& system_); - ~IAudioIn() override; - -private: - void Start(Kernel::HLERequestContext& ctx); - void RegisterBufferEvent(Kernel::HLERequestContext& ctx); - void AppendAudioInBufferAuto(Kernel::HLERequestContext& ctx); - - KernelHelpers::ServiceContext service_context; - - Kernel::KEvent* buffer_event; -}; - class AudInU final : public ServiceFramework { public: explicit AudInU(Core::System& system_); ~AudInU() override; private: - enum class SampleFormat : u32_le { - PCM16 = 2, - }; - - enum class State : u32_le { - Started = 0, - Stopped = 1, - }; - - struct AudInOutParams { - u32_le sample_rate{}; - u32_le channel_count{}; - SampleFormat sample_format{}; - State state{}; - }; - static_assert(sizeof(AudInOutParams) == 0x10, "AudInOutParams is an invalid size"); - - using AudioInDeviceName = std::array; - static constexpr std::array audio_device_names{{ - "BuiltInHeadset", - }}; - void ListAudioIns(Kernel::HLERequestContext& ctx); void ListAudioInsAutoFiltered(Kernel::HLERequestContext& ctx); void OpenInOutImpl(Kernel::HLERequestContext& ctx); void OpenAudioIn(Kernel::HLERequestContext& ctx); void OpenAudioInProtocolSpecified(Kernel::HLERequestContext& ctx); + + KernelHelpers::ServiceContext service_context; + std::unique_ptr impl; }; } // namespace Service::Audio diff --git a/src/core/hle/service/audio/audout_u.cpp b/src/core/hle/service/audio/audout_u.cpp index b0dad60535..a44dd842a0 100644 --- a/src/core/hle/service/audio/audout_u.cpp +++ b/src/core/hle/service/audio/audout_u.cpp @@ -5,56 +5,43 @@ #include #include -#include "audio_core/audio_out.h" -#include "audio_core/codec.h" +#include "audio_core/out/audio_out_system.h" +#include "audio_core/renderer/audio_device.h" #include "common/common_funcs.h" #include "common/logging/log.h" +#include "common/string_util.h" #include "common/swap.h" #include "core/core.h" #include "core/hle/ipc_helpers.h" #include "core/hle/kernel/k_event.h" #include "core/hle/service/audio/audout_u.h" #include "core/hle/service/audio/errors.h" -#include "core/hle/service/kernel_helpers.h" #include "core/memory.h" namespace Service::Audio { - -constexpr std::array DefaultDevice{{"DeviceOut"}}; -constexpr int DefaultSampleRate{48000}; - -struct AudoutParams { - s32_le sample_rate; - u16_le channel_count; - INSERT_PADDING_BYTES_NOINIT(2); -}; -static_assert(sizeof(AudoutParams) == 0x8, "AudoutParams is an invalid size"); - -enum class AudioState : u32 { - Started, - Stopped, -}; +using namespace AudioCore::AudioOut; class IAudioOut final : public ServiceFramework { public: - explicit IAudioOut(Core::System& system_, AudoutParams audio_params_, - AudioCore::AudioOut& audio_core_, std::string&& device_name_, - std::string&& unique_name) + explicit IAudioOut(Core::System& system_, AudioCore::AudioOut::Manager& manager, + size_t session_id, std::string& device_name, + const AudioOutParameter& in_params, u32 handle, u64 applet_resource_user_id) : ServiceFramework{system_, "IAudioOut", ServiceThreadType::CreateNew}, - audio_core{audio_core_}, device_name{std::move(device_name_)}, - audio_params{audio_params_}, main_memory{system.Memory()}, service_context{system_, - "IAudioOut"} { + service_context{system_, "IAudioOut"}, event{service_context.CreateEvent( + "AudioOutEvent")}, + impl{std::make_shared(system_, manager, event, session_id)} { + // clang-format off static const FunctionInfo functions[] = { {0, &IAudioOut::GetAudioOutState, "GetAudioOutState"}, - {1, &IAudioOut::StartAudioOut, "Start"}, - {2, &IAudioOut::StopAudioOut, "Stop"}, - {3, &IAudioOut::AppendAudioOutBufferImpl, "AppendAudioOutBuffer"}, + {1, &IAudioOut::Start, "Start"}, + {2, &IAudioOut::Stop, "Stop"}, + {3, &IAudioOut::AppendAudioOutBuffer, "AppendAudioOutBuffer"}, {4, &IAudioOut::RegisterBufferEvent, "RegisterBufferEvent"}, - {5, &IAudioOut::GetReleasedAudioOutBufferImpl, "GetReleasedAudioOutBuffers"}, + {5, &IAudioOut::GetReleasedAudioOutBuffers, "GetReleasedAudioOutBuffers"}, {6, &IAudioOut::ContainsAudioOutBuffer, "ContainsAudioOutBuffer"}, - {7, &IAudioOut::AppendAudioOutBufferImpl, "AppendAudioOutBufferAuto"}, - {8, &IAudioOut::GetReleasedAudioOutBufferImpl, "GetReleasedAudioOutBufferAuto"}, + {7, &IAudioOut::AppendAudioOutBuffer, "AppendAudioOutBufferAuto"}, + {8, &IAudioOut::GetReleasedAudioOutBuffers, "GetReleasedAudioOutBuffersAuto"}, {9, &IAudioOut::GetAudioOutBufferCount, "GetAudioOutBufferCount"}, {10, &IAudioOut::GetAudioOutPlayedSampleCount, "GetAudioOutPlayedSampleCount"}, {11, &IAudioOut::FlushAudioOutBuffers, "FlushAudioOutBuffers"}, @@ -64,241 +51,263 @@ public: // clang-format on RegisterHandlers(functions); - // This is the event handle used to check if the audio buffer was released - buffer_event = service_context.CreateEvent("IAudioOutBufferReleased"); - - stream = audio_core.OpenStream(system.CoreTiming(), audio_params.sample_rate, - audio_params.channel_count, std::move(unique_name), [this] { - const auto guard = LockService(); - buffer_event->GetWritableEvent().Signal(); - }); + if (impl->GetSystem() + .Initialize(device_name, in_params, handle, applet_resource_user_id) + .IsError()) { + LOG_ERROR(Service_Audio, "Failed to initialize the AudioOut System!"); + } } ~IAudioOut() override { - service_context.CloseEvent(buffer_event); + impl->Free(); + service_context.CloseEvent(event); + } + + [[nodiscard]] std::shared_ptr GetImpl() { + return impl; } private: - struct AudioBuffer { - u64_le next; - u64_le buffer; - u64_le buffer_capacity; - u64_le buffer_size; - u64_le offset; - }; - static_assert(sizeof(AudioBuffer) == 0x28, "AudioBuffer is an invalid size"); - void GetAudioOutState(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + const auto state = static_cast(impl->GetState()); + + LOG_DEBUG(Service_Audio, "called. State={}", state); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(static_cast(stream->IsPlaying() ? AudioState::Started : AudioState::Stopped)); + rb.Push(state); } - void StartAudioOut(Kernel::HLERequestContext& ctx) { + void Start(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_Audio, "called"); - if (stream->IsPlaying()) { - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ERR_OPERATION_FAILED); - return; - } - - audio_core.StartStream(stream); + auto result = impl->StartSystem(); IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + rb.Push(result); } - void StopAudioOut(Kernel::HLERequestContext& ctx) { + void Stop(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_Audio, "called"); - if (stream->IsPlaying()) { - audio_core.StopStream(stream); - } + auto result = impl->StopSystem(); IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + rb.Push(result); + } + + void AppendAudioOutBuffer(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + u64 tag = rp.PopRaw(); + + const auto in_buffer_size{ctx.GetReadBufferSize()}; + if (in_buffer_size < sizeof(AudioOutBuffer)) { + LOG_ERROR(Service_Audio, "Input buffer is too small for an AudioOutBuffer!"); + } + + const auto& in_buffer = ctx.ReadBuffer(); + AudioOutBuffer buffer{}; + std::memcpy(&buffer, in_buffer.data(), sizeof(AudioOutBuffer)); + + [[maybe_unused]] auto sessionid{impl->GetSystem().GetSessionId()}; + LOG_TRACE(Service_Audio, "called. Session {} Appending buffer {:08X}", sessionid, tag); + + auto result = impl->AppendBuffer(buffer, tag); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); } void RegisterBufferEvent(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_Audio, "called"); + auto& buffer_event = impl->GetBufferEvent(); + IPC::ResponseBuilder rb{ctx, 2, 1}; rb.Push(ResultSuccess); - rb.PushCopyObjects(buffer_event->GetReadableEvent()); + rb.PushCopyObjects(buffer_event); } - void AppendAudioOutBufferImpl(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "(STUBBED) called {}", ctx.Description()); - IPC::RequestParser rp{ctx}; + void GetReleasedAudioOutBuffers(Kernel::HLERequestContext& ctx) { + auto write_buffer_size = ctx.GetWriteBufferSize() / sizeof(u64); + std::vector released_buffers(write_buffer_size, 0); - const auto& input_buffer{ctx.ReadBuffer()}; - ASSERT_MSG(input_buffer.size() == sizeof(AudioBuffer), - "AudioBuffer input is an invalid size!"); - AudioBuffer audio_buffer{}; - std::memcpy(&audio_buffer, input_buffer.data(), sizeof(AudioBuffer)); - const u64 tag{rp.Pop()}; + auto count = impl->GetReleasedBuffers(released_buffers); - std::vector samples(audio_buffer.buffer_size / sizeof(s16)); - main_memory.ReadBlock(audio_buffer.buffer, samples.data(), audio_buffer.buffer_size); - - if (!audio_core.QueueBuffer(stream, tag, std::move(samples))) { - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ERR_BUFFER_COUNT_EXCEEDED); - return; + [[maybe_unused]] std::string tags{}; + for (u32 i = 0; i < count; i++) { + tags += fmt::format("{:08X}, ", released_buffers[i]); } + [[maybe_unused]] auto sessionid{impl->GetSystem().GetSessionId()}; + LOG_TRACE(Service_Audio, "called. Session {} released {} buffers: {}", sessionid, count, + tags); - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); - } - - void GetReleasedAudioOutBufferImpl(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called {}", ctx.Description()); - - const u64 max_count{ctx.GetWriteBufferSize() / sizeof(u64)}; - const auto released_buffers{audio_core.GetTagsAndReleaseBuffers(stream, max_count)}; - - std::vector tags{released_buffers}; - tags.resize(max_count); - ctx.WriteBuffer(tags); - + ctx.WriteBuffer(released_buffers); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(static_cast(released_buffers.size())); + rb.Push(count); } void ContainsAudioOutBuffer(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); - IPC::RequestParser rp{ctx}; + const u64 tag{rp.Pop()}; + const auto buffer_queued{impl->ContainsAudioBuffer(tag)}; + + LOG_DEBUG(Service_Audio, "called. Is buffer {:08X} registered? {}", tag, buffer_queued); + IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(stream->ContainsBuffer(tag)); + rb.Push(buffer_queued); } void GetAudioOutBufferCount(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + const auto buffer_count = impl->GetBufferCount(); + + LOG_DEBUG(Service_Audio, "called. Buffer count={}", buffer_count); IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); - rb.Push(static_cast(stream->GetQueueSize())); + rb.Push(buffer_count); } void GetAudioOutPlayedSampleCount(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + const auto samples_played = impl->GetPlayedSampleCount(); + + LOG_DEBUG(Service_Audio, "called. Played samples={}", samples_played); IPC::ResponseBuilder rb{ctx, 4}; + rb.Push(ResultSuccess); - rb.Push(stream->GetPlayedSampleCount()); + rb.Push(samples_played); } void FlushAudioOutBuffers(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + bool flushed{impl->FlushAudioOutBuffers()}; + + LOG_DEBUG(Service_Audio, "called. Were any buffers flushed? {}", flushed); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(stream->Flush()); + rb.Push(flushed); } void SetAudioOutVolume(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - const float volume = rp.Pop(); - LOG_DEBUG(Service_Audio, "called, volume={}", volume); + const auto volume = rp.Pop(); - stream->SetVolume(volume); + LOG_DEBUG(Service_Audio, "called. Volume={}", volume); + + impl->SetVolume(volume); IPC::ResponseBuilder rb{ctx, 2}; rb.Push(ResultSuccess); } void GetAudioOutVolume(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + const auto volume = impl->GetVolume(); + + LOG_DEBUG(Service_Audio, "called. Volume={}", volume); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(stream->GetVolume()); + rb.Push(volume); } - AudioCore::AudioOut& audio_core; - AudioCore::StreamPtr stream; - std::string device_name; - - [[maybe_unused]] AudoutParams audio_params{}; - - Core::Memory::Memory& main_memory; - KernelHelpers::ServiceContext service_context; - - /// This is the event handle used to check if the audio buffer was released - Kernel::KEvent* buffer_event; + Kernel::KEvent* event; + std::shared_ptr impl; }; -AudOutU::AudOutU(Core::System& system_) : ServiceFramework{system_, "audout:u"} { +AudOutU::AudOutU(Core::System& system_) + : ServiceFramework{system_, "audout:u", ServiceThreadType::CreateNew}, + service_context{system_, "AudOutU"}, impl{std::make_unique( + system_)} { // clang-format off static const FunctionInfo functions[] = { - {0, &AudOutU::ListAudioOutsImpl, "ListAudioOuts"}, - {1, &AudOutU::OpenAudioOutImpl, "OpenAudioOut"}, - {2, &AudOutU::ListAudioOutsImpl, "ListAudioOutsAuto"}, - {3, &AudOutU::OpenAudioOutImpl, "OpenAudioOutAuto"}, + {0, &AudOutU::ListAudioOuts, "ListAudioOuts"}, + {1, &AudOutU::OpenAudioOut, "OpenAudioOut"}, + {2, &AudOutU::ListAudioOuts, "ListAudioOutsAuto"}, + {3, &AudOutU::OpenAudioOut, "OpenAudioOutAuto"}, }; // clang-format on RegisterHandlers(functions); - audio_core = std::make_unique(); } AudOutU::~AudOutU() = default; -void AudOutU::ListAudioOutsImpl(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); +void AudOutU::ListAudioOuts(Kernel::HLERequestContext& ctx) { + using namespace AudioCore::AudioRenderer; - ctx.WriteBuffer(DefaultDevice); + std::scoped_lock l{impl->mutex}; + + const auto write_count = + static_cast(ctx.GetWriteBufferSize() / sizeof(AudioDevice::AudioDeviceName)); + std::vector device_names{}; + std::string print_names{}; + if (write_count > 0) { + device_names.push_back(AudioDevice::AudioDeviceName("DeviceOut")); + LOG_DEBUG(Service_Audio, "called. \nName=DeviceOut"); + } else { + LOG_DEBUG(Service_Audio, "called. Empty buffer passed in."); + } + + ctx.WriteBuffer(device_names); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(1); // Amount of audio devices + rb.Push(static_cast(device_names.size())); } -void AudOutU::OpenAudioOutImpl(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); - - const auto device_name_data{ctx.ReadBuffer()}; - std::string device_name; - if (device_name_data[0] != '\0') { - device_name.assign(device_name_data.begin(), device_name_data.end()); - } else { - device_name.assign(DefaultDevice.begin(), DefaultDevice.end()); - } - ctx.WriteBuffer(device_name); - +void AudOutU::OpenAudioOut(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - auto params{rp.PopRaw()}; - if (params.channel_count <= 2) { - // Mono does not exist for audout - params.channel_count = 2; - } else { - params.channel_count = 6; - } - if (!params.sample_rate) { - params.sample_rate = DefaultSampleRate; + auto in_params{rp.PopRaw()}; + auto applet_resource_user_id{rp.PopRaw()}; + const auto device_name_data{ctx.ReadBuffer()}; + auto device_name = Common::StringFromBuffer(device_name_data); + auto handle{ctx.GetCopyHandle(0)}; + + auto link{impl->LinkToManager()}; + if (link.IsError()) { + LOG_ERROR(Service_Audio, "Failed to link Audio Out to Audio Manager"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(link); + return; } - std::string unique_name{fmt::format("{}-{}", device_name, audio_out_interfaces.size())}; - auto audio_out_interface = std::make_shared( - system, params, *audio_core, std::move(device_name), std::move(unique_name)); + size_t new_session_id{}; + auto result{impl->AcquireSessionId(new_session_id)}; + if (result.IsError()) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + return; + } + + LOG_DEBUG(Service_Audio, "Opening new AudioOut, sessionid={}, free sessions={}", new_session_id, + impl->num_free_sessions); + + auto audio_out = std::make_shared(system, *impl, new_session_id, device_name, + in_params, handle, applet_resource_user_id); + + impl->sessions[new_session_id] = audio_out->GetImpl(); + impl->applet_resource_user_ids[new_session_id] = applet_resource_user_id; + + auto& out_system = impl->sessions[new_session_id]->GetSystem(); + AudioOutParameterInternal out_params{.sample_rate = out_system.GetSampleRate(), + .channel_count = out_system.GetChannelCount(), + .sample_format = + static_cast(out_system.GetSampleFormat()), + .state = static_cast(out_system.GetState())}; IPC::ResponseBuilder rb{ctx, 6, 0, 1}; - rb.Push(ResultSuccess); - rb.Push(DefaultSampleRate); - rb.Push(params.channel_count); - rb.Push(static_cast(AudioCore::Codec::PcmFormat::Int16)); - rb.Push(static_cast(AudioState::Stopped)); - rb.PushIpcInterface(audio_out_interface); - audio_out_interfaces.push_back(std::move(audio_out_interface)); + ctx.WriteBuffer(out_system.GetName()); + + rb.Push(ResultSuccess); + rb.PushRaw(out_params); + rb.PushIpcInterface(audio_out); } } // namespace Service::Audio diff --git a/src/core/hle/service/audio/audout_u.h b/src/core/hle/service/audio/audout_u.h index d82004c2e1..fdc0ee754e 100644 --- a/src/core/hle/service/audio/audout_u.h +++ b/src/core/hle/service/audio/audout_u.h @@ -3,13 +3,11 @@ #pragma once -#include +#include "audio_core/audio_out_manager.h" +#include "audio_core/out/audio_out.h" +#include "core/hle/service/kernel_helpers.h" #include "core/hle/service/service.h" -namespace AudioCore { -class AudioOut; -} - namespace Core { class System; } @@ -18,6 +16,11 @@ namespace Kernel { class HLERequestContext; } +namespace AudioCore::AudioOut { +class Manager; +class Out; +} // namespace AudioCore::AudioOut + namespace Service::Audio { class IAudioOut; @@ -28,11 +31,11 @@ public: ~AudOutU() override; private: - void ListAudioOutsImpl(Kernel::HLERequestContext& ctx); - void OpenAudioOutImpl(Kernel::HLERequestContext& ctx); + void ListAudioOuts(Kernel::HLERequestContext& ctx); + void OpenAudioOut(Kernel::HLERequestContext& ctx); - std::vector> audio_out_interfaces; - std::unique_ptr audio_core; + KernelHelpers::ServiceContext service_context; + std::unique_ptr impl; }; } // namespace Service::Audio diff --git a/src/core/hle/service/audio/audren_u.cpp b/src/core/hle/service/audio/audren_u.cpp index 2ce63c0041..381a66ba52 100644 --- a/src/core/hle/service/audio/audren_u.cpp +++ b/src/core/hle/service/audio/audren_u.cpp @@ -4,7 +4,12 @@ #include #include -#include "audio_core/audio_renderer.h" +#include "audio_core/audio_core.h" +#include "audio_core/common/audio_renderer_parameter.h" +#include "audio_core/common/feature_support.h" +#include "audio_core/renderer/audio_device.h" +#include "audio_core/renderer/audio_renderer.h" +#include "audio_core/renderer/voice/voice_info.h" #include "common/alignment.h" #include "common/bit_util.h" #include "common/common_funcs.h" @@ -13,91 +18,112 @@ #include "core/core.h" #include "core/hle/ipc_helpers.h" #include "core/hle/kernel/k_event.h" +#include "core/hle/kernel/k_process.h" +#include "core/hle/kernel/k_transfer_memory.h" #include "core/hle/service/audio/audren_u.h" #include "core/hle/service/audio/errors.h" +#include "core/memory.h" + +using namespace AudioCore::AudioRenderer; namespace Service::Audio { class IAudioRenderer final : public ServiceFramework { public: - explicit IAudioRenderer(Core::System& system_, - const AudioCommon::AudioRendererParameter& audren_params, - const std::size_t instance_number) + explicit IAudioRenderer(Core::System& system_, Manager& manager_, + AudioCore::AudioRendererParameterInternal& params, + Kernel::KTransferMemory* transfer_memory, u64 transfer_memory_size, + u32 process_handle, u64 applet_resource_user_id, s32 session_id) : ServiceFramework{system_, "IAudioRenderer", ServiceThreadType::CreateNew}, - service_context{system_, "IAudioRenderer"} { + service_context{system_, "IAudioRenderer"}, rendered_event{service_context.CreateEvent( + "IAudioRendererEvent")}, + manager{manager_}, impl{std::make_unique(system_, manager, rendered_event)} { // clang-format off static const FunctionInfo functions[] = { {0, &IAudioRenderer::GetSampleRate, "GetSampleRate"}, {1, &IAudioRenderer::GetSampleCount, "GetSampleCount"}, {2, &IAudioRenderer::GetMixBufferCount, "GetMixBufferCount"}, {3, &IAudioRenderer::GetState, "GetState"}, - {4, &IAudioRenderer::RequestUpdateImpl, "RequestUpdate"}, + {4, &IAudioRenderer::RequestUpdate, "RequestUpdate"}, {5, &IAudioRenderer::Start, "Start"}, {6, &IAudioRenderer::Stop, "Stop"}, {7, &IAudioRenderer::QuerySystemEvent, "QuerySystemEvent"}, {8, &IAudioRenderer::SetRenderingTimeLimit, "SetRenderingTimeLimit"}, {9, &IAudioRenderer::GetRenderingTimeLimit, "GetRenderingTimeLimit"}, - {10, &IAudioRenderer::RequestUpdateImpl, "RequestUpdateAuto"}, - {11, &IAudioRenderer::ExecuteAudioRendererRendering, "ExecuteAudioRendererRendering"}, + {10, nullptr, "RequestUpdateAuto"}, + {11, nullptr, "ExecuteAudioRendererRendering"}, }; // clang-format on RegisterHandlers(functions); - system_event = service_context.CreateEvent("IAudioRenderer:SystemEvent"); - renderer = std::make_unique( - system.CoreTiming(), system.Memory(), audren_params, - [this]() { - const auto guard = LockService(); - system_event->GetWritableEvent().Signal(); - }, - instance_number); + impl->Initialize(params, transfer_memory, transfer_memory_size, process_handle, + applet_resource_user_id, session_id); } ~IAudioRenderer() override { - service_context.CloseEvent(system_event); + impl->Finalize(); + service_context.CloseEvent(rendered_event); } private: void GetSampleRate(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + const auto sample_rate{impl->GetSystem().GetSampleRate()}; + + LOG_DEBUG(Service_Audio, "called. Sample rate {}", sample_rate); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(renderer->GetSampleRate()); + rb.Push(sample_rate); } void GetSampleCount(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + const auto sample_count{impl->GetSystem().GetSampleCount()}; + + LOG_DEBUG(Service_Audio, "called. Sample count {}", sample_count); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(renderer->GetSampleCount()); + rb.Push(sample_count); } void GetState(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + const u32 state{!impl->GetSystem().IsActive()}; + + LOG_DEBUG(Service_Audio, "called, state {}", state); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(static_cast(renderer->GetStreamState())); + rb.Push(state); } void GetMixBufferCount(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_Audio, "called"); + const auto buffer_count{impl->GetSystem().GetMixBufferCount()}; + IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(renderer->GetMixBufferCount()); + rb.Push(buffer_count); } - void RequestUpdateImpl(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "(STUBBED) called"); + void RequestUpdate(Kernel::HLERequestContext& ctx) { + LOG_TRACE(Service_Audio, "called"); - std::vector output_params(ctx.GetWriteBufferSize(), 0); - auto result = renderer->UpdateAudioRenderer(ctx.ReadBuffer(), output_params); + std::vector input{ctx.ReadBuffer(0)}; + + // These buffers are written manually to avoid an issue with WriteBuffer throwing errors for + // checking size 0. Performance size is 0 for most games. + const auto buffers{ctx.BufferDescriptorB()}; + std::vector output(buffers[0].Size(), 0); + std::vector performance(buffers[1].Size(), 0); + + auto result = impl->RequestUpdate(input, performance, output); if (result.IsSuccess()) { - ctx.WriteBuffer(output_params); + ctx.WriteBufferB(output.data(), output.size(), 0); + ctx.WriteBufferB(performance.data(), performance.size(), 1); + } else { + LOG_ERROR(Service_Audio, "RequestUpdate failed error 0x{:02X}!", result.description); } IPC::ResponseBuilder rb{ctx, 2}; @@ -105,38 +131,45 @@ private: } void Start(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + LOG_DEBUG(Service_Audio, "called"); - const auto result = renderer->Start(); + impl->Start(); IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(result); + rb.Push(ResultSuccess); } void Stop(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + LOG_DEBUG(Service_Audio, "called"); - const auto result = renderer->Stop(); + impl->Stop(); IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(result); + rb.Push(ResultSuccess); } void QuerySystemEvent(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + LOG_DEBUG(Service_Audio, "called"); + + if (impl->GetSystem().GetExecutionMode() == AudioCore::ExecutionMode::Manual) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERR_NOT_SUPPORTED); + return; + } IPC::ResponseBuilder rb{ctx, 2, 1}; rb.Push(ResultSuccess); - rb.PushCopyObjects(system_event->GetReadableEvent()); + rb.PushCopyObjects(rendered_event->GetReadableEvent()); } void SetRenderingTimeLimit(Kernel::HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - rendering_time_limit_percent = rp.Pop(); - LOG_DEBUG(Service_Audio, "called. rendering_time_limit_percent={}", - rendering_time_limit_percent); + LOG_DEBUG(Service_Audio, "called"); - ASSERT(rendering_time_limit_percent <= 100); + IPC::RequestParser rp{ctx}; + auto limit = rp.PopRaw(); + + auto& system_ = impl->GetSystem(); + system_.SetRenderingTimeLimit(limit); IPC::ResponseBuilder rb{ctx, 2}; rb.Push(ResultSuccess); @@ -145,34 +178,34 @@ private: void GetRenderingTimeLimit(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_Audio, "called"); + auto& system_ = impl->GetSystem(); + auto time = system_.GetRenderingTimeLimit(); + IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(rendering_time_limit_percent); + rb.Push(time); } void ExecuteAudioRendererRendering(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_Audio, "called"); - - // This service command currently only reports an unsupported operation - // error code, or aborts. Given that, we just always return an error - // code in this case. - - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ERR_NOT_SUPPORTED); } KernelHelpers::ServiceContext service_context; - - Kernel::KEvent* system_event; - std::unique_ptr renderer; - u32 rendering_time_limit_percent = 100; + Kernel::KEvent* rendered_event; + Manager& manager; + std::unique_ptr impl; }; class IAudioDevice final : public ServiceFramework { + public: - explicit IAudioDevice(Core::System& system_, Kernel::KEvent* buffer_event_, u32_le revision_) - : ServiceFramework{system_, "IAudioDevice"}, buffer_event{buffer_event_}, revision{ - revision_} { + explicit IAudioDevice(Core::System& system_, u64 applet_resource_user_id, u32 revision, + u32 device_num) + : ServiceFramework{system_, "IAudioDevice", ServiceThreadType::CreateNew}, + service_context{system_, "IAudioDevice"}, impl{std::make_unique( + system_, applet_resource_user_id, + revision)}, + event{service_context.CreateEvent(fmt::format("IAudioDeviceEvent-{}", device_num))} { static const FunctionInfo functions[] = { {0, &IAudioDevice::ListAudioDeviceName, "ListAudioDeviceName"}, {1, &IAudioDevice::SetAudioDeviceOutputVolume, "SetAudioDeviceOutputVolume"}, @@ -186,54 +219,45 @@ public: {10, &IAudioDevice::GetActiveAudioDeviceName, "GetActiveAudioDeviceNameAuto"}, {11, &IAudioDevice::QueryAudioDeviceInputEvent, "QueryAudioDeviceInputEvent"}, {12, &IAudioDevice::QueryAudioDeviceOutputEvent, "QueryAudioDeviceOutputEvent"}, - {13, nullptr, "GetActiveAudioOutputDeviceName"}, - {14, nullptr, "ListAudioOutputDeviceName"}, + {13, &IAudioDevice::GetActiveAudioDeviceName, "GetActiveAudioOutputDeviceName"}, + {14, &IAudioDevice::ListAudioOutputDeviceName, "ListAudioOutputDeviceName"}, }; RegisterHandlers(functions); + + event->GetWritableEvent().Signal(); + } + + ~IAudioDevice() override { + service_context.CloseEvent(event); } private: - using AudioDeviceName = std::array; - static constexpr std::array audio_device_names{{ - "AudioStereoJackOutput", - "AudioBuiltInSpeakerOutput", - "AudioTvOutput", - "AudioUsbDeviceOutput", - }}; - enum class DeviceType { - AHUBHeadphones, - AHUBSpeakers, - HDA, - USBOutput, - }; - void ListAudioDeviceName(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + const size_t in_count = ctx.GetWriteBufferSize() / sizeof(AudioDevice::AudioDeviceName); - const bool usb_output_supported = - IsFeatureSupported(AudioFeatures::AudioUSBDeviceOutput, revision); - const std::size_t count = ctx.GetWriteBufferSize() / sizeof(AudioDeviceName); + std::vector out_names{}; - std::vector name_buffer; - name_buffer.reserve(audio_device_names.size()); + u32 out_count = impl->ListAudioDeviceName(out_names, in_count); - for (std::size_t i = 0; i < count && i < audio_device_names.size(); i++) { - const auto type = static_cast(i); - - if (!usb_output_supported && type == DeviceType::USBOutput) { - continue; + std::string out{}; + for (u32 i = 0; i < out_count; i++) { + std::string a{}; + u32 j = 0; + while (out_names[i].name[j] != '\0') { + a += out_names[i].name[j]; + j++; } - - const auto& device_name = audio_device_names[i]; - auto& entry = name_buffer.emplace_back(); - device_name.copy(entry.data(), device_name.size()); + out += "\n\t" + a; } - ctx.WriteBuffer(name_buffer); + LOG_DEBUG(Service_Audio, "called.\nNames={}", out); IPC::ResponseBuilder rb{ctx, 3}; + + ctx.WriteBuffer(out_names); + rb.Push(ResultSuccess); - rb.Push(static_cast(name_buffer.size())); + rb.Push(out_count); } void SetAudioDeviceOutputVolume(Kernel::HLERequestContext& ctx) { @@ -243,7 +267,11 @@ private: const auto device_name_buffer = ctx.ReadBuffer(); const std::string name = Common::StringFromBuffer(device_name_buffer); - LOG_WARNING(Service_Audio, "(STUBBED) called. name={}, volume={}", name, volume); + LOG_DEBUG(Service_Audio, "called. name={}, volume={}", name, volume); + + if (name == "AudioTvOutput") { + impl->SetDeviceVolumes(volume); + } IPC::ResponseBuilder rb{ctx, 2}; rb.Push(ResultSuccess); @@ -253,53 +281,60 @@ private: const auto device_name_buffer = ctx.ReadBuffer(); const std::string name = Common::StringFromBuffer(device_name_buffer); - LOG_WARNING(Service_Audio, "(STUBBED) called. name={}", name); + LOG_DEBUG(Service_Audio, "called. Name={}", name); + + f32 volume{1.0f}; + if (name == "AudioTvOutput") { + volume = impl->GetDeviceVolume(name); + } IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(1.0f); + rb.Push(volume); } void GetActiveAudioDeviceName(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + const auto write_size = ctx.GetWriteBufferSize() / sizeof(char); + std::string out_name{"AudioTvOutput"}; - // Currently set to always be TV audio output. - const auto& device_name = audio_device_names[2]; + LOG_DEBUG(Service_Audio, "(STUBBED) called. Name={}", out_name); - AudioDeviceName out_device_name{}; - device_name.copy(out_device_name.data(), device_name.size()); + out_name.resize(write_size); - ctx.WriteBuffer(out_device_name); + ctx.WriteBuffer(out_name); IPC::ResponseBuilder rb{ctx, 2}; rb.Push(ResultSuccess); } void QueryAudioDeviceSystemEvent(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + LOG_DEBUG(Service_Audio, "(STUBBED) called"); - buffer_event->GetWritableEvent().Signal(); + event->GetWritableEvent().Signal(); IPC::ResponseBuilder rb{ctx, 2, 1}; rb.Push(ResultSuccess); - rb.PushCopyObjects(buffer_event->GetReadableEvent()); + rb.PushCopyObjects(event->GetReadableEvent()); } void GetActiveChannelCount(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + const auto& sink{system.AudioCore().GetOutputSink()}; + u32 channel_count{sink.GetDeviceChannels()}; + + LOG_DEBUG(Service_Audio, "(STUBBED) called. Channels={}", channel_count); IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); - rb.Push(2); + rb.Push(channel_count); } - // Should be similar to QueryAudioDeviceOutputEvent void QueryAudioDeviceInputEvent(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_Audio, "(STUBBED) called"); + LOG_DEBUG(Service_Audio, "(STUBBED) called"); IPC::ResponseBuilder rb{ctx, 2, 1}; rb.Push(ResultSuccess); - rb.PushCopyObjects(buffer_event->GetReadableEvent()); + rb.PushCopyObjects(event->GetReadableEvent()); } void QueryAudioDeviceOutputEvent(Kernel::HLERequestContext& ctx) { @@ -307,402 +342,167 @@ private: IPC::ResponseBuilder rb{ctx, 2, 1}; rb.Push(ResultSuccess); - rb.PushCopyObjects(buffer_event->GetReadableEvent()); + rb.PushCopyObjects(event->GetReadableEvent()); } - Kernel::KEvent* buffer_event; - u32_le revision = 0; + void ListAudioOutputDeviceName(Kernel::HLERequestContext& ctx) { + const size_t in_count = ctx.GetWriteBufferSize() / sizeof(AudioDevice::AudioDeviceName); + + std::vector out_names{}; + + u32 out_count = impl->ListAudioOutputDeviceName(out_names, in_count); + + std::string out{}; + for (u32 i = 0; i < out_count; i++) { + std::string a{}; + u32 j = 0; + while (out_names[i].name[j] != '\0') { + a += out_names[i].name[j]; + j++; + } + out += "\n\t" + a; + } + + LOG_DEBUG(Service_Audio, "called.\nNames={}", out); + + IPC::ResponseBuilder rb{ctx, 3}; + + ctx.WriteBuffer(out_names); + + rb.Push(ResultSuccess); + rb.Push(out_count); + } + + KernelHelpers::ServiceContext service_context; + std::unique_ptr impl; + Kernel::KEvent* event; }; AudRenU::AudRenU(Core::System& system_) - : ServiceFramework{system_, "audren:u"}, service_context{system_, "audren:u"} { + : ServiceFramework{system_, "audren:u", ServiceThreadType::CreateNew}, + service_context{system_, "audren:u"}, impl{std::make_unique(system_)} { // clang-format off static const FunctionInfo functions[] = { {0, &AudRenU::OpenAudioRenderer, "OpenAudioRenderer"}, - {1, &AudRenU::GetAudioRendererWorkBufferSize, "GetWorkBufferSize"}, + {1, &AudRenU::GetWorkBufferSize, "GetWorkBufferSize"}, {2, &AudRenU::GetAudioDeviceService, "GetAudioDeviceService"}, - {3, &AudRenU::OpenAudioRendererForManualExecution, "OpenAudioRendererForManualExecution"}, + {3, nullptr, "OpenAudioRendererForManualExecution"}, {4, &AudRenU::GetAudioDeviceServiceWithRevisionInfo, "GetAudioDeviceServiceWithRevisionInfo"}, }; // clang-format on RegisterHandlers(functions); - - buffer_event = service_context.CreateEvent("IAudioOutBufferReleasedEvent"); } -AudRenU::~AudRenU() { - service_context.CloseEvent(buffer_event); -} +AudRenU::~AudRenU() = default; void AudRenU::OpenAudioRenderer(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); + IPC::RequestParser rp{ctx}; - OpenAudioRendererImpl(ctx); + AudioCore::AudioRendererParameterInternal params; + rp.PopRaw(params); + auto transfer_memory_handle = ctx.GetCopyHandle(0); + auto process_handle = ctx.GetCopyHandle(1); + auto transfer_memory_size = rp.Pop(); + auto applet_resource_user_id = rp.Pop(); + + if (impl->GetSessionCount() + 1 > AudioCore::MaxRendererSessions) { + LOG_ERROR(Service_Audio, "Too many AudioRenderer sessions open!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERR_MAXIMUM_SESSIONS_REACHED); + return; + } + + const auto& handle_table{system.CurrentProcess()->GetHandleTable()}; + auto process{handle_table.GetObject(process_handle)}; + auto transfer_memory{ + process->GetHandleTable().GetObject(transfer_memory_handle)}; + + const auto session_id{impl->GetSessionId()}; + if (session_id == -1) { + LOG_ERROR(Service_Audio, "Tried to open a session that's already in use!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERR_MAXIMUM_SESSIONS_REACHED); + return; + } + + LOG_DEBUG(Service_Audio, "Opened new AudioRenderer session {} sessions open {}", session_id, + impl->GetSessionCount()); + + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface(system, *impl, params, transfer_memory.GetPointerUnsafe(), + transfer_memory_size, process_handle, + applet_resource_user_id, session_id); } -static u64 CalculateNumPerformanceEntries(const AudioCommon::AudioRendererParameter& params) { - // +1 represents the final mix. - return u64{params.effect_count} + params.submix_count + params.sink_count + params.voice_count + - 1; -} - -void AudRenU::GetAudioRendererWorkBufferSize(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_Audio, "called"); - - // Several calculations below align the sizes being calculated - // onto a 64 byte boundary. - static constexpr u64 buffer_alignment_size = 64; - - // Some calculations that calculate portions of the buffer - // that will contain information, on the other hand, align - // the result of some of their calcularions on a 16 byte boundary. - static constexpr u64 info_field_alignment_size = 16; - - // Maximum detail entries that may exist at one time for performance - // frame statistics. - static constexpr u64 max_perf_detail_entries = 100; - - // Size of the data structure representing the bulk of the voice-related state. - static constexpr u64 voice_state_size_bytes = 0x100; - - // Size of the upsampler manager data structure - constexpr u64 upsampler_manager_size = 0x48; - - // Calculates the part of the size that relates to mix buffers. - const auto calculate_mix_buffer_sizes = [](const AudioCommon::AudioRendererParameter& params) { - // As of 8.0.0 this is the maximum on voice channels. - constexpr u64 max_voice_channels = 6; - - // The service expects the sample_count member of the parameters to either be - // a value of 160 or 240, so the maximum sample count is assumed in order - // to adequately handle all values at runtime. - constexpr u64 default_max_sample_count = 240; - - const u64 total_mix_buffers = params.mix_buffer_count + max_voice_channels; - - u64 size = 0; - size += total_mix_buffers * (sizeof(s32) * params.sample_count); - size += total_mix_buffers * (sizeof(s32) * default_max_sample_count); - size += u64{params.submix_count} + params.sink_count; - size = Common::AlignUp(size, buffer_alignment_size); - size += Common::AlignUp(params.unknown_30, buffer_alignment_size); - size += Common::AlignUp(sizeof(s32) * params.mix_buffer_count, buffer_alignment_size); - return size; - }; - - // Calculates the portion of the size related to the mix data (and the sorting thereof). - const auto calculate_mix_info_size = [](const AudioCommon::AudioRendererParameter& params) { - // The size of the mixing info data structure. - constexpr u64 mix_info_size = 0x940; - - // Consists of total submixes with the final mix included. - const u64 total_mix_count = u64{params.submix_count} + 1; - - // The total number of effects that may be available to the audio renderer at any time. - constexpr u64 max_effects = 256; - - // Calculates the part of the size related to the audio node state. - // This will only be used if the audio revision supports the splitter. - const auto calculate_node_state_size = [](std::size_t num_nodes) { - // Internally within a nodestate, it appears to use a data structure - // similar to a std::bitset<64> twice. - constexpr u64 bit_size = Common::BitSize(); - constexpr u64 num_bitsets = 2; - - // Node state instances have three states internally for performing - // depth-first searches of nodes. Initialized, Found, and Done Sorting. - constexpr u64 num_states = 3; - - u64 size = 0; - size += (num_nodes * num_nodes) * sizeof(s32); - size += num_states * (num_nodes * sizeof(s32)); - size += num_bitsets * (Common::AlignUp(num_nodes, bit_size) / Common::BitSize()); - return size; - }; - - // Calculates the part of the size related to the adjacency (aka edge) matrix. - const auto calculate_edge_matrix_size = [](std::size_t num_nodes) { - return (num_nodes * num_nodes) * sizeof(s32); - }; - - u64 size = 0; - size += Common::AlignUp(sizeof(void*) * total_mix_count, info_field_alignment_size); - size += Common::AlignUp(mix_info_size * total_mix_count, info_field_alignment_size); - size += Common::AlignUp(sizeof(s32) * max_effects * params.submix_count, - info_field_alignment_size); - - if (IsFeatureSupported(AudioFeatures::Splitter, params.revision)) { - size += Common::AlignUp(calculate_node_state_size(total_mix_count) + - calculate_edge_matrix_size(total_mix_count), - info_field_alignment_size); - } - - return size; - }; - - // Calculates the part of the size related to voice channel info. - const auto calculate_voice_info_size = [](const AudioCommon::AudioRendererParameter& params) { - constexpr u64 voice_info_size = 0x220; - constexpr u64 voice_resource_size = 0xD0; - - u64 size = 0; - size += Common::AlignUp(sizeof(void*) * params.voice_count, info_field_alignment_size); - size += Common::AlignUp(voice_info_size * params.voice_count, info_field_alignment_size); - size += - Common::AlignUp(voice_resource_size * params.voice_count, info_field_alignment_size); - size += - Common::AlignUp(voice_state_size_bytes * params.voice_count, info_field_alignment_size); - return size; - }; - - // Calculates the part of the size related to memory pools. - const auto calculate_memory_pools_size = [](const AudioCommon::AudioRendererParameter& params) { - const u64 num_memory_pools = sizeof(s32) * (u64{params.effect_count} + params.voice_count); - const u64 memory_pool_info_size = 0x20; - return Common::AlignUp(num_memory_pools * memory_pool_info_size, info_field_alignment_size); - }; - - // Calculates the part of the size related to the splitter context. - const auto calculate_splitter_context_size = - [](const AudioCommon::AudioRendererParameter& params) -> u64 { - if (!IsFeatureSupported(AudioFeatures::Splitter, params.revision)) { - return 0; - } - - constexpr u64 splitter_info_size = 0x20; - constexpr u64 splitter_destination_data_size = 0xE0; - - u64 size = 0; - size += params.num_splitter_send_channels; - size += - Common::AlignUp(splitter_info_size * params.splitter_count, info_field_alignment_size); - size += Common::AlignUp(splitter_destination_data_size * params.num_splitter_send_channels, - info_field_alignment_size); - - return size; - }; - - // Calculates the part of the size related to the upsampler info. - const auto calculate_upsampler_info_size = - [](const AudioCommon::AudioRendererParameter& params) { - constexpr u64 upsampler_info_size = 0x280; - // Yes, using the buffer size over info alignment size is intentional here. - return Common::AlignUp(upsampler_info_size * - (u64{params.submix_count} + params.sink_count), - buffer_alignment_size); - }; - - // Calculates the part of the size related to effect info. - const auto calculate_effect_info_size = [](const AudioCommon::AudioRendererParameter& params) { - constexpr u64 effect_info_size = 0x2B0; - return Common::AlignUp(effect_info_size * params.effect_count, info_field_alignment_size); - }; - - // Calculates the part of the size related to audio sink info. - const auto calculate_sink_info_size = [](const AudioCommon::AudioRendererParameter& params) { - const u64 sink_info_size = 0x170; - return Common::AlignUp(sink_info_size * params.sink_count, info_field_alignment_size); - }; - - // Calculates the part of the size related to voice state info. - const auto calculate_voice_state_size = [](const AudioCommon::AudioRendererParameter& params) { - const u64 voice_state_size = 0x100; - const u64 additional_size = buffer_alignment_size - 1; - return Common::AlignUp(voice_state_size * params.voice_count + additional_size, - info_field_alignment_size); - }; - - // Calculates the part of the size related to performance statistics. - const auto calculate_perf_size = [](const AudioCommon::AudioRendererParameter& params) { - // Extra size value appended to the end of the calculation. - constexpr u64 appended = 128; - - // Whether or not we assume the newer version of performance metrics data structures. - const bool is_v2 = - IsFeatureSupported(AudioFeatures::PerformanceMetricsVersion2, params.revision); - - // Data structure sizes - constexpr u64 perf_statistics_size = 0x0C; - const u64 header_size = is_v2 ? 0x30 : 0x18; - const u64 entry_size = is_v2 ? 0x18 : 0x10; - const u64 detail_size = is_v2 ? 0x18 : 0x10; - - const u64 entry_count = CalculateNumPerformanceEntries(params); - const u64 size_per_frame = - header_size + (entry_size * entry_count) + (detail_size * max_perf_detail_entries); - - u64 size = 0; - size += Common::AlignUp(size_per_frame * params.performance_frame_count + 1, - buffer_alignment_size); - size += Common::AlignUp(perf_statistics_size, buffer_alignment_size); - size += appended; - return size; - }; - - // Calculates the part of the size that relates to the audio command buffer. - const auto calculate_command_buffer_size = - [](const AudioCommon::AudioRendererParameter& params) { - constexpr u64 alignment = (buffer_alignment_size - 1) * 2; - - if (!IsFeatureSupported(AudioFeatures::VariadicCommandBuffer, params.revision)) { - constexpr u64 command_buffer_size = 0x18000; - - return command_buffer_size + alignment; - } - - // When the variadic command buffer is supported, this means - // the command generator for the audio renderer can issue commands - // that are (as one would expect), variable in size. So what we need to do - // is determine the maximum possible size for a few command data structures - // then multiply them by the amount of present commands indicated by the given - // respective audio parameters. - - constexpr u64 max_biquad_filters = 2; - constexpr u64 max_mix_buffers = 24; - - constexpr u64 biquad_filter_command_size = 0x2C; - - constexpr u64 depop_mix_command_size = 0x24; - constexpr u64 depop_setup_command_size = 0x50; - - constexpr u64 effect_command_max_size = 0x540; - - constexpr u64 mix_command_size = 0x1C; - constexpr u64 mix_ramp_command_size = 0x24; - constexpr u64 mix_ramp_grouped_command_size = 0x13C; - - constexpr u64 perf_command_size = 0x28; - - constexpr u64 sink_command_size = 0x130; - - constexpr u64 submix_command_max_size = - depop_mix_command_size + (mix_command_size * max_mix_buffers) * max_mix_buffers; - - constexpr u64 volume_command_size = 0x1C; - constexpr u64 volume_ramp_command_size = 0x20; - - constexpr u64 voice_biquad_filter_command_size = - biquad_filter_command_size * max_biquad_filters; - constexpr u64 voice_data_command_size = 0x9C; - const u64 voice_command_max_size = - (params.splitter_count * depop_setup_command_size) + - (voice_data_command_size + voice_biquad_filter_command_size + - volume_ramp_command_size + mix_ramp_grouped_command_size); - - // Now calculate the individual elements that comprise the size and add them together. - const u64 effect_commands_size = params.effect_count * effect_command_max_size; - - const u64 final_mix_commands_size = - depop_mix_command_size + volume_command_size * max_mix_buffers; - - const u64 perf_commands_size = - perf_command_size * - (CalculateNumPerformanceEntries(params) + max_perf_detail_entries); - - const u64 sink_commands_size = params.sink_count * sink_command_size; - - const u64 splitter_commands_size = - params.num_splitter_send_channels * max_mix_buffers * mix_ramp_command_size; - - const u64 submix_commands_size = params.submix_count * submix_command_max_size; - - const u64 voice_commands_size = params.voice_count * voice_command_max_size; - - return effect_commands_size + final_mix_commands_size + perf_commands_size + - sink_commands_size + splitter_commands_size + submix_commands_size + - voice_commands_size + alignment; - }; +void AudRenU::GetWorkBufferSize(Kernel::HLERequestContext& ctx) { + AudioCore::AudioRendererParameterInternal params; IPC::RequestParser rp{ctx}; - const auto params = rp.PopRaw(); + rp.PopRaw(params); - u64 size = 0; - size += calculate_mix_buffer_sizes(params); - size += calculate_mix_info_size(params); - size += calculate_voice_info_size(params); - size += upsampler_manager_size; - size += calculate_memory_pools_size(params); - size += calculate_splitter_context_size(params); + u64 size{0}; + auto result = impl->GetWorkBufferSize(params, size); - size = Common::AlignUp(size, buffer_alignment_size); + std::string output_info{}; + output_info += fmt::format("\tRevision {}", AudioCore::GetRevisionNum(params.revision)); + output_info += + fmt::format("\n\tSample Rate {}, Sample Count {}", params.sample_rate, params.sample_count); + output_info += fmt::format("\n\tExecution Mode {}, Voice Drop Enabled {}", + static_cast(params.execution_mode), params.voice_drop_enabled); + output_info += fmt::format( + "\n\tSizes: Effects {:04X}, Mixes {:04X}, Sinks {:04X}, Submixes {:04X}, Splitter Infos " + "{:04X}, Splitter Destinations {:04X}, Voices {:04X}, Performance Frames {:04X} External " + "Context {:04X}", + params.effects, params.mixes, params.sinks, params.sub_mixes, params.splitter_infos, + params.splitter_destinations, params.voices, params.perf_frames, + params.external_context_size); - size += calculate_upsampler_info_size(params); - size += calculate_effect_info_size(params); - size += calculate_sink_info_size(params); - size += calculate_voice_state_size(params); - size += calculate_perf_size(params); - size += calculate_command_buffer_size(params); - - // finally, 4KB page align the size, and we're done. - size = Common::AlignUp(size, 4096); + LOG_DEBUG(Service_Audio, "called.\nInput params:\n{}\nOutput params:\n\tWorkbuffer size {:08X}", + output_info, size); IPC::ResponseBuilder rb{ctx, 4}; - rb.Push(ResultSuccess); + rb.Push(result); rb.Push(size); - - LOG_DEBUG(Service_Audio, "buffer_size=0x{:X}", size); } void AudRenU::GetAudioDeviceService(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - const u64 aruid = rp.Pop(); - LOG_DEBUG(Service_Audio, "called. aruid={:016X}", aruid); + const auto applet_resource_user_id = rp.Pop(); + + LOG_DEBUG(Service_Audio, "called. Applet resource id {}", applet_resource_user_id); - // Revisionless variant of GetAudioDeviceServiceWithRevisionInfo that - // always assumes the initial release revision (REV1). IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); - rb.PushIpcInterface(system, buffer_event, Common::MakeMagic('R', 'E', 'V', '1')); + rb.PushIpcInterface(system, applet_resource_user_id, + ::Common::MakeMagic('R', 'E', 'V', '1'), num_audio_devices++); } void AudRenU::OpenAudioRendererForManualExecution(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_Audio, "called"); - - OpenAudioRendererImpl(ctx); } void AudRenU::GetAudioDeviceServiceWithRevisionInfo(Kernel::HLERequestContext& ctx) { struct Parameters { u32 revision; - u64 aruid; + u64 applet_resource_user_id; }; IPC::RequestParser rp{ctx}; - const auto [revision, aruid] = rp.PopRaw(); - LOG_DEBUG(Service_Audio, "called. revision={:08X}, aruid={:016X}", revision, aruid); + const auto [revision, applet_resource_user_id] = rp.PopRaw(); - IPC::ResponseBuilder rb{ctx, 2, 0, 1}; - rb.Push(ResultSuccess); - rb.PushIpcInterface(system, buffer_event, revision); -} + LOG_DEBUG(Service_Audio, "called. Revision {} Applet resource id {}", + AudioCore::GetRevisionNum(revision), applet_resource_user_id); -void AudRenU::OpenAudioRendererImpl(Kernel::HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const auto params = rp.PopRaw(); IPC::ResponseBuilder rb{ctx, 2, 0, 1}; rb.Push(ResultSuccess); - rb.PushIpcInterface(system, params, audren_instance_count++); -} - -bool IsFeatureSupported(AudioFeatures feature, u32_le revision) { - // Byte swap - const u32_be version_num = revision - Common::MakeMagic('R', 'E', 'V', '0'); - - switch (feature) { - case AudioFeatures::AudioUSBDeviceOutput: - return version_num >= 4U; - case AudioFeatures::Splitter: - return version_num >= 2U; - case AudioFeatures::PerformanceMetricsVersion2: - case AudioFeatures::VariadicCommandBuffer: - return version_num >= 5U; - default: - return false; - } + rb.PushIpcInterface(system, applet_resource_user_id, revision, + num_audio_devices++); } } // namespace Service::Audio diff --git a/src/core/hle/service/audio/audren_u.h b/src/core/hle/service/audio/audren_u.h index 869d390027..4384a9b3c7 100644 --- a/src/core/hle/service/audio/audren_u.h +++ b/src/core/hle/service/audio/audren_u.h @@ -3,6 +3,7 @@ #pragma once +#include "audio_core/audio_render_manager.h" #include "core/hle/service/kernel_helpers.h" #include "core/hle/service/service.h" @@ -15,6 +16,7 @@ class HLERequestContext; } namespace Service::Audio { +class IAudioRenderer; class AudRenU final : public ServiceFramework { public: @@ -23,28 +25,14 @@ public: private: void OpenAudioRenderer(Kernel::HLERequestContext& ctx); - void GetAudioRendererWorkBufferSize(Kernel::HLERequestContext& ctx); + void GetWorkBufferSize(Kernel::HLERequestContext& ctx); void GetAudioDeviceService(Kernel::HLERequestContext& ctx); void OpenAudioRendererForManualExecution(Kernel::HLERequestContext& ctx); void GetAudioDeviceServiceWithRevisionInfo(Kernel::HLERequestContext& ctx); - void OpenAudioRendererImpl(Kernel::HLERequestContext& ctx); - KernelHelpers::ServiceContext service_context; - - std::size_t audren_instance_count = 0; - Kernel::KEvent* buffer_event; + std::unique_ptr impl; + u32 num_audio_devices{0}; }; -// Describes a particular audio feature that may be supported in a particular revision. -enum class AudioFeatures : u32 { - AudioUSBDeviceOutput, - Splitter, - PerformanceMetricsVersion2, - VariadicCommandBuffer, -}; - -// Tests if a particular audio feature is supported with a given audio revision. -bool IsFeatureSupported(AudioFeatures feature, u32_le revision); - } // namespace Service::Audio diff --git a/src/core/hle/service/audio/errors.h b/src/core/hle/service/audio/errors.h index ac6c514af1..d706978cb5 100644 --- a/src/core/hle/service/audio/errors.h +++ b/src/core/hle/service/audio/errors.h @@ -7,8 +7,17 @@ namespace Service::Audio { +constexpr Result ERR_INVALID_DEVICE_NAME{ErrorModule::Audio, 1}; constexpr Result ERR_OPERATION_FAILED{ErrorModule::Audio, 2}; +constexpr Result ERR_INVALID_SAMPLE_RATE{ErrorModule::Audio, 3}; +constexpr Result ERR_INSUFFICIENT_BUFFER_SIZE{ErrorModule::Audio, 4}; +constexpr Result ERR_MAXIMUM_SESSIONS_REACHED{ErrorModule::Audio, 5}; constexpr Result ERR_BUFFER_COUNT_EXCEEDED{ErrorModule::Audio, 8}; +constexpr Result ERR_INVALID_CHANNEL_COUNT{ErrorModule::Audio, 10}; +constexpr Result ERR_INVALID_UPDATE_DATA{ErrorModule::Audio, 41}; +constexpr Result ERR_POOL_MAPPING_FAILED{ErrorModule::Audio, 42}; constexpr Result ERR_NOT_SUPPORTED{ErrorModule::Audio, 513}; +constexpr Result ERR_INVALID_PROCESS_HANDLE{ErrorModule::Audio, 1536}; +constexpr Result ERR_INVALID_REVISION{ErrorModule::Audio, 1537}; } // namespace Service::Audio diff --git a/src/core/hle/service/audio/hwopus.cpp b/src/core/hle/service/audio/hwopus.cpp index 75da659e55..4f2ed2d52e 100644 --- a/src/core/hle/service/audio/hwopus.cpp +++ b/src/core/hle/service/audio/hwopus.cpp @@ -298,7 +298,7 @@ void HwOpus::OpenHardwareOpusDecoderEx(Kernel::HLERequestContext& ctx) { const auto sample_rate = rp.Pop(); const auto channel_count = rp.Pop(); - LOG_CRITICAL(Audio, "called sample_rate={}, channel_count={}", sample_rate, channel_count); + LOG_DEBUG(Audio, "called sample_rate={}, channel_count={}", sample_rate, channel_count); ASSERT_MSG(sample_rate == 48000 || sample_rate == 24000 || sample_rate == 16000 || sample_rate == 12000 || sample_rate == 8000, diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 584808d50d..635449fce0 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -511,7 +511,7 @@ struct Memory::Impl { [[nodiscard]] u8* GetPointerImpl(VAddr vaddr, auto on_unmapped, auto on_rasterizer) const { // AARCH64 masks the upper 16 bit of all memory accesses - vaddr &= 0xffffffffffffLL; + vaddr &= 0xffffffffffffULL; if (vaddr >= 1uLL << current_page_table->GetAddressSpaceBits()) { on_unmapped(); @@ -776,6 +776,10 @@ void Memory::CopyBlock(const Kernel::KProcess& process, VAddr dest_addr, VAddr s impl->CopyBlock(process, dest_addr, src_addr, size); } +void Memory::ZeroBlock(const Kernel::KProcess& process, VAddr dest_addr, const std::size_t size) { + impl->ZeroBlock(process, dest_addr, size); +} + void Memory::RasterizerMarkRegionCached(VAddr vaddr, u64 size, bool cached) { impl->RasterizerMarkRegionCached(vaddr, size, cached); } diff --git a/src/core/memory.h b/src/core/memory.h index f22c0a2d87..780c45385f 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -436,6 +436,19 @@ public: void CopyBlock(const Kernel::KProcess& process, VAddr dest_addr, VAddr src_addr, std::size_t size); + /** + * Zeros a range of bytes within the current process' address space at the specified + * virtual address. + * + * @param process The process that will have data zeroed within its address space. + * @param dest_addr The destination virtual address to zero the data from. + * @param size The size of the range to zero out, in bytes. + * + * @post The range [dest_addr, size) within the process' address space contains the + * value 0. + */ + void ZeroBlock(const Kernel::KProcess& process, VAddr dest_addr, std::size_t size); + /** * Marks each page within the specified address range as cached or uncached. * diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 0a61839da9..2840bc5eb1 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -372,8 +372,9 @@ void Config::ReadAudioValues() { qt_config->beginGroup(QStringLiteral("Audio")); if (global) { - ReadBasicSetting(Settings::values.audio_device_id); ReadBasicSetting(Settings::values.sink_id); + ReadBasicSetting(Settings::values.audio_output_device_id); + ReadBasicSetting(Settings::values.audio_input_device_id); } ReadGlobalSetting(Settings::values.volume); @@ -1027,7 +1028,8 @@ void Config::SaveAudioValues() { if (global) { WriteBasicSetting(Settings::values.sink_id); - WriteBasicSetting(Settings::values.audio_device_id); + WriteBasicSetting(Settings::values.audio_output_device_id); + WriteBasicSetting(Settings::values.audio_input_device_id); } WriteGlobalSetting(Settings::values.volume); diff --git a/src/yuzu/configuration/configure_audio.cpp b/src/yuzu/configuration/configure_audio.cpp index 512bdfc229..19b8b15ef5 100644 --- a/src/yuzu/configuration/configure_audio.cpp +++ b/src/yuzu/configuration/configure_audio.cpp @@ -3,8 +3,8 @@ #include -#include "audio_core/sink.h" -#include "audio_core/sink_details.h" +#include "audio_core/sink/sink.h" +#include "audio_core/sink/sink_details.h" #include "common/settings.h" #include "core/core.h" #include "ui_configure_audio.h" @@ -15,11 +15,11 @@ ConfigureAudio::ConfigureAudio(const Core::System& system_, QWidget* parent) : QWidget(parent), ui(std::make_unique()), system{system_} { ui->setupUi(this); - InitializeAudioOutputSinkComboBox(); + InitializeAudioSinkComboBox(); connect(ui->volume_slider, &QSlider::valueChanged, this, &ConfigureAudio::SetVolumeIndicatorText); - connect(ui->output_sink_combo_box, qOverload(&QComboBox::currentIndexChanged), this, + connect(ui->sink_combo_box, qOverload(&QComboBox::currentIndexChanged), this, &ConfigureAudio::UpdateAudioDevices); ui->volume_label->setVisible(Settings::IsConfiguringGlobal()); @@ -30,8 +30,9 @@ ConfigureAudio::ConfigureAudio(const Core::System& system_, QWidget* parent) SetConfiguration(); const bool is_powered_on = system_.IsPoweredOn(); - ui->output_sink_combo_box->setEnabled(!is_powered_on); - ui->audio_device_combo_box->setEnabled(!is_powered_on); + ui->sink_combo_box->setEnabled(!is_powered_on); + ui->output_combo_box->setEnabled(!is_powered_on); + ui->input_combo_box->setEnabled(!is_powered_on); } ConfigureAudio::~ConfigureAudio() = default; @@ -40,9 +41,9 @@ void ConfigureAudio::SetConfiguration() { SetOutputSinkFromSinkID(); // The device list cannot be pre-populated (nor listed) until the output sink is known. - UpdateAudioDevices(ui->output_sink_combo_box->currentIndex()); + UpdateAudioDevices(ui->sink_combo_box->currentIndex()); - SetAudioDeviceFromDeviceID(); + SetAudioDevicesFromDeviceID(); const auto volume_value = static_cast(Settings::values.volume.GetValue()); ui->volume_slider->setValue(volume_value); @@ -62,32 +63,45 @@ void ConfigureAudio::SetConfiguration() { } void ConfigureAudio::SetOutputSinkFromSinkID() { - [[maybe_unused]] const QSignalBlocker blocker(ui->output_sink_combo_box); + [[maybe_unused]] const QSignalBlocker blocker(ui->sink_combo_box); int new_sink_index = 0; const QString sink_id = QString::fromStdString(Settings::values.sink_id.GetValue()); - for (int index = 0; index < ui->output_sink_combo_box->count(); index++) { - if (ui->output_sink_combo_box->itemText(index) == sink_id) { + for (int index = 0; index < ui->sink_combo_box->count(); index++) { + if (ui->sink_combo_box->itemText(index) == sink_id) { new_sink_index = index; break; } } - ui->output_sink_combo_box->setCurrentIndex(new_sink_index); + ui->sink_combo_box->setCurrentIndex(new_sink_index); } -void ConfigureAudio::SetAudioDeviceFromDeviceID() { +void ConfigureAudio::SetAudioDevicesFromDeviceID() { int new_device_index = -1; - const QString device_id = QString::fromStdString(Settings::values.audio_device_id.GetValue()); - for (int index = 0; index < ui->audio_device_combo_box->count(); index++) { - if (ui->audio_device_combo_box->itemText(index) == device_id) { + const QString output_device_id = + QString::fromStdString(Settings::values.audio_output_device_id.GetValue()); + for (int index = 0; index < ui->output_combo_box->count(); index++) { + if (ui->output_combo_box->itemText(index) == output_device_id) { new_device_index = index; break; } } - ui->audio_device_combo_box->setCurrentIndex(new_device_index); + ui->output_combo_box->setCurrentIndex(new_device_index); + + new_device_index = -1; + const QString input_device_id = + QString::fromStdString(Settings::values.audio_input_device_id.GetValue()); + for (int index = 0; index < ui->input_combo_box->count(); index++) { + if (ui->input_combo_box->itemText(index) == input_device_id) { + new_device_index = index; + break; + } + } + + ui->input_combo_box->setCurrentIndex(new_device_index); } void ConfigureAudio::SetVolumeIndicatorText(int percentage) { @@ -95,14 +109,13 @@ void ConfigureAudio::SetVolumeIndicatorText(int percentage) { } void ConfigureAudio::ApplyConfiguration() { - if (Settings::IsConfiguringGlobal()) { Settings::values.sink_id = - ui->output_sink_combo_box->itemText(ui->output_sink_combo_box->currentIndex()) - .toStdString(); - Settings::values.audio_device_id.SetValue( - ui->audio_device_combo_box->itemText(ui->audio_device_combo_box->currentIndex()) - .toStdString()); + ui->sink_combo_box->itemText(ui->sink_combo_box->currentIndex()).toStdString(); + Settings::values.audio_output_device_id.SetValue( + ui->output_combo_box->itemText(ui->output_combo_box->currentIndex()).toStdString()); + Settings::values.audio_input_device_id.SetValue( + ui->input_combo_box->itemText(ui->input_combo_box->currentIndex()).toStdString()); // Guard if during game and set to game-specific value if (Settings::values.volume.UsingGlobal()) { @@ -129,21 +142,27 @@ void ConfigureAudio::changeEvent(QEvent* event) { } void ConfigureAudio::UpdateAudioDevices(int sink_index) { - ui->audio_device_combo_box->clear(); - ui->audio_device_combo_box->addItem(QString::fromUtf8(AudioCore::auto_device_name)); + ui->output_combo_box->clear(); + ui->output_combo_box->addItem(QString::fromUtf8(AudioCore::Sink::auto_device_name)); - const std::string sink_id = ui->output_sink_combo_box->itemText(sink_index).toStdString(); - for (const auto& device : AudioCore::GetDeviceListForSink(sink_id)) { - ui->audio_device_combo_box->addItem(QString::fromStdString(device)); + const std::string sink_id = ui->sink_combo_box->itemText(sink_index).toStdString(); + for (const auto& device : AudioCore::Sink::GetDeviceListForSink(sink_id, false)) { + ui->output_combo_box->addItem(QString::fromStdString(device)); + } + + ui->input_combo_box->clear(); + ui->input_combo_box->addItem(QString::fromUtf8(AudioCore::Sink::auto_device_name)); + for (const auto& device : AudioCore::Sink::GetDeviceListForSink(sink_id, true)) { + ui->input_combo_box->addItem(QString::fromStdString(device)); } } -void ConfigureAudio::InitializeAudioOutputSinkComboBox() { - ui->output_sink_combo_box->clear(); - ui->output_sink_combo_box->addItem(QString::fromUtf8(AudioCore::auto_device_name)); +void ConfigureAudio::InitializeAudioSinkComboBox() { + ui->sink_combo_box->clear(); + ui->sink_combo_box->addItem(QString::fromUtf8(AudioCore::Sink::auto_device_name)); - for (const char* id : AudioCore::GetSinkIDs()) { - ui->output_sink_combo_box->addItem(QString::fromUtf8(id)); + for (const char* id : AudioCore::Sink::GetSinkIDs()) { + ui->sink_combo_box->addItem(QString::fromUtf8(id)); } } @@ -164,8 +183,10 @@ void ConfigureAudio::SetupPerGameUI() { ConfigurationShared::SetHighlight(ui->volume_layout, index == 1); }); - ui->output_sink_combo_box->setVisible(false); - ui->output_sink_label->setVisible(false); - ui->audio_device_combo_box->setVisible(false); - ui->audio_device_label->setVisible(false); + ui->sink_combo_box->setVisible(false); + ui->sink_label->setVisible(false); + ui->output_combo_box->setVisible(false); + ui->output_label->setVisible(false); + ui->input_combo_box->setVisible(false); + ui->input_label->setVisible(false); } diff --git a/src/yuzu/configuration/configure_audio.h b/src/yuzu/configuration/configure_audio.h index 08c278eeb5..0d03aae1df 100644 --- a/src/yuzu/configuration/configure_audio.h +++ b/src/yuzu/configuration/configure_audio.h @@ -31,14 +31,14 @@ public: private: void changeEvent(QEvent* event) override; - void InitializeAudioOutputSinkComboBox(); + void InitializeAudioSinkComboBox(); void RetranslateUI(); void UpdateAudioDevices(int sink_index); void SetOutputSinkFromSinkID(); - void SetAudioDeviceFromDeviceID(); + void SetAudioDevicesFromDeviceID(); void SetVolumeIndicatorText(int percentage); void SetupPerGameUI(); diff --git a/src/yuzu/configuration/configure_audio.ui b/src/yuzu/configuration/configure_audio.ui index d1ac8ad02d..a5bcee4152 100644 --- a/src/yuzu/configuration/configure_audio.ui +++ b/src/yuzu/configuration/configure_audio.ui @@ -21,30 +21,44 @@ - + - + Output Engine: - + - + - + - Audio Device: + Output Device - + + + + + + + + + + Input Device + + + + + diff --git a/src/yuzu/configuration/configure_debug.cpp b/src/yuzu/configuration/configure_debug.cpp index 343d2aee11..84808f678a 100644 --- a/src/yuzu/configuration/configure_debug.cpp +++ b/src/yuzu/configuration/configure_debug.cpp @@ -44,6 +44,7 @@ void ConfigureDebug::SetConfiguration() { ui->fs_access_log->setEnabled(runtime_lock); ui->fs_access_log->setChecked(Settings::values.enable_fs_access_log.GetValue()); ui->reporting_services->setChecked(Settings::values.reporting_services.GetValue()); + ui->dump_audio_commands->setChecked(Settings::values.dump_audio_commands.GetValue()); ui->quest_flag->setChecked(Settings::values.quest_flag.GetValue()); ui->use_debug_asserts->setChecked(Settings::values.use_debug_asserts.GetValue()); ui->use_auto_stub->setChecked(Settings::values.use_auto_stub.GetValue()); @@ -83,6 +84,7 @@ void ConfigureDebug::ApplyConfiguration() { Settings::values.program_args = ui->homebrew_args_edit->text().toStdString(); Settings::values.enable_fs_access_log = ui->fs_access_log->isChecked(); Settings::values.reporting_services = ui->reporting_services->isChecked(); + Settings::values.dump_audio_commands = ui->dump_audio_commands->isChecked(); Settings::values.quest_flag = ui->quest_flag->isChecked(); Settings::values.use_debug_asserts = ui->use_debug_asserts->isChecked(); Settings::values.use_auto_stub = ui->use_auto_stub->isChecked(); diff --git a/src/yuzu/configuration/configure_debug.ui b/src/yuzu/configuration/configure_debug.ui index 1152fa6c6b..4c16274fc6 100644 --- a/src/yuzu/configuration/configure_debug.ui +++ b/src/yuzu/configuration/configure_debug.ui @@ -235,6 +235,16 @@ + + + Dump Audio Commands To Console** + + + Enable this to output the latest generated audio command list to the console. Only affects games using the audio renderer. + + + + Enable Verbose Reporting Services** @@ -325,6 +335,7 @@ disable_loop_safety_checks fs_access_log reporting_services + dump_audio_commands quest_flag enable_cpu_debugging use_debug_asserts diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index e60d840548..a120f26623 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -1498,6 +1498,8 @@ void GMainWindow::BootGame(const QString& filename, u64 program_id, std::size_t if (!LoadROM(filename, program_id, program_index)) return; + system->SetShuttingDown(false); + // Create and start the emulation thread emu_thread = std::make_unique(*system); emit EmulationStarting(emu_thread.get()); @@ -1588,6 +1590,7 @@ void GMainWindow::ShutdownGame() { AllowOSSleep(); + system->SetShuttingDown(true); system->DetachDebugger(); discord_rpc->Pause(); emu_thread->RequestStop(); diff --git a/src/yuzu_cmd/config.cpp b/src/yuzu_cmd/config.cpp index 5576fb7954..ad7f9d2393 100644 --- a/src/yuzu_cmd/config.cpp +++ b/src/yuzu_cmd/config.cpp @@ -322,7 +322,7 @@ void Config::ReadValues() { // Audio ReadSetting("Audio", Settings::values.sink_id); - ReadSetting("Audio", Settings::values.audio_device_id); + ReadSetting("Audio", Settings::values.audio_output_device_id); ReadSetting("Audio", Settings::values.volume); // Miscellaneous From 4b93ea59dbf15ba82bf711c9713ee7dee7784217 Mon Sep 17 00:00:00 2001 From: lat9nq Date: Fri, 22 Jul 2022 00:51:01 -0400 Subject: [PATCH 23/50] ci,CMake: Drop Conan support for vcpkg Between packages breaking, Conan always being a moving target for minimum required CMake support, and now their moves to Conan 2.0 causing existing packages to break, I suppose this was a long time coming. vcpkg isn't without its drawbacks, but at the moment it seems easier on the project to use for external packages. Mostly removes the logic for Conan from the root CMakeLists file, leaving basic find_package()'s in its place. Sets only the find_package()'s that require CONFIG mode as necessary. clang and linux CI now use the vcpkg toolchain file configured in the Docker container when possible. mingw CI turns off YUZU_TESTS because there's no way on the container to run Windows executables on a Linux host anyway, and it's not easy to get Catch2 there. --- .ci/scripts/clang/docker.sh | 13 ++- .ci/scripts/linux/docker.sh | 1 + .ci/scripts/windows/docker.sh | 5 +- CMakeLists.txt | 160 ++++------------------------------ src/common/CMakeLists.txt | 5 +- 5 files changed, 35 insertions(+), 149 deletions(-) diff --git a/.ci/scripts/clang/docker.sh b/.ci/scripts/clang/docker.sh index 94a9ca0ece..e9719645c1 100755 --- a/.ci/scripts/clang/docker.sh +++ b/.ci/scripts/clang/docker.sh @@ -6,7 +6,18 @@ set -e ccache -s mkdir build || true && cd build -cmake .. -GNinja -DDISPLAY_VERSION=$1 -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/clang -DCMAKE_CXX_COMPILER=/usr/lib/ccache/clang++ -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON -DENABLE_QT_TRANSLATION=ON -DCMAKE_INSTALL_PREFIX="/usr" +cmake .. \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_COMPILER=/usr/lib/ccache/clang++ \ + -DCMAKE_C_COMPILER=/usr/lib/ccache/clang \ + -DCMAKE_INSTALL_PREFIX="/usr" \ + -DCMAKE_TOOLCHAIN_FILE=${VCPKG_TOOLCHAIN_FILE} \ + -DDISPLAY_VERSION=$1 \ + -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON \ + -DENABLE_QT_TRANSLATION=ON \ + -DUSE_DISCORD_PRESENCE=ON \ + -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} \ + -GNinja ninja diff --git a/.ci/scripts/linux/docker.sh b/.ci/scripts/linux/docker.sh index 436155b3de..d4787f6846 100755 --- a/.ci/scripts/linux/docker.sh +++ b/.ci/scripts/linux/docker.sh @@ -12,6 +12,7 @@ cmake .. \ -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ \ -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc \ -DCMAKE_INSTALL_PREFIX="/usr" \ + -DCMAKE_TOOLCHAIN_FILE=${VCPKG_TOOLCHAIN_FILE} \ -DDISPLAY_VERSION=$1 \ -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON \ -DENABLE_QT_TRANSLATION=ON \ diff --git a/.ci/scripts/windows/docker.sh b/.ci/scripts/windows/docker.sh index 5bd5f0b6b8..9f34530d6e 100755 --- a/.ci/scripts/windows/docker.sh +++ b/.ci/scripts/windows/docker.sh @@ -6,10 +6,6 @@ set -e ccache -sv -mkdir -p "$HOME/.conan/profiles" -wget -c "https://github.com/yuzu-emu/build-environments/raw/master/linux-mingw/default" -O "$HOME/.conan/profiles/default" -wget -c "https://github.com/yuzu-emu/build-environments/raw/master/linux-mingw/settings.yml" -O "$HOME/.conan/settings.yml" - mkdir -p build && cd build export LDFLAGS="-fuse-ld=lld" # -femulated-tls required due to an incompatibility between GCC and Clang @@ -24,6 +20,7 @@ cmake .. \ -DUSE_CCACHE=ON \ -DYUZU_USE_BUNDLED_SDL2=OFF \ -DYUZU_USE_EXTERNAL_SDL2=OFF \ + -DYUZU_TESTS=OFF \ -GNinja ninja yuzu yuzu-cmd diff --git a/CMakeLists.txt b/CMakeLists.txt index 80a8d4ed81..aed076deaa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,82 +144,34 @@ endif() set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin) # System imported libraries -# If not found, download any missing through Conan # ======================================================================= -set(CONAN_CMAKE_SILENT_OUTPUT TRUE) -set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) -if (YUZU_CONAN_INSTALLED) - if (IS_MULTI_CONFIG) - include(${CMAKE_BINARY_DIR}/conanbuildinfo_multi.cmake) - else() - include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) - endif() - list(APPEND CMAKE_MODULE_PATH "${CMAKE_BINARY_DIR}") - list(APPEND CMAKE_PREFIX_PATH "${CMAKE_BINARY_DIR}") - conan_basic_setup() - message(STATUS "Adding conan installed libraries to the search path") + +find_package(fmt 8.0.1 REQUIRED CONFIG) +find_package(lz4 1.8 REQUIRED) +find_package(nlohmann_json 3.8 REQUIRED CONFIG) +find_package(ZLIB 1.2 REQUIRED) + +# Search for config-only package first (for vcpkg), then try non-config +find_package(zstd 1.5 CONFIG) +if (NOT zstd_FOUND) + find_package(zstd 1.5 REQUIRED) endif() -macro(yuzu_find_packages) - set(options FORCE_REQUIRED) - cmake_parse_arguments(FN "${options}" "" "" ${ARGN}) +if (YUZU_TESTS) + find_package(Catch2 2.13.7 REQUIRED CONFIG) +endif() - # Cmake has a *serious* lack of 2D array or associative array... - # Capitalization matters here. We need the naming to match the generated paths from Conan - set(REQUIRED_LIBS - # Cmake Pkg Prefix Version Conan Pkg - "fmt 8.0.1 fmt/8.1.1" - "lz4 1.8 lz4/1.9.2" - "nlohmann_json 3.8 nlohmann_json/3.8.0" - "ZLIB 1.2 zlib/1.2.11" - "zstd 1.5 zstd/1.5.0" - # can't use opus until AVX check is fixed: https://github.com/yuzu-emu/yuzu/pull/4068 - #"opus 1.3 opus/1.3.1" - ) - if (YUZU_TESTS) - list(APPEND REQUIRED_LIBS - "Catch2 2.13.7 catch2/2.13.7" - ) - endif() - - foreach(PACKAGE ${REQUIRED_LIBS}) - string(REGEX REPLACE "[ \t\r\n]+" ";" PACKAGE_SPLIT ${PACKAGE}) - list(GET PACKAGE_SPLIT 0 PACKAGE_PREFIX) - list(GET PACKAGE_SPLIT 1 PACKAGE_VERSION) - list(GET PACKAGE_SPLIT 2 PACKAGE_CONAN) - # This function is called twice, once to check if the packages exist on the system already - # and a second time to check if conan installed them properly. The second check passes in FORCE_REQUIRED - if (NOT ${PACKAGE_PREFIX}_FOUND) - if (FN_FORCE_REQUIRED) - find_package(${PACKAGE_PREFIX} ${PACKAGE_VERSION} REQUIRED) - else() - find_package(${PACKAGE_PREFIX} ${PACKAGE_VERSION}) - endif() - endif() - if (NOT ${PACKAGE_PREFIX}_FOUND) - list(APPEND CONAN_REQUIRED_LIBS ${PACKAGE_CONAN}) - else() - # Set a legacy findPackage.cmake style PACKAGE_LIBRARIES variable for subprojects that rely on this - set(${PACKAGE_PREFIX}_LIBRARIES "${PACKAGE_PREFIX}::${PACKAGE_PREFIX}") - endif() - endforeach() - unset(FN_FORCE_REQUIRED) -endmacro() - -find_package(Boost 1.73.0 COMPONENTS context headers) +find_package(Boost 1.73.0 COMPONENTS context) if (Boost_FOUND) set(Boost_LIBRARIES Boost::boost) - # Conditionally add Boost::context only if the active version of the Conan or system Boost package provides it + # Conditionally add Boost::context only if the found Boost package provides it # The old version is missing Boost::context, so we want to avoid adding in that case # The new version requires adding Boost::context to prevent linking issues - # - # This one is used by Conan on subsequent CMake configures, not the first configure. if (TARGET Boost::context) list(APPEND Boost_LIBRARIES Boost::context) endif() else() - message(STATUS "Boost 1.79.0 or newer not found, falling back to Conan") - list(APPEND CONAN_REQUIRED_LIBS "boost/1.79.0") + message(FATAL_ERROR "Boost 1.73.0 or newer not found") endif() # boost:asio has functions that require AcceptEx et al @@ -227,19 +179,9 @@ if (MINGW) find_library(MSWSOCK_LIBRARY mswsock REQUIRED) endif() -# Attempt to locate any packages that are required and report the missing ones in CONAN_REQUIRED_LIBS -yuzu_find_packages() - # Qt5 requires that we find components, so it doesn't fit our pretty little find package function if(ENABLE_QT) set(QT_VERSION 5.15) - # We want to load the generated conan qt config so that we get the QT_ROOT var so that we can use the official - # Qt5Config inside the root folder instead of the conan generated one. - if(EXISTS ${CMAKE_BINARY_DIR}/qtConfig.cmake) - include(${CMAKE_BINARY_DIR}/qtConfig.cmake) - list(APPEND CMAKE_MODULE_PATH "${CONAN_QT_ROOT_RELEASE}") - list(APPEND CMAKE_PREFIX_PATH "${CONAN_QT_ROOT_RELEASE}") - endif() # Check for system Qt on Linux, fallback to bundled Qt if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") @@ -330,9 +272,6 @@ if(ENABLE_QT) set(YUZU_QT_NO_CMAKE_SYSTEM_PATH) - # Workaround for an issue where conan tries to build Qt from scratch instead of download prebuilt binaries - set(QT_PREFIX_HINT) - if(YUZU_USE_BUNDLED_QT) if ((MSVC_VERSION GREATER_EQUAL 1920 AND MSVC_VERSION LESS 1940) AND ARCHITECTURE_x86_64) set(QT_BUILD qt-5.15.2-msvc2019_64) @@ -403,71 +342,8 @@ if (ENABLE_SDL2) endif() endif() -# Install any missing dependencies with conan install -if (CONAN_REQUIRED_LIBS) - message(STATUS "Packages ${CONAN_REQUIRED_LIBS} not found!") - # Use Conan to fetch the libraries that aren't found - # Download conan.cmake automatically, you can also just copy the conan.cmake file - if(NOT EXISTS "${CMAKE_BINARY_DIR}/conan.cmake") - message(STATUS "Downloading conan.cmake from https://github.com/conan-io/cmake-conan") - file(DOWNLOAD "https://raw.githubusercontent.com/conan-io/cmake-conan/release/0.18/conan.cmake" "${CMAKE_BINARY_DIR}/conan.cmake") - endif() - include(${CMAKE_BINARY_DIR}/conan.cmake) - - conan_check(VERSION 1.45.0 REQUIRED) - - # Manually add iconv to fix a dep conflict between qt and sdl2 - # We don't need to add it through find_package or anything since the other two can find it just fine - if ("${CONAN_REQUIRED_LIBS}" MATCHES "qt" AND "${CONAN_REQUIRED_LIBS}" MATCHES "sdl") - list(APPEND CONAN_REQUIRED_LIBS "libiconv/1.16") - endif() - if (IS_MULTI_CONFIG) - conan_cmake_run(REQUIRES ${CONAN_REQUIRED_LIBS} - OPTIONS ${CONAN_LIB_OPTIONS} - BUILD missing - CONFIGURATION_TYPES "Release;Debug" - GENERATORS cmake_multi cmake_find_package_multi) - include(${CMAKE_BINARY_DIR}/conanbuildinfo_multi.cmake) - else() - conan_cmake_run(REQUIRES ${CONAN_REQUIRED_LIBS} - OPTIONS ${CONAN_LIB_OPTIONS} - BUILD missing - GENERATORS cmake cmake_find_package_multi) - include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) - endif() - list(APPEND CMAKE_MODULE_PATH "${CMAKE_BINARY_DIR}") - list(APPEND CMAKE_PREFIX_PATH "${CMAKE_BINARY_DIR}") - conan_basic_setup() - - set(YUZU_CONAN_INSTALLED TRUE CACHE BOOL "If true, the following builds will add conan to the lib search path" FORCE) - - # Now that we've installed what we are missing, try to locate them again, - # this time with required, so we bail if its not found. - yuzu_find_packages(FORCE_REQUIRED) - - if (NOT Boost_FOUND) - find_package(Boost 1.73.0 REQUIRED COMPONENTS context headers) - set(Boost_LIBRARIES Boost::boost) - # Conditionally add Boost::context only if the active version of the Conan Boost package provides it - # The old version is missing Boost::context, so we want to avoid adding in that case - # The new version requires adding Boost::context to prevent linking issues - if (TARGET Boost::context) - list(APPEND Boost_LIBRARIES Boost::context) - endif() - endif() - - # Due to issues with variable scopes in functions, we need to also find_package(qt5) outside of the function - if(ENABLE_QT) - list(APPEND CMAKE_MODULE_PATH "${CONAN_QT_ROOT_RELEASE}") - list(APPEND CMAKE_PREFIX_PATH "${CONAN_QT_ROOT_RELEASE}") - find_package(Qt5 5.15 REQUIRED COMPONENTS Widgets) - if (YUZU_USE_QT_WEB_ENGINE) - find_package(Qt5 REQUIRED COMPONENTS WebEngineCore WebEngineWidgets) - endif() - endif() - -endif() - +# TODO(lat9nq): Determine what if any of this we still need +# # Reexport some targets that are named differently when using the upstream CmakeConfig vs the generated Conan config # In order to ALIAS targets to a new name, they first need to be IMPORTED_GLOBAL # Dynarmic checks for target `boost` and so we want to make sure it can find it through our system instead of using their external diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 73bf626d4c..566695fde7 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -182,8 +182,9 @@ create_target_directory_groups(common) target_link_libraries(common PUBLIC ${Boost_LIBRARIES} fmt::fmt microprofile Threads::Threads) target_link_libraries(common PRIVATE lz4::lz4 xbyak) -if (MSVC) +if (TARGET zstd::zstd) target_link_libraries(common PRIVATE zstd::zstd) else() - target_link_libraries(common PRIVATE zstd) + target_link_libraries(common PRIVATE + $,zstd::libzstd_shared,zstd::libzstd_static>) endif() From 265d1d697935ec65e31d55fc424d96276d41a8c9 Mon Sep 17 00:00:00 2001 From: lat9nq Date: Fri, 22 Jul 2022 14:49:43 -0400 Subject: [PATCH 24/50] ci,CMake: Integrate vcpkg into CMakeLists Uses manifest mode if the bundled vcpkg is used. --- .ci/scripts/clang/docker.sh | 1 - .ci/scripts/linux/docker.sh | 1 - .ci/templates/build-msvc.yml | 4 +--- .github/workflows/verify.yml | 3 +-- .gitmodules | 3 +++ CMakeLists.txt | 10 ++++++++++ externals/vcpkg | 1 + vcpkg.json | 29 +++++++++++++++++++++++++++++ 8 files changed, 45 insertions(+), 7 deletions(-) create mode 160000 externals/vcpkg create mode 100644 vcpkg.json diff --git a/.ci/scripts/clang/docker.sh b/.ci/scripts/clang/docker.sh index e9719645c1..db736f72b8 100755 --- a/.ci/scripts/clang/docker.sh +++ b/.ci/scripts/clang/docker.sh @@ -11,7 +11,6 @@ cmake .. \ -DCMAKE_CXX_COMPILER=/usr/lib/ccache/clang++ \ -DCMAKE_C_COMPILER=/usr/lib/ccache/clang \ -DCMAKE_INSTALL_PREFIX="/usr" \ - -DCMAKE_TOOLCHAIN_FILE=${VCPKG_TOOLCHAIN_FILE} \ -DDISPLAY_VERSION=$1 \ -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON \ -DENABLE_QT_TRANSLATION=ON \ diff --git a/.ci/scripts/linux/docker.sh b/.ci/scripts/linux/docker.sh index d4787f6846..436155b3de 100755 --- a/.ci/scripts/linux/docker.sh +++ b/.ci/scripts/linux/docker.sh @@ -12,7 +12,6 @@ cmake .. \ -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ \ -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc \ -DCMAKE_INSTALL_PREFIX="/usr" \ - -DCMAKE_TOOLCHAIN_FILE=${VCPKG_TOOLCHAIN_FILE} \ -DDISPLAY_VERSION=$1 \ -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON \ -DENABLE_QT_TRANSLATION=ON \ diff --git a/.ci/templates/build-msvc.yml b/.ci/templates/build-msvc.yml index cca3189faf..5d2e861794 100644 --- a/.ci/templates/build-msvc.yml +++ b/.ci/templates/build-msvc.yml @@ -6,9 +6,7 @@ parameters: steps: - script: choco install vulkan-sdk displayName: 'Install vulkan-sdk' -- script: python -m pip install --upgrade pip conan - displayName: 'Install conan' -- script: refreshenv && mkdir build && cd build && cmake -G "Visual Studio 17 2022" -A x64 -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_QT_WEB_ENGINE=ON -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DYUZU_TESTS=OFF -DUSE_DISCORD_PRESENCE=ON -DENABLE_QT_TRANSLATION=ON -DDISPLAY_VERSION=${{ parameters['version'] }} -DCMAKE_BUILD_TYPE=Release .. && cd .. +- script: refreshenv && mkdir build && cd build && cmake -G "Visual Studio 17 2022" -A x64 -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_QT_WEB_ENGINE=ON -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DYUZU_TESTS=OFF -DUSE_DISCORD_PRESENCE=ON -DENABLE_QT_TRANSLATION=ON -DDISPLAY_VERSION=${{ parameters['version'] }} -DCMAKE_BUILD_TYPE=Release -DYUZU_TESTS=OFF -DYUZU_USE_BUNDLED_VCPKG=ON .. && cd .. displayName: 'Configure CMake' - task: MSBuild@1 displayName: 'Build' diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index c1886b9f3c..88e3a9a724 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -80,7 +80,6 @@ jobs: shell: cmd run: | choco install vulkan-sdk wget - python -m pip install --upgrade pip conan call refreshenv wget https://github.com/mbitsnbites/buildcache/releases/download/v0.27.6/buildcache-windows.zip 7z x buildcache-windows.zip @@ -100,7 +99,7 @@ jobs: run: | glslangValidator --version mkdir build - cmake . -B build -GNinja -DCMAKE_TOOLCHAIN_FILE="CMakeModules/MSVCCache.cmake" -DUSE_CCACHE=ON -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_QT_WEB_ENGINE=ON -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=ON -DUSE_DISCORD_PRESENCE=ON -DENABLE_QT_TRANSLATION=ON -DCMAKE_BUILD_TYPE=Release + cmake . -B build -GNinja -DCMAKE_TOOLCHAIN_FILE="CMakeModules/MSVCCache.cmake" -DUSE_CCACHE=ON -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_QT_WEB_ENGINE=ON -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=ON -DUSE_DISCORD_PRESENCE=ON -DENABLE_QT_TRANSLATION=ON -DCMAKE_BUILD_TYPE=Release -DYUZU_TESTS=OFF -DYUZU_USE_BUNDLED_VCPKG=ON - name: Build run: cmake --build build - name: Cache Summary diff --git a/.gitmodules b/.gitmodules index dc92d0a4b5..846e41d263 100644 --- a/.gitmodules +++ b/.gitmodules @@ -40,3 +40,6 @@ [submodule "externals/ffmpeg/ffmpeg"] path = externals/ffmpeg/ffmpeg url = https://git.ffmpeg.org/ffmpeg.git +[submodule "externals/vcpkg"] + path = externals/vcpkg + url = https://github.com/Microsoft/vcpkg.git diff --git a/CMakeLists.txt b/CMakeLists.txt index aed076deaa..6993b6967b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,16 @@ option(YUZU_USE_BUNDLED_OPUS "Compile bundled opus" ON) option(YUZU_TESTS "Compile tests" ON) +option(YUZU_USE_BUNDLED_VCPKG "Use vcpkg for yuzu dependencies" OFF) + +if (YUZU_USE_BUNDLED_VCPKG) + include(${CMAKE_SOURCE_DIR}/externals/vcpkg/scripts/buildsystems/vcpkg.cmake) +elseif(NOT "$ENV{VCPKG_TOOLCHAIN_FILE}" STREQUAL "") + # Disable manifest mode (use vcpkg classic mode) when using a custom vcpkg installation + option(VCPKG_MANIFEST_MODE "") + include("$ENV{VCPKG_TOOLCHAIN_FILE}") +endif() + # Default to a Release build get_property(IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if (NOT IS_MULTI_CONFIG AND NOT CMAKE_BUILD_TYPE) diff --git a/externals/vcpkg b/externals/vcpkg new file mode 160000 index 0000000000..cef0b3ec76 --- /dev/null +++ b/externals/vcpkg @@ -0,0 +1 @@ +Subproject commit cef0b3ec767df6e83806899fe9525f6cf8d7bc91 diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000000..8d3c5919ac --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json", + "name": "yuzu", + "version": "1.0", + "dependencies": [ + "boost-algorithm", + "boost-asio", + "boost-bind", + "boost-config", + "boost-container", + "boost-context", + "boost-crc", + "boost-functional", + "boost-icl", + "boost-intrusive", + "boost-mpl", + "boost-process", + "boost-range", + "boost-spirit", + "boost-test", + "boost-timer", + "boost-variant", + "fmt", + "lz4", + "nlohmann-json", + "zlib", + "zstd" + ] +} From 5b4bef13e7daae9b4280a33490b231a6f4baab6b Mon Sep 17 00:00:00 2001 From: lat9nq Date: Fri, 22 Jul 2022 18:37:42 -0400 Subject: [PATCH 25/50] gitmodules: Remove 'externals' from names of submodules --- .gitmodules | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index 846e41d263..76f13164c9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -34,12 +34,12 @@ [submodule "SDL"] path = externals/SDL url = https://github.com/libsdl-org/SDL.git -[submodule "externals/cpp-httplib"] +[submodule "cpp-httplib"] path = externals/cpp-httplib url = https://github.com/yhirose/cpp-httplib.git -[submodule "externals/ffmpeg/ffmpeg"] +[submodule "ffmpeg"] path = externals/ffmpeg/ffmpeg url = https://git.ffmpeg.org/ffmpeg.git -[submodule "externals/vcpkg"] +[submodule "vcpkg"] path = externals/vcpkg url = https://github.com/Microsoft/vcpkg.git From dc45147736136f5f3c8b63cbeb0dda5e8df19dc5 Mon Sep 17 00:00:00 2001 From: Kyle K <190571+Docteh@users.noreply.github.com> Date: Fri, 22 Jul 2022 02:36:46 -0700 Subject: [PATCH 26/50] ci: pass environment variables to linux docker (AppImage) Variables in question: AZURECIREPO TITLEBARFORMATIDLE TITLEBARFORMATRUNNING DISPLAYVERSION CMakeModules/GenerateSCMRev.cmake has some logic that looks at BUILD_REPOSITORY variable inside CMake src/common/CMakeLists.txt has some logic that takes some items from environment variables and sets variables inside CMake This is the whole section at the moment. if (DEFINED ENV{AZURECIREPO}) set(BUILD_REPOSITORY $ENV{AZURECIREPO}) endif() if (DEFINED ENV{TITLEBARFORMATIDLE}) set(TITLE_BAR_FORMAT_IDLE $ENV{TITLEBARFORMATIDLE}) endif () if (DEFINED ENV{TITLEBARFORMATRUNNING}) set(TITLE_BAR_FORMAT_RUNNING $ENV{TITLEBARFORMATRUNNING}) endif () if (DEFINED ENV{DISPLAYVERSION}) set(DISPLAY_VERSION $ENV{DISPLAYVERSION}) endif () --- .ci/scripts/linux/exec.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.ci/scripts/linux/exec.sh b/.ci/scripts/linux/exec.sh index fc4594d659..78e8aeabf8 100644 --- a/.ci/scripts/linux/exec.sh +++ b/.ci/scripts/linux/exec.sh @@ -4,5 +4,10 @@ mkdir -p "ccache" || true chmod a+x ./.ci/scripts/linux/docker.sh # the UID for the container yuzu user is 1027 sudo chown -R 1027 ./ -docker run -e ENABLE_COMPATIBILITY_REPORTING -e CCACHE_DIR=/yuzu/ccache -v "$(pwd):/yuzu" -w /yuzu yuzuemu/build-environments:linux-fresh /bin/bash /yuzu/.ci/scripts/linux/docker.sh "$1" + +# The environment variables listed below: +# AZURECIREPO TITLEBARFORMATIDLE TITLEBARFORMATRUNNING DISPLAYVERSION +# are requested in src/common/CMakeLists.txt and appear to be provided somewhere in Azure DevOps + +docker run -e AZURECIREPO -e TITLEBARFORMATIDLE -e TITLEBARFORMATRUNNING -e DISPLAYVERSION -e ENABLE_COMPATIBILITY_REPORTING -e CCACHE_DIR=/yuzu/ccache -v "$(pwd):/yuzu" -w /yuzu yuzuemu/build-environments:linux-fresh /bin/bash /yuzu/.ci/scripts/linux/docker.sh "$1" sudo chown -R $UID ./ From 1d700f1dfa6a594792a9f6dfdcf6925ce09a49fc Mon Sep 17 00:00:00 2001 From: lat9nq Date: Sat, 23 Jul 2022 10:16:44 -0400 Subject: [PATCH 27/50] ci/windows: Cleanup unused data before packaging vcpkg data takes up a lot of space, and currently the scripts will package all that data with the source archive which is unnecessary. --- .ci/scripts/windows/upload.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.ci/scripts/windows/upload.ps1 b/.ci/scripts/windows/upload.ps1 index 62483607ba..818fd07705 100644 --- a/.ci/scripts/windows/upload.ps1 +++ b/.ci/scripts/windows/upload.ps1 @@ -25,6 +25,9 @@ $env:BUILD_UPDATE = $MSVC_SEVENZIP $BUILD_DIR = ".\build\bin\Release" +# Cleanup unneeded data in submodules +git submodule foreach git clean -fxd + # Upload debugging symbols mkdir pdb Get-ChildItem "$BUILD_DIR\" -Recurse -Filter "*.pdb" | Copy-Item -destination .\pdb From 5cda63041749bdaaf39c8c2ccf25660b3c13f76d Mon Sep 17 00:00:00 2001 From: Kyle Kienapfel Date: Fri, 15 Jul 2022 19:29:44 -0700 Subject: [PATCH 28/50] package MSVC CI Builds differently, and include yuzu.exe This is related to 8486 Ninja places the exe files into .\build\bin while MSBuild may place them into .\build\bin\Release upload.ps1 was originally written for use with Azure Dev Ops to cough up about 5 files and the script appears to be used for both CI and mainline builds GHA (GitHub Actions) makes available a single zip of the items uploaded by each Upload action (artifacts directory), so we want to work with that. I'm doing changes to upload.ps1 to accomplish this. The changes to the verify.yml are as follows -DGIT_BRANCH=pr-verify changes the header in yuzu, instead of saying HEAD--dirty it'll say pr-verify- -DCLANG_FORMAT_SUFFIX=discordplzdontclang tricks the CMake stuff for discord-rpc to NOT run clang-format, as this was marking CI builds as dirty I'm also making it upload just the exe by itself, as the msvc builds are quite chunky. but maybe this is unnecessary. Currently the MSVC artifact option is a 274MB zip that contains 3 copies of the DLLs, and 4 copies of the source tarball, and zero copies of yuzu.exe This PR should have msvc artifacts of about 190MB that downloads as 81 MB zip --- .ci/scripts/windows/upload.ps1 | 47 ++++++++++++++++++++++++++++++++++ .github/workflows/verify.yml | 9 ++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/.ci/scripts/windows/upload.ps1 b/.ci/scripts/windows/upload.ps1 index 62483607ba..230dd69543 100644 --- a/.ci/scripts/windows/upload.ps1 +++ b/.ci/scripts/windows/upload.ps1 @@ -47,6 +47,49 @@ Copy-Item .\CMakeModules -Recurse -Destination $MSVC_SOURCE 7z a -r -ttar $MSVC_SOURCE_TAR $MSVC_SOURCE 7z a -r -txz $MSVC_SOURCE_TARXZ $MSVC_SOURCE_TAR +# Following section is quick hack to package artifacts differently for GitHub Actions +if ("$env:GITHUB_ACTIONS" -eq "true") { + echo "Hello GitHub Actions" + + # Hopefully there is an exe in either .\build\bin or .\build\bin\Release + cp .\build\bin\yuzu*.exe .\artifacts\ + Copy-Item "$BUILD_DIR\*" -Destination "artifacts" -Recurse + Remove-Item .\artifacts\tests.exe -ErrorAction ignore + + # None of the other GHA builds are including source, so commenting out today + #Copy-Item $MSVC_SOURCE_TARXZ -Destination "artifacts" + + # Are debug symbols important? + # cp .\build\bin\yuzu*.pdb .\pdb\ + + # Write out a tag BUILD_TAG to environment for the Upload step + # We're getting ${{ github.event.number }} as $env:PR_NUMBER" + echo "env:PR_NUMBER: $env:PR_NUMBER" + if (Test-Path env:PR_NUMBER) { + $PR_NUMBER = $env:PR_NUMBER.Substring(2) -as [int] + $PR_NUMBER_TAG = "pr"+([string]$PR_NUMBER).PadLeft(5,'0') + if ($PR_NUMBER -gt 1){ + $BUILD_TAG="verify-$PR_NUMBER_TAG-$GITDATE-$GITREV" + } else { + $BUILD_TAG = "verify-$GITDATE-$GITREV" + } + } else { + # If env:PR_NUMBER isn't set, we should still write out a variable + $BUILD_TAG = "verify-$GITDATE-$GITREV" + } + echo "BUILD_TAG=$BUILD_TAG" + echo "BUILD_TAG=$BUILD_TAG" >> $env:GITHUB_ENV + + # For extra job, just the exe + $INDIVIDUAL_EXE = "yuzu-msvc-$BUILD_TAG.exe" + echo "INDIVIDUAL_EXE=$INDIVIDUAL_EXE" + echo "INDIVIDUAL_EXE=$INDIVIDUAL_EXE" >> $env:GITHUB_ENV + echo "Just the exe: $INDIVIDUAL_EXE" + cp .\artifacts\yuzu.exe .\$INDIVIDUAL_EXE + + +} else { + # Build the final release artifacts Copy-Item $MSVC_SOURCE_TARXZ -Destination $RELEASE_DIST Copy-Item "$BUILD_DIR\*" -Destination $RELEASE_DIST -Recurse @@ -62,3 +105,7 @@ Get-ChildItem "$BUILD_DIR" -Recurse -Filter "QtWebEngineProcess*.exe" | Copy-Ite Get-ChildItem . -Filter "*.zip" | Copy-Item -destination "artifacts" Get-ChildItem . -Filter "*.7z" | Copy-Item -destination "artifacts" Get-ChildItem . -Filter "*.tar.xz" | Copy-Item -destination "artifacts" +} +# Extra items +git status +cp .\build\src\common\scm_rev.cpp .\artifacts diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 88e3a9a724..8872204415 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -3,6 +3,8 @@ name: 'yuzu verify' on: pull_request: branches: [ master ] +env: + PR_NUMBER: pr${{ github.event.number }} jobs: format: @@ -99,7 +101,7 @@ jobs: run: | glslangValidator --version mkdir build - cmake . -B build -GNinja -DCMAKE_TOOLCHAIN_FILE="CMakeModules/MSVCCache.cmake" -DUSE_CCACHE=ON -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_QT_WEB_ENGINE=ON -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=ON -DUSE_DISCORD_PRESENCE=ON -DENABLE_QT_TRANSLATION=ON -DCMAKE_BUILD_TYPE=Release -DYUZU_TESTS=OFF -DYUZU_USE_BUNDLED_VCPKG=ON + cmake . -B build -GNinja -DCMAKE_TOOLCHAIN_FILE="CMakeModules/MSVCCache.cmake" -DUSE_CCACHE=ON -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_QT_WEB_ENGINE=ON -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=ON -DUSE_DISCORD_PRESENCE=ON -DENABLE_QT_TRANSLATION=ON -DCMAKE_BUILD_TYPE=Release -DGIT_BRANCH=pr-verify -DCLANG_FORMAT_SUFFIX=discordplzdontclang -DYUZU_TESTS=OFF -DYUZU_USE_BUNDLED_VCPKG=ON - name: Build run: cmake --build build - name: Cache Summary @@ -112,3 +114,8 @@ jobs: with: name: msvc path: artifacts/ + - name: Upload EXE + uses: actions/upload-artifact@v3 + with: + name: ${{ env.INDIVIDUAL_EXE }} + path: ${{ env.INDIVIDUAL_EXE }} From 5878eb34f493fba74727c610bbb8d86b81d68f02 Mon Sep 17 00:00:00 2001 From: Kyle Kienapfel Date: Sat, 23 Jul 2022 10:09:59 -0700 Subject: [PATCH 29/50] ci,transifex: enable vcpkg on transifex step The slim docker container that runs transifex needs a few packages added in, curl zip unzip I've tested everything except actually pushing to transifex, but it's not November 2022 yet so we're fine for now. Or we're actually using the newer client and all is well. --- .ci/scripts/transifex/docker.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.ci/scripts/transifex/docker.sh b/.ci/scripts/transifex/docker.sh index bafd326f94..05e1a98f54 100755 --- a/.ci/scripts/transifex/docker.sh +++ b/.ci/scripts/transifex/docker.sh @@ -16,8 +16,11 @@ cmake --version gcc -v tx --version +# vcpkg needs these: curl zip unzip tar, have tar +apt-get install -y curl zip unzip + mkdir build && cd build -cmake .. -DENABLE_QT_TRANSLATION=ON -DGENERATE_QT_TRANSLATION=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_SDL2=OFF +cmake .. -DENABLE_QT_TRANSLATION=ON -DGENERATE_QT_TRANSLATION=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_SDL2=OFF -DYUZU_TESTS=OFF -DYUZU_USE_BUNDLED_VCPKG=ON make translation cd .. From f19e7be6e84357234c9fdae3395f988a9bb1ac5b Mon Sep 17 00:00:00 2001 From: german77 Date: Sat, 18 Jun 2022 23:32:07 -0500 Subject: [PATCH 30/50] input_common: Add camera driver --- src/common/input.h | 29 +++++++++- src/common/settings.h | 3 ++ src/input_common/CMakeLists.txt | 2 + src/input_common/drivers/camera.cpp | 82 +++++++++++++++++++++++++++++ src/input_common/drivers/camera.h | 29 ++++++++++ src/input_common/input_engine.cpp | 38 +++++++++++++ src/input_common/input_engine.h | 21 ++++++-- src/input_common/input_poller.cpp | 60 +++++++++++++++++++++ src/input_common/input_poller.h | 11 ++++ src/input_common/main.cpp | 21 ++++++++ src/input_common/main.h | 9 +++- 11 files changed, 299 insertions(+), 6 deletions(-) create mode 100644 src/input_common/drivers/camera.cpp create mode 100644 src/input_common/drivers/camera.h diff --git a/src/common/input.h b/src/common/input.h index bb42aaacc0..995c35d9d5 100644 --- a/src/common/input.h +++ b/src/common/input.h @@ -28,7 +28,7 @@ enum class InputType { Color, Vibration, Nfc, - Ir, + IrSensor, }; // Internal battery charge level @@ -53,6 +53,15 @@ enum class PollingMode { IR, }; +enum class CameraFormat { + Size320x240, + Size160x120, + Size80x60, + Size40x30, + Size20x15, + None, +}; + // Vibration reply from the controller enum class VibrationError { None, @@ -68,6 +77,13 @@ enum class PollingError { Unknown, }; +// Ir camera reply from the controller +enum class CameraError { + None, + NotSupported, + Unknown, +}; + // Hint for amplification curve to be used enum class VibrationAmplificationType { Linear, @@ -176,6 +192,12 @@ struct LedStatus { bool led_4{}; }; +// Raw data fom camera +struct CameraStatus { + CameraFormat format{CameraFormat::None}; + std::vector data{}; +}; + // List of buttons to be passed to Qt that can be translated enum class ButtonNames { Undefined, @@ -233,6 +255,7 @@ struct CallbackStatus { BodyColorStatus color_status{}; BatteryStatus battery_status{}; VibrationStatus vibration_status{}; + CameraStatus camera_status{}; }; // Triggered once every input change @@ -281,6 +304,10 @@ public: virtual PollingError SetPollingMode([[maybe_unused]] PollingMode polling_mode) { return PollingError::NotSupported; } + + virtual CameraError SetCameraFormat([[maybe_unused]] CameraFormat camera_format) { + return CameraError::NotSupported; + } }; /// An abstract class template for a factory that can create input devices. diff --git a/src/common/settings.h b/src/common/settings.h index 06d72c8bff..20959ec89d 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -503,6 +503,9 @@ struct Values { Setting enable_ring_controller{true, "enable_ring_controller"}; RingconRaw ringcon_analogs; + BasicSetting enable_ir_sensor{false, "enable_ir_sensor"}; + BasicSetting ir_sensor_device{"auto", "ir_sensor_device"}; + // Data Storage Setting use_virtual_sd{true, "use_virtual_sd"}; Setting gamecard_inserted{false, "gamecard_inserted"}; diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index 48e799cf58..90dd629c66 100644 --- a/src/input_common/CMakeLists.txt +++ b/src/input_common/CMakeLists.txt @@ -1,4 +1,6 @@ add_library(input_common STATIC + drivers/camera.cpp + drivers/camera.h drivers/gc_adapter.cpp drivers/gc_adapter.h drivers/keyboard.cpp diff --git a/src/input_common/drivers/camera.cpp b/src/input_common/drivers/camera.cpp new file mode 100644 index 0000000000..dceea67e04 --- /dev/null +++ b/src/input_common/drivers/camera.cpp @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "common/param_package.h" +#include "input_common/drivers/camera.h" + +namespace InputCommon { +constexpr PadIdentifier identifier = { + .guid = Common::UUID{}, + .port = 0, + .pad = 0, +}; + +Camera::Camera(std::string input_engine_) : InputEngine(std::move(input_engine_)) { + PreSetController(identifier); +} + +void Camera::SetCameraData(std::size_t width, std::size_t height, std::vector data) { + const std::size_t desired_width = getImageWidth(); + const std::size_t desired_height = getImageHeight(); + status.data.resize(desired_width * desired_height); + + // Resize image to desired format + for (std::size_t y = 0; y < desired_height; y++) { + for (std::size_t x = 0; x < desired_width; x++) { + const std::size_t pixel_index = y * desired_width + x; + const std::size_t old_x = width * x / desired_width; + const std::size_t old_y = height * y / desired_height; + const std::size_t data_pixel_index = old_y * width + old_x; + status.data[pixel_index] = static_cast(data[data_pixel_index] & 0xFF); + } + } + + SetCamera(identifier, status); +} + +std::size_t Camera::getImageWidth() const { + switch (status.format) { + case Common::Input::CameraFormat::Size320x240: + return 320; + case Common::Input::CameraFormat::Size160x120: + return 160; + case Common::Input::CameraFormat::Size80x60: + return 80; + case Common::Input::CameraFormat::Size40x30: + return 40; + case Common::Input::CameraFormat::Size20x15: + return 20; + case Common::Input::CameraFormat::None: + default: + return 0; + } +} + +std::size_t Camera::getImageHeight() const { + switch (status.format) { + case Common::Input::CameraFormat::Size320x240: + return 240; + case Common::Input::CameraFormat::Size160x120: + return 120; + case Common::Input::CameraFormat::Size80x60: + return 60; + case Common::Input::CameraFormat::Size40x30: + return 30; + case Common::Input::CameraFormat::Size20x15: + return 15; + case Common::Input::CameraFormat::None: + default: + return 0; + } +} + +Common::Input::CameraError Camera::SetCameraFormat( + [[maybe_unused]] const PadIdentifier& identifier_, + const Common::Input::CameraFormat camera_format) { + status.format = camera_format; + return Common::Input::CameraError::None; +} + +} // namespace InputCommon diff --git a/src/input_common/drivers/camera.h b/src/input_common/drivers/camera.h new file mode 100644 index 0000000000..b8a7c75e55 --- /dev/null +++ b/src/input_common/drivers/camera.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "input_common/input_engine.h" + +namespace InputCommon { + +/** + * A button device factory representing a keyboard. It receives keyboard events and forward them + * to all button devices it created. + */ +class Camera final : public InputEngine { +public: + explicit Camera(std::string input_engine_); + + void SetCameraData(std::size_t width, std::size_t height, std::vector data); + + std::size_t getImageWidth() const; + std::size_t getImageHeight() const; + + Common::Input::CameraError SetCameraFormat(const PadIdentifier& identifier_, + Common::Input::CameraFormat camera_format) override; + + Common::Input::CameraStatus status{}; +}; + +} // namespace InputCommon diff --git a/src/input_common/input_engine.cpp b/src/input_common/input_engine.cpp index 12214d1462..6ede0e4b0e 100644 --- a/src/input_common/input_engine.cpp +++ b/src/input_common/input_engine.cpp @@ -90,6 +90,18 @@ void InputEngine::SetMotion(const PadIdentifier& identifier, int motion, const B TriggerOnMotionChange(identifier, motion, value); } +void InputEngine::SetCamera(const PadIdentifier& identifier, + const Common::Input::CameraStatus& value) { + { + std::scoped_lock lock{mutex}; + ControllerData& controller = controller_list.at(identifier); + if (!configuring) { + controller.camera = value; + } + } + TriggerOnCameraChange(identifier, value); +} + bool InputEngine::GetButton(const PadIdentifier& identifier, int button) const { std::scoped_lock lock{mutex}; const auto controller_iter = controller_list.find(identifier); @@ -165,6 +177,18 @@ BasicMotion InputEngine::GetMotion(const PadIdentifier& identifier, int motion) return controller.motions.at(motion); } +Common::Input::CameraStatus InputEngine::GetCamera(const PadIdentifier& identifier) const { + std::scoped_lock lock{mutex}; + const auto controller_iter = controller_list.find(identifier); + if (controller_iter == controller_list.cend()) { + LOG_ERROR(Input, "Invalid identifier guid={}, pad={}, port={}", identifier.guid.RawString(), + identifier.pad, identifier.port); + return {}; + } + const ControllerData& controller = controller_iter->second; + return controller.camera; +} + void InputEngine::ResetButtonState() { for (const auto& controller : controller_list) { for (const auto& button : controller.second.buttons) { @@ -317,6 +341,20 @@ void InputEngine::TriggerOnMotionChange(const PadIdentifier& identifier, int mot }); } +void InputEngine::TriggerOnCameraChange(const PadIdentifier& identifier, + [[maybe_unused]] const Common::Input::CameraStatus& value) { + std::scoped_lock lock{mutex_callback}; + for (const auto& poller_pair : callback_list) { + const InputIdentifier& poller = poller_pair.second; + if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::Camera, 0)) { + continue; + } + if (poller.callback.on_change) { + poller.callback.on_change(); + } + } +} + bool InputEngine::IsInputIdentifierEqual(const InputIdentifier& input_identifier, const PadIdentifier& identifier, EngineInputType type, int index) const { diff --git a/src/input_common/input_engine.h b/src/input_common/input_engine.h index 13295bd493..f6b3c46104 100644 --- a/src/input_common/input_engine.h +++ b/src/input_common/input_engine.h @@ -36,11 +36,12 @@ struct BasicMotion { // Types of input that are stored in the engine enum class EngineInputType { None, - Button, - HatButton, Analog, - Motion, Battery, + Button, + Camera, + HatButton, + Motion, }; namespace std { @@ -115,10 +116,17 @@ public: // Sets polling mode to a controller virtual Common::Input::PollingError SetPollingMode( [[maybe_unused]] const PadIdentifier& identifier, - [[maybe_unused]] const Common::Input::PollingMode vibration) { + [[maybe_unused]] const Common::Input::PollingMode polling_mode) { return Common::Input::PollingError::NotSupported; } + // Sets camera format to a controller + virtual Common::Input::CameraError SetCameraFormat( + [[maybe_unused]] const PadIdentifier& identifier, + [[maybe_unused]] Common::Input::CameraFormat camera_format) { + return Common::Input::CameraError::NotSupported; + } + // Returns the engine name [[nodiscard]] const std::string& GetEngineName() const; @@ -174,6 +182,7 @@ public: f32 GetAxis(const PadIdentifier& identifier, int axis) const; Common::Input::BatteryLevel GetBattery(const PadIdentifier& identifier) const; BasicMotion GetMotion(const PadIdentifier& identifier, int motion) const; + Common::Input::CameraStatus GetCamera(const PadIdentifier& identifier) const; int SetCallback(InputIdentifier input_identifier); void SetMappingCallback(MappingCallback callback); @@ -185,6 +194,7 @@ protected: void SetAxis(const PadIdentifier& identifier, int axis, f32 value); void SetBattery(const PadIdentifier& identifier, Common::Input::BatteryLevel value); void SetMotion(const PadIdentifier& identifier, int motion, const BasicMotion& value); + void SetCamera(const PadIdentifier& identifier, const Common::Input::CameraStatus& value); virtual std::string GetHatButtonName([[maybe_unused]] u8 direction_value) const { return "Unknown"; @@ -197,6 +207,7 @@ private: std::unordered_map axes; std::unordered_map motions; Common::Input::BatteryLevel battery{}; + Common::Input::CameraStatus camera{}; }; void TriggerOnButtonChange(const PadIdentifier& identifier, int button, bool value); @@ -205,6 +216,8 @@ private: void TriggerOnBatteryChange(const PadIdentifier& identifier, Common::Input::BatteryLevel value); void TriggerOnMotionChange(const PadIdentifier& identifier, int motion, const BasicMotion& value); + void TriggerOnCameraChange(const PadIdentifier& identifier, + const Common::Input::CameraStatus& value); bool IsInputIdentifierEqual(const InputIdentifier& input_identifier, const PadIdentifier& identifier, EngineInputType type, diff --git a/src/input_common/input_poller.cpp b/src/input_common/input_poller.cpp index 49ccb44223..133422d5cd 100644 --- a/src/input_common/input_poller.cpp +++ b/src/input_common/input_poller.cpp @@ -664,6 +664,47 @@ private: InputEngine* input_engine; }; +class InputFromCamera final : public Common::Input::InputDevice { +public: + explicit InputFromCamera(PadIdentifier identifier_, InputEngine* input_engine_) + : identifier(identifier_), input_engine(input_engine_) { + UpdateCallback engine_callback{[this]() { OnChange(); }}; + const InputIdentifier input_identifier{ + .identifier = identifier, + .type = EngineInputType::Camera, + .index = 0, + .callback = engine_callback, + }; + callback_key = input_engine->SetCallback(input_identifier); + } + + ~InputFromCamera() override { + input_engine->DeleteCallback(callback_key); + } + + Common::Input::CameraStatus GetStatus() const { + return input_engine->GetCamera(identifier); + } + + void ForceUpdate() override { + OnChange(); + } + + void OnChange() { + const Common::Input::CallbackStatus status{ + .type = Common::Input::InputType::IrSensor, + .camera_status = GetStatus(), + }; + + TriggerOnChange(status); + } + +private: + const PadIdentifier identifier; + int callback_key; + InputEngine* input_engine; +}; + class OutputFromIdentifier final : public Common::Input::OutputDevice { public: explicit OutputFromIdentifier(PadIdentifier identifier_, InputEngine* input_engine_) @@ -682,6 +723,10 @@ public: return input_engine->SetPollingMode(identifier, polling_mode); } + Common::Input::CameraError SetCameraFormat(Common::Input::CameraFormat camera_format) override { + return input_engine->SetCameraFormat(identifier, camera_format); + } + private: const PadIdentifier identifier; InputEngine* input_engine; @@ -920,6 +965,18 @@ std::unique_ptr InputFactory::CreateMotionDevice( properties_y, properties_z, input_engine.get()); } +std::unique_ptr InputFactory::CreateCameraDevice( + const Common::ParamPackage& params) { + const PadIdentifier identifier = { + .guid = Common::UUID{params.Get("guid", "")}, + .port = static_cast(params.Get("port", 0)), + .pad = static_cast(params.Get("pad", 0)), + }; + + input_engine->PreSetController(identifier); + return std::make_unique(identifier, input_engine.get()); +} + InputFactory::InputFactory(std::shared_ptr input_engine_) : input_engine(std::move(input_engine_)) {} @@ -928,6 +985,9 @@ std::unique_ptr InputFactory::Create( if (params.Has("battery")) { return CreateBatteryDevice(params); } + if (params.Has("camera")) { + return CreateCameraDevice(params); + } if (params.Has("button") && params.Has("axis")) { return CreateTriggerDevice(params); } diff --git a/src/input_common/input_poller.h b/src/input_common/input_poller.h index 6ebe0dbf51..4410a84154 100644 --- a/src/input_common/input_poller.h +++ b/src/input_common/input_poller.h @@ -211,6 +211,17 @@ private: */ std::unique_ptr CreateMotionDevice(Common::ParamPackage params); + /** + * Creates a camera device from the parameters given. + * @param params contains parameters for creating the device: + * - "guid": text string for identifying controllers + * - "port": port of the connected device + * - "pad": slot of the connected controller + * @returns a unique input device with the parameters specified + */ + std::unique_ptr CreateCameraDevice( + const Common::ParamPackage& params); + std::shared_ptr input_engine; }; } // namespace InputCommon diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp index 21834fb6b3..ca1cb95426 100644 --- a/src/input_common/main.cpp +++ b/src/input_common/main.cpp @@ -5,6 +5,7 @@ #include #include "common/input.h" #include "common/param_package.h" +#include "input_common/drivers/camera.h" #include "input_common/drivers/gc_adapter.h" #include "input_common/drivers/keyboard.h" #include "input_common/drivers/mouse.h" @@ -78,6 +79,15 @@ struct InputSubsystem::Impl { Common::Input::RegisterFactory(tas_input->GetEngineName(), tas_output_factory); + camera = std::make_shared("camera"); + camera->SetMappingCallback(mapping_callback); + camera_input_factory = std::make_shared(camera); + camera_output_factory = std::make_shared(camera); + Common::Input::RegisterFactory(camera->GetEngineName(), + camera_input_factory); + Common::Input::RegisterFactory(camera->GetEngineName(), + camera_output_factory); + #ifdef HAVE_SDL2 sdl = std::make_shared("sdl"); sdl->SetMappingCallback(mapping_callback); @@ -317,6 +327,7 @@ struct InputSubsystem::Impl { std::shared_ptr touch_screen; std::shared_ptr tas_input; std::shared_ptr udp_client; + std::shared_ptr camera; std::shared_ptr keyboard_factory; std::shared_ptr mouse_factory; @@ -324,12 +335,14 @@ struct InputSubsystem::Impl { std::shared_ptr touch_screen_factory; std::shared_ptr udp_client_input_factory; std::shared_ptr tas_input_factory; + std::shared_ptr camera_input_factory; std::shared_ptr keyboard_output_factory; std::shared_ptr mouse_output_factory; std::shared_ptr gcadapter_output_factory; std::shared_ptr udp_client_output_factory; std::shared_ptr tas_output_factory; + std::shared_ptr camera_output_factory; #ifdef HAVE_SDL2 std::shared_ptr sdl; @@ -382,6 +395,14 @@ const TasInput::Tas* InputSubsystem::GetTas() const { return impl->tas_input.get(); } +Camera* InputSubsystem::GetCamera() { + return impl->camera.get(); +} + +const Camera* InputSubsystem::GetCamera() const { + return impl->camera.get(); +} + std::vector InputSubsystem::GetInputDevices() const { return impl->GetInputDevices(); } diff --git a/src/input_common/main.h b/src/input_common/main.h index 147c310c4e..b756bb5c63 100644 --- a/src/input_common/main.h +++ b/src/input_common/main.h @@ -30,6 +30,7 @@ enum Values : int; } namespace InputCommon { +class Camera; class Keyboard; class Mouse; class TouchScreen; @@ -92,9 +93,15 @@ public: /// Retrieves the underlying tas input device. [[nodiscard]] TasInput::Tas* GetTas(); - /// Retrieves the underlying tas input device. + /// Retrieves the underlying tas input device. [[nodiscard]] const TasInput::Tas* GetTas() const; + /// Retrieves the underlying camera input device. + [[nodiscard]] Camera* GetCamera(); + + /// Retrieves the underlying camera input device. + [[nodiscard]] const Camera* GetCamera() const; + /** * Returns all available input devices that this Factory can create a new device with. * Each returned ParamPackage should have a `display` field used for display, a `engine` field From cc83e0a6006667d126a7a83dde23a7f8ae3af994 Mon Sep 17 00:00:00 2001 From: german77 Date: Sat, 18 Jun 2022 23:34:28 -0500 Subject: [PATCH 31/50] yuzu: Hook qt camera to camera driver --- CMakeLists.txt | 6 +- CMakeModules/CopyYuzuQt5Deps.cmake | 7 + src/yuzu/CMakeLists.txt | 5 +- src/yuzu/bootmanager.cpp | 71 ++++++++ src/yuzu/bootmanager.h | 12 ++ src/yuzu/configuration/config.cpp | 12 ++ src/yuzu/configuration/config.h | 2 + src/yuzu/configuration/configure_camera.cpp | 126 +++++++++++++ src/yuzu/configuration/configure_camera.h | 52 ++++++ src/yuzu/configuration/configure_camera.ui | 170 ++++++++++++++++++ src/yuzu/configuration/configure_input.cpp | 5 + .../configure_input_advanced.cpp | 3 + .../configuration/configure_input_advanced.h | 1 + .../configuration/configure_input_advanced.ui | 14 ++ src/yuzu/main.cpp | 9 + 15 files changed, 491 insertions(+), 4 deletions(-) create mode 100644 src/yuzu/configuration/configure_camera.cpp create mode 100644 src/yuzu/configuration/configure_camera.h create mode 100644 src/yuzu/configuration/configure_camera.ui diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f7dcc9247..40ca8b149f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -196,7 +196,7 @@ if(ENABLE_QT) # Check for system Qt on Linux, fallback to bundled Qt if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") if (NOT YUZU_USE_BUNDLED_QT) - find_package(Qt5 ${QT_VERSION} COMPONENTS Widgets DBus) + find_package(Qt5 ${QT_VERSION} COMPONENTS Widgets DBus Multimedia) endif() if (NOT Qt5_FOUND OR YUZU_USE_BUNDLED_QT) # Check for dependencies, then enable bundled Qt download @@ -300,9 +300,9 @@ if(ENABLE_QT) set(YUZU_QT_NO_CMAKE_SYSTEM_PATH "NO_CMAKE_SYSTEM_PATH") endif() if ((${CMAKE_SYSTEM_NAME} STREQUAL "Linux") AND YUZU_USE_BUNDLED_QT) - find_package(Qt5 ${QT_VERSION} REQUIRED COMPONENTS Widgets Concurrent DBus ${QT_PREFIX_HINT} ${YUZU_QT_NO_CMAKE_SYSTEM_PATH}) + find_package(Qt5 ${QT_VERSION} REQUIRED COMPONENTS Widgets Concurrent Multimedia DBus ${QT_PREFIX_HINT} ${YUZU_QT_NO_CMAKE_SYSTEM_PATH}) else() - find_package(Qt5 ${QT_VERSION} REQUIRED COMPONENTS Widgets Concurrent ${QT_PREFIX_HINT} ${YUZU_QT_NO_CMAKE_SYSTEM_PATH}) + find_package(Qt5 ${QT_VERSION} REQUIRED COMPONENTS Widgets Concurrent Multimedia ${QT_PREFIX_HINT} ${YUZU_QT_NO_CMAKE_SYSTEM_PATH}) endif() if (YUZU_USE_QT_WEB_ENGINE) find_package(Qt5 REQUIRED COMPONENTS WebEngineCore WebEngineWidgets) diff --git a/CMakeModules/CopyYuzuQt5Deps.cmake b/CMakeModules/CopyYuzuQt5Deps.cmake index 0c27d51a65..4702a504c6 100644 --- a/CMakeModules/CopyYuzuQt5Deps.cmake +++ b/CMakeModules/CopyYuzuQt5Deps.cmake @@ -10,11 +10,13 @@ function(copy_yuzu_Qt5_deps target_dir) set(Qt5_PLATFORMS_DIR "${Qt5_DIR}/../../../plugins/platforms/") set(Qt5_PLATFORMTHEMES_DIR "${Qt5_DIR}/../../../plugins/platformthemes/") set(Qt5_PLATFORMINPUTCONTEXTS_DIR "${Qt5_DIR}/../../../plugins/platforminputcontexts/") + set(Qt5_MEDIASERVICE_DIR "${Qt5_DIR}/../../../plugins/mediaservice/") set(Qt5_XCBGLINTEGRATIONS_DIR "${Qt5_DIR}/../../../plugins/xcbglintegrations/") set(Qt5_STYLES_DIR "${Qt5_DIR}/../../../plugins/styles/") set(Qt5_IMAGEFORMATS_DIR "${Qt5_DIR}/../../../plugins/imageformats/") set(Qt5_RESOURCES_DIR "${Qt5_DIR}/../../../resources/") set(PLATFORMS ${DLL_DEST}plugins/platforms/) + set(MEDIASERVICE ${DLL_DEST}mediaservice/) set(STYLES ${DLL_DEST}plugins/styles/) set(IMAGEFORMATS ${DLL_DEST}plugins/imageformats/) if (MSVC) @@ -22,6 +24,7 @@ function(copy_yuzu_Qt5_deps target_dir) Qt5Core$<$:d>.* Qt5Gui$<$:d>.* Qt5Widgets$<$:d>.* + Qt5Multimedia$<$:d>.* ) if (YUZU_USE_QT_WEB_ENGINE) @@ -53,6 +56,10 @@ function(copy_yuzu_Qt5_deps target_dir) qjpeg$<$:d>.* qgif$<$:d>.* ) + windows_copy_files(yuzu ${Qt5_MEDIASERVICE_DIR} ${MEDIASERVICE} + dsengine$<$:d>.* + wmfengine$<$:d>.* + ) else() set(Qt5_DLLS "${Qt5_DLL_DIR}libQt5Core.so.5" diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 242867a4f0..a11a3b9084 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -43,6 +43,9 @@ add_executable(yuzu configuration/configure_audio.cpp configuration/configure_audio.h configuration/configure_audio.ui + configuration/configure_camera.cpp + configuration/configure_camera.h + configuration/configure_camera.ui configuration/configure_cpu.cpp configuration/configure_cpu.h configuration/configure_cpu.ui @@ -254,7 +257,7 @@ endif() create_target_directory_groups(yuzu) target_link_libraries(yuzu PRIVATE common core input_common video_core) -target_link_libraries(yuzu PRIVATE Boost::boost glad Qt::Widgets) +target_link_libraries(yuzu PRIVATE Boost::boost glad Qt::Widgets Qt::Multimedia) target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) target_include_directories(yuzu PRIVATE ../../externals/Vulkan-Headers/include) diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp index 01acda22b7..7740858097 100644 --- a/src/yuzu/bootmanager.cpp +++ b/src/yuzu/bootmanager.cpp @@ -5,6 +5,8 @@ #include #include +#include +#include #include #include #include @@ -31,6 +33,7 @@ #include "core/core.h" #include "core/cpu_manager.h" #include "core/frontend/framebuffer_layout.h" +#include "input_common/drivers/camera.h" #include "input_common/drivers/keyboard.h" #include "input_common/drivers/mouse.h" #include "input_common/drivers/tas_input.h" @@ -801,6 +804,74 @@ void GRenderWindow::TouchEndEvent() { input_subsystem->GetTouchScreen()->ReleaseAllTouch(); } +void GRenderWindow::InitializeCamera() { + if (!Settings::values.enable_ir_sensor) { + return; + } + + bool camera_found = false; + const QList cameras = QCameraInfo::availableCameras(); + for (const QCameraInfo& cameraInfo : cameras) { + if (Settings::values.ir_sensor_device.GetValue() == cameraInfo.deviceName().toStdString() || + Settings::values.ir_sensor_device.GetValue() == "Auto") { + camera = std::make_unique(cameraInfo); + camera_found = true; + break; + } + } + + if (!camera_found) { + return; + } + + camera_capture = std::make_unique(camera.get()); + connect(camera_capture.get(), &QCameraImageCapture::imageCaptured, this, + &GRenderWindow::OnCameraCapture); + camera->unload(); + camera->setCaptureMode(QCamera::CaptureViewfinder); + camera->load(); + + camera_timer = std::make_unique(); + connect(camera_timer.get(), &QTimer::timeout, [this] { RequestCameraCapture(); }); + // This timer should be dependent of camera resolution 5ms for every 100 pixels + camera_timer->start(100); +} + +void GRenderWindow::FinalizeCamera() { + if (camera_timer) { + camera_timer->stop(); + } + if (camera) { + camera->unload(); + } +} + +void GRenderWindow::RequestCameraCapture() { + if (!Settings::values.enable_ir_sensor) { + return; + } + + // Idealy one should only call capture but Qt refuses to take a second capture without + // stopping the camera + camera->stop(); + camera->start(); + + camera_capture->capture(); +} + +void GRenderWindow::OnCameraCapture(int requestId, const QImage& img) { + constexpr std::size_t camera_width = 320; + constexpr std::size_t camera_height = 240; + const auto converted = + img.scaled(camera_width, camera_height, Qt::AspectRatioMode::IgnoreAspectRatio, + Qt::TransformationMode::SmoothTransformation) + .mirrored(false, true); + std::vector camera_data{}; + camera_data.resize(camera_width * camera_height); + std::memcpy(camera_data.data(), converted.bits(), camera_width * camera_height * sizeof(u32)); + input_subsystem->GetCamera()->SetCameraData(camera_width, camera_height, camera_data); +} + bool GRenderWindow::event(QEvent* event) { if (event->type() == QEvent::TouchBegin) { TouchBeginEvent(static_cast(event)); diff --git a/src/yuzu/bootmanager.h b/src/yuzu/bootmanager.h index 81fe52c0e6..346201768a 100644 --- a/src/yuzu/bootmanager.h +++ b/src/yuzu/bootmanager.h @@ -20,6 +20,8 @@ class GRenderWindow; class GMainWindow; +class QCamera; +class QCameraImageCapture; class QKeyEvent; namespace Core { @@ -164,6 +166,9 @@ public: void mouseReleaseEvent(QMouseEvent* event) override; void wheelEvent(QWheelEvent* event) override; + void InitializeCamera(); + void FinalizeCamera(); + bool event(QEvent* event) override; void focusOutEvent(QFocusEvent* event) override; @@ -207,6 +212,9 @@ private: void TouchUpdateEvent(const QTouchEvent* event); void TouchEndEvent(); + void RequestCameraCapture(); + void OnCameraCapture(int requestId, const QImage& img); + void OnMinimalClientAreaChangeRequest(std::pair minimal_size) override; bool InitializeOpenGL(); @@ -232,6 +240,10 @@ private: bool first_frame = false; InputCommon::TasInput::TasState last_tas_state; + std::unique_ptr camera; + std::unique_ptr camera_capture; + std::unique_ptr camera_timer; + Core::System& system; protected: diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 2840bc5eb1..1f76e86b9a 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -368,6 +368,11 @@ void Config::ReadHidbusValues() { } } +void Config::ReadIrCameraValues() { + ReadBasicSetting(Settings::values.enable_ir_sensor); + ReadBasicSetting(Settings::values.ir_sensor_device); +} + void Config::ReadAudioValues() { qt_config->beginGroup(QStringLiteral("Audio")); @@ -393,6 +398,7 @@ void Config::ReadControlValues() { ReadTouchscreenValues(); ReadMotionTouchValues(); ReadHidbusValues(); + ReadIrCameraValues(); #ifdef _WIN32 ReadBasicSetting(Settings::values.enable_raw_input); @@ -1005,6 +1011,11 @@ void Config::SaveHidbusValues() { QString::fromStdString(default_param)); } +void Config::SaveIrCameraValues() { + WriteBasicSetting(Settings::values.enable_ir_sensor); + WriteBasicSetting(Settings::values.ir_sensor_device); +} + void Config::SaveValues() { if (global) { SaveControlValues(); @@ -1047,6 +1058,7 @@ void Config::SaveControlValues() { SaveTouchscreenValues(); SaveMotionTouchValues(); SaveHidbusValues(); + SaveIrCameraValues(); WriteGlobalSetting(Settings::values.use_docked_mode); WriteGlobalSetting(Settings::values.vibration_enabled); diff --git a/src/yuzu/configuration/config.h b/src/yuzu/configuration/config.h index d511b3dbd9..a71eabe8ea 100644 --- a/src/yuzu/configuration/config.h +++ b/src/yuzu/configuration/config.h @@ -68,6 +68,7 @@ private: void ReadTouchscreenValues(); void ReadMotionTouchValues(); void ReadHidbusValues(); + void ReadIrCameraValues(); // Read functions bases off the respective config section names. void ReadAudioValues(); @@ -96,6 +97,7 @@ private: void SaveTouchscreenValues(); void SaveMotionTouchValues(); void SaveHidbusValues(); + void SaveIrCameraValues(); // Save functions based off the respective config section names. void SaveAudioValues(); diff --git a/src/yuzu/configuration/configure_camera.cpp b/src/yuzu/configuration/configure_camera.cpp new file mode 100644 index 0000000000..97febb33c6 --- /dev/null +++ b/src/yuzu/configuration/configure_camera.cpp @@ -0,0 +1,126 @@ +// Text : Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include + +#include "input_common/drivers/camera.h" +#include "input_common/main.h" +#include "ui_configure_camera.h" +#include "yuzu/configuration/config.h" +#include "yuzu/configuration/configure_camera.h" + +ConfigureCamera::ConfigureCamera(QWidget* parent, InputCommon::InputSubsystem* input_subsystem_) + : QDialog(parent), input_subsystem{input_subsystem_}, + ui(std::make_unique()) { + ui->setupUi(this); + + connect(ui->restore_defaults_button, &QPushButton::clicked, this, + &ConfigureCamera::RestoreDefaults); + connect(ui->preview_button, &QPushButton::clicked, this, &ConfigureCamera::PreviewCamera); + + auto blank_image = QImage(320, 240, QImage::Format::Format_RGB32); + blank_image.fill(Qt::black); + DisplayCapturedFrame(0, blank_image); + + LoadConfiguration(); + resize(0, 0); +} + +ConfigureCamera::~ConfigureCamera() = default; + +void ConfigureCamera::PreviewCamera() { + const auto index = ui->ir_sensor_combo_box->currentIndex(); + bool camera_found = false; + const QList cameras = QCameraInfo::availableCameras(); + for (const QCameraInfo& cameraInfo : cameras) { + if (input_devices[index] == cameraInfo.deviceName().toStdString() || + input_devices[index] == "Auto") { + LOG_ERROR(Frontend, "Selected Camera {} {}", cameraInfo.description().toStdString(), + cameraInfo.deviceName().toStdString()); + camera = std::make_unique(cameraInfo); + camera_found = true; + break; + } + } + + // Clear previous frame + auto blank_image = QImage(320, 240, QImage::Format::Format_RGB32); + blank_image.fill(Qt::black); + DisplayCapturedFrame(0, blank_image); + + if (!camera_found) { + return; + } + + camera_capture = std::make_unique(camera.get()); + connect(camera_capture.get(), &QCameraImageCapture::imageCaptured, this, + &ConfigureCamera::DisplayCapturedFrame); + camera->unload(); + camera->setCaptureMode(QCamera::CaptureViewfinder); + camera->load(); + + camera_timer = std::make_unique(); + connect(camera_timer.get(), &QTimer::timeout, [this] { + camera->stop(); + camera->start(); + + camera_capture->capture(); + }); + + camera_timer->start(250); +} + +void ConfigureCamera::DisplayCapturedFrame(int requestId, const QImage& img) { + LOG_ERROR(Frontend, "ImageCaptured {} {}", img.width(), img.height()); + const auto converted = img.scaled(320, 240, Qt::AspectRatioMode::IgnoreAspectRatio, + Qt::TransformationMode::SmoothTransformation); + ui->preview_box->setPixmap(QPixmap::fromImage(converted)); +} + +void ConfigureCamera::changeEvent(QEvent* event) { + if (event->type() == QEvent::LanguageChange) { + RetranslateUI(); + } + + QDialog::changeEvent(event); +} + +void ConfigureCamera::RetranslateUI() { + ui->retranslateUi(this); +} + +void ConfigureCamera::ApplyConfiguration() { + const auto index = ui->ir_sensor_combo_box->currentIndex(); + Settings::values.ir_sensor_device.SetValue(input_devices[index]); +} + +void ConfigureCamera::LoadConfiguration() { + input_devices.clear(); + ui->ir_sensor_combo_box->clear(); + input_devices.push_back("Auto"); + ui->ir_sensor_combo_box->addItem(tr("Auto")); + const auto cameras = QCameraInfo::availableCameras(); + for (const QCameraInfo& cameraInfo : cameras) { + input_devices.push_back(cameraInfo.deviceName().toStdString()); + ui->ir_sensor_combo_box->addItem(cameraInfo.description()); + } + + const auto current_device = Settings::values.ir_sensor_device.GetValue(); + + const auto devices_it = std::find_if( + input_devices.begin(), input_devices.end(), + [current_device](const std::string& device) { return device == current_device; }); + const int device_index = + devices_it != input_devices.end() + ? static_cast(std::distance(input_devices.begin(), devices_it)) + : 0; + ui->ir_sensor_combo_box->setCurrentIndex(device_index); +} + +void ConfigureCamera::RestoreDefaults() { + ui->ir_sensor_combo_box->setCurrentIndex(0); +} diff --git a/src/yuzu/configuration/configure_camera.h b/src/yuzu/configuration/configure_camera.h new file mode 100644 index 0000000000..af7551c030 --- /dev/null +++ b/src/yuzu/configuration/configure_camera.h @@ -0,0 +1,52 @@ +// Text : Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +class QTimer; +class QCamera; +class QCameraImageCapture; + +namespace InputCommon { +class InputSubsystem; +} // namespace InputCommon + +namespace Ui { +class ConfigureCamera; +} + +class ConfigureCamera : public QDialog { + Q_OBJECT + +public: + explicit ConfigureCamera(QWidget* parent, InputCommon::InputSubsystem* input_subsystem_); + ~ConfigureCamera() override; + + void ApplyConfiguration(); + +private: + void changeEvent(QEvent* event) override; + void RetranslateUI(); + + /// Load configuration settings. + void LoadConfiguration(); + + /// Restore all buttons to their default values. + void RestoreDefaults(); + + void DisplayCapturedFrame(int requestId, const QImage& img); + + /// Loads and signals the current selected camera to display a frame + void PreviewCamera(); + + InputCommon::InputSubsystem* input_subsystem; + + std::unique_ptr camera; + std::unique_ptr camera_capture; + std::unique_ptr camera_timer; + std::vector input_devices; + std::unique_ptr ui; +}; diff --git a/src/yuzu/configuration/configure_camera.ui b/src/yuzu/configuration/configure_camera.ui new file mode 100644 index 0000000000..976a9b1ec1 --- /dev/null +++ b/src/yuzu/configuration/configure_camera.ui @@ -0,0 +1,170 @@ + + + ConfigureCamera + + + + 0 + 0 + 298 + 339 + + + + Configure Infrared Camera + + + + + + + 280 + 0 + + + + Select where the image of the emulated camera comes from. It may be a virtual camera or a real camera. + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Camera Image Source: + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Input device: + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Preview + + + + + + + 320 + 240 + + + + Resolution: 320*240 + + + + + + + Click to preview + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Restore Defaults + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + ConfigureCamera + accept() + + + buttonBox + rejected() + ConfigureCamera + reject() + + + diff --git a/src/yuzu/configuration/configure_input.cpp b/src/yuzu/configuration/configure_input.cpp index 73d7ba24ba..f1b061b13a 100644 --- a/src/yuzu/configuration/configure_input.cpp +++ b/src/yuzu/configuration/configure_input.cpp @@ -15,6 +15,7 @@ #include "ui_configure_input.h" #include "ui_configure_input_advanced.h" #include "ui_configure_input_player.h" +#include "yuzu/configuration/configure_camera.h" #include "yuzu/configuration/configure_debug_controller.h" #include "yuzu/configuration/configure_input.h" #include "yuzu/configuration/configure_input_advanced.h" @@ -163,6 +164,10 @@ void ConfigureInput::Initialize(InputCommon::InputSubsystem* input_subsystem, [this, input_subsystem, &hid_core] { CallConfigureDialog(*this, input_subsystem, hid_core); }); + connect(advanced, &ConfigureInputAdvanced::CallCameraDialog, + [this, input_subsystem, &hid_core] { + CallConfigureDialog(*this, input_subsystem); + }); connect(ui->vibrationButton, &QPushButton::clicked, [this, &hid_core] { CallConfigureDialog(*this, hid_core); }); diff --git a/src/yuzu/configuration/configure_input_advanced.cpp b/src/yuzu/configuration/configure_input_advanced.cpp index f14bdc831f..10f841b98e 100644 --- a/src/yuzu/configuration/configure_input_advanced.cpp +++ b/src/yuzu/configuration/configure_input_advanced.cpp @@ -89,6 +89,7 @@ ConfigureInputAdvanced::ConfigureInputAdvanced(QWidget* parent) [this] { CallMotionTouchConfigDialog(); }); connect(ui->ring_controller_configure, &QPushButton::clicked, this, [this] { CallRingControllerDialog(); }); + connect(ui->camera_configure, &QPushButton::clicked, this, [this] { CallCameraDialog(); }); #ifndef _WIN32 ui->enable_raw_input->setVisible(false); @@ -136,6 +137,7 @@ void ConfigureInputAdvanced::ApplyConfiguration() { Settings::values.enable_udp_controller = ui->enable_udp_controller->isChecked(); Settings::values.controller_navigation = ui->controller_navigation->isChecked(); Settings::values.enable_ring_controller = ui->enable_ring_controller->isChecked(); + Settings::values.enable_ir_sensor = ui->enable_ir_sensor->isChecked(); } void ConfigureInputAdvanced::LoadConfiguration() { @@ -169,6 +171,7 @@ void ConfigureInputAdvanced::LoadConfiguration() { ui->enable_udp_controller->setChecked(Settings::values.enable_udp_controller.GetValue()); ui->controller_navigation->setChecked(Settings::values.controller_navigation.GetValue()); ui->enable_ring_controller->setChecked(Settings::values.enable_ring_controller.GetValue()); + ui->enable_ir_sensor->setChecked(Settings::values.enable_ir_sensor.GetValue()); UpdateUIEnabled(); } diff --git a/src/yuzu/configuration/configure_input_advanced.h b/src/yuzu/configuration/configure_input_advanced.h index 644e56dd8c..fc12302844 100644 --- a/src/yuzu/configuration/configure_input_advanced.h +++ b/src/yuzu/configuration/configure_input_advanced.h @@ -29,6 +29,7 @@ signals: void CallTouchscreenConfigDialog(); void CallMotionTouchConfigDialog(); void CallRingControllerDialog(); + void CallCameraDialog(); private: void changeEvent(QEvent* event) override; diff --git a/src/yuzu/configuration/configure_input_advanced.ui b/src/yuzu/configuration/configure_input_advanced.ui index 14403cb102..fac8cf827d 100644 --- a/src/yuzu/configuration/configure_input_advanced.ui +++ b/src/yuzu/configuration/configure_input_advanced.ui @@ -2617,6 +2617,20 @@ + + + + Infrared Camera + + + + + + + Configure + + + diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index a120f26623..08ccc1555e 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -1542,6 +1542,8 @@ void GMainWindow::BootGame(const QString& filename, u64 program_id, std::size_t mouse_hide_timer.start(); } + render_window->InitializeCamera(); + std::string title_name; std::string title_version; const auto res = system->GetGameName(title_name); @@ -1623,6 +1625,7 @@ void GMainWindow::ShutdownGame() { tas_label->clear(); input_subsystem->GetTas()->Stop(); OnTasStateChanged(); + render_window->FinalizeCamera(); // Enable all controllers system->HIDCore().SetSupportedStyleTag({Core::HID::NpadStyleSet::All}); @@ -2862,6 +2865,12 @@ void GMainWindow::OnConfigure() { mouse_hide_timer.start(); } + // Restart camera config + if (emulation_running) { + render_window->FinalizeCamera(); + render_window->InitializeCamera(); + } + if (!UISettings::values.has_broken_vulkan) { renderer_status_button->setEnabled(!emulation_running); } From 57311b2c8b1b9ee1d2c5775866e9591605815246 Mon Sep 17 00:00:00 2001 From: german77 Date: Sat, 18 Jun 2022 23:36:29 -0500 Subject: [PATCH 32/50] core: hid: Add cammera support --- src/core/CMakeLists.txt | 1 + src/core/hid/emulated_controller.cpp | 59 ++++++ src/core/hid/emulated_controller.h | 40 +++- src/core/hid/input_converter.cpp | 14 ++ src/core/hid/input_converter.h | 8 + src/core/hid/irs_types.h | 304 +++++++++++++++++++++++++++ 6 files changed, 423 insertions(+), 3 deletions(-) create mode 100644 src/core/hid/irs_types.h diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 11d554bad2..1f8439f91b 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -158,6 +158,7 @@ add_library(core STATIC hid/input_converter.h hid/input_interpreter.cpp hid/input_interpreter.h + hid/irs_types.h hid/motion_input.cpp hid/motion_input.h hle/api_version.h diff --git a/src/core/hid/emulated_controller.cpp b/src/core/hid/emulated_controller.cpp index bd2384515b..8c3895937d 100644 --- a/src/core/hid/emulated_controller.cpp +++ b/src/core/hid/emulated_controller.cpp @@ -126,10 +126,14 @@ void EmulatedController::LoadDevices() { battery_params[LeftIndex].Set("battery", true); battery_params[RightIndex].Set("battery", true); + camera_params = Common::ParamPackage{"engine:camera,camera:1"}; + output_params[LeftIndex] = left_joycon; output_params[RightIndex] = right_joycon; + output_params[2] = camera_params; output_params[LeftIndex].Set("output", true); output_params[RightIndex].Set("output", true); + output_params[2].Set("output", true); LoadTASParams(); @@ -146,6 +150,7 @@ void EmulatedController::LoadDevices() { Common::Input::CreateDevice); std::transform(battery_params.begin(), battery_params.end(), battery_devices.begin(), Common::Input::CreateDevice); + camera_devices = Common::Input::CreateDevice(camera_params); std::transform(output_params.begin(), output_params.end(), output_devices.begin(), Common::Input::CreateDevice); @@ -267,6 +272,14 @@ void EmulatedController::ReloadInput() { motion_devices[index]->ForceUpdate(); } + if (camera_devices) { + camera_devices->SetCallback({ + .on_change = + [this](const Common::Input::CallbackStatus& callback) { SetCamera(callback); }, + }); + camera_devices->ForceUpdate(); + } + // Use a common UUID for TAS static constexpr Common::UUID TAS_UUID = Common::UUID{ {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0xA5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}; @@ -851,6 +864,25 @@ void EmulatedController::SetBattery(const Common::Input::CallbackStatus& callbac TriggerOnChange(ControllerTriggerType::Battery, true); } +void EmulatedController::SetCamera(const Common::Input::CallbackStatus& callback) { + std::unique_lock lock{mutex}; + controller.camera_values = TransformToCamera(callback); + + if (is_configuring) { + lock.unlock(); + TriggerOnChange(ControllerTriggerType::IrSensor, false); + return; + } + + controller.camera_state.sample++; + controller.camera_state.format = + static_cast(controller.camera_values.format); + controller.camera_state.data = controller.camera_values.data; + + lock.unlock(); + TriggerOnChange(ControllerTriggerType::IrSensor, true); +} + bool EmulatedController::SetVibration(std::size_t device_index, VibrationValue vibration) { if (device_index >= output_devices.size()) { return false; @@ -928,6 +960,23 @@ bool EmulatedController::SetPollingMode(Common::Input::PollingMode polling_mode) return output_device->SetPollingMode(polling_mode) == Common::Input::PollingError::None; } +bool EmulatedController::SetCameraFormat( + Core::IrSensor::ImageTransferProcessorFormat camera_format) { + LOG_INFO(Service_HID, "Set camera format {}", camera_format); + + auto& right_output_device = output_devices[static_cast(DeviceIndex::Right)]; + auto& camera_output_device = output_devices[2]; + + if (right_output_device->SetCameraFormat(static_cast( + camera_format)) == Common::Input::CameraError::None) { + return true; + } + + // Fallback to Qt camera if native device doesn't have support + return camera_output_device->SetCameraFormat(static_cast( + camera_format)) == Common::Input::CameraError::None; +} + void EmulatedController::SetLedPattern() { for (auto& device : output_devices) { if (!device) { @@ -1163,6 +1212,11 @@ BatteryValues EmulatedController::GetBatteryValues() const { return controller.battery_values; } +CameraValues EmulatedController::GetCameraValues() const { + std::scoped_lock lock{mutex}; + return controller.camera_values; +} + HomeButtonState EmulatedController::GetHomeButtons() const { std::scoped_lock lock{mutex}; if (is_configuring) { @@ -1251,6 +1305,11 @@ BatteryLevelState EmulatedController::GetBattery() const { return controller.battery_state; } +const CameraState& EmulatedController::GetCamera() const { + std::scoped_lock lock{mutex}; + return controller.camera_state; +} + void EmulatedController::TriggerOnChange(ControllerTriggerType type, bool is_npad_service_update) { std::scoped_lock lock{callback_mutex}; for (const auto& poller_pair : callback_list) { diff --git a/src/core/hid/emulated_controller.h b/src/core/hid/emulated_controller.h index 3f02ed3c07..823c1700c6 100644 --- a/src/core/hid/emulated_controller.h +++ b/src/core/hid/emulated_controller.h @@ -15,10 +15,12 @@ #include "common/settings.h" #include "common/vector_math.h" #include "core/hid/hid_types.h" +#include "core/hid/irs_types.h" #include "core/hid/motion_input.h" namespace Core::HID { const std::size_t max_emulated_controllers = 2; +const std::size_t output_devices = 3; struct ControllerMotionInfo { Common::Input::MotionStatus raw_status{}; MotionInput emulated{}; @@ -34,15 +36,16 @@ using TriggerDevices = std::array, Settings::NativeTrigger::NumTriggers>; using BatteryDevices = std::array, max_emulated_controllers>; -using OutputDevices = - std::array, max_emulated_controllers>; +using CameraDevices = std::unique_ptr; +using OutputDevices = std::array, output_devices>; using ButtonParams = std::array; using StickParams = std::array; using ControllerMotionParams = std::array; using TriggerParams = std::array; using BatteryParams = std::array; -using OutputParams = std::array; +using CameraParams = Common::ParamPackage; +using OutputParams = std::array; using ButtonValues = std::array; using SticksValues = std::array; @@ -51,6 +54,7 @@ using TriggerValues = using ControllerMotionValues = std::array; using ColorValues = std::array; using BatteryValues = std::array; +using CameraValues = Common::Input::CameraStatus; using VibrationValues = std::array; struct AnalogSticks { @@ -70,6 +74,12 @@ struct BatteryLevelState { NpadPowerInfo right{}; }; +struct CameraState { + Core::IrSensor::ImageTransferProcessorFormat format{}; + std::vector data{}; + std::size_t sample{}; +}; + struct ControllerMotion { Common::Vec3f accel{}; Common::Vec3f gyro{}; @@ -96,6 +106,7 @@ struct ControllerStatus { ColorValues color_values{}; BatteryValues battery_values{}; VibrationValues vibration_values{}; + CameraValues camera_values{}; // Data for HID serices HomeButtonState home_button_state{}; @@ -107,6 +118,7 @@ struct ControllerStatus { NpadGcTriggerState gc_trigger_state{}; ControllerColors colors_state{}; BatteryLevelState battery_state{}; + CameraState camera_state{}; }; enum class ControllerTriggerType { @@ -117,6 +129,7 @@ enum class ControllerTriggerType { Color, Battery, Vibration, + IrSensor, Connected, Disconnected, Type, @@ -269,6 +282,9 @@ public: /// Returns the latest battery status from the controller with parameters BatteryValues GetBatteryValues() const; + /// Returns the latest camera status from the controller with parameters + CameraValues GetCameraValues() const; + /// Returns the latest status of button input for the hid::HomeButton service HomeButtonState GetHomeButtons() const; @@ -296,6 +312,9 @@ public: /// Returns the latest battery status from the controller BatteryLevelState GetBattery() const; + /// Returns the latest camera status from the controller + const CameraState& GetCamera() const; + /** * Sends a specific vibration to the output device * @return true if vibration had no errors @@ -315,6 +334,13 @@ public: */ bool SetPollingMode(Common::Input::PollingMode polling_mode); + /** + * Sets the desired camera format to be polled from a controller + * @param camera_format size of each frame + * @return true if SetCameraFormat was successfull + */ + bool SetCameraFormat(Core::IrSensor::ImageTransferProcessorFormat camera_format); + /// Returns the led pattern corresponding to this emulated controller LedPattern GetLedPattern() const; @@ -392,6 +418,12 @@ private: */ void SetBattery(const Common::Input::CallbackStatus& callback, std::size_t index); + /** + * Updates the camera status of the controller + * @param callback A CallbackStatus containing the camera status + */ + void SetCamera(const Common::Input::CallbackStatus& callback); + /** * Triggers a callback that something has changed on the controller status * @param type Input type of the event to trigger @@ -417,6 +449,7 @@ private: ControllerMotionParams motion_params; TriggerParams trigger_params; BatteryParams battery_params; + CameraParams camera_params; OutputParams output_params; ButtonDevices button_devices; @@ -424,6 +457,7 @@ private: ControllerMotionDevices motion_devices; TriggerDevices trigger_devices; BatteryDevices battery_devices; + CameraDevices camera_devices; OutputDevices output_devices; // TAS related variables diff --git a/src/core/hid/input_converter.cpp b/src/core/hid/input_converter.cpp index 18d9f042da..68d143a013 100644 --- a/src/core/hid/input_converter.cpp +++ b/src/core/hid/input_converter.cpp @@ -270,6 +270,20 @@ Common::Input::AnalogStatus TransformToAnalog(const Common::Input::CallbackStatu return status; } +Common::Input::CameraStatus TransformToCamera(const Common::Input::CallbackStatus& callback) { + Common::Input::CameraStatus camera{}; + switch (callback.type) { + case Common::Input::InputType::IrSensor: + camera = callback.camera_status; + break; + default: + LOG_ERROR(Input, "Conversion from type {} to camera not implemented", callback.type); + break; + } + + return camera; +} + void SanitizeAnalog(Common::Input::AnalogStatus& analog, bool clamp_value) { const auto& properties = analog.properties; float& raw_value = analog.raw_value; diff --git a/src/core/hid/input_converter.h b/src/core/hid/input_converter.h index 2be36889fd..143c50cc06 100644 --- a/src/core/hid/input_converter.h +++ b/src/core/hid/input_converter.h @@ -76,6 +76,14 @@ Common::Input::TriggerStatus TransformToTrigger(const Common::Input::CallbackSta */ Common::Input::AnalogStatus TransformToAnalog(const Common::Input::CallbackStatus& callback); +/** + * Converts raw input data into a valid camera status. + * + * @param callback Supported callbacks: Camera. + * @return A valid CameraObject object. + */ +Common::Input::CameraStatus TransformToCamera(const Common::Input::CallbackStatus& callback); + /** * Converts raw analog data into a valid analog value * @param analog An analog object containing raw data and properties diff --git a/src/core/hid/irs_types.h b/src/core/hid/irs_types.h new file mode 100644 index 0000000000..c73d008a07 --- /dev/null +++ b/src/core/hid/irs_types.h @@ -0,0 +1,304 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/common_funcs.h" +#include "common/common_types.h" +#include "core/hid/hid_types.h" + +namespace Core::IrSensor { + +// This is nn::irsensor::CameraAmbientNoiseLevel +enum class CameraAmbientNoiseLevel : u32 { + Low, + Medium, + High, + Unkown3, // This level can't be reached +}; + +// This is nn::irsensor::CameraLightTarget +enum class CameraLightTarget : u32 { + AllLeds, + BrightLeds, + DimLeds, + None, +}; + +// This is nn::irsensor::PackedCameraLightTarget +enum class PackedCameraLightTarget : u8 { + AllLeds, + BrightLeds, + DimLeds, + None, +}; + +// This is nn::irsensor::AdaptiveClusteringMode +enum class AdaptiveClusteringMode : u32 { + StaticFov, + DynamicFov, +}; + +// This is nn::irsensor::AdaptiveClusteringTargetDistance +enum class AdaptiveClusteringTargetDistance : u32 { + Near, + Middle, + Far, +}; + +// This is nn::irsensor::ImageTransferProcessorFormat +enum class ImageTransferProcessorFormat : u32 { + Size320x240, + Size160x120, + Size80x60, + Size40x30, + Size20x15, +}; + +// This is nn::irsensor::PackedImageTransferProcessorFormat +enum class PackedImageTransferProcessorFormat : u8 { + Size320x240, + Size160x120, + Size80x60, + Size40x30, + Size20x15, +}; + +// This is nn::irsensor::IrCameraStatus +enum class IrCameraStatus : u32 { + Available, + Unsupported, + Unconnected, +}; + +// This is nn::irsensor::IrCameraInternalStatus +enum class IrCameraInternalStatus : u32 { + Stopped, + FirmwareUpdateNeeded, + Unkown2, + Unkown3, + Unkown4, + FirmwareVersionRequested, + FirmwareVersionIsInvalid, + Ready, + Setting, +}; + +// This is nn::irsensor::detail::StatusManager::IrSensorMode +enum class IrSensorMode : u64 { + None, + MomentProcessor, + ClusteringProcessor, + ImageTransferProcessor, + PointingProcessorMarker, + TeraPluginProcessor, + IrLedProcessor, +}; + +// This is nn::irsensor::ImageProcessorStatus +enum ImageProcessorStatus : u32 { + Stopped, + Running, +}; + +// This is nn::irsensor::HandAnalysisMode +enum class HandAnalysisMode : u32 { + None, + Silhouette, + Image, + SilhoueteAndImage, + SilhuetteOnly, +}; + +// This is nn::irsensor::IrSensorFunctionLevel +enum class IrSensorFunctionLevel : u8 { + unknown0, + unknown1, + unknown2, + unknown3, + unknown4, +}; + +// This is nn::irsensor::MomentProcessorPreprocess +enum class MomentProcessorPreprocess : u32 { + Unkown0, + Unkown1, +}; + +// This is nn::irsensor::PackedMomentProcessorPreprocess +enum class PackedMomentProcessorPreprocess : u8 { + Unkown0, + Unkown1, +}; + +// This is nn::irsensor::PointingStatus +enum class PointingStatus : u32 { + Unkown0, + Unkown1, +}; + +struct IrsRect { + s16 x; + s16 y; + s16 width; + s16 height; +}; + +struct IrsCentroid { + f32 x; + f32 y; +}; + +struct CameraConfig { + u64 exposure_time; + CameraLightTarget light_target; + u32 gain; + bool is_negative_used; + INSERT_PADDING_BYTES(7); +}; +static_assert(sizeof(CameraConfig) == 0x18, "CameraConfig is an invalid size"); + +struct PackedCameraConfig { + u64 exposure_time; + PackedCameraLightTarget light_target; + u8 gain; + bool is_negative_used; + INSERT_PADDING_BYTES(5); +}; +static_assert(sizeof(PackedCameraConfig) == 0x10, "PackedCameraConfig is an invalid size"); + +// This is nn::irsensor::IrCameraHandle +struct IrCameraHandle { + u8 npad_id{}; + Core::HID::NpadStyleIndex npad_type{Core::HID::NpadStyleIndex::None}; + INSERT_PADDING_BYTES(2); +}; +static_assert(sizeof(IrCameraHandle) == 4, "IrCameraHandle is an invalid size"); + +// This is nn::irsensor::PackedMcuVersion +struct PackedMcuVersion { + u16 major; + u16 minor; +}; +static_assert(sizeof(PackedMcuVersion) == 4, "PackedMcuVersion is an invalid size"); + +// This is nn::irsensor::PackedMomentProcessorConfig +struct PackedMomentProcessorConfig { + PackedCameraConfig camera_config; + IrsRect window_of_interest; + PackedMcuVersion required_mcu_version; + PackedMomentProcessorPreprocess preprocess; + u8 preprocess_intensity_threshold; + INSERT_PADDING_BYTES(2); +}; +static_assert(sizeof(PackedMomentProcessorConfig) == 0x20, + "PackedMomentProcessorConfig is an invalid size"); + +// This is nn::irsensor::PackedClusteringProcessorConfig +struct PackedClusteringProcessorConfig { + PackedCameraConfig camera_config; + IrsRect window_of_interest; + PackedMcuVersion required_mcu_version; + u32 pixel_count_min; + u32 pixel_count_max; + u32 object_intensity_min; + bool is_external_light_filter_enabled; + INSERT_PADDING_BYTES(2); +}; +static_assert(sizeof(PackedClusteringProcessorConfig) == 0x30, + "PackedClusteringProcessorConfig is an invalid size"); + +// This is nn::irsensor::PackedImageTransferProcessorConfig +struct PackedImageTransferProcessorConfig { + PackedCameraConfig camera_config; + PackedMcuVersion required_mcu_version; + PackedImageTransferProcessorFormat format; + INSERT_PADDING_BYTES(3); +}; +static_assert(sizeof(PackedImageTransferProcessorConfig) == 0x18, + "PackedImageTransferProcessorConfig is an invalid size"); + +// This is nn::irsensor::PackedTeraPluginProcessorConfig +struct PackedTeraPluginProcessorConfig { + PackedMcuVersion required_mcu_version; + u8 mode; + u8 unknown_1; + u8 unknown_2; + u8 unknown_3; +}; +static_assert(sizeof(PackedTeraPluginProcessorConfig) == 0x8, + "PackedTeraPluginProcessorConfig is an invalid size"); + +// This is nn::irsensor::PackedPointingProcessorConfig +struct PackedPointingProcessorConfig { + IrsRect window_of_interest; + PackedMcuVersion required_mcu_version; +}; +static_assert(sizeof(PackedPointingProcessorConfig) == 0xC, + "PackedPointingProcessorConfig is an invalid size"); + +// This is nn::irsensor::PackedFunctionLevel +struct PackedFunctionLevel { + IrSensorFunctionLevel function_level; + INSERT_PADDING_BYTES(3); +}; +static_assert(sizeof(PackedFunctionLevel) == 0x4, "PackedFunctionLevel is an invalid size"); + +// This is nn::irsensor::PackedImageTransferProcessorExConfig +struct PackedImageTransferProcessorExConfig { + PackedCameraConfig camera_config; + PackedMcuVersion required_mcu_version; + PackedImageTransferProcessorFormat origin_format; + PackedImageTransferProcessorFormat trimming_format; + u16 trimming_start_x; + u16 trimming_start_y; + bool is_external_light_filter_enabled; + INSERT_PADDING_BYTES(5); +}; +static_assert(sizeof(PackedImageTransferProcessorExConfig) == 0x20, + "PackedImageTransferProcessorExConfig is an invalid size"); + +// This is nn::irsensor::PackedIrLedProcessorConfig +struct PackedIrLedProcessorConfig { + PackedMcuVersion required_mcu_version; + u8 light_target; + INSERT_PADDING_BYTES(3); +}; +static_assert(sizeof(PackedIrLedProcessorConfig) == 0x8, + "PackedIrLedProcessorConfig is an invalid size"); + +// This is nn::irsensor::HandAnalysisConfig +struct HandAnalysisConfig { + HandAnalysisMode mode; +}; +static_assert(sizeof(HandAnalysisConfig) == 0x4, "HandAnalysisConfig is an invalid size"); + +// This is nn::irsensor::detail::ProcessorState +struct ProcessorState { + u64 start{}; + u32 count{}; + INSERT_PADDING_BYTES(4); + std::array processor_raw_data{}; +}; +static_assert(sizeof(ProcessorState) == 0xE20, "ProcessorState is an invalid size"); + +// This is nn::irsensor::detail::DeviceFormat +struct DeviceFormat { + Core::IrSensor::IrCameraStatus camera_status{Core::IrSensor::IrCameraStatus::Unconnected}; + Core::IrSensor::IrCameraInternalStatus camera_internal_status{ + Core::IrSensor::IrCameraInternalStatus::Ready}; + Core::IrSensor::IrSensorMode mode{Core::IrSensor::IrSensorMode::None}; + ProcessorState state{}; +}; +static_assert(sizeof(DeviceFormat) == 0xE30, "DeviceFormat is an invalid size"); + +// This is nn::irsensor::ImageTransferProcessorState +struct ImageTransferProcessorState { + u64 sampling_number; + Core::IrSensor::CameraAmbientNoiseLevel ambient_noise_level; + INSERT_PADDING_BYTES(4); +}; +static_assert(sizeof(ImageTransferProcessorState) == 0x10, + "ImageTransferProcessorState is an invalid size"); + +} // namespace Core::IrSensor From 453970059528d564d9ef88f7f9096c05946fbefe Mon Sep 17 00:00:00 2001 From: german77 Date: Sat, 18 Jun 2022 23:45:06 -0500 Subject: [PATCH 33/50] service: irs: Split processors and implement ImageTransferProcessor --- src/core/CMakeLists.txt | 14 + src/core/hle/service/hid/errors.h | 3 + src/core/hle/service/hid/irs.cpp | 288 +++++++++++++---- src/core/hle/service/hid/irs.h | 291 ++++-------------- .../hid/irsensor/clustering_processor.cpp | 36 +++ .../hid/irsensor/clustering_processor.h | 74 +++++ .../hid/irsensor/image_transfer_processor.cpp | 154 +++++++++ .../hid/irsensor/image_transfer_processor.h | 73 +++++ .../service/hid/irsensor/ir_led_processor.cpp | 29 ++ .../service/hid/irsensor/ir_led_processor.h | 47 +++ .../service/hid/irsensor/moment_processor.cpp | 36 +++ .../service/hid/irsensor/moment_processor.h | 61 ++++ .../hid/irsensor/pointing_processor.cpp | 28 ++ .../service/hid/irsensor/pointing_processor.h | 62 ++++ .../service/hid/irsensor/processor_base.cpp | 67 ++++ .../hle/service/hid/irsensor/processor_base.h | 33 ++ .../hid/irsensor/tera_plugin_processor.cpp | 31 ++ .../hid/irsensor/tera_plugin_processor.h | 53 ++++ 18 files changed, 1090 insertions(+), 290 deletions(-) create mode 100644 src/core/hle/service/hid/irsensor/clustering_processor.cpp create mode 100644 src/core/hle/service/hid/irsensor/clustering_processor.h create mode 100644 src/core/hle/service/hid/irsensor/image_transfer_processor.cpp create mode 100644 src/core/hle/service/hid/irsensor/image_transfer_processor.h create mode 100644 src/core/hle/service/hid/irsensor/ir_led_processor.cpp create mode 100644 src/core/hle/service/hid/irsensor/ir_led_processor.h create mode 100644 src/core/hle/service/hid/irsensor/moment_processor.cpp create mode 100644 src/core/hle/service/hid/irsensor/moment_processor.h create mode 100644 src/core/hle/service/hid/irsensor/pointing_processor.cpp create mode 100644 src/core/hle/service/hid/irsensor/pointing_processor.h create mode 100644 src/core/hle/service/hid/irsensor/processor_base.cpp create mode 100644 src/core/hle/service/hid/irsensor/processor_base.h create mode 100644 src/core/hle/service/hid/irsensor/tera_plugin_processor.cpp create mode 100644 src/core/hle/service/hid/irsensor/tera_plugin_processor.h diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 1f8439f91b..32cc2f3920 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -478,6 +478,20 @@ add_library(core STATIC hle/service/hid/hidbus/starlink.h hle/service/hid/hidbus/stubbed.cpp hle/service/hid/hidbus/stubbed.h + hle/service/hid/irsensor/clustering_processor.cpp + hle/service/hid/irsensor/clustering_processor.h + hle/service/hid/irsensor/image_transfer_processor.cpp + hle/service/hid/irsensor/image_transfer_processor.h + hle/service/hid/irsensor/ir_led_processor.cpp + hle/service/hid/irsensor/ir_led_processor.h + hle/service/hid/irsensor/moment_processor.cpp + hle/service/hid/irsensor/moment_processor.h + hle/service/hid/irsensor/pointing_processor.cpp + hle/service/hid/irsensor/pointing_processor.h + hle/service/hid/irsensor/processor_base.cpp + hle/service/hid/irsensor/processor_base.h + hle/service/hid/irsensor/tera_plugin_processor.cpp + hle/service/hid/irsensor/tera_plugin_processor.h hle/service/jit/jit_context.cpp hle/service/jit/jit_context.h hle/service/jit/jit.cpp diff --git a/src/core/hle/service/hid/errors.h b/src/core/hle/service/hid/errors.h index 46282f42ec..1bd62461cc 100644 --- a/src/core/hle/service/hid/errors.h +++ b/src/core/hle/service/hid/errors.h @@ -18,4 +18,7 @@ constexpr Result NpadIsSameType{ErrorModule::HID, 602}; constexpr Result InvalidNpadId{ErrorModule::HID, 709}; constexpr Result NpadNotConnected{ErrorModule::HID, 710}; +constexpr ResultCode InvalidProcessorState{ErrorModule::Irsensor, 78}; +constexpr ResultCode InvalidIrCameraHandle{ErrorModule::Irsensor, 204}; + } // namespace Service::HID diff --git a/src/core/hle/service/hid/irs.cpp b/src/core/hle/service/hid/irs.cpp index d2a91d9136..53a31df79c 100644 --- a/src/core/hle/service/hid/irs.cpp +++ b/src/core/hle/service/hid/irs.cpp @@ -1,14 +1,26 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include + #include "core/core.h" #include "core/core_timing.h" +#include "core/hid/emulated_controller.h" +#include "core/hid/hid_core.h" #include "core/hle/ipc_helpers.h" #include "core/hle/kernel/k_shared_memory.h" #include "core/hle/kernel/k_transfer_memory.h" #include "core/hle/kernel/kernel.h" #include "core/hle/service/hid/errors.h" #include "core/hle/service/hid/irs.h" +#include "core/hle/service/hid/irsensor/clustering_processor.h" +#include "core/hle/service/hid/irsensor/image_transfer_processor.h" +#include "core/hle/service/hid/irsensor/ir_led_processor.h" +#include "core/hle/service/hid/irsensor/moment_processor.h" +#include "core/hle/service/hid/irsensor/pointing_processor.h" +#include "core/hle/service/hid/irsensor/tera_plugin_processor.h" +#include "core/memory.h" namespace Service::HID { @@ -36,8 +48,13 @@ IRS::IRS(Core::System& system_) : ServiceFramework{system_, "irs"} { }; // clang-format on + u8* raw_shared_memory = system.Kernel().GetIrsSharedMem().GetPointer(); RegisterHandlers(functions); + shared_memory = std::construct_at(reinterpret_cast(raw_shared_memory)); + + npad_device = system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); } +IRS::~IRS() = default; void IRS::ActivateIrsensor(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; @@ -75,7 +92,7 @@ void IRS::GetIrsensorSharedMemoryHandle(Kernel::HLERequestContext& ctx) { void IRS::StopImageProcessor(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; struct Parameters { - IrCameraHandle camera_handle; + Core::IrSensor::IrCameraHandle camera_handle; INSERT_PADDING_WORDS_NOINIT(1); u64 applet_resource_user_id; }; @@ -88,17 +105,23 @@ void IRS::StopImageProcessor(Kernel::HLERequestContext& ctx) { parameters.camera_handle.npad_type, parameters.camera_handle.npad_id, parameters.applet_resource_user_id); + auto result = IsIrCameraHandleValid(parameters.camera_handle); + if (result.IsSuccess()) { + // TODO: Stop Image processor + result = ResultSuccess; + } + IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + rb.Push(result); } void IRS::RunMomentProcessor(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; struct Parameters { - IrCameraHandle camera_handle; + Core::IrSensor::IrCameraHandle camera_handle; INSERT_PADDING_WORDS_NOINIT(1); u64 applet_resource_user_id; - PackedMomentProcessorConfig processor_config; + Core::IrSensor::PackedMomentProcessorConfig processor_config; }; static_assert(sizeof(Parameters) == 0x30, "Parameters has incorrect size."); @@ -109,17 +132,26 @@ void IRS::RunMomentProcessor(Kernel::HLERequestContext& ctx) { parameters.camera_handle.npad_type, parameters.camera_handle.npad_id, parameters.applet_resource_user_id); + const auto result = IsIrCameraHandleValid(parameters.camera_handle); + + if (result.IsSuccess()) { + auto& device = GetIrCameraSharedMemoryDeviceEntry(parameters.camera_handle); + MakeProcessor(parameters.camera_handle, device); + auto& image_transfer_processor = GetProcessor(parameters.camera_handle); + image_transfer_processor.SetConfig(parameters.processor_config); + } + IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + rb.Push(result); } void IRS::RunClusteringProcessor(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; struct Parameters { - IrCameraHandle camera_handle; + Core::IrSensor::IrCameraHandle camera_handle; INSERT_PADDING_WORDS_NOINIT(1); u64 applet_resource_user_id; - PackedClusteringProcessorConfig processor_config; + Core::IrSensor::PackedClusteringProcessorConfig processor_config; }; static_assert(sizeof(Parameters) == 0x40, "Parameters has incorrect size."); @@ -130,17 +162,27 @@ void IRS::RunClusteringProcessor(Kernel::HLERequestContext& ctx) { parameters.camera_handle.npad_type, parameters.camera_handle.npad_id, parameters.applet_resource_user_id); + auto result = IsIrCameraHandleValid(parameters.camera_handle); + + if (result.IsSuccess()) { + auto& device = GetIrCameraSharedMemoryDeviceEntry(parameters.camera_handle); + MakeProcessor(parameters.camera_handle, device); + auto& image_transfer_processor = + GetProcessor(parameters.camera_handle); + image_transfer_processor.SetConfig(parameters.processor_config); + } + IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + rb.Push(result); } void IRS::RunImageTransferProcessor(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; struct Parameters { - IrCameraHandle camera_handle; + Core::IrSensor::IrCameraHandle camera_handle; INSERT_PADDING_WORDS_NOINIT(1); u64 applet_resource_user_id; - PackedImageTransferProcessorConfig processor_config; + Core::IrSensor::PackedImageTransferProcessorConfig processor_config; u32 transfer_memory_size; }; static_assert(sizeof(Parameters) == 0x30, "Parameters has incorrect size."); @@ -151,20 +193,42 @@ void IRS::RunImageTransferProcessor(Kernel::HLERequestContext& ctx) { auto t_mem = system.CurrentProcess()->GetHandleTable().GetObject(t_mem_handle); - LOG_WARNING(Service_IRS, - "(STUBBED) called, npad_type={}, npad_id={}, transfer_memory_size={}, " - "applet_resource_user_id={}", - parameters.camera_handle.npad_type, parameters.camera_handle.npad_id, - parameters.transfer_memory_size, parameters.applet_resource_user_id); + if (t_mem.IsNull()) { + LOG_ERROR(Service_HID, "t_mem is a nullptr for handle=0x{:08X}", t_mem_handle); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultUnknown); + return; + } + + ASSERT_MSG(t_mem->GetSize() == parameters.transfer_memory_size, "t_mem has incorrect size"); + + u8* transfer_memory = system.Memory().GetPointer(t_mem->GetSourceAddress()); + + LOG_INFO(Service_IRS, + "called, npad_type={}, npad_id={}, transfer_memory_size={}, transfer_memory_size={}, " + "applet_resource_user_id={}", + parameters.camera_handle.npad_type, parameters.camera_handle.npad_id, + parameters.transfer_memory_size, t_mem->GetSize(), parameters.applet_resource_user_id); + + const auto result = IsIrCameraHandleValid(parameters.camera_handle); + + if (result.IsSuccess()) { + auto& device = GetIrCameraSharedMemoryDeviceEntry(parameters.camera_handle); + MakeProcessorWithCoreContext(parameters.camera_handle, device); + auto& image_transfer_processor = + GetProcessor(parameters.camera_handle); + image_transfer_processor.SetConfig(parameters.processor_config); + image_transfer_processor.SetTransferMemoryPointer(transfer_memory); + } IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + rb.Push(result); } void IRS::GetImageTransferProcessorState(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; struct Parameters { - IrCameraHandle camera_handle; + Core::IrSensor::IrCameraHandle camera_handle; INSERT_PADDING_WORDS_NOINIT(1); u64 applet_resource_user_id; }; @@ -172,21 +236,40 @@ void IRS::GetImageTransferProcessorState(Kernel::HLERequestContext& ctx) { const auto parameters{rp.PopRaw()}; - LOG_WARNING(Service_IRS, - "(STUBBED) called, npad_type={}, npad_id={}, applet_resource_user_id={}", - parameters.camera_handle.npad_type, parameters.camera_handle.npad_id, - parameters.applet_resource_user_id); + LOG_DEBUG(Service_IRS, "(STUBBED) called, npad_type={}, npad_id={}, applet_resource_user_id={}", + parameters.camera_handle.npad_type, parameters.camera_handle.npad_id, + parameters.applet_resource_user_id); - IPC::ResponseBuilder rb{ctx, 5}; + const auto result = IsIrCameraHandleValid(parameters.camera_handle); + if (result.IsError()) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + return; + } + + const auto& device = GetIrCameraSharedMemoryDeviceEntry(parameters.camera_handle); + + if (device.mode != Core::IrSensor::IrSensorMode::ImageTransferProcessor) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(InvalidProcessorState); + return; + } + + std::vector data{}; + const auto& image_transfer_processor = + GetProcessor(parameters.camera_handle); + const auto& state = image_transfer_processor.GetState(data); + + ctx.WriteBuffer(data); + IPC::ResponseBuilder rb{ctx, 6}; rb.Push(ResultSuccess); - rb.PushRaw(system.CoreTiming().GetCPUTicks()); - rb.PushRaw(0); + rb.PushRaw(state); } void IRS::RunTeraPluginProcessor(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - const auto camera_handle{rp.PopRaw()}; - const auto processor_config{rp.PopRaw()}; + const auto camera_handle{rp.PopRaw()}; + const auto processor_config{rp.PopRaw()}; const auto applet_resource_user_id{rp.Pop()}; LOG_WARNING(Service_IRS, @@ -196,8 +279,17 @@ void IRS::RunTeraPluginProcessor(Kernel::HLERequestContext& ctx) { processor_config.required_mcu_version.major, processor_config.required_mcu_version.minor, applet_resource_user_id); + const auto result = IsIrCameraHandleValid(camera_handle); + + if (result.IsSuccess()) { + auto& device = GetIrCameraSharedMemoryDeviceEntry(camera_handle); + MakeProcessor(camera_handle, device); + auto& image_transfer_processor = GetProcessor(camera_handle); + image_transfer_processor.SetConfig(processor_config); + } + IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + rb.Push(result); } void IRS::GetNpadIrCameraHandle(Kernel::HLERequestContext& ctx) { @@ -211,13 +303,13 @@ void IRS::GetNpadIrCameraHandle(Kernel::HLERequestContext& ctx) { return; } - IrCameraHandle camera_handle{ + Core::IrSensor::IrCameraHandle camera_handle{ .npad_id = static_cast(NpadIdTypeToIndex(npad_id)), .npad_type = Core::HID::NpadStyleIndex::None, }; - LOG_WARNING(Service_IRS, "(STUBBED) called, npad_id={}, camera_npad_id={}, camera_npad_type={}", - npad_id, camera_handle.npad_id, camera_handle.npad_type); + LOG_INFO(Service_IRS, "called, npad_id={}, camera_npad_id={}, camera_npad_type={}", npad_id, + camera_handle.npad_id, camera_handle.npad_type); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); @@ -226,8 +318,8 @@ void IRS::GetNpadIrCameraHandle(Kernel::HLERequestContext& ctx) { void IRS::RunPointingProcessor(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - const auto camera_handle{rp.PopRaw()}; - const auto processor_config{rp.PopRaw()}; + const auto camera_handle{rp.PopRaw()}; + const auto processor_config{rp.PopRaw()}; const auto applet_resource_user_id{rp.Pop()}; LOG_WARNING( @@ -236,14 +328,23 @@ void IRS::RunPointingProcessor(Kernel::HLERequestContext& ctx) { camera_handle.npad_type, camera_handle.npad_id, processor_config.required_mcu_version.major, processor_config.required_mcu_version.minor, applet_resource_user_id); + auto result = IsIrCameraHandleValid(camera_handle); + + if (result.IsSuccess()) { + auto& device = GetIrCameraSharedMemoryDeviceEntry(camera_handle); + MakeProcessor(camera_handle, device); + auto& image_transfer_processor = GetProcessor(camera_handle); + image_transfer_processor.SetConfig(processor_config); + } + IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + rb.Push(result); } void IRS::SuspendImageProcessor(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; struct Parameters { - IrCameraHandle camera_handle; + Core::IrSensor::IrCameraHandle camera_handle; INSERT_PADDING_WORDS_NOINIT(1); u64 applet_resource_user_id; }; @@ -256,14 +357,20 @@ void IRS::SuspendImageProcessor(Kernel::HLERequestContext& ctx) { parameters.camera_handle.npad_type, parameters.camera_handle.npad_id, parameters.applet_resource_user_id); + auto result = IsIrCameraHandleValid(parameters.camera_handle); + if (result.IsSuccess()) { + // TODO: Suspend image processor + result = ResultSuccess; + } + IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + rb.Push(result); } void IRS::CheckFirmwareVersion(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - const auto camera_handle{rp.PopRaw()}; - const auto mcu_version{rp.PopRaw()}; + const auto camera_handle{rp.PopRaw()}; + const auto mcu_version{rp.PopRaw()}; const auto applet_resource_user_id{rp.Pop()}; LOG_WARNING( @@ -272,37 +379,45 @@ void IRS::CheckFirmwareVersion(Kernel::HLERequestContext& ctx) { camera_handle.npad_type, camera_handle.npad_id, applet_resource_user_id, mcu_version.major, mcu_version.minor); + auto result = IsIrCameraHandleValid(camera_handle); + if (result.IsSuccess()) { + // TODO: Check firmware version + result = ResultSuccess; + } + IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + rb.Push(result); } void IRS::SetFunctionLevel(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - struct Parameters { - IrCameraHandle camera_handle; - PackedFunctionLevel function_level; - u64 applet_resource_user_id; - }; - static_assert(sizeof(Parameters) == 0x10, "Parameters has incorrect size."); + const auto camera_handle{rp.PopRaw()}; + const auto function_level{rp.PopRaw()}; + const auto applet_resource_user_id{rp.Pop()}; - const auto parameters{rp.PopRaw()}; + LOG_WARNING( + Service_IRS, + "(STUBBED) called, npad_type={}, npad_id={}, function_level={}, applet_resource_user_id={}", + camera_handle.npad_type, camera_handle.npad_id, function_level.function_level, + applet_resource_user_id); - LOG_WARNING(Service_IRS, - "(STUBBED) called, npad_type={}, npad_id={}, applet_resource_user_id={}", - parameters.camera_handle.npad_type, parameters.camera_handle.npad_id, - parameters.applet_resource_user_id); + auto result = IsIrCameraHandleValid(camera_handle); + if (result.IsSuccess()) { + // TODO: Set Function level + result = ResultSuccess; + } IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + rb.Push(result); } void IRS::RunImageTransferExProcessor(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; struct Parameters { - IrCameraHandle camera_handle; + Core::IrSensor::IrCameraHandle camera_handle; INSERT_PADDING_WORDS_NOINIT(1); u64 applet_resource_user_id; - PackedImageTransferProcessorExConfig processor_config; + Core::IrSensor::PackedImageTransferProcessorExConfig processor_config; u64 transfer_memory_size; }; static_assert(sizeof(Parameters) == 0x38, "Parameters has incorrect size."); @@ -313,20 +428,33 @@ void IRS::RunImageTransferExProcessor(Kernel::HLERequestContext& ctx) { auto t_mem = system.CurrentProcess()->GetHandleTable().GetObject(t_mem_handle); - LOG_WARNING(Service_IRS, - "(STUBBED) called, npad_type={}, npad_id={}, transfer_memory_size={}, " - "applet_resource_user_id={}", - parameters.camera_handle.npad_type, parameters.camera_handle.npad_id, - parameters.transfer_memory_size, parameters.applet_resource_user_id); + u8* transfer_memory = system.Memory().GetPointer(t_mem->GetSourceAddress()); + + LOG_INFO(Service_IRS, + "called, npad_type={}, npad_id={}, transfer_memory_size={}, " + "applet_resource_user_id={}", + parameters.camera_handle.npad_type, parameters.camera_handle.npad_id, + parameters.transfer_memory_size, parameters.applet_resource_user_id); + + auto result = IsIrCameraHandleValid(parameters.camera_handle); + + if (result.IsSuccess()) { + auto& device = GetIrCameraSharedMemoryDeviceEntry(parameters.camera_handle); + MakeProcessorWithCoreContext(parameters.camera_handle, device); + auto& image_transfer_processor = + GetProcessor(parameters.camera_handle); + image_transfer_processor.SetConfig(parameters.processor_config); + image_transfer_processor.SetTransferMemoryPointer(transfer_memory); + } IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + rb.Push(result); } void IRS::RunIrLedProcessor(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - const auto camera_handle{rp.PopRaw()}; - const auto processor_config{rp.PopRaw()}; + const auto camera_handle{rp.PopRaw()}; + const auto processor_config{rp.PopRaw()}; const auto applet_resource_user_id{rp.Pop()}; LOG_WARNING(Service_IRS, @@ -336,14 +464,23 @@ void IRS::RunIrLedProcessor(Kernel::HLERequestContext& ctx) { processor_config.required_mcu_version.major, processor_config.required_mcu_version.minor, applet_resource_user_id); + auto result = IsIrCameraHandleValid(camera_handle); + + if (result.IsSuccess()) { + auto& device = GetIrCameraSharedMemoryDeviceEntry(camera_handle); + MakeProcessor(camera_handle, device); + auto& image_transfer_processor = GetProcessor(camera_handle); + image_transfer_processor.SetConfig(processor_config); + } + IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + rb.Push(result); } void IRS::StopImageProcessorAsync(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; struct Parameters { - IrCameraHandle camera_handle; + Core::IrSensor::IrCameraHandle camera_handle; INSERT_PADDING_WORDS_NOINIT(1); u64 applet_resource_user_id; }; @@ -356,14 +493,20 @@ void IRS::StopImageProcessorAsync(Kernel::HLERequestContext& ctx) { parameters.camera_handle.npad_type, parameters.camera_handle.npad_id, parameters.applet_resource_user_id); + auto result = IsIrCameraHandleValid(parameters.camera_handle); + if (result.IsSuccess()) { + // TODO: Stop image processor async + result = ResultSuccess; + } + IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + rb.Push(result); } void IRS::ActivateIrsensorWithFunctionLevel(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; struct Parameters { - PackedFunctionLevel function_level; + Core::IrSensor::PackedFunctionLevel function_level; INSERT_PADDING_WORDS_NOINIT(1); u64 applet_resource_user_id; }; @@ -378,7 +521,22 @@ void IRS::ActivateIrsensorWithFunctionLevel(Kernel::HLERequestContext& ctx) { rb.Push(ResultSuccess); } -IRS::~IRS() = default; +ResultCode IRS::IsIrCameraHandleValid(const Core::IrSensor::IrCameraHandle& camera_handle) const { + if (camera_handle.npad_id > + static_cast(NpadIdTypeToIndex(Core::HID::NpadIdType::Handheld))) { + return InvalidIrCameraHandle; + } + if (camera_handle.npad_type != Core::HID::NpadStyleIndex::None) { + return InvalidIrCameraHandle; + } + return ResultSuccess; +} + +Core::IrSensor::DeviceFormat& IRS::GetIrCameraSharedMemoryDeviceEntry( + const Core::IrSensor::IrCameraHandle& camera_handle) { + ASSERT_MSG(sizeof(StatusManager::device) > camera_handle.npad_id, "invalid npad_id"); + return shared_memory->device[camera_handle.npad_id]; +} IRS_SYS::IRS_SYS(Core::System& system_) : ServiceFramework{system_, "irs:sys"} { // clang-format off diff --git a/src/core/hle/service/hid/irs.h b/src/core/hle/service/hid/irs.h index 361dc22136..ae7f7719b2 100644 --- a/src/core/hle/service/hid/irs.h +++ b/src/core/hle/service/hid/irs.h @@ -4,12 +4,18 @@ #pragma once #include "core/hid/hid_types.h" +#include "core/hid/irs_types.h" +#include "core/hle/service/hid/irsensor/processor_base.h" #include "core/hle/service/service.h" namespace Core { class System; } +namespace Core::HID { +class EmulatedController; +} // namespace Core::HID + namespace Service::HID { class IRS final : public ServiceFramework { @@ -18,234 +24,19 @@ public: ~IRS() override; private: - // This is nn::irsensor::IrCameraStatus - enum IrCameraStatus : u32 { - Available, - Unsupported, - Unconnected, + // This is nn::irsensor::detail::AruidFormat + struct AruidFormat { + u64 sensor_aruid; + u64 sensor_aruid_status; }; + static_assert(sizeof(AruidFormat) == 0x10, "AruidFormat is an invalid size"); - // This is nn::irsensor::IrCameraInternalStatus - enum IrCameraInternalStatus : u32 { - Stopped, - FirmwareUpdateNeeded, - Unkown2, - Unkown3, - Unkown4, - FirmwareVersionRequested, - FirmwareVersionIsInvalid, - Ready, - Setting, + // This is nn::irsensor::detail::StatusManager + struct StatusManager { + std::array device; + std::array aruid; }; - - // This is nn::irsensor::detail::StatusManager::IrSensorMode - enum IrSensorMode : u64 { - None, - MomentProcessor, - ClusteringProcessor, - ImageTransferProcessor, - PointingProcessorMarker, - TeraPluginProcessor, - IrLedProcessor, - }; - - // This is nn::irsensor::ImageProcessorStatus - enum ImageProcessorStatus : u8 { - stopped, - running, - }; - - // This is nn::irsensor::ImageTransferProcessorFormat - enum ImageTransferProcessorFormat : u8 { - Size320x240, - Size160x120, - Size80x60, - Size40x30, - Size20x15, - }; - - // This is nn::irsensor::AdaptiveClusteringMode - enum AdaptiveClusteringMode : u8 { - StaticFov, - DynamicFov, - }; - - // This is nn::irsensor::AdaptiveClusteringTargetDistance - enum AdaptiveClusteringTargetDistance : u8 { - Near, - Middle, - Far, - }; - - // This is nn::irsensor::IrsHandAnalysisMode - enum IrsHandAnalysisMode : u8 { - Silhouette, - Image, - SilhoueteAndImage, - SilhuetteOnly, - }; - - // This is nn::irsensor::IrSensorFunctionLevel - enum IrSensorFunctionLevel : u8 { - unknown0, - unknown1, - unknown2, - unknown3, - unknown4, - }; - - // This is nn::irsensor::IrCameraHandle - struct IrCameraHandle { - u8 npad_id{}; - Core::HID::NpadStyleIndex npad_type{Core::HID::NpadStyleIndex::None}; - INSERT_PADDING_BYTES(2); - }; - static_assert(sizeof(IrCameraHandle) == 4, "IrCameraHandle is an invalid size"); - - struct IrsRect { - s16 x; - s16 y; - s16 width; - s16 height; - }; - - // This is nn::irsensor::PackedMcuVersion - struct PackedMcuVersion { - u16 major; - u16 minor; - }; - static_assert(sizeof(PackedMcuVersion) == 4, "PackedMcuVersion is an invalid size"); - - // This is nn::irsensor::MomentProcessorConfig - struct MomentProcessorConfig { - u64 exposire_time; - u8 light_target; - u8 gain; - u8 is_negative_used; - INSERT_PADDING_BYTES(7); - IrsRect window_of_interest; - u8 preprocess; - u8 preprocess_intensity_threshold; - INSERT_PADDING_BYTES(5); - }; - static_assert(sizeof(MomentProcessorConfig) == 0x28, - "MomentProcessorConfig is an invalid size"); - - // This is nn::irsensor::PackedMomentProcessorConfig - struct PackedMomentProcessorConfig { - u64 exposire_time; - u8 light_target; - u8 gain; - u8 is_negative_used; - INSERT_PADDING_BYTES(5); - IrsRect window_of_interest; - PackedMcuVersion required_mcu_version; - u8 preprocess; - u8 preprocess_intensity_threshold; - INSERT_PADDING_BYTES(2); - }; - static_assert(sizeof(PackedMomentProcessorConfig) == 0x20, - "PackedMomentProcessorConfig is an invalid size"); - - // This is nn::irsensor::ClusteringProcessorConfig - struct ClusteringProcessorConfig { - u64 exposire_time; - u32 light_target; - u32 gain; - u8 is_negative_used; - INSERT_PADDING_BYTES(7); - IrsRect window_of_interest; - u32 pixel_count_min; - u32 pixel_count_max; - u32 object_intensity_min; - u8 is_external_light_filter_enabled; - INSERT_PADDING_BYTES(3); - }; - static_assert(sizeof(ClusteringProcessorConfig) == 0x30, - "ClusteringProcessorConfig is an invalid size"); - - // This is nn::irsensor::PackedClusteringProcessorConfig - struct PackedClusteringProcessorConfig { - u64 exposire_time; - u8 light_target; - u8 gain; - u8 is_negative_used; - INSERT_PADDING_BYTES(5); - IrsRect window_of_interest; - PackedMcuVersion required_mcu_version; - u32 pixel_count_min; - u32 pixel_count_max; - u32 object_intensity_min; - u8 is_external_light_filter_enabled; - INSERT_PADDING_BYTES(2); - }; - static_assert(sizeof(PackedClusteringProcessorConfig) == 0x30, - "PackedClusteringProcessorConfig is an invalid size"); - - // This is nn::irsensor::PackedImageTransferProcessorConfig - struct PackedImageTransferProcessorConfig { - u64 exposire_time; - u8 light_target; - u8 gain; - u8 is_negative_used; - INSERT_PADDING_BYTES(5); - PackedMcuVersion required_mcu_version; - u8 format; - INSERT_PADDING_BYTES(3); - }; - static_assert(sizeof(PackedImageTransferProcessorConfig) == 0x18, - "PackedImageTransferProcessorConfig is an invalid size"); - - // This is nn::irsensor::PackedTeraPluginProcessorConfig - struct PackedTeraPluginProcessorConfig { - PackedMcuVersion required_mcu_version; - u8 mode; - INSERT_PADDING_BYTES(3); - }; - static_assert(sizeof(PackedTeraPluginProcessorConfig) == 0x8, - "PackedTeraPluginProcessorConfig is an invalid size"); - - // This is nn::irsensor::PackedPointingProcessorConfig - struct PackedPointingProcessorConfig { - IrsRect window_of_interest; - PackedMcuVersion required_mcu_version; - }; - static_assert(sizeof(PackedPointingProcessorConfig) == 0xC, - "PackedPointingProcessorConfig is an invalid size"); - - // This is nn::irsensor::PackedFunctionLevel - struct PackedFunctionLevel { - IrSensorFunctionLevel function_level; - INSERT_PADDING_BYTES(3); - }; - static_assert(sizeof(PackedFunctionLevel) == 0x4, "PackedFunctionLevel is an invalid size"); - - // This is nn::irsensor::PackedImageTransferProcessorExConfig - struct PackedImageTransferProcessorExConfig { - u64 exposire_time; - u8 light_target; - u8 gain; - u8 is_negative_used; - INSERT_PADDING_BYTES(5); - PackedMcuVersion required_mcu_version; - ImageTransferProcessorFormat origin_format; - ImageTransferProcessorFormat trimming_format; - u16 trimming_start_x; - u16 trimming_start_y; - u8 is_external_light_filter_enabled; - INSERT_PADDING_BYTES(3); - }; - static_assert(sizeof(PackedImageTransferProcessorExConfig) == 0x20, - "PackedImageTransferProcessorExConfig is an invalid size"); - - // This is nn::irsensor::PackedIrLedProcessorConfig - struct PackedIrLedProcessorConfig { - PackedMcuVersion required_mcu_version; - u8 light_target; - INSERT_PADDING_BYTES(3); - }; - static_assert(sizeof(PackedIrLedProcessorConfig) == 0x8, - "PackedIrLedProcessorConfig is an invalid size"); + static_assert(sizeof(StatusManager) == 0x8000, "StatusManager is an invalid size"); void ActivateIrsensor(Kernel::HLERequestContext& ctx); void DeactivateIrsensor(Kernel::HLERequestContext& ctx); @@ -265,6 +56,56 @@ private: void RunIrLedProcessor(Kernel::HLERequestContext& ctx); void StopImageProcessorAsync(Kernel::HLERequestContext& ctx); void ActivateIrsensorWithFunctionLevel(Kernel::HLERequestContext& ctx); + + ResultCode IsIrCameraHandleValid(const Core::IrSensor::IrCameraHandle& camera_handle) const; + Core::IrSensor::DeviceFormat& GetIrCameraSharedMemoryDeviceEntry( + const Core::IrSensor::IrCameraHandle& camera_handle); + + template + void MakeProcessor(const Core::IrSensor::IrCameraHandle& handle, + Core::IrSensor::DeviceFormat& device_state) { + const auto index = static_cast(handle.npad_id); + if (index > sizeof(processors)) { + LOG_CRITICAL(Service_IRS, "Invalid index {}", index); + return; + } + processors[index] = std::make_unique(device_state); + } + + template + void MakeProcessorWithCoreContext(const Core::IrSensor::IrCameraHandle& handle, + Core::IrSensor::DeviceFormat& device_state) { + const auto index = static_cast(handle.npad_id); + if (index > sizeof(processors)) { + LOG_CRITICAL(Service_IRS, "Invalid index {}", index); + return; + } + processors[index] = std::make_unique(system.HIDCore(), device_state, index); + } + + template + T& GetProcessor(const Core::IrSensor::IrCameraHandle& handle) { + const auto index = static_cast(handle.npad_id); + if (index > sizeof(processors)) { + LOG_CRITICAL(Service_IRS, "Invalid index {}", index); + return static_cast(*processors[0]); + } + return static_cast(*processors[index]); + } + + template + const T& GetProcessor(const Core::IrSensor::IrCameraHandle& handle) const { + const auto index = static_cast(handle.npad_id); + if (index > sizeof(processors)) { + LOG_CRITICAL(Service_IRS, "Invalid index {}", index); + return static_cast(*processors[0]); + } + return static_cast(*processors[index]); + } + + Core::HID::EmulatedController* npad_device = nullptr; + StatusManager* shared_memory = nullptr; + std::array, 9> processors{}; }; class IRS_SYS final : public ServiceFramework { diff --git a/src/core/hle/service/hid/irsensor/clustering_processor.cpp b/src/core/hle/service/hid/irsensor/clustering_processor.cpp new file mode 100644 index 0000000000..aac3e4fec9 --- /dev/null +++ b/src/core/hle/service/hid/irsensor/clustering_processor.cpp @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "core/hle/service/hid/irsensor/clustering_processor.h" + +namespace Service::HID { +ClusteringProcessor::ClusteringProcessor(Core::IrSensor::DeviceFormat& device_format) + : device(device_format) { + device.mode = Core::IrSensor::IrSensorMode::ClusteringProcessor; + device.camera_status = Core::IrSensor::IrCameraStatus::Unconnected; + device.camera_internal_status = Core::IrSensor::IrCameraInternalStatus::Stopped; + device.state.start = 0; + device.state.count = 0; +} + +ClusteringProcessor::~ClusteringProcessor() = default; + +void ClusteringProcessor::StartProcessor() {} + +void ClusteringProcessor::SuspendProcessor() {} + +void ClusteringProcessor::StopProcessor() {} + +void ClusteringProcessor::SetConfig(Core::IrSensor::PackedClusteringProcessorConfig config) { + current_config.camera_config.exposure_time = config.camera_config.exposure_time; + current_config.camera_config.gain = config.camera_config.gain; + current_config.camera_config.is_negative_used = config.camera_config.is_negative_used; + current_config.camera_config.light_target = + static_cast(config.camera_config.light_target); + current_config.pixel_count_min = config.pixel_count_min; + current_config.pixel_count_max = config.pixel_count_max; + current_config.is_external_light_filter_enabled = config.is_external_light_filter_enabled; + current_config.object_intensity_min = config.object_intensity_min; +} + +} // namespace Service::HID diff --git a/src/core/hle/service/hid/irsensor/clustering_processor.h b/src/core/hle/service/hid/irsensor/clustering_processor.h new file mode 100644 index 0000000000..58b2acf1c2 --- /dev/null +++ b/src/core/hle/service/hid/irsensor/clustering_processor.h @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/common_types.h" +#include "core/hid/irs_types.h" +#include "core/hle/service/hid/irsensor/processor_base.h" + +namespace Service::HID { +class ClusteringProcessor final : public ProcessorBase { +public: + explicit ClusteringProcessor(Core::IrSensor::DeviceFormat& device_format); + ~ClusteringProcessor() override; + + // Called when the processor is initialized + void StartProcessor() override; + + // Called when the processor is suspended + void SuspendProcessor() override; + + // Called when the processor is stopped + void StopProcessor() override; + + // Sets config parameters of the camera + void SetConfig(Core::IrSensor::PackedClusteringProcessorConfig config); + +private: + // This is nn::irsensor::ClusteringProcessorConfig + struct ClusteringProcessorConfig { + Core::IrSensor::CameraConfig camera_config; + Core::IrSensor::IrsRect window_of_interest; + u32 pixel_count_min; + u32 pixel_count_max; + u32 object_intensity_min; + bool is_external_light_filter_enabled; + INSERT_PADDING_BYTES(3); + }; + static_assert(sizeof(ClusteringProcessorConfig) == 0x30, + "ClusteringProcessorConfig is an invalid size"); + + // This is nn::irsensor::AdaptiveClusteringProcessorConfig + struct AdaptiveClusteringProcessorConfig { + Core::IrSensor::AdaptiveClusteringMode mode; + Core::IrSensor::AdaptiveClusteringTargetDistance target_distance; + }; + static_assert(sizeof(AdaptiveClusteringProcessorConfig) == 0x8, + "AdaptiveClusteringProcessorConfig is an invalid size"); + + // This is nn::irsensor::ClusteringData + struct ClusteringData { + f32 average_intensity; + Core::IrSensor::IrsCentroid centroid; + u32 pixel_count; + Core::IrSensor::IrsRect bound; + }; + static_assert(sizeof(ClusteringData) == 0x18, "ClusteringData is an invalid size"); + + // This is nn::irsensor::ClusteringProcessorState + struct ClusteringProcessorState { + s64 sampling_number; + u64 timestamp; + u8 object_count; + INSERT_PADDING_BYTES(3); + Core::IrSensor::CameraAmbientNoiseLevel ambient_noise_level; + std::array data; + }; + static_assert(sizeof(ClusteringProcessorState) == 0x198, + "ClusteringProcessorState is an invalid size"); + + ClusteringProcessorConfig current_config{}; + Core::IrSensor::DeviceFormat& device; +}; +} // namespace Service::HID diff --git a/src/core/hle/service/hid/irsensor/image_transfer_processor.cpp b/src/core/hle/service/hid/irsensor/image_transfer_processor.cpp new file mode 100644 index 0000000000..703e825e64 --- /dev/null +++ b/src/core/hle/service/hid/irsensor/image_transfer_processor.cpp @@ -0,0 +1,154 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "core/hid/emulated_controller.h" +#include "core/hid/hid_core.h" +#include "core/hle/service/hid/irsensor/image_transfer_processor.h" + +namespace Service::HID { +ImageTransferProcessor::ImageTransferProcessor(Core::HID::HIDCore& hid_core_, + Core::IrSensor::DeviceFormat& device_format, + std::size_t npad_index) + : device{device_format} { + npad_device = hid_core_.GetEmulatedControllerByIndex(npad_index); + + Core::HID::ControllerUpdateCallback engine_callback{ + .on_change = [this](Core::HID::ControllerTriggerType type) { OnControllerUpdate(type); }, + .is_npad_service = true, + }; + callback_key = npad_device->SetCallback(engine_callback); + + device.mode = Core::IrSensor::IrSensorMode::ImageTransferProcessor; + device.camera_status = Core::IrSensor::IrCameraStatus::Unconnected; + device.camera_internal_status = Core::IrSensor::IrCameraInternalStatus::Stopped; + device.state.start = 0; + device.state.count = 0; +} + +ImageTransferProcessor::~ImageTransferProcessor() { + npad_device->DeleteCallback(callback_key); +}; + +void ImageTransferProcessor::StartProcessor() { + is_active = true; + device.camera_status = Core::IrSensor::IrCameraStatus::Available; + device.camera_internal_status = Core::IrSensor::IrCameraInternalStatus::Ready; + device.state.start = 0; + device.state.count = 1; + processor_state.sampling_number = 0; + processor_state.ambient_noise_level = Core::IrSensor::CameraAmbientNoiseLevel::Low; +} + +void ImageTransferProcessor::SuspendProcessor() {} + +void ImageTransferProcessor::StopProcessor() {} + +void ImageTransferProcessor::OnControllerUpdate(Core::HID::ControllerTriggerType type) { + if (type != Core::HID::ControllerTriggerType::IrSensor) { + return; + } + if (!is_transfer_memory_set) { + return; + } + + const auto camera_data = npad_device->GetCamera(); + + // This indicates how much ambient light is precent + processor_state.ambient_noise_level = Core::IrSensor::CameraAmbientNoiseLevel::Low; + processor_state.sampling_number = camera_data.sample; + + if (camera_data.format != current_config.origin_format) { + LOG_WARNING(Service_IRS, "Wrong Input format {} expected {}", camera_data.format, + current_config.origin_format); + memset(transfer_memory, 0, GetDataSize(current_config.trimming_format)); + return; + } + + if (current_config.origin_format > current_config.trimming_format) { + LOG_WARNING(Service_IRS, "Origin format {} is smaller than trimming format {}", + current_config.origin_format, current_config.trimming_format); + memset(transfer_memory, 0, GetDataSize(current_config.trimming_format)); + return; + } + + std::vector window_data{}; + const auto origin_width = GetDataWidth(current_config.origin_format); + const auto origin_height = GetDataHeight(current_config.origin_format); + const auto trimming_width = GetDataWidth(current_config.trimming_format); + const auto trimming_height = GetDataHeight(current_config.trimming_format); + window_data.resize(GetDataSize(current_config.trimming_format)); + + if (trimming_width + current_config.trimming_start_x > origin_width || + trimming_height + current_config.trimming_start_y > origin_height) { + LOG_WARNING(Service_IRS, + "Trimming area ({}, {}, {}, {}) is outside of origin area ({}, {})", + current_config.trimming_start_x, current_config.trimming_start_y, + trimming_width, trimming_height, origin_width, origin_height); + memset(transfer_memory, 0, GetDataSize(current_config.trimming_format)); + return; + } + + for (std::size_t y = 0; y < trimming_height; y++) { + for (std::size_t x = 0; x < trimming_width; x++) { + const std::size_t window_index = (y * trimming_width) + x; + const std::size_t origin_index = + ((y + current_config.trimming_start_y) * origin_width) + x + + current_config.trimming_start_x; + window_data[window_index] = camera_data.data[origin_index]; + } + } + + memcpy(transfer_memory, window_data.data(), GetDataSize(current_config.trimming_format)); + + if (!IsProcessorActive()) { + StartProcessor(); + } +} + +void ImageTransferProcessor::SetConfig(Core::IrSensor::PackedImageTransferProcessorConfig config) { + current_config.camera_config.exposure_time = config.camera_config.exposure_time; + current_config.camera_config.gain = config.camera_config.gain; + current_config.camera_config.is_negative_used = config.camera_config.is_negative_used; + current_config.camera_config.light_target = + static_cast(config.camera_config.light_target); + current_config.origin_format = + static_cast(config.format); + current_config.trimming_format = + static_cast(config.format); + current_config.trimming_start_x = 0; + current_config.trimming_start_y = 0; + + npad_device->SetCameraFormat(current_config.origin_format); +} + +void ImageTransferProcessor::SetConfig( + Core::IrSensor::PackedImageTransferProcessorExConfig config) { + current_config.camera_config.exposure_time = config.camera_config.exposure_time; + current_config.camera_config.gain = config.camera_config.gain; + current_config.camera_config.is_negative_used = config.camera_config.is_negative_used; + current_config.camera_config.light_target = + static_cast(config.camera_config.light_target); + current_config.origin_format = + static_cast(config.origin_format); + current_config.trimming_format = + static_cast(config.trimming_format); + current_config.trimming_start_x = config.trimming_start_x; + current_config.trimming_start_y = config.trimming_start_y; + + npad_device->SetCameraFormat(current_config.origin_format); +} + +void ImageTransferProcessor::SetTransferMemoryPointer(u8* t_mem) { + is_transfer_memory_set = true; + transfer_memory = t_mem; +} + +Core::IrSensor::ImageTransferProcessorState ImageTransferProcessor::GetState( + std::vector& data) const { + const auto size = GetDataSize(current_config.trimming_format); + data.resize(size); + memcpy(data.data(), transfer_memory, size); + return processor_state; +} + +} // namespace Service::HID diff --git a/src/core/hle/service/hid/irsensor/image_transfer_processor.h b/src/core/hle/service/hid/irsensor/image_transfer_processor.h new file mode 100644 index 0000000000..b557eaf204 --- /dev/null +++ b/src/core/hle/service/hid/irsensor/image_transfer_processor.h @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/common_types.h" +#include "core/hid/irs_types.h" +#include "core/hle/service/hid/irsensor/processor_base.h" + +namespace Core::HID { +class EmulatedController; +} // namespace Core::HID + +namespace Service::HID { +class ImageTransferProcessor final : public ProcessorBase { +public: + explicit ImageTransferProcessor(Core::HID::HIDCore& hid_core_, + Core::IrSensor::DeviceFormat& device_format, + std::size_t npad_index); + ~ImageTransferProcessor() override; + + // Called when the processor is initialized + void StartProcessor() override; + + // Called when the processor is suspended + void SuspendProcessor() override; + + // Called when the processor is stopped + void StopProcessor() override; + + // Sets config parameters of the camera + void SetConfig(Core::IrSensor::PackedImageTransferProcessorConfig config); + void SetConfig(Core::IrSensor::PackedImageTransferProcessorExConfig config); + + // Transfer memory where the image data will be stored + void SetTransferMemoryPointer(u8* t_mem); + + Core::IrSensor::ImageTransferProcessorState GetState(std::vector& data) const; + +private: + // This is nn::irsensor::ImageTransferProcessorConfig + struct ImageTransferProcessorConfig { + Core::IrSensor::CameraConfig camera_config; + Core::IrSensor::ImageTransferProcessorFormat format; + }; + static_assert(sizeof(ImageTransferProcessorConfig) == 0x20, + "ImageTransferProcessorConfig is an invalid size"); + + // This is nn::irsensor::ImageTransferProcessorExConfig + struct ImageTransferProcessorExConfig { + Core::IrSensor::CameraConfig camera_config; + Core::IrSensor::ImageTransferProcessorFormat origin_format; + Core::IrSensor::ImageTransferProcessorFormat trimming_format; + u16 trimming_start_x; + u16 trimming_start_y; + bool is_external_light_filter_enabled; + INSERT_PADDING_BYTES(3); + }; + static_assert(sizeof(ImageTransferProcessorExConfig) == 0x28, + "ImageTransferProcessorExConfig is an invalid size"); + + void OnControllerUpdate(Core::HID::ControllerTriggerType type); + + ImageTransferProcessorExConfig current_config{}; + Core::IrSensor::ImageTransferProcessorState processor_state{}; + Core::IrSensor::DeviceFormat& device; + Core::HID::EmulatedController* npad_device; + int callback_key{}; + + u8* transfer_memory = nullptr; + bool is_transfer_memory_set = false; +}; +} // namespace Service::HID diff --git a/src/core/hle/service/hid/irsensor/ir_led_processor.cpp b/src/core/hle/service/hid/irsensor/ir_led_processor.cpp new file mode 100644 index 0000000000..656e17c955 --- /dev/null +++ b/src/core/hle/service/hid/irsensor/ir_led_processor.cpp @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "core/hle/service/hid/irsensor/ir_led_processor.h" + +namespace Service::HID { +IrLedProcessor::IrLedProcessor(Core::IrSensor::DeviceFormat& device_format) + : device(device_format) { + device.mode = Core::IrSensor::IrSensorMode::IrLedProcessor; + device.camera_status = Core::IrSensor::IrCameraStatus::Unconnected; + device.camera_internal_status = Core::IrSensor::IrCameraInternalStatus::Stopped; + device.state.start = 0; + device.state.count = 0; +} + +IrLedProcessor::~IrLedProcessor() = default; + +void IrLedProcessor::StartProcessor() {} + +void IrLedProcessor::SuspendProcessor() {} + +void IrLedProcessor::StopProcessor() {} + +void IrLedProcessor::SetConfig(Core::IrSensor::PackedIrLedProcessorConfig config) { + current_config.light_target = + static_cast(config.light_target); +} + +} // namespace Service::HID diff --git a/src/core/hle/service/hid/irsensor/ir_led_processor.h b/src/core/hle/service/hid/irsensor/ir_led_processor.h new file mode 100644 index 0000000000..cb04d0b941 --- /dev/null +++ b/src/core/hle/service/hid/irsensor/ir_led_processor.h @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/bit_field.h" +#include "common/common_types.h" +#include "core/hid/irs_types.h" +#include "core/hle/service/hid/irsensor/processor_base.h" + +namespace Service::HID { +class IrLedProcessor final : public ProcessorBase { +public: + explicit IrLedProcessor(Core::IrSensor::DeviceFormat& device_format); + ~IrLedProcessor() override; + + // Called when the processor is initialized + void StartProcessor() override; + + // Called when the processor is suspended + void SuspendProcessor() override; + + // Called when the processor is stopped + void StopProcessor() override; + + // Sets config parameters of the camera + void SetConfig(Core::IrSensor::PackedIrLedProcessorConfig config); + +private: + // This is nn::irsensor::IrLedProcessorConfig + struct IrLedProcessorConfig { + Core::IrSensor::CameraLightTarget light_target; + }; + static_assert(sizeof(IrLedProcessorConfig) == 0x4, "IrLedProcessorConfig is an invalid size"); + + struct IrLedProcessorState { + s64 sampling_number; + u64 timestamp; + std::array data; + }; + static_assert(sizeof(IrLedProcessorState) == 0x18, "IrLedProcessorState is an invalid size"); + + IrLedProcessorConfig current_config{}; + Core::IrSensor::DeviceFormat& device; +}; + +} // namespace Service::HID diff --git a/src/core/hle/service/hid/irsensor/moment_processor.cpp b/src/core/hle/service/hid/irsensor/moment_processor.cpp new file mode 100644 index 0000000000..fb36be577c --- /dev/null +++ b/src/core/hle/service/hid/irsensor/moment_processor.cpp @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "core/hle/service/hid/irsensor/moment_processor.h" + +namespace Service::HID { +MomentProcessor::MomentProcessor(Core::IrSensor::DeviceFormat& device_format) + : device(device_format) { + device.mode = Core::IrSensor::IrSensorMode::MomentProcessor; + device.camera_status = Core::IrSensor::IrCameraStatus::Unconnected; + device.camera_internal_status = Core::IrSensor::IrCameraInternalStatus::Stopped; + device.state.start = 0; + device.state.count = 0; +} + +MomentProcessor::~MomentProcessor() = default; + +void MomentProcessor::StartProcessor() {} + +void MomentProcessor::SuspendProcessor() {} + +void MomentProcessor::StopProcessor() {} + +void MomentProcessor::SetConfig(Core::IrSensor::PackedMomentProcessorConfig config) { + current_config.camera_config.exposure_time = config.camera_config.exposure_time; + current_config.camera_config.gain = config.camera_config.gain; + current_config.camera_config.is_negative_used = config.camera_config.is_negative_used; + current_config.camera_config.light_target = + static_cast(config.camera_config.light_target); + current_config.window_of_interest = config.window_of_interest; + current_config.preprocess = + static_cast(config.preprocess); + current_config.preprocess_intensity_threshold = config.preprocess_intensity_threshold; +} + +} // namespace Service::HID diff --git a/src/core/hle/service/hid/irsensor/moment_processor.h b/src/core/hle/service/hid/irsensor/moment_processor.h new file mode 100644 index 0000000000..63cdadfda7 --- /dev/null +++ b/src/core/hle/service/hid/irsensor/moment_processor.h @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/bit_field.h" +#include "common/common_types.h" +#include "core/hid/irs_types.h" +#include "core/hle/service/hid/irsensor/processor_base.h" + +namespace Service::HID { +class MomentProcessor final : public ProcessorBase { +public: + explicit MomentProcessor(Core::IrSensor::DeviceFormat& device_format); + ~MomentProcessor() override; + + // Called when the processor is initialized + void StartProcessor() override; + + // Called when the processor is suspended + void SuspendProcessor() override; + + // Called when the processor is stopped + void StopProcessor() override; + + // Sets config parameters of the camera + void SetConfig(Core::IrSensor::PackedMomentProcessorConfig config); + +private: + // This is nn::irsensor::MomentProcessorConfig + struct MomentProcessorConfig { + Core::IrSensor::CameraConfig camera_config; + Core::IrSensor::IrsRect window_of_interest; + Core::IrSensor::MomentProcessorPreprocess preprocess; + u32 preprocess_intensity_threshold; + }; + static_assert(sizeof(MomentProcessorConfig) == 0x28, + "MomentProcessorConfig is an invalid size"); + + // This is nn::irsensor::MomentStatistic + struct MomentStatistic { + f32 average_intensity; + Core::IrSensor::IrsCentroid centroid; + }; + static_assert(sizeof(MomentStatistic) == 0xC, "MomentStatistic is an invalid size"); + + // This is nn::irsensor::MomentProcessorState + struct MomentProcessorState { + s64 sampling_number; + u64 timestamp; + Core::IrSensor::CameraAmbientNoiseLevel ambient_noise_level; + INSERT_PADDING_BYTES(4); + std::array stadistic; + }; + static_assert(sizeof(MomentProcessorState) == 0x258, "MomentProcessorState is an invalid size"); + + MomentProcessorConfig current_config{}; + Core::IrSensor::DeviceFormat& device; +}; + +} // namespace Service::HID diff --git a/src/core/hle/service/hid/irsensor/pointing_processor.cpp b/src/core/hle/service/hid/irsensor/pointing_processor.cpp new file mode 100644 index 0000000000..7eb1236363 --- /dev/null +++ b/src/core/hle/service/hid/irsensor/pointing_processor.cpp @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "core/hle/service/hid/irsensor/pointing_processor.h" + +namespace Service::HID { +PointingProcessor::PointingProcessor(Core::IrSensor::DeviceFormat& device_format) + : device(device_format) { + device.mode = Core::IrSensor::IrSensorMode::PointingProcessorMarker; + device.camera_status = Core::IrSensor::IrCameraStatus::Unconnected; + device.camera_internal_status = Core::IrSensor::IrCameraInternalStatus::Stopped; + device.state.start = 0; + device.state.count = 0; +} + +PointingProcessor::~PointingProcessor() = default; + +void PointingProcessor::StartProcessor() {} + +void PointingProcessor::SuspendProcessor() {} + +void PointingProcessor::StopProcessor() {} + +void PointingProcessor::SetConfig(Core::IrSensor::PackedPointingProcessorConfig config) { + current_config.window_of_interest = config.window_of_interest; +} + +} // namespace Service::HID diff --git a/src/core/hle/service/hid/irsensor/pointing_processor.h b/src/core/hle/service/hid/irsensor/pointing_processor.h new file mode 100644 index 0000000000..2f67eff919 --- /dev/null +++ b/src/core/hle/service/hid/irsensor/pointing_processor.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/bit_field.h" +#include "common/common_types.h" +#include "core/hid/irs_types.h" +#include "core/hle/service/hid/irsensor/processor_base.h" + +namespace Service::HID { +class PointingProcessor final : public ProcessorBase { +public: + explicit PointingProcessor(Core::IrSensor::DeviceFormat& device_format); + ~PointingProcessor() override; + + // Called when the processor is initialized + void StartProcessor() override; + + // Called when the processor is suspended + void SuspendProcessor() override; + + // Called when the processor is stopped + void StopProcessor() override; + + // Sets config parameters of the camera + void SetConfig(Core::IrSensor::PackedPointingProcessorConfig config); + +private: + // This is nn::irsensor::PointingProcessorConfig + struct PointingProcessorConfig { + Core::IrSensor::IrsRect window_of_interest; + }; + static_assert(sizeof(PointingProcessorConfig) == 0x8, + "PointingProcessorConfig is an invalid size"); + + struct PointingProcessorMarkerData { + u8 pointing_status; + INSERT_PADDING_BYTES(3); + u32 unknown; + float unkown_float1; + float position_x; + float position_y; + float unkown_float2; + Core::IrSensor::IrsRect window_of_interest; + }; + static_assert(sizeof(PointingProcessorMarkerData) == 0x20, + "PointingProcessorMarkerData is an invalid size"); + + struct PointingProcessorMarkerState { + s64 sampling_number; + u64 timestamp; + std::array data; + }; + static_assert(sizeof(PointingProcessorMarkerState) == 0x70, + "PointingProcessorMarkerState is an invalid size"); + + PointingProcessorConfig current_config{}; + Core::IrSensor::DeviceFormat& device; +}; + +} // namespace Service::HID diff --git a/src/core/hle/service/hid/irsensor/processor_base.cpp b/src/core/hle/service/hid/irsensor/processor_base.cpp new file mode 100644 index 0000000000..bc8025375c --- /dev/null +++ b/src/core/hle/service/hid/irsensor/processor_base.cpp @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "core/hle/service/hid/irsensor/processor_base.h" + +namespace Service::HID { + +ProcessorBase::ProcessorBase() {} +ProcessorBase::~ProcessorBase() = default; + +bool ProcessorBase::IsProcessorActive() const { + return is_active; +} + +std::size_t ProcessorBase::GetDataSize(Core::IrSensor::ImageTransferProcessorFormat format) const { + switch (format) { + case Core::IrSensor::ImageTransferProcessorFormat::Size320x240: + return 320 * 240; + case Core::IrSensor::ImageTransferProcessorFormat::Size160x120: + return 160 * 120; + case Core::IrSensor::ImageTransferProcessorFormat::Size80x60: + return 80 * 60; + case Core::IrSensor::ImageTransferProcessorFormat::Size40x30: + return 40 * 30; + case Core::IrSensor::ImageTransferProcessorFormat::Size20x15: + return 20 * 15; + default: + return 0; + } +} + +std::size_t ProcessorBase::GetDataWidth(Core::IrSensor::ImageTransferProcessorFormat format) const { + switch (format) { + case Core::IrSensor::ImageTransferProcessorFormat::Size320x240: + return 320; + case Core::IrSensor::ImageTransferProcessorFormat::Size160x120: + return 160; + case Core::IrSensor::ImageTransferProcessorFormat::Size80x60: + return 80; + case Core::IrSensor::ImageTransferProcessorFormat::Size40x30: + return 40; + case Core::IrSensor::ImageTransferProcessorFormat::Size20x15: + return 20; + default: + return 0; + } +} + +std::size_t ProcessorBase::GetDataHeight( + Core::IrSensor::ImageTransferProcessorFormat format) const { + switch (format) { + case Core::IrSensor::ImageTransferProcessorFormat::Size320x240: + return 240; + case Core::IrSensor::ImageTransferProcessorFormat::Size160x120: + return 120; + case Core::IrSensor::ImageTransferProcessorFormat::Size80x60: + return 60; + case Core::IrSensor::ImageTransferProcessorFormat::Size40x30: + return 30; + case Core::IrSensor::ImageTransferProcessorFormat::Size20x15: + return 15; + default: + return 0; + } +} + +} // namespace Service::HID diff --git a/src/core/hle/service/hid/irsensor/processor_base.h b/src/core/hle/service/hid/irsensor/processor_base.h new file mode 100644 index 0000000000..49831aab6b --- /dev/null +++ b/src/core/hle/service/hid/irsensor/processor_base.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/common_types.h" +#include "core/hid/irs_types.h" + +namespace Service::HID { +class ProcessorBase { +public: + explicit ProcessorBase(); + virtual ~ProcessorBase(); + + virtual void StartProcessor() = 0; + virtual void SuspendProcessor() = 0; + virtual void StopProcessor() = 0; + + bool IsProcessorActive() const; + +protected: + /// Returns the number of bytes the image uses + std::size_t GetDataSize(Core::IrSensor::ImageTransferProcessorFormat format) const; + + /// Returns the width of the image + std::size_t GetDataWidth(Core::IrSensor::ImageTransferProcessorFormat format) const; + + /// Returns the height of the image + std::size_t GetDataHeight(Core::IrSensor::ImageTransferProcessorFormat format) const; + + bool is_active{false}; +}; +} // namespace Service::HID diff --git a/src/core/hle/service/hid/irsensor/tera_plugin_processor.cpp b/src/core/hle/service/hid/irsensor/tera_plugin_processor.cpp new file mode 100644 index 0000000000..deec22072e --- /dev/null +++ b/src/core/hle/service/hid/irsensor/tera_plugin_processor.cpp @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "core/hle/service/hid/irsensor/tera_plugin_processor.h" + +namespace Service::HID { +TeraPluginProcessor::TeraPluginProcessor(Core::IrSensor::DeviceFormat& device_format) + : device(device_format) { + device.mode = Core::IrSensor::IrSensorMode::TeraPluginProcessor; + device.camera_status = Core::IrSensor::IrCameraStatus::Unconnected; + device.camera_internal_status = Core::IrSensor::IrCameraInternalStatus::Stopped; + device.state.start = 0; + device.state.count = 0; +} + +TeraPluginProcessor::~TeraPluginProcessor() = default; + +void TeraPluginProcessor::StartProcessor() {} + +void TeraPluginProcessor::SuspendProcessor() {} + +void TeraPluginProcessor::StopProcessor() {} + +void TeraPluginProcessor::SetConfig(Core::IrSensor::PackedTeraPluginProcessorConfig config) { + current_config.mode = config.mode; + current_config.unknown_1 = config.unknown_1; + current_config.unknown_2 = config.unknown_2; + current_config.unknown_3 = config.unknown_3; +} + +} // namespace Service::HID diff --git a/src/core/hle/service/hid/irsensor/tera_plugin_processor.h b/src/core/hle/service/hid/irsensor/tera_plugin_processor.h new file mode 100644 index 0000000000..60f8057a5d --- /dev/null +++ b/src/core/hle/service/hid/irsensor/tera_plugin_processor.h @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/bit_field.h" +#include "common/common_types.h" +#include "core/hid/irs_types.h" +#include "core/hle/service/hid/irsensor/processor_base.h" + +namespace Service::HID { +class TeraPluginProcessor final : public ProcessorBase { +public: + explicit TeraPluginProcessor(Core::IrSensor::DeviceFormat& device_format); + ~TeraPluginProcessor() override; + + // Called when the processor is initialized + void StartProcessor() override; + + // Called when the processor is suspended + void SuspendProcessor() override; + + // Called when the processor is stopped + void StopProcessor() override; + + // Sets config parameters of the camera + void SetConfig(Core::IrSensor::PackedTeraPluginProcessorConfig config); + +private: + // This is nn::irsensor::TeraPluginProcessorConfig + struct TeraPluginProcessorConfig { + u8 mode; + u8 unknown_1; + u8 unknown_2; + u8 unknown_3; + }; + static_assert(sizeof(TeraPluginProcessorConfig) == 0x4, + "TeraPluginProcessorConfig is an invalid size"); + + struct TeraPluginProcessorState { + s64 sampling_number; + u64 timestamp; + Core::IrSensor::CameraAmbientNoiseLevel ambient_noise_level; + std::array data; + }; + static_assert(sizeof(TeraPluginProcessorState) == 0x140, + "TeraPluginProcessorState is an invalid size"); + + TeraPluginProcessorConfig current_config{}; + Core::IrSensor::DeviceFormat& device; +}; + +} // namespace Service::HID From 097785e19e3865ac1060b9fae564a3a1e36c695d Mon Sep 17 00:00:00 2001 From: german77 Date: Sun, 19 Jun 2022 16:27:48 -0500 Subject: [PATCH 34/50] service: irs: Move to IRS namespace and minor fixes --- CMakeModules/CopyYuzuQt5Deps.cmake | 1 + src/core/hid/irs_types.h | 11 ++-- src/core/hle/service/hid/errors.h | 6 ++- src/core/hle/service/hid/hid.cpp | 4 +- src/core/hle/service/hid/irs.cpp | 50 +++++++++++-------- src/core/hle/service/hid/irs.h | 4 +- .../hid/irsensor/clustering_processor.cpp | 6 +-- .../hid/irsensor/clustering_processor.h | 4 +- .../hid/irsensor/image_transfer_processor.cpp | 8 +-- .../hid/irsensor/image_transfer_processor.h | 4 +- .../service/hid/irsensor/ir_led_processor.cpp | 6 +-- .../service/hid/irsensor/ir_led_processor.h | 4 +- .../service/hid/irsensor/moment_processor.cpp | 6 +-- .../service/hid/irsensor/moment_processor.h | 4 +- .../hid/irsensor/pointing_processor.cpp | 6 +-- .../service/hid/irsensor/pointing_processor.h | 5 +- .../service/hid/irsensor/processor_base.cpp | 4 +- .../hle/service/hid/irsensor/processor_base.h | 4 +- .../hid/irsensor/tera_plugin_processor.cpp | 6 +-- .../hid/irsensor/tera_plugin_processor.h | 4 +- 20 files changed, 71 insertions(+), 76 deletions(-) diff --git a/CMakeModules/CopyYuzuQt5Deps.cmake b/CMakeModules/CopyYuzuQt5Deps.cmake index 4702a504c6..6c5044caa7 100644 --- a/CMakeModules/CopyYuzuQt5Deps.cmake +++ b/CMakeModules/CopyYuzuQt5Deps.cmake @@ -25,6 +25,7 @@ function(copy_yuzu_Qt5_deps target_dir) Qt5Gui$<$:d>.* Qt5Widgets$<$:d>.* Qt5Multimedia$<$:d>.* + Qt5Network$<$:d>.* ) if (YUZU_USE_QT_WEB_ENGINE) diff --git a/src/core/hid/irs_types.h b/src/core/hid/irs_types.h index c73d008a07..88c5b016d4 100644 --- a/src/core/hid/irs_types.h +++ b/src/core/hid/irs_types.h @@ -201,11 +201,11 @@ struct PackedClusteringProcessorConfig { PackedMcuVersion required_mcu_version; u32 pixel_count_min; u32 pixel_count_max; - u32 object_intensity_min; + u8 object_intensity_min; bool is_external_light_filter_enabled; INSERT_PADDING_BYTES(2); }; -static_assert(sizeof(PackedClusteringProcessorConfig) == 0x30, +static_assert(sizeof(PackedClusteringProcessorConfig) == 0x28, "PackedClusteringProcessorConfig is an invalid size"); // This is nn::irsensor::PackedImageTransferProcessorConfig @@ -273,12 +273,9 @@ struct HandAnalysisConfig { }; static_assert(sizeof(HandAnalysisConfig) == 0x4, "HandAnalysisConfig is an invalid size"); -// This is nn::irsensor::detail::ProcessorState +// This is nn::irsensor::detail::ProcessorState contents are different for each processor struct ProcessorState { - u64 start{}; - u32 count{}; - INSERT_PADDING_BYTES(4); - std::array processor_raw_data{}; + std::array processor_raw_data{}; }; static_assert(sizeof(ProcessorState) == 0xE20, "ProcessorState is an invalid size"); diff --git a/src/core/hle/service/hid/errors.h b/src/core/hle/service/hid/errors.h index 1bd62461cc..accc2b646c 100644 --- a/src/core/hle/service/hid/errors.h +++ b/src/core/hle/service/hid/errors.h @@ -18,7 +18,11 @@ constexpr Result NpadIsSameType{ErrorModule::HID, 602}; constexpr Result InvalidNpadId{ErrorModule::HID, 709}; constexpr Result NpadNotConnected{ErrorModule::HID, 710}; +} // namespace Service::HID + +namespace Service::IRS { + constexpr ResultCode InvalidProcessorState{ErrorModule::Irsensor, 78}; constexpr ResultCode InvalidIrCameraHandle{ErrorModule::Irsensor, 204}; -} // namespace Service::HID +} // namespace Service::IRS diff --git a/src/core/hle/service/hid/hid.cpp b/src/core/hle/service/hid/hid.cpp index 89bb124429..5ecbddf94d 100644 --- a/src/core/hle/service/hid/hid.cpp +++ b/src/core/hle/service/hid/hid.cpp @@ -2345,8 +2345,8 @@ void InstallInterfaces(SM::ServiceManager& service_manager, Core::System& system std::make_shared(system)->InstallAsService(service_manager); std::make_shared(system)->InstallAsService(service_manager); - std::make_shared(system)->InstallAsService(service_manager); - std::make_shared(system)->InstallAsService(service_manager); + std::make_shared(system)->InstallAsService(service_manager); + std::make_shared(system)->InstallAsService(service_manager); std::make_shared(system)->InstallAsService(service_manager); } diff --git a/src/core/hle/service/hid/irs.cpp b/src/core/hle/service/hid/irs.cpp index 53a31df79c..b6e7c07922 100644 --- a/src/core/hle/service/hid/irs.cpp +++ b/src/core/hle/service/hid/irs.cpp @@ -22,7 +22,7 @@ #include "core/hle/service/hid/irsensor/tera_plugin_processor.h" #include "core/memory.h" -namespace Service::HID { +namespace Service::IRS { IRS::IRS(Core::System& system_) : ServiceFramework{system_, "irs"} { // clang-format off @@ -60,7 +60,7 @@ void IRS::ActivateIrsensor(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; const auto applet_resource_user_id{rp.Pop()}; - LOG_WARNING(Service_HID, "(STUBBED) called, applet_resource_user_id={}", + LOG_WARNING(Service_IRS, "(STUBBED) called, applet_resource_user_id={}", applet_resource_user_id); IPC::ResponseBuilder rb{ctx, 2}; @@ -71,7 +71,7 @@ void IRS::DeactivateIrsensor(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; const auto applet_resource_user_id{rp.Pop()}; - LOG_WARNING(Service_HID, "(STUBBED) called, applet_resource_user_id={}", + LOG_WARNING(Service_IRS, "(STUBBED) called, applet_resource_user_id={}", applet_resource_user_id); IPC::ResponseBuilder rb{ctx, 2}; @@ -153,7 +153,7 @@ void IRS::RunClusteringProcessor(Kernel::HLERequestContext& ctx) { u64 applet_resource_user_id; Core::IrSensor::PackedClusteringProcessorConfig processor_config; }; - static_assert(sizeof(Parameters) == 0x40, "Parameters has incorrect size."); + static_assert(sizeof(Parameters) == 0x38, "Parameters has incorrect size."); const auto parameters{rp.PopRaw()}; @@ -194,7 +194,7 @@ void IRS::RunImageTransferProcessor(Kernel::HLERequestContext& ctx) { system.CurrentProcess()->GetHandleTable().GetObject(t_mem_handle); if (t_mem.IsNull()) { - LOG_ERROR(Service_HID, "t_mem is a nullptr for handle=0x{:08X}", t_mem_handle); + LOG_ERROR(Service_IRS, "t_mem is a nullptr for handle=0x{:08X}", t_mem_handle); IPC::ResponseBuilder rb{ctx, 2}; rb.Push(ResultUnknown); return; @@ -268,24 +268,32 @@ void IRS::GetImageTransferProcessorState(Kernel::HLERequestContext& ctx) { void IRS::RunTeraPluginProcessor(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - const auto camera_handle{rp.PopRaw()}; - const auto processor_config{rp.PopRaw()}; - const auto applet_resource_user_id{rp.Pop()}; + struct Parameters { + Core::IrSensor::IrCameraHandle camera_handle; + Core::IrSensor::PackedTeraPluginProcessorConfig processor_config; + INSERT_PADDING_WORDS_NOINIT(1); + u64 applet_resource_user_id; + }; + static_assert(sizeof(Parameters) == 0x18, "Parameters has incorrect size."); - LOG_WARNING(Service_IRS, - "(STUBBED) called, npad_type={}, npad_id={}, mode={}, mcu_version={}.{}, " - "applet_resource_user_id={}", - camera_handle.npad_type, camera_handle.npad_id, processor_config.mode, - processor_config.required_mcu_version.major, - processor_config.required_mcu_version.minor, applet_resource_user_id); + const auto parameters{rp.PopRaw()}; - const auto result = IsIrCameraHandleValid(camera_handle); + LOG_WARNING( + Service_IRS, + "(STUBBED) called, npad_type={}, npad_id={}, mode={}, mcu_version={}.{}, " + "applet_resource_user_id={}", + parameters.camera_handle.npad_type, parameters.camera_handle.npad_id, + parameters.processor_config.mode, parameters.processor_config.required_mcu_version.major, + parameters.processor_config.required_mcu_version.minor, parameters.applet_resource_user_id); + + const auto result = IsIrCameraHandleValid(parameters.camera_handle); if (result.IsSuccess()) { - auto& device = GetIrCameraSharedMemoryDeviceEntry(camera_handle); - MakeProcessor(camera_handle, device); - auto& image_transfer_processor = GetProcessor(camera_handle); - image_transfer_processor.SetConfig(processor_config); + auto& device = GetIrCameraSharedMemoryDeviceEntry(parameters.camera_handle); + MakeProcessor(parameters.camera_handle, device); + auto& image_transfer_processor = + GetProcessor(parameters.camera_handle); + image_transfer_processor.SetConfig(parameters.processor_config); } IPC::ResponseBuilder rb{ctx, 2}; @@ -299,7 +307,7 @@ void IRS::GetNpadIrCameraHandle(Kernel::HLERequestContext& ctx) { if (npad_id > Core::HID::NpadIdType::Player8 && npad_id != Core::HID::NpadIdType::Invalid && npad_id != Core::HID::NpadIdType::Handheld) { IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(InvalidNpadId); + rb.Push(Service::HID::InvalidNpadId); return; } @@ -553,4 +561,4 @@ IRS_SYS::IRS_SYS(Core::System& system_) : ServiceFramework{system_, "irs:sys"} { IRS_SYS::~IRS_SYS() = default; -} // namespace Service::HID +} // namespace Service::IRS diff --git a/src/core/hle/service/hid/irs.h b/src/core/hle/service/hid/irs.h index ae7f7719b2..516620b4d7 100644 --- a/src/core/hle/service/hid/irs.h +++ b/src/core/hle/service/hid/irs.h @@ -16,7 +16,7 @@ namespace Core::HID { class EmulatedController; } // namespace Core::HID -namespace Service::HID { +namespace Service::IRS { class IRS final : public ServiceFramework { public: @@ -114,4 +114,4 @@ public: ~IRS_SYS() override; }; -} // namespace Service::HID +} // namespace Service::IRS diff --git a/src/core/hle/service/hid/irsensor/clustering_processor.cpp b/src/core/hle/service/hid/irsensor/clustering_processor.cpp index aac3e4fec9..6479af2129 100644 --- a/src/core/hle/service/hid/irsensor/clustering_processor.cpp +++ b/src/core/hle/service/hid/irsensor/clustering_processor.cpp @@ -3,14 +3,12 @@ #include "core/hle/service/hid/irsensor/clustering_processor.h" -namespace Service::HID { +namespace Service::IRS { ClusteringProcessor::ClusteringProcessor(Core::IrSensor::DeviceFormat& device_format) : device(device_format) { device.mode = Core::IrSensor::IrSensorMode::ClusteringProcessor; device.camera_status = Core::IrSensor::IrCameraStatus::Unconnected; device.camera_internal_status = Core::IrSensor::IrCameraInternalStatus::Stopped; - device.state.start = 0; - device.state.count = 0; } ClusteringProcessor::~ClusteringProcessor() = default; @@ -33,4 +31,4 @@ void ClusteringProcessor::SetConfig(Core::IrSensor::PackedClusteringProcessorCon current_config.object_intensity_min = config.object_intensity_min; } -} // namespace Service::HID +} // namespace Service::IRS diff --git a/src/core/hle/service/hid/irsensor/clustering_processor.h b/src/core/hle/service/hid/irsensor/clustering_processor.h index 58b2acf1c2..6e2ba8846b 100644 --- a/src/core/hle/service/hid/irsensor/clustering_processor.h +++ b/src/core/hle/service/hid/irsensor/clustering_processor.h @@ -7,7 +7,7 @@ #include "core/hid/irs_types.h" #include "core/hle/service/hid/irsensor/processor_base.h" -namespace Service::HID { +namespace Service::IRS { class ClusteringProcessor final : public ProcessorBase { public: explicit ClusteringProcessor(Core::IrSensor::DeviceFormat& device_format); @@ -71,4 +71,4 @@ private: ClusteringProcessorConfig current_config{}; Core::IrSensor::DeviceFormat& device; }; -} // namespace Service::HID +} // namespace Service::IRS diff --git a/src/core/hle/service/hid/irsensor/image_transfer_processor.cpp b/src/core/hle/service/hid/irsensor/image_transfer_processor.cpp index 703e825e64..98f0c579de 100644 --- a/src/core/hle/service/hid/irsensor/image_transfer_processor.cpp +++ b/src/core/hle/service/hid/irsensor/image_transfer_processor.cpp @@ -5,7 +5,7 @@ #include "core/hid/hid_core.h" #include "core/hle/service/hid/irsensor/image_transfer_processor.h" -namespace Service::HID { +namespace Service::IRS { ImageTransferProcessor::ImageTransferProcessor(Core::HID::HIDCore& hid_core_, Core::IrSensor::DeviceFormat& device_format, std::size_t npad_index) @@ -21,8 +21,6 @@ ImageTransferProcessor::ImageTransferProcessor(Core::HID::HIDCore& hid_core_, device.mode = Core::IrSensor::IrSensorMode::ImageTransferProcessor; device.camera_status = Core::IrSensor::IrCameraStatus::Unconnected; device.camera_internal_status = Core::IrSensor::IrCameraInternalStatus::Stopped; - device.state.start = 0; - device.state.count = 0; } ImageTransferProcessor::~ImageTransferProcessor() { @@ -33,8 +31,6 @@ void ImageTransferProcessor::StartProcessor() { is_active = true; device.camera_status = Core::IrSensor::IrCameraStatus::Available; device.camera_internal_status = Core::IrSensor::IrCameraInternalStatus::Ready; - device.state.start = 0; - device.state.count = 1; processor_state.sampling_number = 0; processor_state.ambient_noise_level = Core::IrSensor::CameraAmbientNoiseLevel::Low; } @@ -151,4 +147,4 @@ Core::IrSensor::ImageTransferProcessorState ImageTransferProcessor::GetState( return processor_state; } -} // namespace Service::HID +} // namespace Service::IRS diff --git a/src/core/hle/service/hid/irsensor/image_transfer_processor.h b/src/core/hle/service/hid/irsensor/image_transfer_processor.h index b557eaf204..393df492d7 100644 --- a/src/core/hle/service/hid/irsensor/image_transfer_processor.h +++ b/src/core/hle/service/hid/irsensor/image_transfer_processor.h @@ -11,7 +11,7 @@ namespace Core::HID { class EmulatedController; } // namespace Core::HID -namespace Service::HID { +namespace Service::IRS { class ImageTransferProcessor final : public ProcessorBase { public: explicit ImageTransferProcessor(Core::HID::HIDCore& hid_core_, @@ -70,4 +70,4 @@ private: u8* transfer_memory = nullptr; bool is_transfer_memory_set = false; }; -} // namespace Service::HID +} // namespace Service::IRS diff --git a/src/core/hle/service/hid/irsensor/ir_led_processor.cpp b/src/core/hle/service/hid/irsensor/ir_led_processor.cpp index 656e17c955..8e6dd99e49 100644 --- a/src/core/hle/service/hid/irsensor/ir_led_processor.cpp +++ b/src/core/hle/service/hid/irsensor/ir_led_processor.cpp @@ -3,14 +3,12 @@ #include "core/hle/service/hid/irsensor/ir_led_processor.h" -namespace Service::HID { +namespace Service::IRS { IrLedProcessor::IrLedProcessor(Core::IrSensor::DeviceFormat& device_format) : device(device_format) { device.mode = Core::IrSensor::IrSensorMode::IrLedProcessor; device.camera_status = Core::IrSensor::IrCameraStatus::Unconnected; device.camera_internal_status = Core::IrSensor::IrCameraInternalStatus::Stopped; - device.state.start = 0; - device.state.count = 0; } IrLedProcessor::~IrLedProcessor() = default; @@ -26,4 +24,4 @@ void IrLedProcessor::SetConfig(Core::IrSensor::PackedIrLedProcessorConfig config static_cast(config.light_target); } -} // namespace Service::HID +} // namespace Service::IRS diff --git a/src/core/hle/service/hid/irsensor/ir_led_processor.h b/src/core/hle/service/hid/irsensor/ir_led_processor.h index cb04d0b941..c3d8693c9e 100644 --- a/src/core/hle/service/hid/irsensor/ir_led_processor.h +++ b/src/core/hle/service/hid/irsensor/ir_led_processor.h @@ -8,7 +8,7 @@ #include "core/hid/irs_types.h" #include "core/hle/service/hid/irsensor/processor_base.h" -namespace Service::HID { +namespace Service::IRS { class IrLedProcessor final : public ProcessorBase { public: explicit IrLedProcessor(Core::IrSensor::DeviceFormat& device_format); @@ -44,4 +44,4 @@ private: Core::IrSensor::DeviceFormat& device; }; -} // namespace Service::HID +} // namespace Service::IRS diff --git a/src/core/hle/service/hid/irsensor/moment_processor.cpp b/src/core/hle/service/hid/irsensor/moment_processor.cpp index fb36be577c..dbaca420ad 100644 --- a/src/core/hle/service/hid/irsensor/moment_processor.cpp +++ b/src/core/hle/service/hid/irsensor/moment_processor.cpp @@ -3,14 +3,12 @@ #include "core/hle/service/hid/irsensor/moment_processor.h" -namespace Service::HID { +namespace Service::IRS { MomentProcessor::MomentProcessor(Core::IrSensor::DeviceFormat& device_format) : device(device_format) { device.mode = Core::IrSensor::IrSensorMode::MomentProcessor; device.camera_status = Core::IrSensor::IrCameraStatus::Unconnected; device.camera_internal_status = Core::IrSensor::IrCameraInternalStatus::Stopped; - device.state.start = 0; - device.state.count = 0; } MomentProcessor::~MomentProcessor() = default; @@ -33,4 +31,4 @@ void MomentProcessor::SetConfig(Core::IrSensor::PackedMomentProcessorConfig conf current_config.preprocess_intensity_threshold = config.preprocess_intensity_threshold; } -} // namespace Service::HID +} // namespace Service::IRS diff --git a/src/core/hle/service/hid/irsensor/moment_processor.h b/src/core/hle/service/hid/irsensor/moment_processor.h index 63cdadfda7..d4bd22e0f2 100644 --- a/src/core/hle/service/hid/irsensor/moment_processor.h +++ b/src/core/hle/service/hid/irsensor/moment_processor.h @@ -8,7 +8,7 @@ #include "core/hid/irs_types.h" #include "core/hle/service/hid/irsensor/processor_base.h" -namespace Service::HID { +namespace Service::IRS { class MomentProcessor final : public ProcessorBase { public: explicit MomentProcessor(Core::IrSensor::DeviceFormat& device_format); @@ -58,4 +58,4 @@ private: Core::IrSensor::DeviceFormat& device; }; -} // namespace Service::HID +} // namespace Service::IRS diff --git a/src/core/hle/service/hid/irsensor/pointing_processor.cpp b/src/core/hle/service/hid/irsensor/pointing_processor.cpp index 7eb1236363..929f177fc6 100644 --- a/src/core/hle/service/hid/irsensor/pointing_processor.cpp +++ b/src/core/hle/service/hid/irsensor/pointing_processor.cpp @@ -3,14 +3,12 @@ #include "core/hle/service/hid/irsensor/pointing_processor.h" -namespace Service::HID { +namespace Service::IRS { PointingProcessor::PointingProcessor(Core::IrSensor::DeviceFormat& device_format) : device(device_format) { device.mode = Core::IrSensor::IrSensorMode::PointingProcessorMarker; device.camera_status = Core::IrSensor::IrCameraStatus::Unconnected; device.camera_internal_status = Core::IrSensor::IrCameraInternalStatus::Stopped; - device.state.start = 0; - device.state.count = 0; } PointingProcessor::~PointingProcessor() = default; @@ -25,4 +23,4 @@ void PointingProcessor::SetConfig(Core::IrSensor::PackedPointingProcessorConfig current_config.window_of_interest = config.window_of_interest; } -} // namespace Service::HID +} // namespace Service::IRS diff --git a/src/core/hle/service/hid/irsensor/pointing_processor.h b/src/core/hle/service/hid/irsensor/pointing_processor.h index 2f67eff919..cf49307944 100644 --- a/src/core/hle/service/hid/irsensor/pointing_processor.h +++ b/src/core/hle/service/hid/irsensor/pointing_processor.h @@ -3,12 +3,11 @@ #pragma once -#include "common/bit_field.h" #include "common/common_types.h" #include "core/hid/irs_types.h" #include "core/hle/service/hid/irsensor/processor_base.h" -namespace Service::HID { +namespace Service::IRS { class PointingProcessor final : public ProcessorBase { public: explicit PointingProcessor(Core::IrSensor::DeviceFormat& device_format); @@ -59,4 +58,4 @@ private: Core::IrSensor::DeviceFormat& device; }; -} // namespace Service::HID +} // namespace Service::IRS diff --git a/src/core/hle/service/hid/irsensor/processor_base.cpp b/src/core/hle/service/hid/irsensor/processor_base.cpp index bc8025375c..4d43ca17ac 100644 --- a/src/core/hle/service/hid/irsensor/processor_base.cpp +++ b/src/core/hle/service/hid/irsensor/processor_base.cpp @@ -3,7 +3,7 @@ #include "core/hle/service/hid/irsensor/processor_base.h" -namespace Service::HID { +namespace Service::IRS { ProcessorBase::ProcessorBase() {} ProcessorBase::~ProcessorBase() = default; @@ -64,4 +64,4 @@ std::size_t ProcessorBase::GetDataHeight( } } -} // namespace Service::HID +} // namespace Service::IRS diff --git a/src/core/hle/service/hid/irsensor/processor_base.h b/src/core/hle/service/hid/irsensor/processor_base.h index 49831aab6b..bc0d2977bc 100644 --- a/src/core/hle/service/hid/irsensor/processor_base.h +++ b/src/core/hle/service/hid/irsensor/processor_base.h @@ -6,7 +6,7 @@ #include "common/common_types.h" #include "core/hid/irs_types.h" -namespace Service::HID { +namespace Service::IRS { class ProcessorBase { public: explicit ProcessorBase(); @@ -30,4 +30,4 @@ protected: bool is_active{false}; }; -} // namespace Service::HID +} // namespace Service::IRS diff --git a/src/core/hle/service/hid/irsensor/tera_plugin_processor.cpp b/src/core/hle/service/hid/irsensor/tera_plugin_processor.cpp index deec22072e..e691c840a4 100644 --- a/src/core/hle/service/hid/irsensor/tera_plugin_processor.cpp +++ b/src/core/hle/service/hid/irsensor/tera_plugin_processor.cpp @@ -3,14 +3,12 @@ #include "core/hle/service/hid/irsensor/tera_plugin_processor.h" -namespace Service::HID { +namespace Service::IRS { TeraPluginProcessor::TeraPluginProcessor(Core::IrSensor::DeviceFormat& device_format) : device(device_format) { device.mode = Core::IrSensor::IrSensorMode::TeraPluginProcessor; device.camera_status = Core::IrSensor::IrCameraStatus::Unconnected; device.camera_internal_status = Core::IrSensor::IrCameraInternalStatus::Stopped; - device.state.start = 0; - device.state.count = 0; } TeraPluginProcessor::~TeraPluginProcessor() = default; @@ -28,4 +26,4 @@ void TeraPluginProcessor::SetConfig(Core::IrSensor::PackedTeraPluginProcessorCon current_config.unknown_3 = config.unknown_3; } -} // namespace Service::HID +} // namespace Service::IRS diff --git a/src/core/hle/service/hid/irsensor/tera_plugin_processor.h b/src/core/hle/service/hid/irsensor/tera_plugin_processor.h index 60f8057a5d..bbea7ed0b5 100644 --- a/src/core/hle/service/hid/irsensor/tera_plugin_processor.h +++ b/src/core/hle/service/hid/irsensor/tera_plugin_processor.h @@ -8,7 +8,7 @@ #include "core/hid/irs_types.h" #include "core/hle/service/hid/irsensor/processor_base.h" -namespace Service::HID { +namespace Service::IRS { class TeraPluginProcessor final : public ProcessorBase { public: explicit TeraPluginProcessor(Core::IrSensor::DeviceFormat& device_format); @@ -50,4 +50,4 @@ private: Core::IrSensor::DeviceFormat& device; }; -} // namespace Service::HID +} // namespace Service::IRS From 403bdc4dafe89a463f3d93b9a389a1010ca5ff16 Mon Sep 17 00:00:00 2001 From: Narr the Reg Date: Tue, 28 Jun 2022 19:35:51 -0500 Subject: [PATCH 35/50] yuzu: Add webcam support and rebase to latest master --- src/common/settings.h | 4 ++-- src/core/hle/service/hid/errors.h | 4 ++-- src/core/hle/service/hid/irs.cpp | 2 +- src/core/hle/service/hid/irs.h | 2 +- src/yuzu/bootmanager.cpp | 19 ++++++++++++---- src/yuzu/bootmanager.h | 2 ++ src/yuzu/configuration/configure_camera.cpp | 24 +++++++++++++++------ src/yuzu/configuration/configure_camera.h | 2 ++ 8 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/common/settings.h b/src/common/settings.h index 20959ec89d..1079cf8cb4 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -503,8 +503,8 @@ struct Values { Setting enable_ring_controller{true, "enable_ring_controller"}; RingconRaw ringcon_analogs; - BasicSetting enable_ir_sensor{false, "enable_ir_sensor"}; - BasicSetting ir_sensor_device{"auto", "ir_sensor_device"}; + Setting enable_ir_sensor{false, "enable_ir_sensor"}; + Setting ir_sensor_device{"auto", "ir_sensor_device"}; // Data Storage Setting use_virtual_sd{true, "use_virtual_sd"}; diff --git a/src/core/hle/service/hid/errors.h b/src/core/hle/service/hid/errors.h index accc2b646c..4613a4e607 100644 --- a/src/core/hle/service/hid/errors.h +++ b/src/core/hle/service/hid/errors.h @@ -22,7 +22,7 @@ constexpr Result NpadNotConnected{ErrorModule::HID, 710}; namespace Service::IRS { -constexpr ResultCode InvalidProcessorState{ErrorModule::Irsensor, 78}; -constexpr ResultCode InvalidIrCameraHandle{ErrorModule::Irsensor, 204}; +constexpr Result InvalidProcessorState{ErrorModule::Irsensor, 78}; +constexpr Result InvalidIrCameraHandle{ErrorModule::Irsensor, 204}; } // namespace Service::IRS diff --git a/src/core/hle/service/hid/irs.cpp b/src/core/hle/service/hid/irs.cpp index b6e7c07922..d5107e41f1 100644 --- a/src/core/hle/service/hid/irs.cpp +++ b/src/core/hle/service/hid/irs.cpp @@ -529,7 +529,7 @@ void IRS::ActivateIrsensorWithFunctionLevel(Kernel::HLERequestContext& ctx) { rb.Push(ResultSuccess); } -ResultCode IRS::IsIrCameraHandleValid(const Core::IrSensor::IrCameraHandle& camera_handle) const { +Result IRS::IsIrCameraHandleValid(const Core::IrSensor::IrCameraHandle& camera_handle) const { if (camera_handle.npad_id > static_cast(NpadIdTypeToIndex(Core::HID::NpadIdType::Handheld))) { return InvalidIrCameraHandle; diff --git a/src/core/hle/service/hid/irs.h b/src/core/hle/service/hid/irs.h index 516620b4d7..2e6115c73e 100644 --- a/src/core/hle/service/hid/irs.h +++ b/src/core/hle/service/hid/irs.h @@ -57,7 +57,7 @@ private: void StopImageProcessorAsync(Kernel::HLERequestContext& ctx); void ActivateIrsensorWithFunctionLevel(Kernel::HLERequestContext& ctx); - ResultCode IsIrCameraHandleValid(const Core::IrSensor::IrCameraHandle& camera_handle) const; + Result IsIrCameraHandleValid(const Core::IrSensor::IrCameraHandle& camera_handle) const; Core::IrSensor::DeviceFormat& GetIrCameraSharedMemoryDeviceEntry( const Core::IrSensor::IrCameraHandle& camera_handle); diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp index 7740858097..0ee3820a2d 100644 --- a/src/yuzu/bootmanager.cpp +++ b/src/yuzu/bootmanager.cpp @@ -830,6 +830,10 @@ void GRenderWindow::InitializeCamera() { camera->unload(); camera->setCaptureMode(QCamera::CaptureViewfinder); camera->load(); + camera->start(); + + pending_camera_snapshots = 0; + is_virtual_camera = false; camera_timer = std::make_unique(); connect(camera_timer.get(), &QTimer::timeout, [this] { RequestCameraCapture(); }); @@ -851,11 +855,17 @@ void GRenderWindow::RequestCameraCapture() { return; } - // Idealy one should only call capture but Qt refuses to take a second capture without - // stopping the camera - camera->stop(); - camera->start(); + // If the camera doesn't capture, test for virtual cameras + if (pending_camera_snapshots > 5) { + is_virtual_camera = true; + } + // Virtual cameras like obs need to reset the camera every capture + if (is_virtual_camera) { + camera->stop(); + camera->start(); + } + pending_camera_snapshots++; camera_capture->capture(); } @@ -870,6 +880,7 @@ void GRenderWindow::OnCameraCapture(int requestId, const QImage& img) { camera_data.resize(camera_width * camera_height); std::memcpy(camera_data.data(), converted.bits(), camera_width * camera_height * sizeof(u32)); input_subsystem->GetCamera()->SetCameraData(camera_width, camera_height, camera_data); + pending_camera_snapshots = 0; } bool GRenderWindow::event(QEvent* event) { diff --git a/src/yuzu/bootmanager.h b/src/yuzu/bootmanager.h index 346201768a..b4781e6975 100644 --- a/src/yuzu/bootmanager.h +++ b/src/yuzu/bootmanager.h @@ -240,6 +240,8 @@ private: bool first_frame = false; InputCommon::TasInput::TasState last_tas_state; + bool is_virtual_camera; + int pending_camera_snapshots; std::unique_ptr camera; std::unique_ptr camera_capture; std::unique_ptr camera_timer; diff --git a/src/yuzu/configuration/configure_camera.cpp b/src/yuzu/configuration/configure_camera.cpp index 97febb33c6..73cdcf3f21 100644 --- a/src/yuzu/configuration/configure_camera.cpp +++ b/src/yuzu/configuration/configure_camera.cpp @@ -39,8 +39,8 @@ void ConfigureCamera::PreviewCamera() { for (const QCameraInfo& cameraInfo : cameras) { if (input_devices[index] == cameraInfo.deviceName().toStdString() || input_devices[index] == "Auto") { - LOG_ERROR(Frontend, "Selected Camera {} {}", cameraInfo.description().toStdString(), - cameraInfo.deviceName().toStdString()); + LOG_INFO(Frontend, "Selected Camera {} {}", cameraInfo.description().toStdString(), + cameraInfo.deviceName().toStdString()); camera = std::make_unique(cameraInfo); camera_found = true; break; @@ -62,12 +62,23 @@ void ConfigureCamera::PreviewCamera() { camera->unload(); camera->setCaptureMode(QCamera::CaptureViewfinder); camera->load(); + camera->start(); + + pending_snapshots = 0; + is_virtual_camera = false; camera_timer = std::make_unique(); connect(camera_timer.get(), &QTimer::timeout, [this] { - camera->stop(); - camera->start(); - + // If the camera doesn't capture, test for virtual cameras + if (pending_snapshots > 5) { + is_virtual_camera = true; + } + // Virtual cameras like obs need to reset the camera every capture + if (is_virtual_camera) { + camera->stop(); + camera->start(); + } + pending_snapshots++; camera_capture->capture(); }); @@ -75,10 +86,11 @@ void ConfigureCamera::PreviewCamera() { } void ConfigureCamera::DisplayCapturedFrame(int requestId, const QImage& img) { - LOG_ERROR(Frontend, "ImageCaptured {} {}", img.width(), img.height()); + LOG_INFO(Frontend, "ImageCaptured {} {}", img.width(), img.height()); const auto converted = img.scaled(320, 240, Qt::AspectRatioMode::IgnoreAspectRatio, Qt::TransformationMode::SmoothTransformation); ui->preview_box->setPixmap(QPixmap::fromImage(converted)); + pending_snapshots = 0; } void ConfigureCamera::changeEvent(QEvent* event) { diff --git a/src/yuzu/configuration/configure_camera.h b/src/yuzu/configuration/configure_camera.h index af7551c030..db9833b5c2 100644 --- a/src/yuzu/configuration/configure_camera.h +++ b/src/yuzu/configuration/configure_camera.h @@ -44,6 +44,8 @@ private: InputCommon::InputSubsystem* input_subsystem; + bool is_virtual_camera; + int pending_snapshots; std::unique_ptr camera; std::unique_ptr camera_capture; std::unique_ptr camera_timer; From dcfe0a5febb252e3a4f40c1b25765740a269467f Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Wed, 6 Jul 2022 02:20:39 +0200 Subject: [PATCH 36/50] network: Add initial files and enet dependency --- .gitmodules | 3 + externals/CMakeLists.txt | 4 + externals/enet | 1 + src/CMakeLists.txt | 1 + src/network/CMakeLists.txt | 16 + src/network/network.cpp | 50 ++ src/network/network.h | 25 + src/network/packet.cpp | 263 +++++++++ src/network/packet.h | 166 ++++++ src/network/room.cpp | 1111 +++++++++++++++++++++++++++++++++++ src/network/room.h | 173 ++++++ src/network/room_member.cpp | 694 ++++++++++++++++++++++ src/network/room_member.h | 327 +++++++++++ src/network/verify_user.cpp | 18 + src/network/verify_user.h | 46 ++ 15 files changed, 2898 insertions(+) create mode 160000 externals/enet create mode 100644 src/network/CMakeLists.txt create mode 100644 src/network/network.cpp create mode 100644 src/network/network.h create mode 100644 src/network/packet.cpp create mode 100644 src/network/packet.h create mode 100644 src/network/room.cpp create mode 100644 src/network/room.h create mode 100644 src/network/room_member.cpp create mode 100644 src/network/room_member.h create mode 100644 src/network/verify_user.cpp create mode 100644 src/network/verify_user.h diff --git a/.gitmodules b/.gitmodules index 3c0d15951d..d8e04923ad 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ +[submodule "enet"] + path = externals/enet + url = https://github.com/lsalzman/enet.git [submodule "inih"] path = externals/inih/inih url = https://github.com/benhoyt/inih.git diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index bd01f4c4db..b4570bb697 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -73,6 +73,10 @@ if (YUZU_USE_EXTERNAL_SDL2) add_library(SDL2 ALIAS SDL2-static) endif() +# ENet +add_subdirectory(enet) +target_include_directories(enet INTERFACE ./enet/include) + # Cubeb if(ENABLE_CUBEB) set(BUILD_TESTS OFF CACHE BOOL "") diff --git a/externals/enet b/externals/enet new file mode 160000 index 0000000000..39a72ab199 --- /dev/null +++ b/externals/enet @@ -0,0 +1 @@ +Subproject commit 39a72ab1990014eb399cee9d538fd529df99c6a0 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 39ae573b26..9367f67c10 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -156,6 +156,7 @@ add_subdirectory(common) add_subdirectory(core) add_subdirectory(audio_core) add_subdirectory(video_core) +add_subdirectory(network) add_subdirectory(input_common) add_subdirectory(shader_recompiler) diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt new file mode 100644 index 0000000000..382a69e2f7 --- /dev/null +++ b/src/network/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(network STATIC + network.cpp + network.h + packet.cpp + packet.h + room.cpp + room.h + room_member.cpp + room_member.h + verify_user.cpp + verify_user.h +) + +create_target_directory_groups(network) + +target_link_libraries(network PRIVATE common enet Boost::boost) diff --git a/src/network/network.cpp b/src/network/network.cpp new file mode 100644 index 0000000000..51b5d6a9fa --- /dev/null +++ b/src/network/network.cpp @@ -0,0 +1,50 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/assert.h" +#include "common/logging/log.h" +#include "enet/enet.h" +#include "network/network.h" + +namespace Network { + +static std::shared_ptr g_room_member; ///< RoomMember (Client) for network games +static std::shared_ptr g_room; ///< Room (Server) for network games +// TODO(B3N30): Put these globals into a networking class + +bool Init() { + if (enet_initialize() != 0) { + LOG_ERROR(Network, "Error initalizing ENet"); + return false; + } + g_room = std::make_shared(); + g_room_member = std::make_shared(); + LOG_DEBUG(Network, "initialized OK"); + return true; +} + +std::weak_ptr GetRoom() { + return g_room; +} + +std::weak_ptr GetRoomMember() { + return g_room_member; +} + +void Shutdown() { + if (g_room_member) { + if (g_room_member->IsConnected()) + g_room_member->Leave(); + g_room_member.reset(); + } + if (g_room) { + if (g_room->GetState() == Room::State::Open) + g_room->Destroy(); + g_room.reset(); + } + enet_deinitialize(); + LOG_DEBUG(Network, "shutdown OK"); +} + +} // namespace Network diff --git a/src/network/network.h b/src/network/network.h new file mode 100644 index 0000000000..6d002d693f --- /dev/null +++ b/src/network/network.h @@ -0,0 +1,25 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "network/room.h" +#include "network/room_member.h" + +namespace Network { + +/// Initializes and registers the network device, the room, and the room member. +bool Init(); + +/// Returns a pointer to the room handle +std::weak_ptr GetRoom(); + +/// Returns a pointer to the room member handle +std::weak_ptr GetRoomMember(); + +/// Unregisters the network device, the room, and the room member and shut them down. +void Shutdown(); + +} // namespace Network diff --git a/src/network/packet.cpp b/src/network/packet.cpp new file mode 100644 index 0000000000..8fc5feabd4 --- /dev/null +++ b/src/network/packet.cpp @@ -0,0 +1,263 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#ifdef _WIN32 +#include +#else +#include +#endif +#include +#include +#include "network/packet.h" + +namespace Network { + +#ifndef htonll +u64 htonll(u64 x) { + return ((1 == htonl(1)) ? (x) : ((uint64_t)htonl((x)&0xFFFFFFFF) << 32) | htonl((x) >> 32)); +} +#endif + +#ifndef ntohll +u64 ntohll(u64 x) { + return ((1 == ntohl(1)) ? (x) : ((uint64_t)ntohl((x)&0xFFFFFFFF) << 32) | ntohl((x) >> 32)); +} +#endif + +void Packet::Append(const void* in_data, std::size_t size_in_bytes) { + if (in_data && (size_in_bytes > 0)) { + std::size_t start = data.size(); + data.resize(start + size_in_bytes); + std::memcpy(&data[start], in_data, size_in_bytes); + } +} + +void Packet::Read(void* out_data, std::size_t size_in_bytes) { + if (out_data && CheckSize(size_in_bytes)) { + std::memcpy(out_data, &data[read_pos], size_in_bytes); + read_pos += size_in_bytes; + } +} + +void Packet::Clear() { + data.clear(); + read_pos = 0; + is_valid = true; +} + +const void* Packet::GetData() const { + return !data.empty() ? &data[0] : nullptr; +} + +void Packet::IgnoreBytes(u32 length) { + read_pos += length; +} + +std::size_t Packet::GetDataSize() const { + return data.size(); +} + +bool Packet::EndOfPacket() const { + return read_pos >= data.size(); +} + +Packet::operator bool() const { + return is_valid; +} + +Packet& Packet::operator>>(bool& out_data) { + u8 value; + if (*this >> value) { + out_data = (value != 0); + } + return *this; +} + +Packet& Packet::operator>>(s8& out_data) { + Read(&out_data, sizeof(out_data)); + return *this; +} + +Packet& Packet::operator>>(u8& out_data) { + Read(&out_data, sizeof(out_data)); + return *this; +} + +Packet& Packet::operator>>(s16& out_data) { + s16 value; + Read(&value, sizeof(value)); + out_data = ntohs(value); + return *this; +} + +Packet& Packet::operator>>(u16& out_data) { + u16 value; + Read(&value, sizeof(value)); + out_data = ntohs(value); + return *this; +} + +Packet& Packet::operator>>(s32& out_data) { + s32 value; + Read(&value, sizeof(value)); + out_data = ntohl(value); + return *this; +} + +Packet& Packet::operator>>(u32& out_data) { + u32 value; + Read(&value, sizeof(value)); + out_data = ntohl(value); + return *this; +} + +Packet& Packet::operator>>(s64& out_data) { + s64 value; + Read(&value, sizeof(value)); + out_data = ntohll(value); + return *this; +} + +Packet& Packet::operator>>(u64& out_data) { + u64 value; + Read(&value, sizeof(value)); + out_data = ntohll(value); + return *this; +} + +Packet& Packet::operator>>(float& out_data) { + Read(&out_data, sizeof(out_data)); + return *this; +} + +Packet& Packet::operator>>(double& out_data) { + Read(&out_data, sizeof(out_data)); + return *this; +} + +Packet& Packet::operator>>(char* out_data) { + // First extract string length + u32 length = 0; + *this >> length; + + if ((length > 0) && CheckSize(length)) { + // Then extract characters + std::memcpy(out_data, &data[read_pos], length); + out_data[length] = '\0'; + + // Update reading position + read_pos += length; + } + + return *this; +} + +Packet& Packet::operator>>(std::string& out_data) { + // First extract string length + u32 length = 0; + *this >> length; + + out_data.clear(); + if ((length > 0) && CheckSize(length)) { + // Then extract characters + out_data.assign(&data[read_pos], length); + + // Update reading position + read_pos += length; + } + + return *this; +} + +Packet& Packet::operator<<(bool in_data) { + *this << static_cast(in_data); + return *this; +} + +Packet& Packet::operator<<(s8 in_data) { + Append(&in_data, sizeof(in_data)); + return *this; +} + +Packet& Packet::operator<<(u8 in_data) { + Append(&in_data, sizeof(in_data)); + return *this; +} + +Packet& Packet::operator<<(s16 in_data) { + s16 toWrite = htons(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::operator<<(u16 in_data) { + u16 toWrite = htons(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::operator<<(s32 in_data) { + s32 toWrite = htonl(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::operator<<(u32 in_data) { + u32 toWrite = htonl(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::operator<<(s64 in_data) { + s64 toWrite = htonll(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::operator<<(u64 in_data) { + u64 toWrite = htonll(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::operator<<(float in_data) { + Append(&in_data, sizeof(in_data)); + return *this; +} + +Packet& Packet::operator<<(double in_data) { + Append(&in_data, sizeof(in_data)); + return *this; +} + +Packet& Packet::operator<<(const char* in_data) { + // First insert string length + u32 length = static_cast(std::strlen(in_data)); + *this << length; + + // Then insert characters + Append(in_data, length * sizeof(char)); + + return *this; +} + +Packet& Packet::operator<<(const std::string& in_data) { + // First insert string length + u32 length = static_cast(in_data.size()); + *this << length; + + // Then insert characters + if (length > 0) + Append(in_data.c_str(), length * sizeof(std::string::value_type)); + + return *this; +} + +bool Packet::CheckSize(std::size_t size) { + is_valid = is_valid && (read_pos + size <= data.size()); + + return is_valid; +} + +} // namespace Network diff --git a/src/network/packet.h b/src/network/packet.h new file mode 100644 index 0000000000..7bdc3da958 --- /dev/null +++ b/src/network/packet.h @@ -0,0 +1,166 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "common/common_types.h" + +namespace Network { + +/// A class that serializes data for network transfer. It also handles endianess +class Packet { +public: + Packet() = default; + ~Packet() = default; + + /** + * Append data to the end of the packet + * @param data Pointer to the sequence of bytes to append + * @param size_in_bytes Number of bytes to append + */ + void Append(const void* data, std::size_t size_in_bytes); + + /** + * Reads data from the current read position of the packet + * @param out_data Pointer where the data should get written to + * @param size_in_bytes Number of bytes to read + */ + void Read(void* out_data, std::size_t size_in_bytes); + + /** + * Clear the packet + * After calling Clear, the packet is empty. + */ + void Clear(); + + /** + * Ignores bytes while reading + * @param length THe number of bytes to ignore + */ + void IgnoreBytes(u32 length); + + /** + * Get a pointer to the data contained in the packet + * @return Pointer to the data + */ + const void* GetData() const; + + /** + * This function returns the number of bytes pointed to by + * what getData returns. + * @return Data size, in bytes + */ + std::size_t GetDataSize() const; + + /** + * This function is useful to know if there is some data + * left to be read, without actually reading it. + * @return True if all data was read, false otherwise + */ + bool EndOfPacket() const; + + explicit operator bool() const; + + /// Overloads of operator >> to read data from the packet + Packet& operator>>(bool& out_data); + Packet& operator>>(s8& out_data); + Packet& operator>>(u8& out_data); + Packet& operator>>(s16& out_data); + Packet& operator>>(u16& out_data); + Packet& operator>>(s32& out_data); + Packet& operator>>(u32& out_data); + Packet& operator>>(s64& out_data); + Packet& operator>>(u64& out_data); + Packet& operator>>(float& out_data); + Packet& operator>>(double& out_data); + Packet& operator>>(char* out_data); + Packet& operator>>(std::string& out_data); + template + Packet& operator>>(std::vector& out_data); + template + Packet& operator>>(std::array& out_data); + + /// Overloads of operator << to write data into the packet + Packet& operator<<(bool in_data); + Packet& operator<<(s8 in_data); + Packet& operator<<(u8 in_data); + Packet& operator<<(s16 in_data); + Packet& operator<<(u16 in_data); + Packet& operator<<(s32 in_data); + Packet& operator<<(u32 in_data); + Packet& operator<<(s64 in_data); + Packet& operator<<(u64 in_data); + Packet& operator<<(float in_data); + Packet& operator<<(double in_data); + Packet& operator<<(const char* in_data); + Packet& operator<<(const std::string& in_data); + template + Packet& operator<<(const std::vector& in_data); + template + Packet& operator<<(const std::array& data); + +private: + /** + * Check if the packet can extract a given number of bytes + * This function updates accordingly the state of the packet. + * @param size Size to check + * @return True if size bytes can be read from the packet + */ + bool CheckSize(std::size_t size); + + // Member data + std::vector data; ///< Data stored in the packet + std::size_t read_pos = 0; ///< Current reading position in the packet + bool is_valid = true; ///< Reading state of the packet +}; + +template +Packet& Packet::operator>>(std::vector& out_data) { + // First extract the size + u32 size = 0; + *this >> size; + out_data.resize(size); + + // Then extract the data + for (std::size_t i = 0; i < out_data.size(); ++i) { + T character; + *this >> character; + out_data[i] = character; + } + return *this; +} + +template +Packet& Packet::operator>>(std::array& out_data) { + for (std::size_t i = 0; i < out_data.size(); ++i) { + T character; + *this >> character; + out_data[i] = character; + } + return *this; +} + +template +Packet& Packet::operator<<(const std::vector& in_data) { + // First insert the size + *this << static_cast(in_data.size()); + + // Then insert the data + for (std::size_t i = 0; i < in_data.size(); ++i) { + *this << in_data[i]; + } + return *this; +} + +template +Packet& Packet::operator<<(const std::array& in_data) { + for (std::size_t i = 0; i < in_data.size(); ++i) { + *this << in_data[i]; + } + return *this; +} + +} // namespace Network diff --git a/src/network/room.cpp b/src/network/room.cpp new file mode 100644 index 0000000000..cd0c0ebc49 --- /dev/null +++ b/src/network/room.cpp @@ -0,0 +1,1111 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "enet/enet.h" +#include "network/packet.h" +#include "network/room.h" +#include "network/verify_user.h" + +namespace Network { + +class Room::RoomImpl { +public: + // This MAC address is used to generate a 'Nintendo' like Mac address. + const MacAddress NintendoOUI; + std::mt19937 random_gen; ///< Random number generator. Used for GenerateMacAddress + + ENetHost* server = nullptr; ///< Network interface. + + std::atomic state{State::Closed}; ///< Current state of the room. + RoomInformation room_information; ///< Information about this room. + + std::string verify_UID; ///< A GUID which may be used for verfication. + mutable std::mutex verify_UID_mutex; ///< Mutex for verify_UID + + std::string password; ///< The password required to connect to this room. + + struct Member { + std::string nickname; ///< The nickname of the member. + std::string console_id_hash; ///< A hash of the console ID of the member. + GameInfo game_info; ///< The current game of the member + MacAddress mac_address; ///< The assigned mac address of the member. + /// Data of the user, often including authenticated forum username. + VerifyUser::UserData user_data; + ENetPeer* peer; ///< The remote peer. + }; + using MemberList = std::vector; + MemberList members; ///< Information about the members of this room + mutable std::mutex member_mutex; ///< Mutex for locking the members list + /// This should be a std::shared_mutex as soon as C++17 is supported + + UsernameBanList username_ban_list; ///< List of banned usernames + IPBanList ip_ban_list; ///< List of banned IP addresses + mutable std::mutex ban_list_mutex; ///< Mutex for the ban lists + + RoomImpl() + : NintendoOUI{0x00, 0x1F, 0x32, 0x00, 0x00, 0x00}, random_gen(std::random_device()()) {} + + /// Thread that receives and dispatches network packets + std::unique_ptr room_thread; + + /// Verification backend of the room + std::unique_ptr verify_backend; + + /// Thread function that will receive and dispatch messages until the room is destroyed. + void ServerLoop(); + void StartLoop(); + + /** + * Parses and answers a room join request from a client. + * Validates the uniqueness of the username and assigns the MAC address + * that the client will use for the remainder of the connection. + */ + void HandleJoinRequest(const ENetEvent* event); + + /** + * Parses and answers a kick request from a client. + * Validates the permissions and that the given user exists and then kicks the member. + */ + void HandleModKickPacket(const ENetEvent* event); + + /** + * Parses and answers a ban request from a client. + * Validates the permissions and bans the user (by forum username or IP). + */ + void HandleModBanPacket(const ENetEvent* event); + + /** + * Parses and answers a unban request from a client. + * Validates the permissions and unbans the address. + */ + void HandleModUnbanPacket(const ENetEvent* event); + + /** + * Parses and answers a get ban list request from a client. + * Validates the permissions and returns the ban list. + */ + void HandleModGetBanListPacket(const ENetEvent* event); + + /** + * Returns whether the nickname is valid, ie. isn't already taken by someone else in the room. + */ + bool IsValidNickname(const std::string& nickname) const; + + /** + * Returns whether the MAC address is valid, ie. isn't already taken by someone else in the + * room. + */ + bool IsValidMacAddress(const MacAddress& address) const; + + /** + * Returns whether the console ID (hash) is valid, ie. isn't already taken by someone else in + * the room. + */ + bool IsValidConsoleId(const std::string& console_id_hash) const; + + /** + * Returns whether a user has mod permissions. + */ + bool HasModPermission(const ENetPeer* client) const; + + /** + * Sends a ID_ROOM_IS_FULL message telling the client that the room is full. + */ + void SendRoomIsFull(ENetPeer* client); + + /** + * Sends a ID_ROOM_NAME_COLLISION message telling the client that the name is invalid. + */ + void SendNameCollision(ENetPeer* client); + + /** + * Sends a ID_ROOM_MAC_COLLISION message telling the client that the MAC is invalid. + */ + void SendMacCollision(ENetPeer* client); + + /** + * Sends a IdConsoleIdCollison message telling the client that another member with the same + * console ID exists. + */ + void SendConsoleIdCollision(ENetPeer* client); + + /** + * Sends a ID_ROOM_VERSION_MISMATCH message telling the client that the version is invalid. + */ + void SendVersionMismatch(ENetPeer* client); + + /** + * Sends a ID_ROOM_WRONG_PASSWORD message telling the client that the password is wrong. + */ + void SendWrongPassword(ENetPeer* client); + + /** + * Notifies the member that its connection attempt was successful, + * and it is now part of the room. + */ + void SendJoinSuccess(ENetPeer* client, MacAddress mac_address); + + /** + * Notifies the member that its connection attempt was successful, + * and it is now part of the room, and it has been granted mod permissions. + */ + void SendJoinSuccessAsMod(ENetPeer* client, MacAddress mac_address); + + /** + * Sends a IdHostKicked message telling the client that they have been kicked. + */ + void SendUserKicked(ENetPeer* client); + + /** + * Sends a IdHostBanned message telling the client that they have been banned. + */ + void SendUserBanned(ENetPeer* client); + + /** + * Sends a IdModPermissionDenied message telling the client that they do not have mod + * permission. + */ + void SendModPermissionDenied(ENetPeer* client); + + /** + * Sends a IdModNoSuchUser message telling the client that the given user could not be found. + */ + void SendModNoSuchUser(ENetPeer* client); + + /** + * Sends the ban list in response to a client's request for getting ban list. + */ + void SendModBanListResponse(ENetPeer* client); + + /** + * Notifies the members that the room is closed, + */ + void SendCloseMessage(); + + /** + * Sends a system message to all the connected clients. + */ + void SendStatusMessage(StatusMessageTypes type, const std::string& nickname, + const std::string& username, const std::string& ip); + + /** + * Sends the information about the room, along with the list of members + * to every connected client in the room. + * The packet has the structure: + * ID_ROOM_INFORMATION + * room_name + * room_description + * member_slots: The max number of clients allowed in this room + * uid + * port + * num_members: the number of currently joined clients + * This is followed by the following three values for each member: + * nickname of that member + * mac_address of that member + * game_name of that member + */ + void BroadcastRoomInformation(); + + /** + * Generates a free MAC address to assign to a new client. + * The first 3 bytes are the NintendoOUI 0x00, 0x1F, 0x32 + */ + MacAddress GenerateMacAddress(); + + /** + * Broadcasts this packet to all members except the sender. + * @param event The ENet event containing the data + */ + void HandleWifiPacket(const ENetEvent* event); + + /** + * Extracts a chat entry from a received ENet packet and adds it to the chat queue. + * @param event The ENet event that was received. + */ + void HandleChatPacket(const ENetEvent* event); + + /** + * Extracts the game name from a received ENet packet and broadcasts it. + * @param event The ENet event that was received. + */ + void HandleGameNamePacket(const ENetEvent* event); + + /** + * Removes the client from the members list if it was in it and announces the change + * to all other clients. + */ + void HandleClientDisconnection(ENetPeer* client); +}; + +// RoomImpl +void Room::RoomImpl::ServerLoop() { + while (state != State::Closed) { + ENetEvent event; + if (enet_host_service(server, &event, 50) > 0) { + switch (event.type) { + case ENET_EVENT_TYPE_RECEIVE: + switch (event.packet->data[0]) { + case IdJoinRequest: + HandleJoinRequest(&event); + break; + case IdSetGameInfo: + HandleGameNamePacket(&event); + break; + case IdWifiPacket: + HandleWifiPacket(&event); + break; + case IdChatMessage: + HandleChatPacket(&event); + break; + // Moderation + case IdModKick: + HandleModKickPacket(&event); + break; + case IdModBan: + HandleModBanPacket(&event); + break; + case IdModUnban: + HandleModUnbanPacket(&event); + break; + case IdModGetBanList: + HandleModGetBanListPacket(&event); + break; + } + enet_packet_destroy(event.packet); + break; + case ENET_EVENT_TYPE_DISCONNECT: + HandleClientDisconnection(event.peer); + break; + case ENET_EVENT_TYPE_NONE: + case ENET_EVENT_TYPE_CONNECT: + break; + } + } + } + // Close the connection to all members: + SendCloseMessage(); +} + +void Room::RoomImpl::StartLoop() { + room_thread = std::make_unique(&Room::RoomImpl::ServerLoop, this); +} + +void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { + { + std::lock_guard lock(member_mutex); + if (members.size() >= room_information.member_slots) { + SendRoomIsFull(event->peer); + return; + } + } + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + std::string nickname; + packet >> nickname; + + std::string console_id_hash; + packet >> console_id_hash; + + MacAddress preferred_mac; + packet >> preferred_mac; + + u32 client_version; + packet >> client_version; + + std::string pass; + packet >> pass; + + std::string token; + packet >> token; + + if (pass != password) { + SendWrongPassword(event->peer); + return; + } + + if (!IsValidNickname(nickname)) { + SendNameCollision(event->peer); + return; + } + + if (preferred_mac != NoPreferredMac) { + // Verify if the preferred mac is available + if (!IsValidMacAddress(preferred_mac)) { + SendMacCollision(event->peer); + return; + } + } else { + // Assign a MAC address of this client automatically + preferred_mac = GenerateMacAddress(); + } + + if (!IsValidConsoleId(console_id_hash)) { + SendConsoleIdCollision(event->peer); + return; + } + + if (client_version != network_version) { + SendVersionMismatch(event->peer); + return; + } + + // At this point the client is ready to be added to the room. + Member member{}; + member.mac_address = preferred_mac; + member.console_id_hash = console_id_hash; + member.nickname = nickname; + member.peer = event->peer; + + std::string uid; + { + std::lock_guard lock(verify_UID_mutex); + uid = verify_UID; + } + member.user_data = verify_backend->LoadUserData(uid, token); + + std::string ip; + { + std::lock_guard lock(ban_list_mutex); + + // Check username ban + if (!member.user_data.username.empty() && + std::find(username_ban_list.begin(), username_ban_list.end(), + member.user_data.username) != username_ban_list.end()) { + + SendUserBanned(event->peer); + return; + } + + // Check IP ban + char ip_raw[256]; + enet_address_get_host_ip(&event->peer->address, ip_raw, sizeof(ip_raw) - 1); + ip = ip_raw; + + if (std::find(ip_ban_list.begin(), ip_ban_list.end(), ip) != ip_ban_list.end()) { + SendUserBanned(event->peer); + return; + } + } + + // Notify everyone that the user has joined. + SendStatusMessage(IdMemberJoin, member.nickname, member.user_data.username, ip); + + { + std::lock_guard lock(member_mutex); + members.push_back(std::move(member)); + } + + // Notify everyone that the room information has changed. + BroadcastRoomInformation(); + if (HasModPermission(event->peer)) { + SendJoinSuccessAsMod(event->peer, preferred_mac); + } else { + SendJoinSuccess(event->peer, preferred_mac); + } +} + +void Room::RoomImpl::HandleModKickPacket(const ENetEvent* event) { + if (!HasModPermission(event->peer)) { + SendModPermissionDenied(event->peer); + return; + } + + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + std::string nickname; + packet >> nickname; + + std::string username, ip; + { + std::lock_guard lock(member_mutex); + const auto target_member = + std::find_if(members.begin(), members.end(), + [&nickname](const auto& member) { return member.nickname == nickname; }); + if (target_member == members.end()) { + SendModNoSuchUser(event->peer); + return; + } + + // Notify the kicked member + SendUserKicked(target_member->peer); + + username = target_member->user_data.username; + + char ip_raw[256]; + enet_address_get_host_ip(&target_member->peer->address, ip_raw, sizeof(ip_raw) - 1); + ip = ip_raw; + + enet_peer_disconnect(target_member->peer, 0); + members.erase(target_member); + } + + // Announce the change to all clients. + SendStatusMessage(IdMemberKicked, nickname, username, ip); + BroadcastRoomInformation(); +} + +void Room::RoomImpl::HandleModBanPacket(const ENetEvent* event) { + if (!HasModPermission(event->peer)) { + SendModPermissionDenied(event->peer); + return; + } + + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + std::string nickname; + packet >> nickname; + + std::string username, ip; + { + std::lock_guard lock(member_mutex); + const auto target_member = + std::find_if(members.begin(), members.end(), + [&nickname](const auto& member) { return member.nickname == nickname; }); + if (target_member == members.end()) { + SendModNoSuchUser(event->peer); + return; + } + + // Notify the banned member + SendUserBanned(target_member->peer); + + nickname = target_member->nickname; + username = target_member->user_data.username; + + char ip_raw[256]; + enet_address_get_host_ip(&target_member->peer->address, ip_raw, sizeof(ip_raw) - 1); + ip = ip_raw; + + enet_peer_disconnect(target_member->peer, 0); + members.erase(target_member); + } + + { + std::lock_guard lock(ban_list_mutex); + + if (!username.empty()) { + // Ban the forum username + if (std::find(username_ban_list.begin(), username_ban_list.end(), username) == + username_ban_list.end()) { + + username_ban_list.emplace_back(username); + } + } + + // Ban the member's IP as well + if (std::find(ip_ban_list.begin(), ip_ban_list.end(), ip) == ip_ban_list.end()) { + ip_ban_list.emplace_back(ip); + } + } + + // Announce the change to all clients. + SendStatusMessage(IdMemberBanned, nickname, username, ip); + BroadcastRoomInformation(); +} + +void Room::RoomImpl::HandleModUnbanPacket(const ENetEvent* event) { + if (!HasModPermission(event->peer)) { + SendModPermissionDenied(event->peer); + return; + } + + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + std::string address; + packet >> address; + + bool unbanned = false; + { + std::lock_guard lock(ban_list_mutex); + + auto it = std::find(username_ban_list.begin(), username_ban_list.end(), address); + if (it != username_ban_list.end()) { + unbanned = true; + username_ban_list.erase(it); + } + + it = std::find(ip_ban_list.begin(), ip_ban_list.end(), address); + if (it != ip_ban_list.end()) { + unbanned = true; + ip_ban_list.erase(it); + } + } + + if (unbanned) { + SendStatusMessage(IdAddressUnbanned, address, "", ""); + } else { + SendModNoSuchUser(event->peer); + } +} + +void Room::RoomImpl::HandleModGetBanListPacket(const ENetEvent* event) { + if (!HasModPermission(event->peer)) { + SendModPermissionDenied(event->peer); + return; + } + + SendModBanListResponse(event->peer); +} + +bool Room::RoomImpl::IsValidNickname(const std::string& nickname) const { + // A nickname is valid if it matches the regex and is not already taken by anybody else in the + // room. + const std::regex nickname_regex("^[ a-zA-Z0-9._-]{4,20}$"); + if (!std::regex_match(nickname, nickname_regex)) + return false; + + std::lock_guard lock(member_mutex); + return std::all_of(members.begin(), members.end(), + [&nickname](const auto& member) { return member.nickname != nickname; }); +} + +bool Room::RoomImpl::IsValidMacAddress(const MacAddress& address) const { + // A MAC address is valid if it is not already taken by anybody else in the room. + std::lock_guard lock(member_mutex); + return std::all_of(members.begin(), members.end(), + [&address](const auto& member) { return member.mac_address != address; }); +} + +bool Room::RoomImpl::IsValidConsoleId(const std::string& console_id_hash) const { + // A Console ID is valid if it is not already taken by anybody else in the room. + std::lock_guard lock(member_mutex); + return std::all_of(members.begin(), members.end(), [&console_id_hash](const auto& member) { + return member.console_id_hash != console_id_hash; + }); +} + +bool Room::RoomImpl::HasModPermission(const ENetPeer* client) const { + std::lock_guard lock(member_mutex); + const auto sending_member = + std::find_if(members.begin(), members.end(), + [client](const auto& member) { return member.peer == client; }); + if (sending_member == members.end()) { + return false; + } + if (room_information.enable_citra_mods && + sending_member->user_data.moderator) { // Community moderator + + return true; + } + if (!room_information.host_username.empty() && + sending_member->user_data.username == room_information.host_username) { // Room host + + return true; + } + return false; +} + +void Room::RoomImpl::SendNameCollision(ENetPeer* client) { + Packet packet; + packet << static_cast(IdNameCollision); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendMacCollision(ENetPeer* client) { + Packet packet; + packet << static_cast(IdMacCollision); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendConsoleIdCollision(ENetPeer* client) { + Packet packet; + packet << static_cast(IdConsoleIdCollision); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendWrongPassword(ENetPeer* client) { + Packet packet; + packet << static_cast(IdWrongPassword); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendRoomIsFull(ENetPeer* client) { + Packet packet; + packet << static_cast(IdRoomIsFull); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendVersionMismatch(ENetPeer* client) { + Packet packet; + packet << static_cast(IdVersionMismatch); + packet << network_version; + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendJoinSuccess(ENetPeer* client, MacAddress mac_address) { + Packet packet; + packet << static_cast(IdJoinSuccess); + packet << mac_address; + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendJoinSuccessAsMod(ENetPeer* client, MacAddress mac_address) { + Packet packet; + packet << static_cast(IdJoinSuccessAsMod); + packet << mac_address; + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendUserKicked(ENetPeer* client) { + Packet packet; + packet << static_cast(IdHostKicked); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendUserBanned(ENetPeer* client) { + Packet packet; + packet << static_cast(IdHostBanned); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendModPermissionDenied(ENetPeer* client) { + Packet packet; + packet << static_cast(IdModPermissionDenied); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendModNoSuchUser(ENetPeer* client) { + Packet packet; + packet << static_cast(IdModNoSuchUser); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendModBanListResponse(ENetPeer* client) { + Packet packet; + packet << static_cast(IdModBanListResponse); + { + std::lock_guard lock(ban_list_mutex); + packet << username_ban_list; + packet << ip_ban_list; + } + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendCloseMessage() { + Packet packet; + packet << static_cast(IdCloseRoom); + std::lock_guard lock(member_mutex); + if (!members.empty()) { + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + for (auto& member : members) { + enet_peer_send(member.peer, 0, enet_packet); + } + } + enet_host_flush(server); + for (auto& member : members) { + enet_peer_disconnect(member.peer, 0); + } +} + +void Room::RoomImpl::SendStatusMessage(StatusMessageTypes type, const std::string& nickname, + const std::string& username, const std::string& ip) { + Packet packet; + packet << static_cast(IdStatusMessage); + packet << static_cast(type); + packet << nickname; + packet << username; + std::lock_guard lock(member_mutex); + if (!members.empty()) { + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + for (auto& member : members) { + enet_peer_send(member.peer, 0, enet_packet); + } + } + enet_host_flush(server); + + const std::string display_name = + username.empty() ? nickname : fmt::format("{} ({})", nickname, username); + + switch (type) { + case IdMemberJoin: + LOG_INFO(Network, "[{}] {} has joined.", ip, display_name); + break; + case IdMemberLeave: + LOG_INFO(Network, "[{}] {} has left.", ip, display_name); + break; + case IdMemberKicked: + LOG_INFO(Network, "[{}] {} has been kicked.", ip, display_name); + break; + case IdMemberBanned: + LOG_INFO(Network, "[{}] {} has been banned.", ip, display_name); + break; + case IdAddressUnbanned: + LOG_INFO(Network, "{} has been unbanned.", display_name); + break; + } +} + +void Room::RoomImpl::BroadcastRoomInformation() { + Packet packet; + packet << static_cast(IdRoomInformation); + packet << room_information.name; + packet << room_information.description; + packet << room_information.member_slots; + packet << room_information.port; + packet << room_information.preferred_game; + packet << room_information.host_username; + + packet << static_cast(members.size()); + { + std::lock_guard lock(member_mutex); + for (const auto& member : members) { + packet << member.nickname; + packet << member.mac_address; + packet << member.game_info.name; + packet << member.game_info.id; + packet << member.user_data.username; + packet << member.user_data.display_name; + packet << member.user_data.avatar_url; + } + } + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_host_broadcast(server, 0, enet_packet); + enet_host_flush(server); +} + +MacAddress Room::RoomImpl::GenerateMacAddress() { + MacAddress result_mac = + NintendoOUI; // The first three bytes of each MAC address will be the NintendoOUI + std::uniform_int_distribution<> dis(0x00, 0xFF); // Random byte between 0 and 0xFF + do { + for (std::size_t i = 3; i < result_mac.size(); ++i) { + result_mac[i] = dis(random_gen); + } + } while (!IsValidMacAddress(result_mac)); + return result_mac; +} + +void Room::RoomImpl::HandleWifiPacket(const ENetEvent* event) { + Packet in_packet; + in_packet.Append(event->packet->data, event->packet->dataLength); + in_packet.IgnoreBytes(sizeof(u8)); // Message type + in_packet.IgnoreBytes(sizeof(u8)); // WifiPacket Type + in_packet.IgnoreBytes(sizeof(u8)); // WifiPacket Channel + in_packet.IgnoreBytes(sizeof(MacAddress)); // WifiPacket Transmitter Address + MacAddress destination_address; + in_packet >> destination_address; + + Packet out_packet; + out_packet.Append(event->packet->data, event->packet->dataLength); + ENetPacket* enet_packet = enet_packet_create(out_packet.GetData(), out_packet.GetDataSize(), + ENET_PACKET_FLAG_RELIABLE); + + if (destination_address == BroadcastMac) { // Send the data to everyone except the sender + std::lock_guard lock(member_mutex); + bool sent_packet = false; + for (const auto& member : members) { + if (member.peer != event->peer) { + sent_packet = true; + enet_peer_send(member.peer, 0, enet_packet); + } + } + + if (!sent_packet) { + enet_packet_destroy(enet_packet); + } + } else { // Send the data only to the destination client + std::lock_guard lock(member_mutex); + auto member = std::find_if(members.begin(), members.end(), + [destination_address](const Member& member) -> bool { + return member.mac_address == destination_address; + }); + if (member != members.end()) { + enet_peer_send(member->peer, 0, enet_packet); + } else { + LOG_ERROR(Network, + "Attempting to send to unknown MAC address: " + "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", + destination_address[0], destination_address[1], destination_address[2], + destination_address[3], destination_address[4], destination_address[5]); + enet_packet_destroy(enet_packet); + } + } + enet_host_flush(server); +} + +void Room::RoomImpl::HandleChatPacket(const ENetEvent* event) { + Packet in_packet; + in_packet.Append(event->packet->data, event->packet->dataLength); + + in_packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + std::string message; + in_packet >> message; + auto CompareNetworkAddress = [event](const Member member) -> bool { + return member.peer == event->peer; + }; + + std::lock_guard lock(member_mutex); + const auto sending_member = std::find_if(members.begin(), members.end(), CompareNetworkAddress); + if (sending_member == members.end()) { + return; // Received a chat message from a unknown sender + } + + // Limit the size of chat messages to MaxMessageSize + message.resize(std::min(static_cast(message.size()), MaxMessageSize)); + + Packet out_packet; + out_packet << static_cast(IdChatMessage); + out_packet << sending_member->nickname; + out_packet << sending_member->user_data.username; + out_packet << message; + + ENetPacket* enet_packet = enet_packet_create(out_packet.GetData(), out_packet.GetDataSize(), + ENET_PACKET_FLAG_RELIABLE); + bool sent_packet = false; + for (const auto& member : members) { + if (member.peer != event->peer) { + sent_packet = true; + enet_peer_send(member.peer, 0, enet_packet); + } + } + + if (!sent_packet) { + enet_packet_destroy(enet_packet); + } + + enet_host_flush(server); + + if (sending_member->user_data.username.empty()) { + LOG_INFO(Network, "{}: {}", sending_member->nickname, message); + } else { + LOG_INFO(Network, "{} ({}): {}", sending_member->nickname, + sending_member->user_data.username, message); + } +} + +void Room::RoomImpl::HandleGameNamePacket(const ENetEvent* event) { + Packet in_packet; + in_packet.Append(event->packet->data, event->packet->dataLength); + + in_packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + GameInfo game_info; + in_packet >> game_info.name; + in_packet >> game_info.id; + + { + std::lock_guard lock(member_mutex); + auto member = + std::find_if(members.begin(), members.end(), [event](const Member& member) -> bool { + return member.peer == event->peer; + }); + if (member != members.end()) { + member->game_info = game_info; + + const std::string display_name = + member->user_data.username.empty() + ? member->nickname + : fmt::format("{} ({})", member->nickname, member->user_data.username); + + if (game_info.name.empty()) { + LOG_INFO(Network, "{} is not playing", display_name); + } else { + LOG_INFO(Network, "{} is playing {}", display_name, game_info.name); + } + } + } + BroadcastRoomInformation(); +} + +void Room::RoomImpl::HandleClientDisconnection(ENetPeer* client) { + // Remove the client from the members list. + std::string nickname, username, ip; + { + std::lock_guard lock(member_mutex); + auto member = std::find_if(members.begin(), members.end(), [client](const Member& member) { + return member.peer == client; + }); + if (member != members.end()) { + nickname = member->nickname; + username = member->user_data.username; + + char ip_raw[256]; + enet_address_get_host_ip(&member->peer->address, ip_raw, sizeof(ip_raw) - 1); + ip = ip_raw; + + members.erase(member); + } + } + + // Announce the change to all clients. + enet_peer_disconnect(client, 0); + if (!nickname.empty()) + SendStatusMessage(IdMemberLeave, nickname, username, ip); + BroadcastRoomInformation(); +} + +// Room +Room::Room() : room_impl{std::make_unique()} {} + +Room::~Room() = default; + +bool Room::Create(const std::string& name, const std::string& description, + const std::string& server_address, u16 server_port, const std::string& password, + const u32 max_connections, const std::string& host_username, + const std::string& preferred_game, u64 preferred_game_id, + std::unique_ptr verify_backend, + const Room::BanList& ban_list, bool enable_citra_mods) { + ENetAddress address; + address.host = ENET_HOST_ANY; + if (!server_address.empty()) { + enet_address_set_host(&address, server_address.c_str()); + } + address.port = server_port; + + // In order to send the room is full message to the connecting client, we need to leave one + // slot open so enet won't reject the incoming connection without telling us + room_impl->server = enet_host_create(&address, max_connections + 1, NumChannels, 0, 0); + if (!room_impl->server) { + return false; + } + room_impl->state = State::Open; + + room_impl->room_information.name = name; + room_impl->room_information.description = description; + room_impl->room_information.member_slots = max_connections; + room_impl->room_information.port = server_port; + room_impl->room_information.preferred_game = preferred_game; + room_impl->room_information.preferred_game_id = preferred_game_id; + room_impl->room_information.host_username = host_username; + room_impl->room_information.enable_citra_mods = enable_citra_mods; + room_impl->password = password; + room_impl->verify_backend = std::move(verify_backend); + room_impl->username_ban_list = ban_list.first; + room_impl->ip_ban_list = ban_list.second; + + room_impl->StartLoop(); + return true; +} + +Room::State Room::GetState() const { + return room_impl->state; +} + +const RoomInformation& Room::GetRoomInformation() const { + return room_impl->room_information; +} + +std::string Room::GetVerifyUID() const { + std::lock_guard lock(room_impl->verify_UID_mutex); + return room_impl->verify_UID; +} + +Room::BanList Room::GetBanList() const { + std::lock_guard lock(room_impl->ban_list_mutex); + return {room_impl->username_ban_list, room_impl->ip_ban_list}; +} + +std::vector Room::GetRoomMemberList() const { + std::vector member_list; + std::lock_guard lock(room_impl->member_mutex); + for (const auto& member_impl : room_impl->members) { + Member member; + member.nickname = member_impl.nickname; + member.username = member_impl.user_data.username; + member.display_name = member_impl.user_data.display_name; + member.avatar_url = member_impl.user_data.avatar_url; + member.mac_address = member_impl.mac_address; + member.game_info = member_impl.game_info; + member_list.push_back(member); + } + return member_list; +} + +bool Room::HasPassword() const { + return !room_impl->password.empty(); +} + +void Room::SetVerifyUID(const std::string& uid) { + std::lock_guard lock(room_impl->verify_UID_mutex); + room_impl->verify_UID = uid; +} + +void Room::Destroy() { + room_impl->state = State::Closed; + room_impl->room_thread->join(); + room_impl->room_thread.reset(); + + if (room_impl->server) { + enet_host_destroy(room_impl->server); + } + room_impl->room_information = {}; + room_impl->server = nullptr; + { + std::lock_guard lock(room_impl->member_mutex); + room_impl->members.clear(); + } + room_impl->room_information.member_slots = 0; + room_impl->room_information.name.clear(); +} + +} // namespace Network diff --git a/src/network/room.h b/src/network/room.h new file mode 100644 index 0000000000..a679848370 --- /dev/null +++ b/src/network/room.h @@ -0,0 +1,173 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include "common/common_types.h" +#include "network/verify_user.h" + +namespace Network { + +constexpr u32 network_version = 4; ///< The version of this Room and RoomMember + +constexpr u16 DefaultRoomPort = 24872; + +constexpr u32 MaxMessageSize = 500; + +/// Maximum number of concurrent connections allowed to this room. +static constexpr u32 MaxConcurrentConnections = 254; + +constexpr std::size_t NumChannels = 1; // Number of channels used for the connection + +struct RoomInformation { + std::string name; ///< Name of the server + std::string description; ///< Server description + u32 member_slots; ///< Maximum number of members in this room + u16 port; ///< The port of this room + std::string preferred_game; ///< Game to advertise that you want to play + u64 preferred_game_id; ///< Title ID for the advertised game + std::string host_username; ///< Forum username of the host + bool enable_citra_mods; ///< Allow Citra Moderators to moderate on this room +}; + +struct GameInfo { + std::string name{""}; + u64 id{0}; +}; + +using MacAddress = std::array; +/// A special MAC address that tells the room we're joining to assign us a MAC address +/// automatically. +constexpr MacAddress NoPreferredMac = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + +// 802.11 broadcast MAC address +constexpr MacAddress BroadcastMac = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + +// The different types of messages that can be sent. The first byte of each packet defines the type +enum RoomMessageTypes : u8 { + IdJoinRequest = 1, + IdJoinSuccess, + IdRoomInformation, + IdSetGameInfo, + IdWifiPacket, + IdChatMessage, + IdNameCollision, + IdMacCollision, + IdVersionMismatch, + IdWrongPassword, + IdCloseRoom, + IdRoomIsFull, + IdConsoleIdCollision, + IdStatusMessage, + IdHostKicked, + IdHostBanned, + /// Moderation requests + IdModKick, + IdModBan, + IdModUnban, + IdModGetBanList, + // Moderation responses + IdModBanListResponse, + IdModPermissionDenied, + IdModNoSuchUser, + IdJoinSuccessAsMod, +}; + +/// Types of system status messages +enum StatusMessageTypes : u8 { + IdMemberJoin = 1, ///< Member joining + IdMemberLeave, ///< Member leaving + IdMemberKicked, ///< A member is kicked from the room + IdMemberBanned, ///< A member is banned from the room + IdAddressUnbanned, ///< A username / ip address is unbanned from the room +}; + +/// This is what a server [person creating a server] would use. +class Room final { +public: + enum class State : u8 { + Open, ///< The room is open and ready to accept connections. + Closed, ///< The room is not opened and can not accept connections. + }; + + struct Member { + std::string nickname; ///< The nickname of the member. + std::string username; ///< The web services username of the member. Can be empty. + std::string display_name; ///< The web services display name of the member. Can be empty. + std::string avatar_url; ///< Url to the member's avatar. Can be empty. + GameInfo game_info; ///< The current game of the member + MacAddress mac_address; ///< The assigned mac address of the member. + }; + + Room(); + ~Room(); + + /** + * Gets the current state of the room. + */ + State GetState() const; + + /** + * Gets the room information of the room. + */ + const RoomInformation& GetRoomInformation() const; + + /** + * Gets the verify UID of this room. + */ + std::string GetVerifyUID() const; + + /** + * Gets a list of the mbmers connected to the room. + */ + std::vector GetRoomMemberList() const; + + /** + * Checks if the room is password protected + */ + bool HasPassword() const; + + using UsernameBanList = std::vector; + using IPBanList = std::vector; + + using BanList = std::pair; + + /** + * Creates the socket for this room. Will bind to default address if + * server is empty string. + */ + bool Create(const std::string& name, const std::string& description = "", + const std::string& server = "", u16 server_port = DefaultRoomPort, + const std::string& password = "", + const u32 max_connections = MaxConcurrentConnections, + const std::string& host_username = "", const std::string& preferred_game = "", + u64 preferred_game_id = 0, + std::unique_ptr verify_backend = nullptr, + const BanList& ban_list = {}, bool enable_citra_mods = false); + + /** + * Sets the verification GUID of the room. + */ + void SetVerifyUID(const std::string& uid); + + /** + * Gets the ban list (including banned forum usernames and IPs) of the room. + */ + BanList GetBanList() const; + + /** + * Destroys the socket + */ + void Destroy(); + +private: + class RoomImpl; + std::unique_ptr room_impl; +}; + +} // namespace Network diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp new file mode 100644 index 0000000000..e430040271 --- /dev/null +++ b/src/network/room_member.cpp @@ -0,0 +1,694 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include "common/assert.h" +#include "enet/enet.h" +#include "network/packet.h" +#include "network/room_member.h" + +namespace Network { + +constexpr u32 ConnectionTimeoutMs = 5000; + +class RoomMember::RoomMemberImpl { +public: + ENetHost* client = nullptr; ///< ENet network interface. + ENetPeer* server = nullptr; ///< The server peer the client is connected to + + /// Information about the clients connected to the same room as us. + MemberList member_information; + /// Information about the room we're connected to. + RoomInformation room_information; + + /// The current game name, id and version + GameInfo current_game_info; + + std::atomic state{State::Idle}; ///< Current state of the RoomMember. + void SetState(const State new_state); + void SetError(const Error new_error); + bool IsConnected() const; + + std::string nickname; ///< The nickname of this member. + + std::string username; ///< The username of this member. + mutable std::mutex username_mutex; ///< Mutex for locking username. + + MacAddress mac_address; ///< The mac_address of this member. + + std::mutex network_mutex; ///< Mutex that controls access to the `client` variable. + /// Thread that receives and dispatches network packets + std::unique_ptr loop_thread; + std::mutex send_list_mutex; ///< Mutex that controls access to the `send_list` variable. + std::list send_list; ///< A list that stores all packets to send the async + + template + using CallbackSet = std::set>; + std::mutex callback_mutex; ///< The mutex used for handling callbacks + + class Callbacks { + public: + template + CallbackSet& Get(); + + private: + CallbackSet callback_set_wifi_packet; + CallbackSet callback_set_chat_messages; + CallbackSet callback_set_status_messages; + CallbackSet callback_set_room_information; + CallbackSet callback_set_state; + CallbackSet callback_set_error; + CallbackSet callback_set_ban_list; + }; + Callbacks callbacks; ///< All CallbackSets to all events + + void MemberLoop(); + + void StartLoop(); + + /** + * Sends data to the room. It will be send on channel 0 with flag RELIABLE + * @param packet The data to send + */ + void Send(Packet&& packet); + + /** + * Sends a request to the server, asking for permission to join a room with the specified + * nickname and preferred mac. + * @params nickname The desired nickname. + * @params console_id_hash A hash of the Console ID. + * @params preferred_mac The preferred MAC address to use in the room, the NoPreferredMac tells + * @params password The password for the room + * the server to assign one for us. + */ + void SendJoinRequest(const std::string& nickname, const std::string& console_id_hash, + const MacAddress& preferred_mac = NoPreferredMac, + const std::string& password = "", const std::string& token = ""); + + /** + * Extracts a MAC Address from a received ENet packet. + * @param event The ENet event that was received. + */ + void HandleJoinPacket(const ENetEvent* event); + /** + * Extracts RoomInformation and MemberInformation from a received ENet packet. + * @param event The ENet event that was received. + */ + void HandleRoomInformationPacket(const ENetEvent* event); + + /** + * Extracts a WifiPacket from a received ENet packet. + * @param event The ENet event that was received. + */ + void HandleWifiPackets(const ENetEvent* event); + + /** + * Extracts a chat entry from a received ENet packet and adds it to the chat queue. + * @param event The ENet event that was received. + */ + void HandleChatPacket(const ENetEvent* event); + + /** + * Extracts a system message entry from a received ENet packet and adds it to the system message + * queue. + * @param event The ENet event that was received. + */ + void HandleStatusMessagePacket(const ENetEvent* event); + + /** + * Extracts a ban list request response from a received ENet packet. + * @param event The ENet event that was received. + */ + void HandleModBanListResponsePacket(const ENetEvent* event); + + /** + * Disconnects the RoomMember from the Room + */ + void Disconnect(); + + template + void Invoke(const T& data); + + template + CallbackHandle Bind(std::function callback); +}; + +// RoomMemberImpl +void RoomMember::RoomMemberImpl::SetState(const State new_state) { + if (state != new_state) { + state = new_state; + Invoke(state); + } +} + +void RoomMember::RoomMemberImpl::SetError(const Error new_error) { + Invoke(new_error); +} + +bool RoomMember::RoomMemberImpl::IsConnected() const { + return state == State::Joining || state == State::Joined || state == State::Moderator; +} + +void RoomMember::RoomMemberImpl::MemberLoop() { + // Receive packets while the connection is open + while (IsConnected()) { + std::lock_guard lock(network_mutex); + ENetEvent event; + if (enet_host_service(client, &event, 100) > 0) { + switch (event.type) { + case ENET_EVENT_TYPE_RECEIVE: + switch (event.packet->data[0]) { + case IdWifiPacket: + HandleWifiPackets(&event); + break; + case IdChatMessage: + HandleChatPacket(&event); + break; + case IdStatusMessage: + HandleStatusMessagePacket(&event); + break; + case IdRoomInformation: + HandleRoomInformationPacket(&event); + break; + case IdJoinSuccess: + case IdJoinSuccessAsMod: + // The join request was successful, we are now in the room. + // If we joined successfully, there must be at least one client in the room: us. + ASSERT_MSG(member_information.size() > 0, + "We have not yet received member information."); + HandleJoinPacket(&event); // Get the MAC Address for the client + if (event.packet->data[0] == IdJoinSuccessAsMod) { + SetState(State::Moderator); + } else { + SetState(State::Joined); + } + break; + case IdModBanListResponse: + HandleModBanListResponsePacket(&event); + break; + case IdRoomIsFull: + SetState(State::Idle); + SetError(Error::RoomIsFull); + break; + case IdNameCollision: + SetState(State::Idle); + SetError(Error::NameCollision); + break; + case IdMacCollision: + SetState(State::Idle); + SetError(Error::MacCollision); + break; + case IdConsoleIdCollision: + SetState(State::Idle); + SetError(Error::ConsoleIdCollision); + break; + case IdVersionMismatch: + SetState(State::Idle); + SetError(Error::WrongVersion); + break; + case IdWrongPassword: + SetState(State::Idle); + SetError(Error::WrongPassword); + break; + case IdCloseRoom: + SetState(State::Idle); + SetError(Error::LostConnection); + break; + case IdHostKicked: + SetState(State::Idle); + SetError(Error::HostKicked); + break; + case IdHostBanned: + SetState(State::Idle); + SetError(Error::HostBanned); + break; + case IdModPermissionDenied: + SetError(Error::PermissionDenied); + break; + case IdModNoSuchUser: + SetError(Error::NoSuchUser); + break; + } + enet_packet_destroy(event.packet); + break; + case ENET_EVENT_TYPE_DISCONNECT: + if (state == State::Joined || state == State::Moderator) { + SetState(State::Idle); + SetError(Error::LostConnection); + } + break; + case ENET_EVENT_TYPE_NONE: + break; + case ENET_EVENT_TYPE_CONNECT: + // The ENET_EVENT_TYPE_CONNECT event can not possibly happen here because we're + // already connected + ASSERT_MSG(false, "Received unexpected connect event while already connected"); + break; + } + } + { + std::lock_guard lock(send_list_mutex); + for (const auto& packet : send_list) { + ENetPacket* enetPacket = enet_packet_create(packet.GetData(), packet.GetDataSize(), + ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(server, 0, enetPacket); + } + enet_host_flush(client); + send_list.clear(); + } + } + Disconnect(); +}; + +void RoomMember::RoomMemberImpl::StartLoop() { + loop_thread = std::make_unique(&RoomMember::RoomMemberImpl::MemberLoop, this); +} + +void RoomMember::RoomMemberImpl::Send(Packet&& packet) { + std::lock_guard lock(send_list_mutex); + send_list.push_back(std::move(packet)); +} + +void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname, + const std::string& console_id_hash, + const MacAddress& preferred_mac, + const std::string& password, + const std::string& token) { + Packet packet; + packet << static_cast(IdJoinRequest); + packet << nickname; + packet << console_id_hash; + packet << preferred_mac; + packet << network_version; + packet << password; + packet << token; + Send(std::move(packet)); +} + +void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* event) { + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + RoomInformation info{}; + packet >> info.name; + packet >> info.description; + packet >> info.member_slots; + packet >> info.port; + packet >> info.preferred_game; + packet >> info.host_username; + room_information.name = info.name; + room_information.description = info.description; + room_information.member_slots = info.member_slots; + room_information.port = info.port; + room_information.preferred_game = info.preferred_game; + room_information.host_username = info.host_username; + + u32 num_members; + packet >> num_members; + member_information.resize(num_members); + + for (auto& member : member_information) { + packet >> member.nickname; + packet >> member.mac_address; + packet >> member.game_info.name; + packet >> member.game_info.id; + packet >> member.username; + packet >> member.display_name; + packet >> member.avatar_url; + + { + std::lock_guard lock(username_mutex); + if (member.nickname == nickname) { + username = member.username; + } + } + } + Invoke(room_information); +} + +void RoomMember::RoomMemberImpl::HandleJoinPacket(const ENetEvent* event) { + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + // Parse the MAC Address from the packet + packet >> mac_address; +} + +void RoomMember::RoomMemberImpl::HandleWifiPackets(const ENetEvent* event) { + WifiPacket wifi_packet{}; + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + // Parse the WifiPacket from the packet + u8 frame_type; + packet >> frame_type; + WifiPacket::PacketType type = static_cast(frame_type); + + wifi_packet.type = type; + packet >> wifi_packet.channel; + packet >> wifi_packet.transmitter_address; + packet >> wifi_packet.destination_address; + packet >> wifi_packet.data; + + Invoke(wifi_packet); +} + +void RoomMember::RoomMemberImpl::HandleChatPacket(const ENetEvent* event) { + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); + + ChatEntry chat_entry{}; + packet >> chat_entry.nickname; + packet >> chat_entry.username; + packet >> chat_entry.message; + Invoke(chat_entry); +} + +void RoomMember::RoomMemberImpl::HandleStatusMessagePacket(const ENetEvent* event) { + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); + + StatusMessageEntry status_message_entry{}; + u8 type{}; + packet >> type; + status_message_entry.type = static_cast(type); + packet >> status_message_entry.nickname; + packet >> status_message_entry.username; + Invoke(status_message_entry); +} + +void RoomMember::RoomMemberImpl::HandleModBanListResponsePacket(const ENetEvent* event) { + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); + + Room::BanList ban_list = {}; + packet >> ban_list.first; + packet >> ban_list.second; + Invoke(ban_list); +} + +void RoomMember::RoomMemberImpl::Disconnect() { + member_information.clear(); + room_information.member_slots = 0; + room_information.name.clear(); + + if (!server) + return; + enet_peer_disconnect(server, 0); + + ENetEvent event; + while (enet_host_service(client, &event, ConnectionTimeoutMs) > 0) { + switch (event.type) { + case ENET_EVENT_TYPE_RECEIVE: + enet_packet_destroy(event.packet); // Ignore all incoming data + break; + case ENET_EVENT_TYPE_DISCONNECT: + server = nullptr; + return; + case ENET_EVENT_TYPE_NONE: + case ENET_EVENT_TYPE_CONNECT: + break; + } + } + // didn't disconnect gracefully force disconnect + enet_peer_reset(server); + server = nullptr; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet& RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_wifi_packet; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_state; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_error; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_room_information; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet& RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_chat_messages; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_status_messages; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_ban_list; +} + +template +void RoomMember::RoomMemberImpl::Invoke(const T& data) { + std::lock_guard lock(callback_mutex); + CallbackSet callback_set = callbacks.Get(); + for (auto const& callback : callback_set) + (*callback)(data); +} + +template +RoomMember::CallbackHandle RoomMember::RoomMemberImpl::Bind( + std::function callback) { + std::lock_guard lock(callback_mutex); + CallbackHandle handle; + handle = std::make_shared>(callback); + callbacks.Get().insert(handle); + return handle; +} + +// RoomMember +RoomMember::RoomMember() : room_member_impl{std::make_unique()} {} + +RoomMember::~RoomMember() { + ASSERT_MSG(!IsConnected(), "RoomMember is being destroyed while connected"); + if (room_member_impl->loop_thread) { + Leave(); + } +} + +RoomMember::State RoomMember::GetState() const { + return room_member_impl->state; +} + +const RoomMember::MemberList& RoomMember::GetMemberInformation() const { + return room_member_impl->member_information; +} + +const std::string& RoomMember::GetNickname() const { + return room_member_impl->nickname; +} + +const std::string& RoomMember::GetUsername() const { + std::lock_guard lock(room_member_impl->username_mutex); + return room_member_impl->username; +} + +const MacAddress& RoomMember::GetMacAddress() const { + ASSERT_MSG(IsConnected(), "Tried to get MAC address while not connected"); + return room_member_impl->mac_address; +} + +RoomInformation RoomMember::GetRoomInformation() const { + return room_member_impl->room_information; +} + +void RoomMember::Join(const std::string& nick, const std::string& console_id_hash, + const char* server_addr, u16 server_port, u16 client_port, + const MacAddress& preferred_mac, const std::string& password, + const std::string& token) { + // If the member is connected, kill the connection first + if (room_member_impl->loop_thread && room_member_impl->loop_thread->joinable()) { + Leave(); + } + // If the thread isn't running but the ptr still exists, reset it + else if (room_member_impl->loop_thread) { + room_member_impl->loop_thread.reset(); + } + + if (!room_member_impl->client) { + room_member_impl->client = enet_host_create(nullptr, 1, NumChannels, 0, 0); + ASSERT_MSG(room_member_impl->client != nullptr, "Could not create client"); + } + + room_member_impl->SetState(State::Joining); + + ENetAddress address{}; + enet_address_set_host(&address, server_addr); + address.port = server_port; + room_member_impl->server = + enet_host_connect(room_member_impl->client, &address, NumChannels, 0); + + if (!room_member_impl->server) { + room_member_impl->SetState(State::Idle); + room_member_impl->SetError(Error::UnknownError); + return; + } + + ENetEvent event{}; + int net = enet_host_service(room_member_impl->client, &event, ConnectionTimeoutMs); + if (net > 0 && event.type == ENET_EVENT_TYPE_CONNECT) { + room_member_impl->nickname = nick; + room_member_impl->StartLoop(); + room_member_impl->SendJoinRequest(nick, console_id_hash, preferred_mac, password, token); + SendGameInfo(room_member_impl->current_game_info); + } else { + enet_peer_disconnect(room_member_impl->server, 0); + room_member_impl->SetState(State::Idle); + room_member_impl->SetError(Error::CouldNotConnect); + } +} + +bool RoomMember::IsConnected() const { + return room_member_impl->IsConnected(); +} + +void RoomMember::SendWifiPacket(const WifiPacket& wifi_packet) { + Packet packet; + packet << static_cast(IdWifiPacket); + packet << static_cast(wifi_packet.type); + packet << wifi_packet.channel; + packet << wifi_packet.transmitter_address; + packet << wifi_packet.destination_address; + packet << wifi_packet.data; + room_member_impl->Send(std::move(packet)); +} + +void RoomMember::SendChatMessage(const std::string& message) { + Packet packet; + packet << static_cast(IdChatMessage); + packet << message; + room_member_impl->Send(std::move(packet)); +} + +void RoomMember::SendGameInfo(const GameInfo& game_info) { + room_member_impl->current_game_info = game_info; + if (!IsConnected()) + return; + + Packet packet; + packet << static_cast(IdSetGameInfo); + packet << game_info.name; + packet << game_info.id; + room_member_impl->Send(std::move(packet)); +} + +void RoomMember::SendModerationRequest(RoomMessageTypes type, const std::string& nickname) { + ASSERT_MSG(type == IdModKick || type == IdModBan || type == IdModUnban, + "type is not a moderation request"); + if (!IsConnected()) + return; + + Packet packet; + packet << static_cast(type); + packet << nickname; + room_member_impl->Send(std::move(packet)); +} + +void RoomMember::RequestBanList() { + if (!IsConnected()) + return; + + Packet packet; + packet << static_cast(IdModGetBanList); + room_member_impl->Send(std::move(packet)); +} + +RoomMember::CallbackHandle RoomMember::BindOnStateChanged( + std::function callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle RoomMember::BindOnError( + std::function callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle RoomMember::BindOnWifiPacketReceived( + std::function callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle RoomMember::BindOnRoomInformationChanged( + std::function callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle RoomMember::BindOnChatMessageRecieved( + std::function callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle RoomMember::BindOnStatusMessageReceived( + std::function callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle RoomMember::BindOnBanListReceived( + std::function callback) { + return room_member_impl->Bind(callback); +} + +template +void RoomMember::Unbind(CallbackHandle handle) { + std::lock_guard lock(room_member_impl->callback_mutex); + room_member_impl->callbacks.Get().erase(handle); +} + +void RoomMember::Leave() { + room_member_impl->SetState(State::Idle); + room_member_impl->loop_thread->join(); + room_member_impl->loop_thread.reset(); + + enet_host_destroy(room_member_impl->client); + room_member_impl->client = nullptr; +} + +template void RoomMember::Unbind(CallbackHandle); +template void RoomMember::Unbind(CallbackHandle); +template void RoomMember::Unbind(CallbackHandle); +template void RoomMember::Unbind(CallbackHandle); +template void RoomMember::Unbind(CallbackHandle); +template void RoomMember::Unbind(CallbackHandle); +template void RoomMember::Unbind(CallbackHandle); + +} // namespace Network diff --git a/src/network/room_member.h b/src/network/room_member.h new file mode 100644 index 0000000000..ee1c921d46 --- /dev/null +++ b/src/network/room_member.h @@ -0,0 +1,327 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include "common/common_types.h" +#include "network/room.h" + +namespace Network { + +/// Information about the received WiFi packets. +/// Acts as our own 802.11 header. +struct WifiPacket { + enum class PacketType : u8 { + Beacon, + Data, + Authentication, + AssociationResponse, + Deauthentication, + NodeMap + }; + PacketType type; ///< The type of 802.11 frame. + std::vector data; ///< Raw 802.11 frame data, starting at the management frame header + /// for management frames. + MacAddress transmitter_address; ///< Mac address of the transmitter. + MacAddress destination_address; ///< Mac address of the receiver. + u8 channel; ///< WiFi channel where this frame was transmitted. + +private: + template + void serialize(Archive& ar, const unsigned int) { + ar& type; + ar& data; + ar& transmitter_address; + ar& destination_address; + ar& channel; + } + friend class boost::serialization::access; +}; + +/// Represents a chat message. +struct ChatEntry { + std::string nickname; ///< Nickname of the client who sent this message. + /// Web services username of the client who sent this message, can be empty. + std::string username; + std::string message; ///< Body of the message. +}; + +/// Represents a system status message. +struct StatusMessageEntry { + StatusMessageTypes type; ///< Type of the message + /// Subject of the message. i.e. the user who is joining/leaving/being banned, etc. + std::string nickname; + std::string username; +}; + +/** + * This is what a client [person joining a server] would use. + * It also has to be used if you host a game yourself (You'd create both, a Room and a + * RoomMembership for yourself) + */ +class RoomMember final { +public: + enum class State : u8 { + Uninitialized, ///< Not initialized + Idle, ///< Default state (i.e. not connected) + Joining, ///< The client is attempting to join a room. + Joined, ///< The client is connected to the room and is ready to send/receive packets. + Moderator, ///< The client is connnected to the room and is granted mod permissions. + }; + + enum class Error : u8 { + // Reasons why connection was closed + LostConnection, ///< Connection closed + HostKicked, ///< Kicked by the host + + // Reasons why connection was rejected + UnknownError, ///< Some error [permissions to network device missing or something] + NameCollision, ///< Somebody is already using this name + MacCollision, ///< Somebody is already using that mac-address + ConsoleIdCollision, ///< Somebody in the room has the same Console ID + WrongVersion, ///< The room version is not the same as for this RoomMember + WrongPassword, ///< The password doesn't match the one from the Room + CouldNotConnect, ///< The room is not responding to a connection attempt + RoomIsFull, ///< Room is already at the maximum number of players + HostBanned, ///< The user is banned by the host + + // Reasons why moderation request failed + PermissionDenied, ///< The user does not have mod permissions + NoSuchUser, ///< The nickname the user attempts to kick/ban does not exist + }; + + struct MemberInformation { + std::string nickname; ///< Nickname of the member. + std::string username; ///< The web services username of the member. Can be empty. + std::string display_name; ///< The web services display name of the member. Can be empty. + std::string avatar_url; ///< Url to the member's avatar. Can be empty. + GameInfo game_info; ///< Name of the game they're currently playing, or empty if they're + /// not playing anything. + MacAddress mac_address; ///< MAC address associated with this member. + }; + using MemberList = std::vector; + + // The handle for the callback functions + template + using CallbackHandle = std::shared_ptr>; + + /** + * Unbinds a callback function from the events. + * @param handle The connection handle to disconnect + */ + template + void Unbind(CallbackHandle handle); + + RoomMember(); + ~RoomMember(); + + /** + * Returns the status of our connection to the room. + */ + State GetState() const; + + /** + * Returns information about the members in the room we're currently connected to. + */ + const MemberList& GetMemberInformation() const; + + /** + * Returns the nickname of the RoomMember. + */ + const std::string& GetNickname() const; + + /** + * Returns the username of the RoomMember. + */ + const std::string& GetUsername() const; + + /** + * Returns the MAC address of the RoomMember. + */ + const MacAddress& GetMacAddress() const; + + /** + * Returns information about the room we're currently connected to. + */ + RoomInformation GetRoomInformation() const; + + /** + * Returns whether we're connected to a server or not. + */ + bool IsConnected() const; + + /** + * Attempts to join a room at the specified address and port, using the specified nickname. + * A console ID hash is passed in to check console ID conflicts. + * This may fail if the username or console ID is already taken. + */ + void Join(const std::string& nickname, const std::string& console_id_hash, + const char* server_addr = "127.0.0.1", u16 server_port = DefaultRoomPort, + u16 client_port = 0, const MacAddress& preferred_mac = NoPreferredMac, + const std::string& password = "", const std::string& token = ""); + + /** + * Sends a WiFi packet to the room. + * @param packet The WiFi packet to send. + */ + void SendWifiPacket(const WifiPacket& packet); + + /** + * Sends a chat message to the room. + * @param message The contents of the message. + */ + void SendChatMessage(const std::string& message); + + /** + * Sends the current game info to the room. + * @param game_info The game information. + */ + void SendGameInfo(const GameInfo& game_info); + + /** + * Sends a moderation request to the room. + * @param type Moderation request type. + * @param nickname The subject of the request. (i.e. the user you want to kick/ban) + */ + void SendModerationRequest(RoomMessageTypes type, const std::string& nickname); + + /** + * Attempts to retrieve ban list from the room. + * If success, the ban list callback would be called. Otherwise an error would be emitted. + */ + void RequestBanList(); + + /** + * Binds a function to an event that will be triggered every time the State of the member + * changed. The function wil be called every time the event is triggered. The callback function + * must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle BindOnStateChanged(std::function callback); + + /** + * Binds a function to an event that will be triggered every time an error happened. The + * function wil be called every time the event is triggered. The callback function must not bind + * or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle BindOnError(std::function callback); + + /** + * Binds a function to an event that will be triggered every time a WifiPacket is received. + * The function wil be called everytime the event is triggered. + * The callback function must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle BindOnWifiPacketReceived( + std::function callback); + + /** + * Binds a function to an event that will be triggered every time the RoomInformation changes. + * The function wil be called every time the event is triggered. + * The callback function must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle BindOnRoomInformationChanged( + std::function callback); + + /** + * Binds a function to an event that will be triggered every time a ChatMessage is received. + * The function wil be called every time the event is triggered. + * The callback function must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle BindOnChatMessageRecieved( + std::function callback); + + /** + * Binds a function to an event that will be triggered every time a StatusMessage is + * received. The function will be called every time the event is triggered. The callback + * function must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle BindOnStatusMessageReceived( + std::function callback); + + /** + * Binds a function to an event that will be triggered every time a requested ban list + * received. The function will be called every time the event is triggered. The callback + * function must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle BindOnBanListReceived( + std::function callback); + + /** + * Leaves the current room. + */ + void Leave(); + +private: + class RoomMemberImpl; + std::unique_ptr room_member_impl; +}; + +inline const char* GetStateStr(const RoomMember::State& s) { + switch (s) { + case RoomMember::State::Uninitialized: + return "Uninitialized"; + case RoomMember::State::Idle: + return "Idle"; + case RoomMember::State::Joining: + return "Joining"; + case RoomMember::State::Joined: + return "Joined"; + case RoomMember::State::Moderator: + return "Moderator"; + } + return "Unknown"; +} + +inline const char* GetErrorStr(const RoomMember::Error& e) { + switch (e) { + case RoomMember::Error::LostConnection: + return "LostConnection"; + case RoomMember::Error::HostKicked: + return "HostKicked"; + case RoomMember::Error::UnknownError: + return "UnknownError"; + case RoomMember::Error::NameCollision: + return "NameCollision"; + case RoomMember::Error::MacCollision: + return "MaxCollision"; + case RoomMember::Error::ConsoleIdCollision: + return "ConsoleIdCollision"; + case RoomMember::Error::WrongVersion: + return "WrongVersion"; + case RoomMember::Error::WrongPassword: + return "WrongPassword"; + case RoomMember::Error::CouldNotConnect: + return "CouldNotConnect"; + case RoomMember::Error::RoomIsFull: + return "RoomIsFull"; + case RoomMember::Error::HostBanned: + return "HostBanned"; + case RoomMember::Error::PermissionDenied: + return "PermissionDenied"; + case RoomMember::Error::NoSuchUser: + return "NoSuchUser"; + default: + return "Unknown"; + } +} + +} // namespace Network diff --git a/src/network/verify_user.cpp b/src/network/verify_user.cpp new file mode 100644 index 0000000000..d9d98e4955 --- /dev/null +++ b/src/network/verify_user.cpp @@ -0,0 +1,18 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "network/verify_user.h" + +namespace Network::VerifyUser { + +Backend::~Backend() = default; + +NullBackend::~NullBackend() = default; + +UserData NullBackend::LoadUserData([[maybe_unused]] const std::string& verify_UID, + [[maybe_unused]] const std::string& token) { + return {}; +} + +} // namespace Network::VerifyUser diff --git a/src/network/verify_user.h b/src/network/verify_user.h new file mode 100644 index 0000000000..01b9877c86 --- /dev/null +++ b/src/network/verify_user.h @@ -0,0 +1,46 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "common/logging/log.h" + +namespace Network::VerifyUser { + +struct UserData { + std::string username; + std::string display_name; + std::string avatar_url; + bool moderator = false; ///< Whether the user is a Citra Moderator. +}; + +/** + * A backend used for verifying users and loading user data. + */ +class Backend { +public: + virtual ~Backend(); + + /** + * Verifies the given token and loads the information into a UserData struct. + * @param verify_UID A GUID that may be used for verification. + * @param token A token that contains user data and verification data. The format and content is + * decided by backends. + */ + virtual UserData LoadUserData(const std::string& verify_UID, const std::string& token) = 0; +}; + +/** + * A null backend where the token is ignored. + * No verification is performed here and the function returns an empty UserData. + */ +class NullBackend final : public Backend { +public: + ~NullBackend(); + + UserData LoadUserData(const std::string& verify_UID, const std::string& token) override; +}; + +} // namespace Network::VerifyUser From 705f7db84dd85555a6aef1e136cf251725cef293 Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Sat, 25 Dec 2021 20:27:52 +0100 Subject: [PATCH 37/50] yuzu: Add ui files for multiplayer rooms --- dist/license.md | 9 + .../colorful/icons/16x16/connected.png | Bin 0 -> 362 bytes .../icons/16x16/connected_notification.png | Bin 0 -> 607 bytes .../colorful/icons/16x16/disconnected.png | Bin 0 -> 784 bytes dist/qt_themes/colorful/style.qrc | 3 + dist/qt_themes/colorful_dark/style.qrc | 4 + dist/qt_themes/default/default.qrc | 4 + .../default/icons/16x16/connected.png | Bin 0 -> 269 bytes .../icons/16x16/connected_notification.png | Bin 0 -> 517 bytes .../default/icons/16x16/disconnected.png | Bin 0 -> 306 bytes .../default/icons/48x48/no_avatar.png | Bin 0 -> 588 bytes .../qdarkstyle/icons/16x16/connected.png | Bin 0 -> 397 bytes .../icons/16x16/connected_notification.png | Bin 0 -> 526 bytes .../qdarkstyle/icons/16x16/disconnected.png | Bin 0 -> 444 bytes .../qdarkstyle/icons/48x48/no_avatar.png | Bin 0 -> 708 bytes dist/qt_themes/qdarkstyle/style.qrc | 4 + externals/cpp-jwt | 1 + src/common/CMakeLists.txt | 1 + src/common/announce_multiplayer_room.h | 138 +++++ src/core/CMakeLists.txt | 14 +- src/core/announce_multiplayer_session.cpp | 165 ++++++ src/core/announce_multiplayer_session.h | 96 ++++ src/core/core.cpp | 17 +- src/core/hle/service/nifm/nifm.cpp | 4 +- src/core/hle/service/sockets/bsd.cpp | 4 +- src/core/hle/service/sockets/bsd.h | 2 +- .../hle/service/sockets/sockets_translate.cpp | 2 +- .../hle/service/sockets/sockets_translate.h | 2 +- .../{network => internal_network}/network.cpp | 6 +- .../{network => internal_network}/network.h | 0 .../network_interface.cpp | 2 +- .../network_interface.h | 0 .../{network => internal_network}/sockets.h | 3 +- src/network/room.cpp | 8 +- src/network/room.h | 4 +- src/network/room_member.cpp | 25 +- src/network/verify_user.h | 2 +- src/tests/CMakeLists.txt | 2 +- .../{network => internal_network}/network.cpp | 4 +- src/web_service/CMakeLists.txt | 4 +- src/web_service/announce_room_json.cpp | 157 ++++++ src/web_service/announce_room_json.h | 46 ++ src/yuzu/CMakeLists.txt | 28 +- src/yuzu/configuration/config.cpp | 79 +++ src/yuzu/configuration/config.h | 2 + src/yuzu/configuration/configure_dialog.cpp | 8 +- src/yuzu/configuration/configure_dialog.h | 3 +- src/yuzu/configuration/configure_network.cpp | 2 +- src/yuzu/configuration/configure_web.cpp | 5 + src/yuzu/configuration/configure_web.h | 1 + src/yuzu/configuration/configure_web.ui | 10 + src/yuzu/game_list.cpp | 6 + src/yuzu/game_list.h | 8 + src/yuzu/main.cpp | 39 +- src/yuzu/main.h | 5 + src/yuzu/main.ui | 52 ++ src/yuzu/multiplayer/chat_room.cpp | 490 ++++++++++++++++++ src/yuzu/multiplayer/chat_room.h | 74 +++ src/yuzu/multiplayer/chat_room.ui | 59 +++ src/yuzu/multiplayer/client_room.cpp | 115 ++++ src/yuzu/multiplayer/client_room.h | 39 ++ src/yuzu/multiplayer/client_room.ui | 80 +++ src/yuzu/multiplayer/direct_connect.cpp | 129 +++++ src/yuzu/multiplayer/direct_connect.h | 43 ++ src/yuzu/multiplayer/direct_connect.ui | 168 ++++++ src/yuzu/multiplayer/host_room.cpp | 237 +++++++++ src/yuzu/multiplayer/host_room.h | 74 +++ src/yuzu/multiplayer/host_room.ui | 207 ++++++++ src/yuzu/multiplayer/lobby.cpp | 360 +++++++++++++ src/yuzu/multiplayer/lobby.h | 127 +++++ src/yuzu/multiplayer/lobby.ui | 123 +++++ src/yuzu/multiplayer/lobby_p.h | 239 +++++++++ src/yuzu/multiplayer/message.cpp | 79 +++ src/yuzu/multiplayer/message.h | 65 +++ src/yuzu/multiplayer/moderation_dialog.cpp | 113 ++++ src/yuzu/multiplayer/moderation_dialog.h | 42 ++ src/yuzu/multiplayer/moderation_dialog.ui | 84 +++ src/yuzu/multiplayer/state.cpp | 299 +++++++++++ src/yuzu/multiplayer/state.h | 92 ++++ src/yuzu/multiplayer/validation.h | 49 ++ src/yuzu/uisettings.h | 13 + src/yuzu/util/clickable_label.cpp | 12 + src/yuzu/util/clickable_label.h | 22 + src/yuzu_cmd/yuzu.cpp | 158 ++++++ 84 files changed, 4524 insertions(+), 49 deletions(-) create mode 100644 dist/qt_themes/colorful/icons/16x16/connected.png create mode 100644 dist/qt_themes/colorful/icons/16x16/connected_notification.png create mode 100644 dist/qt_themes/colorful/icons/16x16/disconnected.png create mode 100644 dist/qt_themes/default/icons/16x16/connected.png create mode 100644 dist/qt_themes/default/icons/16x16/connected_notification.png create mode 100644 dist/qt_themes/default/icons/16x16/disconnected.png create mode 100644 dist/qt_themes/default/icons/48x48/no_avatar.png create mode 100644 dist/qt_themes/qdarkstyle/icons/16x16/connected.png create mode 100644 dist/qt_themes/qdarkstyle/icons/16x16/connected_notification.png create mode 100644 dist/qt_themes/qdarkstyle/icons/16x16/disconnected.png create mode 100644 dist/qt_themes/qdarkstyle/icons/48x48/no_avatar.png create mode 160000 externals/cpp-jwt create mode 100644 src/common/announce_multiplayer_room.h create mode 100644 src/core/announce_multiplayer_session.cpp create mode 100644 src/core/announce_multiplayer_session.h rename src/core/{network => internal_network}/network.cpp (99%) rename src/core/{network => internal_network}/network.h (100%) rename src/core/{network => internal_network}/network_interface.cpp (99%) rename src/core/{network => internal_network}/network_interface.h (100%) rename src/core/{network => internal_network}/sockets.h (97%) rename src/tests/core/{network => internal_network}/network.cpp (90%) create mode 100644 src/web_service/announce_room_json.cpp create mode 100644 src/web_service/announce_room_json.h create mode 100644 src/yuzu/multiplayer/chat_room.cpp create mode 100644 src/yuzu/multiplayer/chat_room.h create mode 100644 src/yuzu/multiplayer/chat_room.ui create mode 100644 src/yuzu/multiplayer/client_room.cpp create mode 100644 src/yuzu/multiplayer/client_room.h create mode 100644 src/yuzu/multiplayer/client_room.ui create mode 100644 src/yuzu/multiplayer/direct_connect.cpp create mode 100644 src/yuzu/multiplayer/direct_connect.h create mode 100644 src/yuzu/multiplayer/direct_connect.ui create mode 100644 src/yuzu/multiplayer/host_room.cpp create mode 100644 src/yuzu/multiplayer/host_room.h create mode 100644 src/yuzu/multiplayer/host_room.ui create mode 100644 src/yuzu/multiplayer/lobby.cpp create mode 100644 src/yuzu/multiplayer/lobby.h create mode 100644 src/yuzu/multiplayer/lobby.ui create mode 100644 src/yuzu/multiplayer/lobby_p.h create mode 100644 src/yuzu/multiplayer/message.cpp create mode 100644 src/yuzu/multiplayer/message.h create mode 100644 src/yuzu/multiplayer/moderation_dialog.cpp create mode 100644 src/yuzu/multiplayer/moderation_dialog.h create mode 100644 src/yuzu/multiplayer/moderation_dialog.ui create mode 100644 src/yuzu/multiplayer/state.cpp create mode 100644 src/yuzu/multiplayer/state.h create mode 100644 src/yuzu/multiplayer/validation.h create mode 100644 src/yuzu/util/clickable_label.cpp create mode 100644 src/yuzu/util/clickable_label.h diff --git a/dist/license.md b/dist/license.md index 7bdebfec1f..c5ebe9c003 100644 --- a/dist/license.md +++ b/dist/license.md @@ -3,6 +3,9 @@ The icons in this folder and its subfolders have the following licenses: Icon Name | License | Origin/Author --- | --- | --- qt_themes/default/icons/16x16/checked.png | CC BY-ND 3.0 | https://icons8.com +qt_themes/default/icons/16x16/connected.png | CC BY-ND 3.0 | https://icons8.com +qt_themes/default/icons/16x16/connected_notification.png | CC BY-ND 3.0 | https://icons8.com +qt_themes/default/icons/16x16/disconnected.png | CC BY-ND 3.0 | https://icons8.com qt_themes/default/icons/16x16/failed.png | CC BY-ND 3.0 | https://icons8.com qt_themes/default/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com qt_themes/default/icons/16x16/view-refresh.png | Apache 2.0 | https://material.io @@ -10,18 +13,24 @@ qt_themes/default/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8. qt_themes/default/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com qt_themes/default/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com qt_themes/default/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com +qt_themes/default/icons/48x48/no_avatar.png | CC BY-ND 3.0 | https://icons8.com qt_themes/default/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team qt_themes/default/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com qt_themes/default/icons/48x48/star.png | CC BY-ND 3.0 | https://icons8.com +qt_themes/qdarkstyle/icons/16x16/connected.png | CC BY-ND 3.0 | https://icons8.com +qt_themes/qdarkstyle/icons/16x16/connected_notification.png | CC BY-ND 3.0 | https://icons8.com qt_themes/qdarkstyle/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com qt_themes/qdarkstyle/icons/16x16/view-refresh.png | Apache 2.0 | https://material.io qt_themes/qdarkstyle/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com qt_themes/qdarkstyle/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com qt_themes/qdarkstyle/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com qt_themes/qdarkstyle/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com +qt_themes/qdarkstyle/icons/48x48/no_avatar.png | CC BY-ND 3.0 | https://icons8.com qt_themes/qdarkstyle/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team qt_themes/qdarkstyle/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com qt_themes/qdarkstyle/icons/48x48/star.png | CC BY-ND 3.0 | https://icons8.com +qt_themes/colorful/icons/16x16/connected.png | CC BY-ND 3.0 | https://icons8.com +qt_themes/colorful/icons/16x16/connected_notification.png | CC BY-ND 3.0 | https://icons8.com qt_themes/colorful/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com qt_themes/colorful/icons/16x16/view-refresh.png | Apache 2.0 | https://material.io qt_themes/colorful/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com diff --git a/dist/qt_themes/colorful/icons/16x16/connected.png b/dist/qt_themes/colorful/icons/16x16/connected.png new file mode 100644 index 0000000000000000000000000000000000000000..d6052f1a09a9a828bf8d43ad1827d4d9d9cbae0f GIT binary patch literal 362 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc!T+8vjv*HQ$r4owhbC=p{o8&0tK-fV$N!7I zG75{Wk2Xy_x$CCz^fmhIm;O7pIV>$~UU0|jc-Os|kq&3=6=xmz@v22N`_uxjg657l zK`STRITf1dYARnXzHVnz%7Op?j%KGjO-*Ef=z4Y2!JVP;O`o&4nm=o8Um(r&<$utM zpf?GRCYAmx4Sx2X_vGqd?TH*0tQ9I(1o>RpUa?m2%z2>1TX%C75w(D&HyAH45$5j4PH{Kc+44&>bodk5m->}@RC7fLxAcI z#!1f(d3s;Z@QK)^x266fTk#5$LAl}+;~l5Q zvTbMg9^UuieCIpx-&Ifi=FyAs#93blXAAVk6T1cLkfNhvWKhv_s&Zmhz^+*5hT57% zOF_;n8U!$wS#BAtE4F_}x9_uN3h#||R;qIF^}7#eT+d|uz94mKnfhXH31H9huIfo8 zi^zay+&fR64K1E@-JxlR%=;^#xv6PIV?+G_s+UCMg=h4^)8}bd)f^GE)~e{qV!srN z3)gS0wp1Shp|S7zWIB^sFUGWeiInyCIgX))T9mw`_&DPPf>r?`06RRtwXvb@^0G)- zvhTjLk|#o z6b%F40BRWfRG*xU)HfORdVHeMwzg(YsI)*YB#UnSgU(w<`ka z({=BSp$~)N!i*09^m-ixwy0%@^JLsMc);uRIsiZ) zuLI9}Ao~5H-7!{nzHnbt1h`(`n!P@CZF^;XOBDdj2{VlYef>Elg$2zM_TEx)f%gM| zUZ=yD!}+%K$gbM}Kq7aFCTBFu)Nxkps?w_aZPdD~J>>xKvH4(vCB=E_n|RDx~z-v`(u{Ck(>^ z0K?FTjg5ud&4Z?C%4}L$RQtHS76>VRIGUfI`z*_{6mBpK4XsuKH!pyuX^Kna4xOuM z+^h(UdmYV_Q&T;Qn}iUk)hg+yeWcSi;>fQo&hJ(N2*<9-;|zukPUl1ymtfdJ7=}o) zEICHUE|eYJU8xid7X<*9>zeX|uHNTr)5hE=LNY|r-*L8JZ`1$60Pq`<)BU@nD5;_V O0000 icons/index.theme + icons/16x16/connected.png + icons/16x16/connected_notification.png + icons/16x16/disconnected.png icons/16x16/lock.png icons/48x48/bad_folder.png icons/48x48/chip.png diff --git a/dist/qt_themes/colorful_dark/style.qrc b/dist/qt_themes/colorful_dark/style.qrc index 0abcb4e83d..602c71b328 100644 --- a/dist/qt_themes/colorful_dark/style.qrc +++ b/dist/qt_themes/colorful_dark/style.qrc @@ -1,11 +1,15 @@ + ../colorful/icons/16x16/connected.png + ../colorful/icons/16x16/connected_notification.png + ../colorful/icons/16x16/disconnected.png icons/index.theme icons/16x16/lock.png icons/16x16/view-refresh.png ../colorful/icons/48x48/bad_folder.png ../colorful/icons/48x48/chip.png ../colorful/icons/48x48/folder.png + ../qdarkstyle/icons/48x48/no_avatar.png ../colorful/icons/48x48/plus.png ../colorful/icons/48x48/sd_card.png ../colorful/icons/256x256/plus_folder.png diff --git a/dist/qt_themes/default/default.qrc b/dist/qt_themes/default/default.qrc index b195747a37..41842e5d83 100644 --- a/dist/qt_themes/default/default.qrc +++ b/dist/qt_themes/default/default.qrc @@ -4,10 +4,14 @@ icons/16x16/checked.png icons/16x16/failed.png icons/16x16/lock.png + icons/16x16/connected.png + icons/16x16/disconnected.png + icons/16x16/connected_notification.png icons/16x16/view-refresh.png icons/48x48/bad_folder.png icons/48x48/chip.png icons/48x48/folder.png + icons/48x48/no_avatar.png icons/48x48/plus.png icons/48x48/sd_card.png icons/48x48/star.png diff --git a/dist/qt_themes/default/icons/16x16/connected.png b/dist/qt_themes/default/icons/16x16/connected.png new file mode 100644 index 0000000000000000000000000000000000000000..afa7973948c2fd69a5b838ebc993a4a618e20e31 GIT binary patch literal 269 zcmV+o0rLKdP)wVS)a5(TtL3F*6^H+fmi83={^g~HAo7u1ZbyiW#=5CU=?7R{1!Zbe$F9tyMfYy zZs6*qt^z68Vfj6G&{i5N*B8S8soODKE0Ee2()LDxTpa4zWdu+_YwLCwvHtx4JVP#E TIr_f000000NkvXXu0mjf{AX#) literal 0 HcmV?d00001 diff --git a/dist/qt_themes/default/icons/16x16/connected_notification.png b/dist/qt_themes/default/icons/16x16/connected_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..e64901378b000db1871d03db88af5f06c454cb01 GIT binary patch literal 517 zcmV+g0{Z=lP)=oH5kv??W6KBf~W^uD5aJbuU z?1_l^zTebLSsabzI+llOtNV%$o|VX}coVWS{*HQ<)uxI9Ck6S4=9S_D*7>#60 zKOeWZFLYif{X3q+e8sGmn$7ygk(FzSW2Em%_GsHe=P~>O_ZNK92NF;*00000NkvXX Hu0mjf^1s=j literal 0 HcmV?d00001 diff --git a/dist/qt_themes/default/icons/16x16/disconnected.png b/dist/qt_themes/default/icons/16x16/disconnected.png new file mode 100644 index 0000000000000000000000000000000000000000..835b1f0d6b5ceeb11e9a2b7d68c61521aef7bcb8 GIT binary patch literal 306 zcmV-20nPr2P)au7)Yo#YK5^hRAPlWv+5i9m07*qoM6N<$ Ef=+yWl>h($ literal 0 HcmV?d00001 diff --git a/dist/qt_themes/default/icons/48x48/no_avatar.png b/dist/qt_themes/default/icons/48x48/no_avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..d4bf82026a63d3498c57615ed744ef6ad1a11db4 GIT binary patch literal 588 zcmV-S0<-;zP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0pUqRK~!i%?bts^ z)L|UQ@k_%$K?EW+2x>Uh5Tcv9ID|t(AUGrhij56zIR}A+gAO9Jr08b!2O=6m2Zt72 zf~GJKrJ<=1OR>`Tcl&XCxO<+v^S%%9d&6t}-N*C({O&2_<>l49c1+H(cMu9ry_~F2o$%>&5}pz)}{_h$OO) zLp8FLVI))R7uq*mL^55%F4S1%5t8c`j-du;eT4mvPO?u>qqAIZpUbrkH99NlKOp2k za39IG12sD9A(E^gf1yTa*^%P~B-H@+pav_?k;D;W4;B&f%onro4Lgyj`$wvsFyF>I zR5HBScRWES&NX|c9^fm~kZReX?+f&yrr8^A68lgOTx)jR_zGR9Iy?7YgLmcf a6$)pIccPOKD6@6|0000Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0U}96K~y+TrIbBN z15p%(2ThUA!elK%1O;&sLOKPvAQqN^S%76$5m9U;Tacs`0+KRKh!$yP^1YdppLy@j zBbpBm?{V)rhhg%Q973Mw%`D4aP+z0Jqmp;@i!L;dL#Tnd!+DKbUdg3waWGz?Z^8u% zk9EdN@ZV$lKwY#fE2=PmlHCTw9lAj?WlY$Q6tTfDbS3CER>p3rhz*9WjL|aY6GcK; zx)-~7C$0pUWy~HGtHREG#M9EY#VX_e#&N7@6_#hEEo8S0El^vr5SHi4nW$v_WT|2y zEKe}ain4kx)4O#2)N;bb8 r=102j2jPKV@-HZOM*IDr)RH7C%sED}gz8E800000NkvXXu0mjf2Q8tG literal 0 HcmV?d00001 diff --git a/dist/qt_themes/qdarkstyle/icons/16x16/connected_notification.png b/dist/qt_themes/qdarkstyle/icons/16x16/connected_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..7cd8b9d2930d99340778360c68bf28923a66609c GIT binary patch literal 526 zcmV+p0`dKcP)2WpUc5F_z--H^xh8V++PPa z33XEKY?gdgT~jBj|8aa2I1da0uYrTWBVZ~bo|TbKsi966PC1e~wUOg4^}G7fa*Xuu zD}ChCz!%^SaA1RG7s}j2{h+R>H`N~{31-R!rn93J^+qAGq+VB#t4(!Zk5kRaL%_XE zZwD|7j010%$4)l2G8(}uq%QEa{|BSMKwqd&%+I&Gt@=}pSAmByUBvjj+&Xa?AhHBA zg=3C-cdUL$odY1E5y>+Kw}I7x%KDK!!OK4KI`Cf7rGGvQYAIF5bAb`SY)*g}wJhau z3ikh)8lzs_F#r7`PS@PYUsN&*fWd_-?rE zoG=u!$6)85fB0bU!s5G7mw@{bF;~)+PNPxVI=FT#;4H+?AU)r`u=oPtH#>3~=2X*b QzyJUM07*qoM6N<$g7iq=m;e9( literal 0 HcmV?d00001 diff --git a/dist/qt_themes/qdarkstyle/icons/16x16/disconnected.png b/dist/qt_themes/qdarkstyle/icons/16x16/disconnected.png new file mode 100644 index 0000000000000000000000000000000000000000..fc5f23894e4aedae7a49981f68ba017a177b065b GIT binary patch literal 444 zcmV;t0YmPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0Z~arK~y+Tm6X3q z!(bG}BM3T)4oat1>3fJGI5_zRqJs}n!QHVSy7&}|Z{Q;C6&zcsljtHSLgMcv$HI{dexom*ghNEXcBKF-_BL>^@v~d;_m6`5%aP;4{rR@W0^`{D7O5#ex19(LFqb zU&V+JY)3s}}a0Qf@czHi&OA^DY-4`2a_->y&b9cID?Mf&*K<#kR|Tpm5}?1j7X4 z4jjTqBrVk)Us_UtgqE$`6a4DQ_TU|*mQ&iX713e2_G4@Sa=K!0vPRIeRkTiGreO$! zH&MmZ!_eyCMZY6~+NXx$F1sZT1bRk;ysHB8+LJc+0^2+Y(KXbspbuX?;02!GhRfCj m8qQ()KfzyOz?{P=kt9FFYiX8^{jL1~0000Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0$E8!K~!i%?U+4C z98nlWS0ieKAc&Ach>BHGNYF0YqzD!%gn)&DplF%GF4lqk-Z(wLx~!5;{cLSkWI zlQe-;q9BTeRU~K>b=Nb#TM5I?JlFRgG6yc>;`{D*#@}Uf85tR+rBEo;=kxg?@`?Q5 z^-r#wvA0p3&g6)M?Qe5o~yzyh)l{YEq|GP zp&CCRQH#Wx@ktM}>#Fht4n0Q24sIagsp{Or@@ctPLr0hD+{5w(d00b7zv|q>@^A96 zhK@efxrhCqjfOqkK)34L!}1Av+=Z8_a}PH#Bnc~+KjBiTI`<&ZYEtG4KGTZ(fhEyH-1lVHtS&9|Mh|_zk~Xa@4Rn`LI!F%8s{4gK5zC}e3ii-fP0^yM zb_;utiTz_oI-;NTRa55&_MejuDW!w?0aN1_ZeW|F|4YTtA9Z*KmtJWlgMntv1_3?` qQcDIioeb>+lqIQUWMn)Xa=B}!H(avGT(DUH0000 icons/index.theme + icons/16x16/connected.png + icons/16x16/disconnected.png + icons/16x16/connected_notification.png icons/16x16/lock.png icons/16x16/view-refresh.png icons/48x48/bad_folder.png icons/48x48/chip.png icons/48x48/folder.png + icons/48x48/no_avatar.png icons/48x48/plus.png icons/48x48/sd_card.png icons/48x48/star.png diff --git a/externals/cpp-jwt b/externals/cpp-jwt new file mode 160000 index 0000000000..e12ef06218 --- /dev/null +++ b/externals/cpp-jwt @@ -0,0 +1 @@ +Subproject commit e12ef06218596b52d9b5d6e1639484866a8e7067 diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index d574e4b792..05fdfea829 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -41,6 +41,7 @@ add_custom_command(OUTPUT scm_rev.cpp add_library(common STATIC algorithm.h alignment.h + announce_multiplayer_room.h assert.cpp assert.h atomic_helpers.h diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h new file mode 100644 index 0000000000..5ca5893ef0 --- /dev/null +++ b/src/common/announce_multiplayer_room.h @@ -0,0 +1,138 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include "common/common_types.h" +#include "web_service/web_result.h" + +namespace AnnounceMultiplayerRoom { + +using MacAddress = std::array; + +struct Room { + struct Member { + std::string username; + std::string nickname; + std::string avatar_url; + MacAddress mac_address; + std::string game_name; + u64 game_id; + }; + std::string id; + std::string verify_UID; ///< UID used for verification + std::string name; + std::string description; + std::string owner; + std::string ip; + u16 port; + u32 max_player; + u32 net_version; + bool has_password; + std::string preferred_game; + u64 preferred_game_id; + + std::vector members; +}; +using RoomList = std::vector; + +/** + * A AnnounceMultiplayerRoom interface class. A backend to submit/get to/from a web service should + * implement this interface. + */ +class Backend { +public: + virtual ~Backend() = default; + + /** + * Sets the Information that gets used for the announce + * @param uid The Id of the room + * @param name The name of the room + * @param description The room description + * @param port The port of the room + * @param net_version The version of the libNetwork that gets used + * @param has_password True if the room is passowrd protected + * @param preferred_game The preferred game of the room + * @param preferred_game_id The title id of the preferred game + */ + virtual void SetRoomInformation(const std::string& name, const std::string& description, + const u16 port, const u32 max_player, const u32 net_version, + const bool has_password, const std::string& preferred_game, + const u64 preferred_game_id) = 0; + /** + * Adds a player information to the data that gets announced + * @param nickname The nickname of the player + * @param mac_address The MAC Address of the player + * @param game_id The title id of the game the player plays + * @param game_name The name of the game the player plays + */ + virtual void AddPlayer(const std::string& username, const std::string& nickname, + const std::string& avatar_url, const MacAddress& mac_address, + const u64 game_id, const std::string& game_name) = 0; + + /** + * Updates the data in the announce service. Re-register the room when required. + * @result The result of the update attempt + */ + virtual WebService::WebResult Update() = 0; + + /** + * Registers the data in the announce service + * @result The result of the register attempt. When the result code is Success, A global Guid of + * the room which may be used for verification will be in the result's returned_data. + */ + virtual WebService::WebResult Register() = 0; + + /** + * Empties the stored players + */ + virtual void ClearPlayers() = 0; + + /** + * Get the room information from the announce service + * @result A list of all rooms the announce service has + */ + virtual RoomList GetRoomList() = 0; + + /** + * Sends a delete message to the announce service + */ + virtual void Delete() = 0; +}; + +/** + * Empty implementation of AnnounceMultiplayerRoom interface that drops all data. Used when a + * functional backend implementation is not available. + */ +class NullBackend : public Backend { +public: + ~NullBackend() = default; + void SetRoomInformation(const std::string& /*name*/, const std::string& /*description*/, + const u16 /*port*/, const u32 /*max_player*/, const u32 /*net_version*/, + const bool /*has_password*/, const std::string& /*preferred_game*/, + const u64 /*preferred_game_id*/) override {} + void AddPlayer(const std::string& /*username*/, const std::string& /*nickname*/, + const std::string& /*avatar_url*/, const MacAddress& /*mac_address*/, + const u64 /*game_id*/, const std::string& /*game_name*/) override {} + WebService::WebResult Update() override { + return WebService::WebResult{WebService::WebResult::Code::NoWebservice, + "WebService is missing"}; + } + WebService::WebResult Register() override { + return WebService::WebResult{WebService::WebResult::Code::NoWebservice, + "WebService is missing"}; + } + void ClearPlayers() override {} + RoomList GetRoomList() override { + return RoomList{}; + } + + void Delete() override {} +}; + +} // namespace AnnounceMultiplayerRoom diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 32cc2f3920..48f5c1ee0a 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,4 +1,6 @@ add_library(core STATIC + announce_multiplayer_session.cpp + announce_multiplayer_session.h arm/arm_interface.h arm/arm_interface.cpp arm/cpu_interrupt_handler.cpp @@ -741,11 +743,11 @@ add_library(core STATIC memory/dmnt_cheat_vm.h memory.cpp memory.h - network/network.cpp - network/network.h - network/network_interface.cpp - network/network_interface.h - network/sockets.h + internal_network/network.cpp + internal_network/network.h + internal_network/network_interface.cpp + internal_network/network_interface.h + internal_network/sockets.h perf_stats.cpp perf_stats.h reporter.cpp @@ -780,7 +782,7 @@ endif() create_target_directory_groups(core) -target_link_libraries(core PUBLIC common PRIVATE audio_core video_core) +target_link_libraries(core PUBLIC common PRIVATE audio_core network video_core) target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt::fmt nlohmann_json::nlohmann_json mbedtls Opus::Opus) if (MINGW) target_link_libraries(core PRIVATE ${MSWSOCK_LIBRARY}) diff --git a/src/core/announce_multiplayer_session.cpp b/src/core/announce_multiplayer_session.cpp new file mode 100644 index 0000000000..a680ad202e --- /dev/null +++ b/src/core/announce_multiplayer_session.cpp @@ -0,0 +1,165 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "announce_multiplayer_session.h" +#include "common/announce_multiplayer_room.h" +#include "common/assert.h" +#include "common/settings.h" +#include "network/network.h" + +#ifdef ENABLE_WEB_SERVICE +#include "web_service/announce_room_json.h" +#endif + +namespace Core { + +// Time between room is announced to web_service +static constexpr std::chrono::seconds announce_time_interval(15); + +AnnounceMultiplayerSession::AnnounceMultiplayerSession() { +#ifdef ENABLE_WEB_SERVICE + backend = std::make_unique(Settings::values.web_api_url.GetValue(), + Settings::values.yuzu_username.GetValue(), + Settings::values.yuzu_token.GetValue()); +#else + backend = std::make_unique(); +#endif +} + +WebService::WebResult AnnounceMultiplayerSession::Register() { + std::shared_ptr room = Network::GetRoom().lock(); + if (!room) { + return WebService::WebResult{WebService::WebResult::Code::LibError, + "Network is not initialized"}; + } + if (room->GetState() != Network::Room::State::Open) { + return WebService::WebResult{WebService::WebResult::Code::LibError, "Room is not open"}; + } + UpdateBackendData(room); + WebService::WebResult result = backend->Register(); + if (result.result_code != WebService::WebResult::Code::Success) { + return result; + } + LOG_INFO(WebService, "Room has been registered"); + room->SetVerifyUID(result.returned_data); + registered = true; + return WebService::WebResult{WebService::WebResult::Code::Success}; +} + +void AnnounceMultiplayerSession::Start() { + if (announce_multiplayer_thread) { + Stop(); + } + shutdown_event.Reset(); + announce_multiplayer_thread = + std::make_unique(&AnnounceMultiplayerSession::AnnounceMultiplayerLoop, this); +} + +void AnnounceMultiplayerSession::Stop() { + if (announce_multiplayer_thread) { + shutdown_event.Set(); + announce_multiplayer_thread->join(); + announce_multiplayer_thread.reset(); + backend->Delete(); + registered = false; + } +} + +AnnounceMultiplayerSession::CallbackHandle AnnounceMultiplayerSession::BindErrorCallback( + std::function function) { + std::lock_guard lock(callback_mutex); + auto handle = std::make_shared>(function); + error_callbacks.insert(handle); + return handle; +} + +void AnnounceMultiplayerSession::UnbindErrorCallback(CallbackHandle handle) { + std::lock_guard lock(callback_mutex); + error_callbacks.erase(handle); +} + +AnnounceMultiplayerSession::~AnnounceMultiplayerSession() { + Stop(); +} + +void AnnounceMultiplayerSession::UpdateBackendData(std::shared_ptr room) { + Network::RoomInformation room_information = room->GetRoomInformation(); + std::vector memberlist = room->GetRoomMemberList(); + backend->SetRoomInformation( + room_information.name, room_information.description, room_information.port, + room_information.member_slots, Network::network_version, room->HasPassword(), + room_information.preferred_game, room_information.preferred_game_id); + backend->ClearPlayers(); + for (const auto& member : memberlist) { + backend->AddPlayer(member.username, member.nickname, member.avatar_url, member.mac_address, + member.game_info.id, member.game_info.name); + } +} + +void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { + // Invokes all current bound error callbacks. + const auto ErrorCallback = [this](WebService::WebResult result) { + std::lock_guard lock(callback_mutex); + for (auto callback : error_callbacks) { + (*callback)(result); + } + }; + + if (!registered) { + WebService::WebResult result = Register(); + if (result.result_code != WebService::WebResult::Code::Success) { + ErrorCallback(result); + return; + } + } + + auto update_time = std::chrono::steady_clock::now(); + std::future future; + while (!shutdown_event.WaitUntil(update_time)) { + update_time += announce_time_interval; + std::shared_ptr room = Network::GetRoom().lock(); + if (!room) { + break; + } + if (room->GetState() != Network::Room::State::Open) { + break; + } + UpdateBackendData(room); + WebService::WebResult result = backend->Update(); + if (result.result_code != WebService::WebResult::Code::Success) { + ErrorCallback(result); + } + if (result.result_string == "404") { + registered = false; + // Needs to register the room again + WebService::WebResult register_result = Register(); + if (register_result.result_code != WebService::WebResult::Code::Success) { + ErrorCallback(register_result); + } + } + } +} + +AnnounceMultiplayerRoom::RoomList AnnounceMultiplayerSession::GetRoomList() { + return backend->GetRoomList(); +} + +bool AnnounceMultiplayerSession::IsRunning() const { + return announce_multiplayer_thread != nullptr; +} + +void AnnounceMultiplayerSession::UpdateCredentials() { + ASSERT_MSG(!IsRunning(), "Credentials can only be updated when session is not running"); + +#ifdef ENABLE_WEB_SERVICE + backend = std::make_unique(Settings::values.web_api_url.GetValue(), + Settings::values.yuzu_username.GetValue(), + Settings::values.yuzu_token.GetValue()); +#endif +} + +} // namespace Core diff --git a/src/core/announce_multiplayer_session.h b/src/core/announce_multiplayer_session.h new file mode 100644 index 0000000000..2aaf550177 --- /dev/null +++ b/src/core/announce_multiplayer_session.h @@ -0,0 +1,96 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "common/announce_multiplayer_room.h" +#include "common/common_types.h" +#include "common/thread.h" + +namespace Network { +class Room; +} + +namespace Core { + +/** + * Instruments AnnounceMultiplayerRoom::Backend. + * Creates a thread that regularly updates the room information and submits them + * An async get of room information is also possible + */ +class AnnounceMultiplayerSession { +public: + using CallbackHandle = std::shared_ptr>; + AnnounceMultiplayerSession(); + ~AnnounceMultiplayerSession(); + + /** + * Allows to bind a function that will get called if the announce encounters an error + * @param function The function that gets called + * @return A handle that can be used the unbind the function + */ + CallbackHandle BindErrorCallback(std::function function); + + /** + * Unbind a function from the error callbacks + * @param handle The handle for the function that should get unbind + */ + void UnbindErrorCallback(CallbackHandle handle); + + /** + * Registers a room to web services + * @return The result of the registration attempt. + */ + WebService::WebResult Register(); + + /** + * Starts the announce of a room to web services + */ + void Start(); + + /** + * Stops the announce to web services + */ + void Stop(); + + /** + * Returns a list of all room information the backend got + * @param func A function that gets executed when the async get finished, e.g. a signal + * @return a list of rooms received from the web service + */ + AnnounceMultiplayerRoom::RoomList GetRoomList(); + + /** + * Whether the announce session is still running + */ + bool IsRunning() const; + + /** + * Recreates the backend, updating the credentials. + * This can only be used when the announce session is not running. + */ + void UpdateCredentials(); + +private: + Common::Event shutdown_event; + std::mutex callback_mutex; + std::set error_callbacks; + std::unique_ptr announce_multiplayer_thread; + + /// Backend interface that logs fields + std::unique_ptr backend; + + std::atomic_bool registered = false; ///< Whether the room has been registered + + void UpdateBackendData(std::shared_ptr room); + void AnnounceMultiplayerLoop(); +}; + +} // namespace Core diff --git a/src/core/core.cpp b/src/core/core.cpp index 0ede0d85c0..5df32c1e7d 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -43,14 +43,15 @@ #include "core/hle/service/service.h" #include "core/hle/service/sm/sm.h" #include "core/hle/service/time/time_manager.h" +#include "core/internal_network/network.h" #include "core/loader/loader.h" #include "core/memory.h" #include "core/memory/cheat_engine.h" -#include "core/network/network.h" #include "core/perf_stats.h" #include "core/reporter.h" #include "core/telemetry_session.h" #include "core/tools/freezer.h" +#include "network/network.h" #include "video_core/renderer_base.h" #include "video_core/video_core.h" @@ -315,6 +316,15 @@ struct System::Impl { GetAndResetPerfStats(); perf_stats->BeginSystemFrame(); + std::string name = "Unknown Game"; + const Loader::ResultStatus res{app_loader->ReadTitle(name)}; + if (auto room_member = Network::GetRoomMember().lock()) { + Network::GameInfo game_info; + game_info.name = name; + game_info.id = program_id; + room_member->SendGameInfo(game_info); + } + status = SystemResultStatus::Success; return status; } @@ -362,6 +372,11 @@ struct System::Impl { memory.Reset(); applet_manager.ClearAll(); + if (auto room_member = Network::GetRoomMember().lock()) { + Network::GameInfo game_info{}; + room_member->SendGameInfo(game_info); + } + LOG_DEBUG(Core, "Shutdown OK"); } diff --git a/src/core/hle/service/nifm/nifm.cpp b/src/core/hle/service/nifm/nifm.cpp index 7055ea93e7..2889973e46 100644 --- a/src/core/hle/service/nifm/nifm.cpp +++ b/src/core/hle/service/nifm/nifm.cpp @@ -18,8 +18,8 @@ namespace { } // Anonymous namespace -#include "core/network/network.h" -#include "core/network/network_interface.h" +#include "core/internal_network/network.h" +#include "core/internal_network/network_interface.h" namespace Service::NIFM { diff --git a/src/core/hle/service/sockets/bsd.cpp b/src/core/hle/service/sockets/bsd.cpp index 3e9dc4a139..c7194731ed 100644 --- a/src/core/hle/service/sockets/bsd.cpp +++ b/src/core/hle/service/sockets/bsd.cpp @@ -13,8 +13,8 @@ #include "core/hle/kernel/k_thread.h" #include "core/hle/service/sockets/bsd.h" #include "core/hle/service/sockets/sockets_translate.h" -#include "core/network/network.h" -#include "core/network/sockets.h" +#include "core/internal_network/network.h" +#include "core/internal_network/sockets.h" namespace Service::Sockets { diff --git a/src/core/hle/service/sockets/bsd.h b/src/core/hle/service/sockets/bsd.h index fed740d878..9ea36428d6 100644 --- a/src/core/hle/service/sockets/bsd.h +++ b/src/core/hle/service/sockets/bsd.h @@ -16,7 +16,7 @@ class System; namespace Network { class Socket; -} +} // namespace Network namespace Service::Sockets { diff --git a/src/core/hle/service/sockets/sockets_translate.cpp b/src/core/hle/service/sockets/sockets_translate.cpp index 9c0936d973..2db10ec810 100644 --- a/src/core/hle/service/sockets/sockets_translate.cpp +++ b/src/core/hle/service/sockets/sockets_translate.cpp @@ -7,7 +7,7 @@ #include "common/common_types.h" #include "core/hle/service/sockets/sockets.h" #include "core/hle/service/sockets/sockets_translate.h" -#include "core/network/network.h" +#include "core/internal_network/network.h" namespace Service::Sockets { diff --git a/src/core/hle/service/sockets/sockets_translate.h b/src/core/hle/service/sockets/sockets_translate.h index 5e9809add2..c93291d3ee 100644 --- a/src/core/hle/service/sockets/sockets_translate.h +++ b/src/core/hle/service/sockets/sockets_translate.h @@ -7,7 +7,7 @@ #include "common/common_types.h" #include "core/hle/service/sockets/sockets.h" -#include "core/network/network.h" +#include "core/internal_network/network.h" namespace Service::Sockets { diff --git a/src/core/network/network.cpp b/src/core/internal_network/network.cpp similarity index 99% rename from src/core/network/network.cpp rename to src/core/internal_network/network.cpp index fdafbea923..36c43cc8ff 100644 --- a/src/core/network/network.cpp +++ b/src/core/internal_network/network.cpp @@ -29,9 +29,9 @@ #include "common/common_types.h" #include "common/logging/log.h" #include "common/settings.h" -#include "core/network/network.h" -#include "core/network/network_interface.h" -#include "core/network/sockets.h" +#include "core/internal_network/network.h" +#include "core/internal_network/network_interface.h" +#include "core/internal_network/sockets.h" namespace Network { diff --git a/src/core/network/network.h b/src/core/internal_network/network.h similarity index 100% rename from src/core/network/network.h rename to src/core/internal_network/network.h diff --git a/src/core/network/network_interface.cpp b/src/core/internal_network/network_interface.cpp similarity index 99% rename from src/core/network/network_interface.cpp rename to src/core/internal_network/network_interface.cpp index 15ecc6abf5..0f0a661606 100644 --- a/src/core/network/network_interface.cpp +++ b/src/core/internal_network/network_interface.cpp @@ -11,7 +11,7 @@ #include "common/logging/log.h" #include "common/settings.h" #include "common/string_util.h" -#include "core/network/network_interface.h" +#include "core/internal_network/network_interface.h" #ifdef _WIN32 #include diff --git a/src/core/network/network_interface.h b/src/core/internal_network/network_interface.h similarity index 100% rename from src/core/network/network_interface.h rename to src/core/internal_network/network_interface.h diff --git a/src/core/network/sockets.h b/src/core/internal_network/sockets.h similarity index 97% rename from src/core/network/sockets.h rename to src/core/internal_network/sockets.h index f889159f5f..77e27e9280 100644 --- a/src/core/network/sockets.h +++ b/src/core/internal_network/sockets.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include @@ -12,7 +13,7 @@ #endif #include "common/common_types.h" -#include "core/network/network.h" +#include "core/internal_network/network.h" // TODO: C++20 Replace std::vector usages with std::span diff --git a/src/network/room.cpp b/src/network/room.cpp index cd0c0ebc49..5288671469 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -251,7 +251,7 @@ public: void Room::RoomImpl::ServerLoop() { while (state != State::Closed) { ENetEvent event; - if (enet_host_service(server, &event, 50) > 0) { + if (enet_host_service(server, &event, 16) > 0) { switch (event.type) { case ENET_EVENT_TYPE_RECEIVE: switch (event.packet->data[0]) { @@ -599,7 +599,7 @@ bool Room::RoomImpl::HasModPermission(const ENetPeer* client) const { if (sending_member == members.end()) { return false; } - if (room_information.enable_citra_mods && + if (room_information.enable_yuzu_mods && sending_member->user_data.moderator) { // Community moderator return true; @@ -1014,7 +1014,7 @@ bool Room::Create(const std::string& name, const std::string& description, const u32 max_connections, const std::string& host_username, const std::string& preferred_game, u64 preferred_game_id, std::unique_ptr verify_backend, - const Room::BanList& ban_list, bool enable_citra_mods) { + const Room::BanList& ban_list, bool enable_yuzu_mods) { ENetAddress address; address.host = ENET_HOST_ANY; if (!server_address.empty()) { @@ -1037,7 +1037,7 @@ bool Room::Create(const std::string& name, const std::string& description, room_impl->room_information.preferred_game = preferred_game; room_impl->room_information.preferred_game_id = preferred_game_id; room_impl->room_information.host_username = host_username; - room_impl->room_information.enable_citra_mods = enable_citra_mods; + room_impl->room_information.enable_yuzu_mods = enable_yuzu_mods; room_impl->password = password; room_impl->verify_backend = std::move(verify_backend); room_impl->username_ban_list = ban_list.first; diff --git a/src/network/room.h b/src/network/room.h index a679848370..5d4371c166 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -32,7 +32,7 @@ struct RoomInformation { std::string preferred_game; ///< Game to advertise that you want to play u64 preferred_game_id; ///< Title ID for the advertised game std::string host_username; ///< Forum username of the host - bool enable_citra_mods; ///< Allow Citra Moderators to moderate on this room + bool enable_yuzu_mods; ///< Allow yuzu Moderators to moderate on this room }; struct GameInfo { @@ -148,7 +148,7 @@ public: const std::string& host_username = "", const std::string& preferred_game = "", u64 preferred_game_id = 0, std::unique_ptr verify_backend = nullptr, - const BanList& ban_list = {}, bool enable_citra_mods = false); + const BanList& ban_list = {}, bool enable_yuzu_mods = false); /** * Sets the verification GUID of the room. diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp index e430040271..d6ace9b39d 100644 --- a/src/network/room_member.cpp +++ b/src/network/room_member.cpp @@ -86,7 +86,7 @@ public: * @params password The password for the room * the server to assign one for us. */ - void SendJoinRequest(const std::string& nickname, const std::string& console_id_hash, + void SendJoinRequest(const std::string& nickname_, const std::string& console_id_hash, const MacAddress& preferred_mac = NoPreferredMac, const std::string& password = "", const std::string& token = ""); @@ -159,7 +159,7 @@ void RoomMember::RoomMemberImpl::MemberLoop() { while (IsConnected()) { std::lock_guard lock(network_mutex); ENetEvent event; - if (enet_host_service(client, &event, 100) > 0) { + if (enet_host_service(client, &event, 16) > 0) { switch (event.type) { case ENET_EVENT_TYPE_RECEIVE: switch (event.packet->data[0]) { @@ -251,16 +251,17 @@ void RoomMember::RoomMemberImpl::MemberLoop() { break; } } + std::list packets; { - std::lock_guard lock(send_list_mutex); - for (const auto& packet : send_list) { - ENetPacket* enetPacket = enet_packet_create(packet.GetData(), packet.GetDataSize(), - ENET_PACKET_FLAG_RELIABLE); - enet_peer_send(server, 0, enetPacket); - } - enet_host_flush(client); - send_list.clear(); + std::lock_guard send_lock(send_list_mutex); + packets.swap(send_list); } + for (const auto& packet : packets) { + ENetPacket* enetPacket = enet_packet_create(packet.GetData(), packet.GetDataSize(), + ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(server, 0, enetPacket); + } + enet_host_flush(client); } Disconnect(); }; @@ -274,14 +275,14 @@ void RoomMember::RoomMemberImpl::Send(Packet&& packet) { send_list.push_back(std::move(packet)); } -void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname, +void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname_, const std::string& console_id_hash, const MacAddress& preferred_mac, const std::string& password, const std::string& token) { Packet packet; packet << static_cast(IdJoinRequest); - packet << nickname; + packet << nickname_; packet << console_id_hash; packet << preferred_mac; packet << network_version; diff --git a/src/network/verify_user.h b/src/network/verify_user.h index 01b9877c86..5c3852d4aa 100644 --- a/src/network/verify_user.h +++ b/src/network/verify_user.h @@ -13,7 +13,7 @@ struct UserData { std::string username; std::string display_name; std::string avatar_url; - bool moderator = false; ///< Whether the user is a Citra Moderator. + bool moderator = false; ///< Whether the user is a yuzu Moderator. }; /** diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index a69ccb2644..fbbcf673ac 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -7,7 +7,7 @@ add_executable(tests common/ring_buffer.cpp common/unique_function.cpp core/core_timing.cpp - core/network/network.cpp + core/internal_network/network.cpp tests.cpp video_core/buffer_base.cpp input_common/calibration_configuration_job.cpp diff --git a/src/tests/core/network/network.cpp b/src/tests/core/internal_network/network.cpp similarity index 90% rename from src/tests/core/network/network.cpp rename to src/tests/core/internal_network/network.cpp index 1bbb8372fb..164b0ff245 100644 --- a/src/tests/core/network/network.cpp +++ b/src/tests/core/internal_network/network.cpp @@ -3,8 +3,8 @@ #include -#include "core/network/network.h" -#include "core/network/sockets.h" +#include "core/internal_network/network.h" +#include "core/internal_network/sockets.h" TEST_CASE("Network::Errors", "[core]") { Network::NetworkInstance network_instance; // initialize network diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt index ae85a72ea0..aff81f26d8 100644 --- a/src/web_service/CMakeLists.txt +++ b/src/web_service/CMakeLists.txt @@ -1,4 +1,6 @@ add_library(web_service STATIC + announce_room_json.cpp + announce_room_json.h telemetry_json.cpp telemetry_json.h verify_login.cpp @@ -9,4 +11,4 @@ add_library(web_service STATIC ) create_target_directory_groups(web_service) -target_link_libraries(web_service PRIVATE common nlohmann_json::nlohmann_json httplib) +target_link_libraries(web_service PRIVATE common network nlohmann_json::nlohmann_json httplib) diff --git a/src/web_service/announce_room_json.cpp b/src/web_service/announce_room_json.cpp new file mode 100644 index 0000000000..31804d2b1c --- /dev/null +++ b/src/web_service/announce_room_json.cpp @@ -0,0 +1,157 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "common/detached_tasks.h" +#include "common/logging/log.h" +#include "web_service/announce_room_json.h" +#include "web_service/web_backend.h" + +namespace AnnounceMultiplayerRoom { + +void to_json(nlohmann::json& json, const Room::Member& member) { + if (!member.username.empty()) { + json["username"] = member.username; + } + json["nickname"] = member.nickname; + if (!member.avatar_url.empty()) { + json["avatarUrl"] = member.avatar_url; + } + json["gameName"] = member.game_name; + json["gameId"] = member.game_id; +} + +void from_json(const nlohmann::json& json, Room::Member& member) { + member.nickname = json.at("nickname").get(); + member.game_name = json.at("gameName").get(); + member.game_id = json.at("gameId").get(); + try { + member.username = json.at("username").get(); + member.avatar_url = json.at("avatarUrl").get(); + } catch (const nlohmann::detail::out_of_range&) { + member.username = member.avatar_url = ""; + LOG_DEBUG(Network, "Member \'{}\' isn't authenticated", member.nickname); + } +} + +void to_json(nlohmann::json& json, const Room& room) { + json["port"] = room.port; + json["name"] = room.name; + if (!room.description.empty()) { + json["description"] = room.description; + } + json["preferredGameName"] = room.preferred_game; + json["preferredGameId"] = room.preferred_game_id; + json["maxPlayers"] = room.max_player; + json["netVersion"] = room.net_version; + json["hasPassword"] = room.has_password; + if (room.members.size() > 0) { + nlohmann::json member_json = room.members; + json["players"] = member_json; + } +} + +void from_json(const nlohmann::json& json, Room& room) { + room.verify_UID = json.at("externalGuid").get(); + room.ip = json.at("address").get(); + room.name = json.at("name").get(); + try { + room.description = json.at("description").get(); + } catch (const nlohmann::detail::out_of_range&) { + room.description = ""; + LOG_DEBUG(Network, "Room \'{}\' doesn't contain a description", room.name); + } + room.owner = json.at("owner").get(); + room.port = json.at("port").get(); + room.preferred_game = json.at("preferredGameName").get(); + room.preferred_game_id = json.at("preferredGameId").get(); + room.max_player = json.at("maxPlayers").get(); + room.net_version = json.at("netVersion").get(); + room.has_password = json.at("hasPassword").get(); + try { + room.members = json.at("players").get>(); + } catch (const nlohmann::detail::out_of_range& e) { + LOG_DEBUG(Network, "Out of range {}", e.what()); + } +} + +} // namespace AnnounceMultiplayerRoom + +namespace WebService { + +void RoomJson::SetRoomInformation(const std::string& name, const std::string& description, + const u16 port, const u32 max_player, const u32 net_version, + const bool has_password, const std::string& preferred_game, + const u64 preferred_game_id) { + room.name = name; + room.description = description; + room.port = port; + room.max_player = max_player; + room.net_version = net_version; + room.has_password = has_password; + room.preferred_game = preferred_game; + room.preferred_game_id = preferred_game_id; +} +void RoomJson::AddPlayer(const std::string& username_, const std::string& nickname_, + const std::string& avatar_url, + const AnnounceMultiplayerRoom::MacAddress& mac_address, const u64 game_id, + const std::string& game_name) { + AnnounceMultiplayerRoom::Room::Member member; + member.username = username_; + member.nickname = nickname_; + member.avatar_url = avatar_url; + member.mac_address = mac_address; + member.game_id = game_id; + member.game_name = game_name; + room.members.push_back(member); +} + +WebService::WebResult RoomJson::Update() { + if (room_id.empty()) { + LOG_ERROR(WebService, "Room must be registered to be updated"); + return WebService::WebResult{WebService::WebResult::Code::LibError, + "Room is not registered"}; + } + nlohmann::json json{{"players", room.members}}; + return client.PostJson(fmt::format("/lobby/{}", room_id), json.dump(), false); +} + +WebService::WebResult RoomJson::Register() { + nlohmann::json json = room; + auto result = client.PostJson("/lobby", json.dump(), false); + if (result.result_code != WebService::WebResult::Code::Success) { + return result; + } + auto reply_json = nlohmann::json::parse(result.returned_data); + room = reply_json.get(); + room_id = reply_json.at("id").get(); + return WebService::WebResult{WebService::WebResult::Code::Success, "", room.verify_UID}; +} + +void RoomJson::ClearPlayers() { + room.members.clear(); +} + +AnnounceMultiplayerRoom::RoomList RoomJson::GetRoomList() { + auto reply = client.GetJson("/lobby", true).returned_data; + if (reply.empty()) { + return {}; + } + return nlohmann::json::parse(reply).at("rooms").get(); +} + +void RoomJson::Delete() { + if (room_id.empty()) { + LOG_ERROR(WebService, "Room must be registered to be deleted"); + return; + } + Common::DetachedTasks::AddTask( + [host{this->host}, username{this->username}, token{this->token}, room_id{this->room_id}]() { + // create a new client here because the this->client might be destroyed. + Client{host, username, token}.DeleteJson(fmt::format("/lobby/{}", room_id), "", false); + }); +} + +} // namespace WebService diff --git a/src/web_service/announce_room_json.h b/src/web_service/announce_room_json.h new file mode 100644 index 0000000000..ac02af5b17 --- /dev/null +++ b/src/web_service/announce_room_json.h @@ -0,0 +1,46 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "common/announce_multiplayer_room.h" +#include "web_service/web_backend.h" + +namespace WebService { + +/** + * Implementation of AnnounceMultiplayerRoom::Backend that (de)serializes room information into/from + * JSON, and submits/gets it to/from the yuzu web service + */ +class RoomJson : public AnnounceMultiplayerRoom::Backend { +public: + RoomJson(const std::string& host, const std::string& username, const std::string& token) + : client(host, username, token), host(host), username(username), token(token) {} + ~RoomJson() = default; + void SetRoomInformation(const std::string& name, const std::string& description, const u16 port, + const u32 max_player, const u32 net_version, const bool has_password, + const std::string& preferred_game, + const u64 preferred_game_id) override; + void AddPlayer(const std::string& username_, const std::string& nickname_, + const std::string& avatar_url, + const AnnounceMultiplayerRoom::MacAddress& mac_address, const u64 game_id, + const std::string& game_name) override; + WebResult Update() override; + WebResult Register() override; + void ClearPlayers() override; + AnnounceMultiplayerRoom::RoomList GetRoomList() override; + void Delete() override; + +private: + AnnounceMultiplayerRoom::Room room; + Client client; + std::string host; + std::string username; + std::string token; + std::string room_id; +}; + +} // namespace WebService diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 57e0e70253..f3cad8f311 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -156,10 +156,36 @@ add_executable(yuzu main.cpp main.h main.ui + multiplayer/chat_room.cpp + multiplayer/chat_room.h + multiplayer/chat_room.ui + multiplayer/client_room.h + multiplayer/client_room.cpp + multiplayer/client_room.ui + multiplayer/direct_connect.cpp + multiplayer/direct_connect.h + multiplayer/direct_connect.ui + multiplayer/host_room.cpp + multiplayer/host_room.h + multiplayer/host_room.ui + multiplayer/lobby.cpp + multiplayer/lobby.h + multiplayer/lobby.ui + multiplayer/lobby_p.h + multiplayer/message.cpp + multiplayer/message.h + multiplayer/moderation_dialog.cpp + multiplayer/moderation_dialog.h + multiplayer/moderation_dialog.ui + multiplayer/state.cpp + multiplayer/state.h + multiplayer/validation.h startup_checks.cpp startup_checks.h uisettings.cpp uisettings.h + util/clickable_label.cpp + util/clickable_label.h util/controller_navigation.cpp util/controller_navigation.h util/limitable_input_dialog.cpp @@ -256,7 +282,7 @@ endif() create_target_directory_groups(yuzu) -target_link_libraries(yuzu PRIVATE common core input_common video_core) +target_link_libraries(yuzu PRIVATE common core input_common network video_core) target_link_libraries(yuzu PRIVATE Boost::boost glad Qt::Widgets Qt::Multimedia) target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index c841843f00..7c11e2c034 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -11,6 +11,7 @@ #include "core/hle/service/acc/profile_manager.h" #include "core/hle/service/hid/controllers/npad.h" #include "input_common/main.h" +#include "network/network.h" #include "yuzu/configuration/config.h" namespace FS = Common::FS; @@ -584,6 +585,48 @@ void Config::ReadMiscellaneousValues() { qt_config->endGroup(); } +void Config::ReadMultiplayerValues() { + qt_config->beginGroup(QStringLiteral("Multiplayer")); + + UISettings::values.nickname = ReadSetting(QStringLiteral("nickname"), QString{}).toString(); + UISettings::values.ip = ReadSetting(QStringLiteral("ip"), QString{}).toString(); + UISettings::values.port = + ReadSetting(QStringLiteral("port"), Network::DefaultRoomPort).toString(); + UISettings::values.room_nickname = + ReadSetting(QStringLiteral("room_nickname"), QString{}).toString(); + UISettings::values.room_name = ReadSetting(QStringLiteral("room_name"), QString{}).toString(); + UISettings::values.room_port = + ReadSetting(QStringLiteral("room_port"), QStringLiteral("24872")).toString(); + bool ok; + UISettings::values.host_type = ReadSetting(QStringLiteral("host_type"), 0).toUInt(&ok); + if (!ok) { + UISettings::values.host_type = 0; + } + UISettings::values.max_player = ReadSetting(QStringLiteral("max_player"), 8).toUInt(); + UISettings::values.game_id = ReadSetting(QStringLiteral("game_id"), 0).toULongLong(); + UISettings::values.room_description = + ReadSetting(QStringLiteral("room_description"), QString{}).toString(); + // Read ban list back + int size = qt_config->beginReadArray(QStringLiteral("username_ban_list")); + UISettings::values.ban_list.first.resize(size); + for (int i = 0; i < size; ++i) { + qt_config->setArrayIndex(i); + UISettings::values.ban_list.first[i] = + ReadSetting(QStringLiteral("username")).toString().toStdString(); + } + qt_config->endArray(); + size = qt_config->beginReadArray(QStringLiteral("ip_ban_list")); + UISettings::values.ban_list.second.resize(size); + for (int i = 0; i < size; ++i) { + qt_config->setArrayIndex(i); + UISettings::values.ban_list.second[i] = + ReadSetting(QStringLiteral("ip")).toString().toStdString(); + } + qt_config->endArray(); + + qt_config->endGroup(); +} + void Config::ReadPathValues() { qt_config->beginGroup(QStringLiteral("Paths")); @@ -794,6 +837,7 @@ void Config::ReadUIValues() { ReadPathValues(); ReadScreenshotValues(); ReadShortcutValues(); + ReadMultiplayerValues(); ReadBasicSetting(UISettings::values.single_window_mode); ReadBasicSetting(UISettings::values.fullscreen); @@ -1161,6 +1205,40 @@ void Config::SaveMiscellaneousValues() { qt_config->endGroup(); } +void Config::SaveMultiplayerValues() { + qt_config->beginGroup(QStringLiteral("Multiplayer")); + + WriteSetting(QStringLiteral("nickname"), UISettings::values.nickname, QString{}); + WriteSetting(QStringLiteral("ip"), UISettings::values.ip, QString{}); + WriteSetting(QStringLiteral("port"), UISettings::values.port, Network::DefaultRoomPort); + WriteSetting(QStringLiteral("room_nickname"), UISettings::values.room_nickname, QString{}); + WriteSetting(QStringLiteral("room_name"), UISettings::values.room_name, QString{}); + WriteSetting(QStringLiteral("room_port"), UISettings::values.room_port, + QStringLiteral("24872")); + WriteSetting(QStringLiteral("host_type"), UISettings::values.host_type, 0); + WriteSetting(QStringLiteral("max_player"), UISettings::values.max_player, 8); + WriteSetting(QStringLiteral("game_id"), UISettings::values.game_id, 0); + WriteSetting(QStringLiteral("room_description"), UISettings::values.room_description, + QString{}); + // Write ban list + qt_config->beginWriteArray(QStringLiteral("username_ban_list")); + for (std::size_t i = 0; i < UISettings::values.ban_list.first.size(); ++i) { + qt_config->setArrayIndex(static_cast(i)); + WriteSetting(QStringLiteral("username"), + QString::fromStdString(UISettings::values.ban_list.first[i])); + } + qt_config->endArray(); + qt_config->beginWriteArray(QStringLiteral("ip_ban_list")); + for (std::size_t i = 0; i < UISettings::values.ban_list.second.size(); ++i) { + qt_config->setArrayIndex(static_cast(i)); + WriteSetting(QStringLiteral("ip"), + QString::fromStdString(UISettings::values.ban_list.second[i])); + } + qt_config->endArray(); + + qt_config->endGroup(); +} + void Config::SavePathValues() { qt_config->beginGroup(QStringLiteral("Paths")); @@ -1347,6 +1425,7 @@ void Config::SaveUIValues() { SavePathValues(); SaveScreenshotValues(); SaveShortcutValues(); + SaveMultiplayerValues(); WriteBasicSetting(UISettings::values.single_window_mode); WriteBasicSetting(UISettings::values.fullscreen); diff --git a/src/yuzu/configuration/config.h b/src/yuzu/configuration/config.h index a71eabe8ea..937b2d95bf 100644 --- a/src/yuzu/configuration/config.h +++ b/src/yuzu/configuration/config.h @@ -89,6 +89,7 @@ private: void ReadUIGamelistValues(); void ReadUILayoutValues(); void ReadWebServiceValues(); + void ReadMultiplayerValues(); void SaveValues(); void SavePlayerValue(std::size_t player_index); @@ -118,6 +119,7 @@ private: void SaveUIGamelistValues(); void SaveUILayoutValues(); void SaveWebServiceValues(); + void SaveMultiplayerValues(); /** * Reads a setting from the qt_config. diff --git a/src/yuzu/configuration/configure_dialog.cpp b/src/yuzu/configuration/configure_dialog.cpp index e99657bd6a..92ef4467bd 100644 --- a/src/yuzu/configuration/configure_dialog.cpp +++ b/src/yuzu/configuration/configure_dialog.cpp @@ -29,9 +29,10 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, InputCommon::InputSubsystem* input_subsystem, - Core::System& system_) - : QDialog(parent), ui{std::make_unique()}, registry{registry_}, - system{system_}, audio_tab{std::make_unique(system_, this)}, + Core::System& system_, bool enable_web_config) + : QDialog(parent), ui{std::make_unique()}, + registry(registry_), system{system_}, audio_tab{std::make_unique(system_, + this)}, cpu_tab{std::make_unique(system_, this)}, debug_tab_tab{std::make_unique(system_, this)}, filesystem_tab{std::make_unique(this)}, @@ -64,6 +65,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, ui->tabWidget->addTab(ui_tab.get(), tr("Game List")); ui->tabWidget->addTab(web_tab.get(), tr("Web")); + web_tab->SetWebServiceConfigEnabled(enable_web_config); hotkeys_tab->Populate(registry); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); diff --git a/src/yuzu/configuration/configure_dialog.h b/src/yuzu/configuration/configure_dialog.h index 12cf25daf2..cec1610ad6 100644 --- a/src/yuzu/configuration/configure_dialog.h +++ b/src/yuzu/configuration/configure_dialog.h @@ -41,7 +41,8 @@ class ConfigureDialog : public QDialog { public: explicit ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, - InputCommon::InputSubsystem* input_subsystem, Core::System& system_); + InputCommon::InputSubsystem* input_subsystem, Core::System& system_, + bool enable_web_config = true); ~ConfigureDialog() override; void ApplyConfiguration(); diff --git a/src/yuzu/configuration/configure_network.cpp b/src/yuzu/configuration/configure_network.cpp index 8ed08fa6a8..ba1986eb1b 100644 --- a/src/yuzu/configuration/configure_network.cpp +++ b/src/yuzu/configuration/configure_network.cpp @@ -4,7 +4,7 @@ #include #include "common/settings.h" #include "core/core.h" -#include "core/network/network_interface.h" +#include "core/internal_network/network_interface.h" #include "ui_configure_network.h" #include "yuzu/configuration/configure_network.h" diff --git a/src/yuzu/configuration/configure_web.cpp b/src/yuzu/configuration/configure_web.cpp index d779251b42..ff4bf44f44 100644 --- a/src/yuzu/configuration/configure_web.cpp +++ b/src/yuzu/configuration/configure_web.cpp @@ -169,3 +169,8 @@ void ConfigureWeb::OnLoginVerified() { "correctly, and that your internet connection is working.")); } } + +void ConfigureWeb::SetWebServiceConfigEnabled(bool enabled) { + ui->label_disable_info->setVisible(!enabled); + ui->groupBoxWebConfig->setEnabled(enabled); +} diff --git a/src/yuzu/configuration/configure_web.h b/src/yuzu/configuration/configure_web.h index 9054711ea3..041b511498 100644 --- a/src/yuzu/configuration/configure_web.h +++ b/src/yuzu/configuration/configure_web.h @@ -20,6 +20,7 @@ public: ~ConfigureWeb() override; void ApplyConfiguration(); + void SetWebServiceConfigEnabled(bool enabled); private: void changeEvent(QEvent* event) override; diff --git a/src/yuzu/configuration/configure_web.ui b/src/yuzu/configuration/configure_web.ui index 35b4274b0a..3ac3864bea 100644 --- a/src/yuzu/configuration/configure_web.ui +++ b/src/yuzu/configuration/configure_web.ui @@ -112,6 +112,16 @@ + + + + Web Service configuration can only be changed when a public room isn't being hosted. + + + true + + + diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 05d309827f..5bcf582bf6 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -499,6 +499,8 @@ void GameList::DonePopulating(const QStringList& watch_list) { } item_model->sort(tree_view->header()->sortIndicatorSection(), tree_view->header()->sortIndicatorOrder()); + + emit PopulatingCompleted(); } void GameList::PopupContextMenu(const QPoint& menu_location) { @@ -752,6 +754,10 @@ void GameList::LoadCompatibilityList() { } } +QStandardItemModel* GameList::GetModel() const { + return item_model; +} + void GameList::PopulateAsync(QVector& game_dirs) { tree_view->setEnabled(false); diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index bc36d015a9..9605985cc1 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h @@ -16,9 +16,14 @@ #include #include "common/common_types.h" +#include "core/core.h" #include "uisettings.h" #include "yuzu/compatibility_list.h" +namespace Core { +class System; +} + class ControllerNavigation; class GameListWorker; class GameListSearchField; @@ -84,6 +89,8 @@ public: void SaveInterfaceLayout(); void LoadInterfaceLayout(); + QStandardItemModel* GetModel() const; + /// Disables events from the emulated controller void UnloadController(); @@ -108,6 +115,7 @@ signals: void OpenDirectory(const QString& directory); void AddDirectory(); void ShowList(bool show); + void PopulatingCompleted(); private slots: void OnItemExpanded(const QModelIndex& item); diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 2814548eb9..5d81326739 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -32,6 +32,7 @@ #include "core/hle/service/am/applet_ae.h" #include "core/hle/service/am/applet_oe.h" #include "core/hle/service/am/applets/applets.h" +#include "yuzu/multiplayer/state.h" #include "yuzu/util/controller_navigation.h" // These are wrappers to avoid the calls to CreateDirectory and CreateFile because of the Windows @@ -132,6 +133,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "yuzu/main.h" #include "yuzu/startup_checks.h" #include "yuzu/uisettings.h" +#include "yuzu/util/clickable_label.h" using namespace Common::Literals; @@ -271,6 +273,8 @@ GMainWindow::GMainWindow(bool has_broken_vulkan) SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); discord_rpc->Update(); + Network::Init(); + RegisterMetaTypes(); InitializeWidgets(); @@ -459,6 +463,7 @@ GMainWindow::~GMainWindow() { if (render_window->parent() == nullptr) { delete render_window; } + Network::Shutdown(); } void GMainWindow::RegisterMetaTypes() { @@ -822,6 +827,10 @@ void GMainWindow::InitializeWidgets() { } }); + multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui->action_Leave_Room, + ui->action_Show_Room); + multiplayer_state->setVisible(false); + // Create status bar message_label = new QLabel(); // Configured separately for left alignment @@ -854,6 +863,9 @@ void GMainWindow::InitializeWidgets() { statusBar()->addPermanentWidget(label); } + statusBar()->addPermanentWidget(multiplayer_state->GetStatusText(), 0); + statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon(), 0); + tas_label = new QLabel(); tas_label->setObjectName(QStringLiteral("TASlabel")); tas_label->setFocusPolicy(Qt::NoFocus); @@ -1163,6 +1175,8 @@ void GMainWindow::ConnectWidgetEvents() { connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); + connect(game_list, &GameList::PopulatingCompleted, + [this] { multiplayer_state->UpdateGameList(game_list->GetModel()); }); connect(game_list, &GameList::OpenPerGameGeneralRequested, this, &GMainWindow::OnGameListOpenPerGameProperties); @@ -1180,6 +1194,9 @@ void GMainWindow::ConnectWidgetEvents() { connect(this, &GMainWindow::EmulationStopping, this, &GMainWindow::SoftwareKeyboardExit); connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar); + + connect(this, &GMainWindow::UpdateThemedIcons, multiplayer_state, + &MultiplayerState::UpdateThemedIcons); } void GMainWindow::ConnectMenuEvents() { @@ -1223,6 +1240,18 @@ void GMainWindow::ConnectMenuEvents() { ui->action_Reset_Window_Size_900, ui->action_Reset_Window_Size_1080}); + // Multiplayer + connect(ui->action_View_Lobby, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnViewLobby); + connect(ui->action_Start_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCreateRoom); + connect(ui->action_Leave_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCloseRoom); + connect(ui->action_Connect_To_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnDirectConnectToRoom); + connect(ui->action_Show_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnOpenNetworkRoom); + // Tools connect_menu(ui->action_Rederive, std::bind(&GMainWindow::OnReinitializeKeys, this, ReinitializeKeyBehavior::Warning)); @@ -2783,7 +2812,8 @@ void GMainWindow::OnConfigure() { const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue(); Settings::SetConfiguringGlobal(true); - ConfigureDialog configure_dialog(this, hotkey_registry, input_subsystem.get(), *system); + ConfigureDialog configure_dialog(this, hotkey_registry, input_subsystem.get(), *system, + !multiplayer_state->IsHostingPublicRoom()); connect(&configure_dialog, &ConfigureDialog::LanguageChanged, this, &GMainWindow::OnLanguageChanged); @@ -2840,6 +2870,11 @@ void GMainWindow::OnConfigure() { if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) { SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); } + + if (!multiplayer_state->IsHostingPublicRoom()) { + multiplayer_state->UpdateCredentials(); + } + emit UpdateThemedIcons(); const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); @@ -3660,6 +3695,7 @@ void GMainWindow::closeEvent(QCloseEvent* event) { } render_window->close(); + multiplayer_state->Close(); QWidget::closeEvent(event); } @@ -3856,6 +3892,7 @@ void GMainWindow::OnLanguageChanged(const QString& locale) { UISettings::values.language = locale; LoadTranslation(); ui->retranslateUi(this); + multiplayer_state->retranslateUi(); UpdateWindowTitle(); } diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 27204f5a2c..8d5c1398f0 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -11,6 +11,7 @@ #include #include +#include "common/announce_multiplayer_room.h" #include "common/common_types.h" #include "yuzu/compatibility_list.h" #include "yuzu/hotkeys.h" @@ -22,6 +23,7 @@ #endif class Config; +class ClickableLabel; class EmuThread; class GameList; class GImageInfo; @@ -31,6 +33,7 @@ class MicroProfileDialog; class ProfilerWidget; class ControllerDialog; class QLabel; +class MultiplayerState; class QPushButton; class QProgressDialog; class WaitTreeWidget; @@ -200,6 +203,8 @@ private: void ConnectMenuEvents(); void UpdateMenuState(); + MultiplayerState* multiplayer_state = nullptr; + void PreventOSSleep(); void AllowOSSleep(); diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 6ab95b9a5d..60a8deab1c 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -120,6 +120,20 @@ + + + true + + + Multiplayer + + + + + + + + &Tools @@ -154,6 +168,7 @@ + @@ -245,6 +260,43 @@ Show Status Bar + + + true + + + Browse Public Game Lobby + + + + + true + + + Create Room + + + + + false + + + Leave Room + + + + + Direct Connect to Room + + + + + false + + + Show Current Room + + true diff --git a/src/yuzu/multiplayer/chat_room.cpp b/src/yuzu/multiplayer/chat_room.cpp new file mode 100644 index 0000000000..6dd4bc36d6 --- /dev/null +++ b/src/yuzu/multiplayer/chat_room.cpp @@ -0,0 +1,490 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "core/announce_multiplayer_session.h" +#include "ui_chat_room.h" +#include "yuzu/game_list_p.h" +#include "yuzu/multiplayer/chat_room.h" +#include "yuzu/multiplayer/message.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/web_backend.h" +#endif + +class ChatMessage { +public: + explicit ChatMessage(const Network::ChatEntry& chat, QTime ts = {}) { + /// Convert the time to their default locale defined format + QLocale locale; + timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat); + nickname = QString::fromStdString(chat.nickname); + username = QString::fromStdString(chat.username); + message = QString::fromStdString(chat.message); + + // Check for user pings + QString cur_nickname, cur_username; + if (auto room = Network::GetRoomMember().lock()) { + cur_nickname = QString::fromStdString(room->GetNickname()); + cur_username = QString::fromStdString(room->GetUsername()); + } + + // Handle pings at the beginning and end of message + QString fixed_message = QStringLiteral(" %1 ").arg(message); + if (fixed_message.contains(QStringLiteral(" @%1 ").arg(cur_nickname)) || + (!cur_username.isEmpty() && + fixed_message.contains(QStringLiteral(" @%1 ").arg(cur_username)))) { + + contains_ping = true; + } else { + contains_ping = false; + } + } + + bool ContainsPing() const { + return contains_ping; + } + + /// Format the message using the players color + QString GetPlayerChatMessage(u16 player) const { + auto color = player_color[player % 16]; + QString name; + if (username.isEmpty() || username == nickname) { + name = nickname; + } else { + name = QStringLiteral("%1 (%2)").arg(nickname, username); + } + + QString style, text_color; + if (ContainsPing()) { + // Add a background color to these messages + style = QStringLiteral("background-color: %1").arg(QString::fromStdString(ping_color)); + // Add a font color + text_color = QStringLiteral("color='#000000'"); + } + + return QStringLiteral("[%1] <%3> %6") + .arg(timestamp, QString::fromStdString(color), name.toHtmlEscaped(), style, text_color, + message.toHtmlEscaped()); + } + +private: + static constexpr std::array player_color = { + {"#0000FF", "#FF0000", "#8A2BE2", "#FF69B4", "#1E90FF", "#008000", "#00FF7F", "#B22222", + "#DAA520", "#FF4500", "#2E8B57", "#5F9EA0", "#D2691E", "#9ACD32", "#FF7F50", "FFFF00"}}; + static constexpr char ping_color[] = "#FFFF00"; + + QString timestamp; + QString nickname; + QString username; + QString message; + bool contains_ping; +}; + +class StatusMessage { +public: + explicit StatusMessage(const QString& msg, QTime ts = {}) { + /// Convert the time to their default locale defined format + QLocale locale; + timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat); + message = msg; + } + + QString GetSystemChatMessage() const { + return QStringLiteral("[%1] * %3") + .arg(timestamp, QString::fromStdString(system_color), message); + } + +private: + static constexpr const char system_color[] = "#FF8C00"; + QString timestamp; + QString message; +}; + +class PlayerListItem : public QStandardItem { +public: + static const int NicknameRole = Qt::UserRole + 1; + static const int UsernameRole = Qt::UserRole + 2; + static const int AvatarUrlRole = Qt::UserRole + 3; + static const int GameNameRole = Qt::UserRole + 4; + + PlayerListItem() = default; + explicit PlayerListItem(const std::string& nickname, const std::string& username, + const std::string& avatar_url, const std::string& game_name) { + setEditable(false); + setData(QString::fromStdString(nickname), NicknameRole); + setData(QString::fromStdString(username), UsernameRole); + setData(QString::fromStdString(avatar_url), AvatarUrlRole); + if (game_name.empty()) { + setData(QObject::tr("Not playing a game"), GameNameRole); + } else { + setData(QString::fromStdString(game_name), GameNameRole); + } + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return QStandardItem::data(role); + } + QString name; + const QString nickname = data(NicknameRole).toString(); + const QString username = data(UsernameRole).toString(); + if (username.isEmpty() || username == nickname) { + name = nickname; + } else { + name = QStringLiteral("%1 (%2)").arg(nickname, username); + } + return QStringLiteral("%1\n %2").arg(name, data(GameNameRole).toString()); + } +}; + +ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique()) { + ui->setupUi(this); + + // set the item_model for player_view + + player_list = new QStandardItemModel(ui->player_view); + ui->player_view->setModel(player_list); + ui->player_view->setContextMenuPolicy(Qt::CustomContextMenu); + // set a header to make it look better though there is only one column + player_list->insertColumns(0, 1); + player_list->setHeaderData(0, Qt::Horizontal, tr("Members")); + + ui->chat_history->document()->setMaximumBlockCount(max_chat_lines); + + // register the network structs to use in slots and signals + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + + // setup the callbacks for network updates + if (auto member = Network::GetRoomMember().lock()) { + member->BindOnChatMessageRecieved( + [this](const Network::ChatEntry& chat) { emit ChatReceived(chat); }); + member->BindOnStatusMessageReceived( + [this](const Network::StatusMessageEntry& status_message) { + emit StatusMessageReceived(status_message); + }); + connect(this, &ChatRoom::ChatReceived, this, &ChatRoom::OnChatReceive); + connect(this, &ChatRoom::StatusMessageReceived, this, &ChatRoom::OnStatusMessageReceive); + } else { + // TODO (jroweboy) network was not initialized? + } + + // Connect all the widgets to the appropriate events + connect(ui->player_view, &QTreeView::customContextMenuRequested, this, + &ChatRoom::PopupContextMenu); + connect(ui->chat_message, &QLineEdit::returnPressed, this, &ChatRoom::OnSendChat); + connect(ui->chat_message, &QLineEdit::textChanged, this, &ChatRoom::OnChatTextChanged); + connect(ui->send_message, &QPushButton::clicked, this, &ChatRoom::OnSendChat); +} + +ChatRoom::~ChatRoom() = default; + +void ChatRoom::SetModPerms(bool is_mod) { + has_mod_perms = is_mod; +} + +void ChatRoom::RetranslateUi() { + ui->retranslateUi(this); +} + +void ChatRoom::Clear() { + ui->chat_history->clear(); + block_list.clear(); +} + +void ChatRoom::AppendStatusMessage(const QString& msg) { + ui->chat_history->append(StatusMessage(msg).GetSystemChatMessage()); +} + +void ChatRoom::AppendChatMessage(const QString& msg) { + ui->chat_history->append(msg); +} + +void ChatRoom::SendModerationRequest(Network::RoomMessageTypes type, const std::string& nickname) { + if (auto room = Network::GetRoomMember().lock()) { + auto members = room->GetMemberInformation(); + auto it = std::find_if(members.begin(), members.end(), + [&nickname](const Network::RoomMember::MemberInformation& member) { + return member.nickname == nickname; + }); + if (it == members.end()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::NO_SUCH_USER); + return; + } + room->SendModerationRequest(type, nickname); + } +} + +bool ChatRoom::ValidateMessage(const std::string& msg) { + return !msg.empty(); +} + +void ChatRoom::OnRoomUpdate(const Network::RoomInformation& info) { + // TODO(B3N30): change title + if (auto room_member = Network::GetRoomMember().lock()) { + SetPlayerList(room_member->GetMemberInformation()); + } +} + +void ChatRoom::Disable() { + ui->send_message->setDisabled(true); + ui->chat_message->setDisabled(true); +} + +void ChatRoom::Enable() { + ui->send_message->setEnabled(true); + ui->chat_message->setEnabled(true); +} + +void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) { + if (!ValidateMessage(chat.message)) { + return; + } + if (auto room = Network::GetRoomMember().lock()) { + // get the id of the player + auto members = room->GetMemberInformation(); + auto it = std::find_if(members.begin(), members.end(), + [&chat](const Network::RoomMember::MemberInformation& member) { + return member.nickname == chat.nickname && + member.username == chat.username; + }); + if (it == members.end()) { + LOG_INFO(Network, "Chat message received from unknown player. Ignoring it."); + return; + } + if (block_list.count(chat.nickname)) { + LOG_INFO(Network, "Chat message received from blocked player {}. Ignoring it.", + chat.nickname); + return; + } + auto player = std::distance(members.begin(), it); + ChatMessage m(chat); + if (m.ContainsPing()) { + emit UserPinged(); + } + AppendChatMessage(m.GetPlayerChatMessage(player)); + } +} + +void ChatRoom::OnStatusMessageReceive(const Network::StatusMessageEntry& status_message) { + QString name; + if (status_message.username.empty() || status_message.username == status_message.nickname) { + name = QString::fromStdString(status_message.nickname); + } else { + name = QStringLiteral("%1 (%2)").arg(QString::fromStdString(status_message.nickname), + QString::fromStdString(status_message.username)); + } + QString message; + switch (status_message.type) { + case Network::IdMemberJoin: + message = tr("%1 has joined").arg(name); + break; + case Network::IdMemberLeave: + message = tr("%1 has left").arg(name); + break; + case Network::IdMemberKicked: + message = tr("%1 has been kicked").arg(name); + break; + case Network::IdMemberBanned: + message = tr("%1 has been banned").arg(name); + break; + case Network::IdAddressUnbanned: + message = tr("%1 has been unbanned").arg(name); + break; + } + if (!message.isEmpty()) + AppendStatusMessage(message); +} + +void ChatRoom::OnSendChat() { + if (auto room = Network::GetRoomMember().lock()) { + if (room->GetState() != Network::RoomMember::State::Joined && + room->GetState() != Network::RoomMember::State::Moderator) { + + return; + } + auto message = ui->chat_message->text().toStdString(); + if (!ValidateMessage(message)) { + return; + } + auto nick = room->GetNickname(); + auto username = room->GetUsername(); + Network::ChatEntry chat{nick, username, message}; + + auto members = room->GetMemberInformation(); + auto it = std::find_if(members.begin(), members.end(), + [&chat](const Network::RoomMember::MemberInformation& member) { + return member.nickname == chat.nickname && + member.username == chat.username; + }); + if (it == members.end()) { + LOG_INFO(Network, "Cannot find self in the player list when sending a message."); + } + auto player = std::distance(members.begin(), it); + ChatMessage m(chat); + room->SendChatMessage(message); + AppendChatMessage(m.GetPlayerChatMessage(player)); + ui->chat_message->clear(); + } +} + +void ChatRoom::UpdateIconDisplay() { + for (int row = 0; row < player_list->invisibleRootItem()->rowCount(); ++row) { + QStandardItem* item = player_list->invisibleRootItem()->child(row); + const std::string avatar_url = + item->data(PlayerListItem::AvatarUrlRole).toString().toStdString(); + if (icon_cache.count(avatar_url)) { + item->setData(icon_cache.at(avatar_url), Qt::DecorationRole); + } else { + item->setData(QIcon::fromTheme(QStringLiteral("no_avatar")).pixmap(48), + Qt::DecorationRole); + } + } +} + +void ChatRoom::SetPlayerList(const Network::RoomMember::MemberList& member_list) { + // TODO(B3N30): Remember which row is selected + player_list->removeRows(0, player_list->rowCount()); + for (const auto& member : member_list) { + if (member.nickname.empty()) + continue; + QStandardItem* name_item = new PlayerListItem(member.nickname, member.username, + member.avatar_url, member.game_info.name); + +#ifdef ENABLE_WEB_SERVICE + if (!icon_cache.count(member.avatar_url) && !member.avatar_url.empty()) { + // Start a request to get the member's avatar + const QUrl url(QString::fromStdString(member.avatar_url)); + QFuture future = QtConcurrent::run([url] { + WebService::Client client( + QStringLiteral("%1://%2").arg(url.scheme(), url.host()).toStdString(), "", ""); + auto result = client.GetImage(url.path().toStdString(), true); + if (result.returned_data.empty()) { + LOG_ERROR(WebService, "Failed to get avatar"); + } + return result.returned_data; + }); + auto* future_watcher = new QFutureWatcher(this); + connect(future_watcher, &QFutureWatcher::finished, this, + [this, future_watcher, avatar_url = member.avatar_url] { + const std::string result = future_watcher->result(); + if (result.empty()) + return; + QPixmap pixmap; + if (!pixmap.loadFromData(reinterpret_cast(result.data()), + result.size())) + return; + icon_cache[avatar_url] = + pixmap.scaled(48, 48, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + // Update all the displayed icons with the new icon_cache + UpdateIconDisplay(); + }); + future_watcher->setFuture(future); + } +#endif + + player_list->invisibleRootItem()->appendRow(name_item); + } + UpdateIconDisplay(); + // TODO(B3N30): Restore row selection +} + +void ChatRoom::OnChatTextChanged() { + if (ui->chat_message->text().length() > static_cast(Network::MaxMessageSize)) + ui->chat_message->setText( + ui->chat_message->text().left(static_cast(Network::MaxMessageSize))); +} + +void ChatRoom::PopupContextMenu(const QPoint& menu_location) { + QModelIndex item = ui->player_view->indexAt(menu_location); + if (!item.isValid()) + return; + + std::string nickname = + player_list->item(item.row())->data(PlayerListItem::NicknameRole).toString().toStdString(); + + QMenu context_menu; + + QString username = player_list->item(item.row())->data(PlayerListItem::UsernameRole).toString(); + if (!username.isEmpty()) { + QAction* view_profile_action = context_menu.addAction(tr("View Profile")); + connect(view_profile_action, &QAction::triggered, [username] { + QDesktopServices::openUrl( + QUrl(QStringLiteral("https://community.citra-emu.org/u/%1").arg(username))); + }); + } + + std::string cur_nickname; + if (auto room = Network::GetRoomMember().lock()) { + cur_nickname = room->GetNickname(); + } + + if (nickname != cur_nickname) { // You can't block yourself + QAction* block_action = context_menu.addAction(tr("Block Player")); + + block_action->setCheckable(true); + block_action->setChecked(block_list.count(nickname) > 0); + + connect(block_action, &QAction::triggered, [this, nickname] { + if (block_list.count(nickname)) { + block_list.erase(nickname); + } else { + QMessageBox::StandardButton result = QMessageBox::question( + this, tr("Block Player"), + tr("When you block a player, you will no longer receive chat messages from " + "them.

Are you sure you would like to block %1?") + .arg(QString::fromStdString(nickname)), + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::Yes) + block_list.emplace(nickname); + } + }); + } + + if (has_mod_perms && nickname != cur_nickname) { // You can't kick or ban yourself + context_menu.addSeparator(); + + QAction* kick_action = context_menu.addAction(tr("Kick")); + QAction* ban_action = context_menu.addAction(tr("Ban")); + + connect(kick_action, &QAction::triggered, [this, nickname] { + QMessageBox::StandardButton result = + QMessageBox::question(this, tr("Kick Player"), + tr("Are you sure you would like to kick %1?") + .arg(QString::fromStdString(nickname)), + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::Yes) + SendModerationRequest(Network::IdModKick, nickname); + }); + connect(ban_action, &QAction::triggered, [this, nickname] { + QMessageBox::StandardButton result = QMessageBox::question( + this, tr("Ban Player"), + tr("Are you sure you would like to kick and ban %1?\n\nThis would " + "ban both their forum username and their IP address.") + .arg(QString::fromStdString(nickname)), + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::Yes) + SendModerationRequest(Network::IdModBan, nickname); + }); + } + + context_menu.exec(ui->player_view->viewport()->mapToGlobal(menu_location)); +} diff --git a/src/yuzu/multiplayer/chat_room.h b/src/yuzu/multiplayer/chat_room.h new file mode 100644 index 0000000000..a810377f7a --- /dev/null +++ b/src/yuzu/multiplayer/chat_room.h @@ -0,0 +1,74 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "network/network.h" + +namespace Ui { +class ChatRoom; +} + +namespace Core { +class AnnounceMultiplayerSession; +} + +class ConnectionError; +class ComboBoxProxyModel; + +class ChatMessage; + +class ChatRoom : public QWidget { + Q_OBJECT + +public: + explicit ChatRoom(QWidget* parent); + void RetranslateUi(); + void SetPlayerList(const Network::RoomMember::MemberList& member_list); + void Clear(); + void AppendStatusMessage(const QString& msg); + ~ChatRoom(); + + void SetModPerms(bool is_mod); + void UpdateIconDisplay(); + +public slots: + void OnRoomUpdate(const Network::RoomInformation& info); + void OnChatReceive(const Network::ChatEntry&); + void OnStatusMessageReceive(const Network::StatusMessageEntry&); + void OnSendChat(); + void OnChatTextChanged(); + void PopupContextMenu(const QPoint& menu_location); + void Disable(); + void Enable(); + +signals: + void ChatReceived(const Network::ChatEntry&); + void StatusMessageReceived(const Network::StatusMessageEntry&); + void UserPinged(); + +private: + static constexpr u32 max_chat_lines = 1000; + void AppendChatMessage(const QString&); + bool ValidateMessage(const std::string&); + void SendModerationRequest(Network::RoomMessageTypes type, const std::string& nickname); + + bool has_mod_perms = false; + QStandardItemModel* player_list; + std::unique_ptr ui; + std::unordered_set block_list; + std::unordered_map icon_cache; +}; + +Q_DECLARE_METATYPE(Network::ChatEntry); +Q_DECLARE_METATYPE(Network::StatusMessageEntry); +Q_DECLARE_METATYPE(Network::RoomInformation); +Q_DECLARE_METATYPE(Network::RoomMember::State); +Q_DECLARE_METATYPE(Network::RoomMember::Error); diff --git a/src/yuzu/multiplayer/chat_room.ui b/src/yuzu/multiplayer/chat_room.ui new file mode 100644 index 0000000000..f2b31b5da1 --- /dev/null +++ b/src/yuzu/multiplayer/chat_room.ui @@ -0,0 +1,59 @@ + + + ChatRoom + + + + 0 + 0 + 807 + 432 + + + + Room Window + + + + + + + + + + + false + + + true + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + Send Chat Message + + + + + + + Send Message + + + + + + + + + + + + diff --git a/src/yuzu/multiplayer/client_room.cpp b/src/yuzu/multiplayer/client_room.cpp new file mode 100644 index 0000000000..7b2e16e06e --- /dev/null +++ b/src/yuzu/multiplayer/client_room.cpp @@ -0,0 +1,115 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "core/announce_multiplayer_session.h" +#include "ui_client_room.h" +#include "yuzu/game_list_p.h" +#include "yuzu/multiplayer/client_room.h" +#include "yuzu/multiplayer/message.h" +#include "yuzu/multiplayer/moderation_dialog.h" +#include "yuzu/multiplayer/state.h" + +ClientRoomWindow::ClientRoomWindow(QWidget* parent) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(std::make_unique()) { + ui->setupUi(this); + + // setup the callbacks for network updates + if (auto member = Network::GetRoomMember().lock()) { + member->BindOnRoomInformationChanged( + [this](const Network::RoomInformation& info) { emit RoomInformationChanged(info); }); + member->BindOnStateChanged( + [this](const Network::RoomMember::State& state) { emit StateChanged(state); }); + + connect(this, &ClientRoomWindow::RoomInformationChanged, this, + &ClientRoomWindow::OnRoomUpdate); + connect(this, &ClientRoomWindow::StateChanged, this, &::ClientRoomWindow::OnStateChange); + // Update the state + OnStateChange(member->GetState()); + } else { + // TODO (jroweboy) network was not initialized? + } + + connect(ui->disconnect, &QPushButton::clicked, this, &ClientRoomWindow::Disconnect); + ui->disconnect->setDefault(false); + ui->disconnect->setAutoDefault(false); + connect(ui->moderation, &QPushButton::clicked, [this] { + ModerationDialog dialog(this); + dialog.exec(); + }); + ui->moderation->setDefault(false); + ui->moderation->setAutoDefault(false); + connect(ui->chat, &ChatRoom::UserPinged, this, &ClientRoomWindow::ShowNotification); + UpdateView(); +} + +ClientRoomWindow::~ClientRoomWindow() = default; + +void ClientRoomWindow::SetModPerms(bool is_mod) { + ui->chat->SetModPerms(is_mod); + ui->moderation->setVisible(is_mod); + ui->moderation->setDefault(false); + ui->moderation->setAutoDefault(false); +} + +void ClientRoomWindow::RetranslateUi() { + ui->retranslateUi(this); + ui->chat->RetranslateUi(); +} + +void ClientRoomWindow::OnRoomUpdate(const Network::RoomInformation& info) { + UpdateView(); +} + +void ClientRoomWindow::OnStateChange(const Network::RoomMember::State& state) { + if (state == Network::RoomMember::State::Joined || + state == Network::RoomMember::State::Moderator) { + + ui->chat->Clear(); + ui->chat->AppendStatusMessage(tr("Connected")); + SetModPerms(state == Network::RoomMember::State::Moderator); + } + UpdateView(); +} + +void ClientRoomWindow::Disconnect() { + auto parent = static_cast(parentWidget()); + if (parent->OnCloseRoom()) { + ui->chat->AppendStatusMessage(tr("Disconnected")); + close(); + } +} + +void ClientRoomWindow::UpdateView() { + if (auto member = Network::GetRoomMember().lock()) { + if (member->IsConnected()) { + ui->chat->Enable(); + ui->disconnect->setEnabled(true); + auto memberlist = member->GetMemberInformation(); + ui->chat->SetPlayerList(memberlist); + const auto information = member->GetRoomInformation(); + setWindowTitle(QString(tr("%1 (%2/%3 members) - connected")) + .arg(QString::fromStdString(information.name)) + .arg(memberlist.size()) + .arg(information.member_slots)); + ui->description->setText(QString::fromStdString(information.description)); + return; + } + } + // TODO(B3N30): can't get RoomMember*, show error and close window + close(); +} + +void ClientRoomWindow::UpdateIconDisplay() { + ui->chat->UpdateIconDisplay(); +} diff --git a/src/yuzu/multiplayer/client_room.h b/src/yuzu/multiplayer/client_room.h new file mode 100644 index 0000000000..607b4073d0 --- /dev/null +++ b/src/yuzu/multiplayer/client_room.h @@ -0,0 +1,39 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "yuzu/multiplayer/chat_room.h" + +namespace Ui { +class ClientRoom; +} + +class ClientRoomWindow : public QDialog { + Q_OBJECT + +public: + explicit ClientRoomWindow(QWidget* parent); + ~ClientRoomWindow(); + + void RetranslateUi(); + void UpdateIconDisplay(); + +public slots: + void OnRoomUpdate(const Network::RoomInformation&); + void OnStateChange(const Network::RoomMember::State&); + +signals: + void RoomInformationChanged(const Network::RoomInformation&); + void StateChanged(const Network::RoomMember::State&); + void ShowNotification(); + +private: + void Disconnect(); + void UpdateView(); + void SetModPerms(bool is_mod); + + QStandardItemModel* player_list; + std::unique_ptr ui; +}; diff --git a/src/yuzu/multiplayer/client_room.ui b/src/yuzu/multiplayer/client_room.ui new file mode 100644 index 0000000000..97e88b502d --- /dev/null +++ b/src/yuzu/multiplayer/client_room.ui @@ -0,0 +1,80 @@ + + + ClientRoom + + + + 0 + 0 + 807 + 432 + + + + Room Window + + + + + + + + 0 + + + + + Room Description + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Moderation... + + + false + + + + + + + Leave Room + + + + + + + + + + + + + + + ChatRoom + QWidget +
multiplayer/chat_room.h
+ 1 +
+
+ + +
diff --git a/src/yuzu/multiplayer/direct_connect.cpp b/src/yuzu/multiplayer/direct_connect.cpp new file mode 100644 index 0000000000..27741d657f --- /dev/null +++ b/src/yuzu/multiplayer/direct_connect.cpp @@ -0,0 +1,129 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include "common/settings.h" +#include "network/network.h" +#include "ui_direct_connect.h" +#include "yuzu/main.h" +#include "yuzu/multiplayer/client_room.h" +#include "yuzu/multiplayer/direct_connect.h" +#include "yuzu/multiplayer/message.h" +#include "yuzu/multiplayer/state.h" +#include "yuzu/multiplayer/validation.h" +#include "yuzu/uisettings.h" + +enum class ConnectionType : u8 { TraversalServer, IP }; + +DirectConnectWindow::DirectConnectWindow(QWidget* parent) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(std::make_unique()) { + + ui->setupUi(this); + + // setup the watcher for background connections + watcher = new QFutureWatcher; + connect(watcher, &QFutureWatcher::finished, this, &DirectConnectWindow::OnConnection); + + ui->nickname->setValidator(validation.GetNickname()); + ui->nickname->setText(UISettings::values.nickname); + if (ui->nickname->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) { + // Use yuzu Web Service user name as nickname by default + ui->nickname->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue())); + } + ui->ip->setValidator(validation.GetIP()); + ui->ip->setText(UISettings::values.ip); + ui->port->setValidator(validation.GetPort()); + ui->port->setText(UISettings::values.port); + + // TODO(jroweboy): Show or hide the connection options based on the current value of the combo + // box. Add this back in when the traversal server support is added. + connect(ui->connect, &QPushButton::clicked, this, &DirectConnectWindow::Connect); +} + +DirectConnectWindow::~DirectConnectWindow() = default; + +void DirectConnectWindow::RetranslateUi() { + ui->retranslateUi(this); +} + +void DirectConnectWindow::Connect() { + if (!ui->nickname->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID); + return; + } + if (const auto member = Network::GetRoomMember().lock()) { + // Prevent the user from trying to join a room while they are already joining. + if (member->GetState() == Network::RoomMember::State::Joining) { + return; + } else if (member->IsConnected()) { + // And ask if they want to leave the room if they are already in one. + if (!NetworkMessage::WarnDisconnect()) { + return; + } + } + } + switch (static_cast(ui->connection_type->currentIndex())) { + case ConnectionType::TraversalServer: + break; + case ConnectionType::IP: + if (!ui->ip->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError( + NetworkMessage::ErrorManager::IP_ADDRESS_NOT_VALID); + return; + } + if (!ui->port->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::PORT_NOT_VALID); + return; + } + break; + } + + // Store settings + UISettings::values.nickname = ui->nickname->text(); + UISettings::values.ip = ui->ip->text(); + UISettings::values.port = (ui->port->isModified() && !ui->port->text().isEmpty()) + ? ui->port->text() + : UISettings::values.port; + + // attempt to connect in a different thread + QFuture f = QtConcurrent::run([&] { + if (auto room_member = Network::GetRoomMember().lock()) { + auto port = UISettings::values.port.toUInt(); + room_member->Join(ui->nickname->text().toStdString(), "", + ui->ip->text().toStdString().c_str(), port, 0, + Network::NoPreferredMac, ui->password->text().toStdString().c_str()); + } + }); + watcher->setFuture(f); + // and disable widgets and display a connecting while we wait + BeginConnecting(); +} + +void DirectConnectWindow::BeginConnecting() { + ui->connect->setEnabled(false); + ui->connect->setText(tr("Connecting")); +} + +void DirectConnectWindow::EndConnecting() { + ui->connect->setEnabled(true); + ui->connect->setText(tr("Connect")); +} + +void DirectConnectWindow::OnConnection() { + EndConnecting(); + + if (auto room_member = Network::GetRoomMember().lock()) { + if (room_member->GetState() == Network::RoomMember::State::Joined || + room_member->GetState() == Network::RoomMember::State::Moderator) { + + close(); + } + } +} diff --git a/src/yuzu/multiplayer/direct_connect.h b/src/yuzu/multiplayer/direct_connect.h new file mode 100644 index 0000000000..e38961ed04 --- /dev/null +++ b/src/yuzu/multiplayer/direct_connect.h @@ -0,0 +1,43 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "yuzu/multiplayer/validation.h" + +namespace Ui { +class DirectConnect; +} + +class DirectConnectWindow : public QDialog { + Q_OBJECT + +public: + explicit DirectConnectWindow(QWidget* parent = nullptr); + ~DirectConnectWindow(); + + void RetranslateUi(); + +signals: + /** + * Signalled by this widget when it is closing itself and destroying any state such as + * connections that it might have. + */ + void Closed(); + +private slots: + void OnConnection(); + +private: + void Connect(); + void BeginConnecting(); + void EndConnecting(); + + QFutureWatcher* watcher; + std::unique_ptr ui; + Validation validation; +}; diff --git a/src/yuzu/multiplayer/direct_connect.ui b/src/yuzu/multiplayer/direct_connect.ui new file mode 100644 index 0000000000..681b6bf69d --- /dev/null +++ b/src/yuzu/multiplayer/direct_connect.ui @@ -0,0 +1,168 @@ + + + DirectConnect + + + + 0 + 0 + 455 + 161 + + + + Direct Connect + + + + + + + + + + 0 + + + 0 + + + + + + IP Address + + + + + + + + + 5 + + + 0 + + + 0 + + + 0 + + + + + IP + + + + + + + <html><head/><body><p>IPv4 address of the host</p></body></html> + + + 16 + + + + + + + Port + + + + + + + <html><head/><body><p>Port number the host is listening on</p></body></html> + + + 5 + + + 24872 + + + + + + + + + + + + + + Nickname + + + + + + + 20 + + + + + + + Password + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Connect + + + + + + + + + + + + diff --git a/src/yuzu/multiplayer/host_room.cpp b/src/yuzu/multiplayer/host_room.cpp new file mode 100644 index 0000000000..1b73e2bec5 --- /dev/null +++ b/src/yuzu/multiplayer/host_room.cpp @@ -0,0 +1,237 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "common/settings.h" +#include "core/announce_multiplayer_session.h" +#include "ui_host_room.h" +#include "yuzu/game_list_p.h" +#include "yuzu/main.h" +#include "yuzu/multiplayer/host_room.h" +#include "yuzu/multiplayer/message.h" +#include "yuzu/multiplayer/state.h" +#include "yuzu/multiplayer/validation.h" +#include "yuzu/uisettings.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/verify_user_jwt.h" +#endif + +HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, + std::shared_ptr session) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(std::make_unique()), announce_multiplayer_session(session) { + ui->setupUi(this); + + // set up validation for all of the fields + ui->room_name->setValidator(validation.GetRoomName()); + ui->username->setValidator(validation.GetNickname()); + ui->port->setValidator(validation.GetPort()); + ui->port->setPlaceholderText(QString::number(Network::DefaultRoomPort)); + + // Create a proxy to the game list to display the list of preferred games + game_list = new QStandardItemModel; + UpdateGameList(list); + + proxy = new ComboBoxProxyModel; + proxy->setSourceModel(game_list); + proxy->sort(0, Qt::AscendingOrder); + ui->game_list->setModel(proxy); + + // Connect all the widgets to the appropriate events + connect(ui->host, &QPushButton::clicked, this, &HostRoomWindow::Host); + + // Restore the settings: + ui->username->setText(UISettings::values.room_nickname); + if (ui->username->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) { + // Use yuzu Web Service user name as nickname by default + ui->username->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue())); + } + ui->room_name->setText(UISettings::values.room_name); + ui->port->setText(UISettings::values.room_port); + ui->max_player->setValue(UISettings::values.max_player); + int index = UISettings::values.host_type; + if (index < ui->host_type->count()) { + ui->host_type->setCurrentIndex(index); + } + index = ui->game_list->findData(UISettings::values.game_id, GameListItemPath::ProgramIdRole); + if (index != -1) { + ui->game_list->setCurrentIndex(index); + } + ui->room_description->setText(UISettings::values.room_description); +} + +HostRoomWindow::~HostRoomWindow() = default; + +void HostRoomWindow::UpdateGameList(QStandardItemModel* list) { + game_list->clear(); + for (int i = 0; i < list->rowCount(); i++) { + auto parent = list->item(i, 0); + for (int j = 0; j < parent->rowCount(); j++) { + game_list->appendRow(parent->child(j)->clone()); + } + } +} + +void HostRoomWindow::RetranslateUi() { + ui->retranslateUi(this); +} + +std::unique_ptr HostRoomWindow::CreateVerifyBackend( + bool use_validation) const { + std::unique_ptr verify_backend; + if (use_validation) { +#ifdef ENABLE_WEB_SERVICE + verify_backend = std::make_unique(Settings::values.web_api_url); +#else + verify_backend = std::make_unique(); +#endif + } else { + verify_backend = std::make_unique(); + } + return verify_backend; +} + +void HostRoomWindow::Host() { + if (!ui->username->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID); + return; + } + if (!ui->room_name->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::ROOMNAME_NOT_VALID); + return; + } + if (!ui->port->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::PORT_NOT_VALID); + return; + } + if (ui->game_list->currentIndex() == -1) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::GAME_NOT_SELECTED); + return; + } + if (auto member = Network::GetRoomMember().lock()) { + if (member->GetState() == Network::RoomMember::State::Joining) { + return; + } else if (member->IsConnected()) { + auto parent = static_cast(parentWidget()); + if (!parent->OnCloseRoom()) { + close(); + return; + } + } + ui->host->setDisabled(true); + + auto game_name = ui->game_list->currentData(Qt::DisplayRole).toString(); + auto game_id = ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong(); + auto port = ui->port->isModified() ? ui->port->text().toInt() : Network::DefaultRoomPort; + auto password = ui->password->text().toStdString(); + const bool is_public = ui->host_type->currentIndex() == 0; + Network::Room::BanList ban_list{}; + if (ui->load_ban_list->isChecked()) { + ban_list = UISettings::values.ban_list; + } + if (auto room = Network::GetRoom().lock()) { + bool created = room->Create( + ui->room_name->text().toStdString(), + ui->room_description->toPlainText().toStdString(), "", port, password, + ui->max_player->value(), Settings::values.yuzu_username.GetValue(), + game_name.toStdString(), game_id, CreateVerifyBackend(is_public), ban_list); + if (!created) { + NetworkMessage::ErrorManager::ShowError( + NetworkMessage::ErrorManager::COULD_NOT_CREATE_ROOM); + LOG_ERROR(Network, "Could not create room!"); + ui->host->setEnabled(true); + return; + } + } + // Start the announce session if they chose Public + if (is_public) { + if (auto session = announce_multiplayer_session.lock()) { + // Register the room first to ensure verify_UID is present when we connect + WebService::WebResult result = session->Register(); + if (result.result_code != WebService::WebResult::Code::Success) { + QMessageBox::warning( + this, tr("Error"), + tr("Failed to announce the room to the public lobby. In order to host a " + "room publicly, you must have a valid yuzu account configured in " + "Emulation -> Configure -> Web. If you do not want to publish a room in " + "the public lobby, then select Unlisted instead.\nDebug Message: ") + + QString::fromStdString(result.result_string), + QMessageBox::Ok); + ui->host->setEnabled(true); + if (auto room = Network::GetRoom().lock()) { + room->Destroy(); + } + return; + } + session->Start(); + } else { + LOG_ERROR(Network, "Starting announce session failed"); + } + } + std::string token; +#ifdef ENABLE_WEB_SERVICE + if (is_public) { + WebService::Client client(Settings::values.web_api_url, Settings::values.yuzu_username, + Settings::values.yuzu_token); + if (auto room = Network::GetRoom().lock()) { + token = client.GetExternalJWT(room->GetVerifyUID()).returned_data; + } + if (token.empty()) { + LOG_ERROR(WebService, "Could not get external JWT, verification may fail"); + } else { + LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size()); + } + } +#endif + // TODO: Check what to do with this + member->Join(ui->username->text().toStdString(), "", "127.0.0.1", port, 0, + Network::NoPreferredMac, password, token); + + // Store settings + UISettings::values.room_nickname = ui->username->text(); + UISettings::values.room_name = ui->room_name->text(); + UISettings::values.game_id = + ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong(); + UISettings::values.max_player = ui->max_player->value(); + + UISettings::values.host_type = ui->host_type->currentIndex(); + UISettings::values.room_port = (ui->port->isModified() && !ui->port->text().isEmpty()) + ? ui->port->text() + : QString::number(Network::DefaultRoomPort); + UISettings::values.room_description = ui->room_description->toPlainText(); + ui->host->setEnabled(true); + close(); + } +} + +QVariant ComboBoxProxyModel::data(const QModelIndex& idx, int role) const { + if (role != Qt::DisplayRole) { + auto val = QSortFilterProxyModel::data(idx, role); + // If its the icon, shrink it to 16x16 + if (role == Qt::DecorationRole) + val = val.value().scaled(16, 16, Qt::KeepAspectRatio); + return val; + } + std::string filename; + Common::SplitPath( + QSortFilterProxyModel::data(idx, GameListItemPath::FullPathRole).toString().toStdString(), + nullptr, &filename, nullptr); + QString title = QSortFilterProxyModel::data(idx, GameListItemPath::TitleRole).toString(); + return title.isEmpty() ? QString::fromStdString(filename) : title; +} + +bool ComboBoxProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { + auto leftData = left.data(GameListItemPath::TitleRole).toString(); + auto rightData = right.data(GameListItemPath::TitleRole).toString(); + return leftData.compare(rightData) < 0; +} diff --git a/src/yuzu/multiplayer/host_room.h b/src/yuzu/multiplayer/host_room.h new file mode 100644 index 0000000000..d84f93ffde --- /dev/null +++ b/src/yuzu/multiplayer/host_room.h @@ -0,0 +1,74 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include "network/network.h" +#include "yuzu/multiplayer/chat_room.h" +#include "yuzu/multiplayer/validation.h" + +namespace Ui { +class HostRoom; +} + +namespace Core { +class AnnounceMultiplayerSession; +} + +class ConnectionError; +class ComboBoxProxyModel; + +class ChatMessage; + +namespace Network::VerifyUser { +class Backend; +}; + +class HostRoomWindow : public QDialog { + Q_OBJECT + +public: + explicit HostRoomWindow(QWidget* parent, QStandardItemModel* list, + std::shared_ptr session); + ~HostRoomWindow(); + + /** + * Updates the dialog with a new game list model. + * This model should be the original model of the game list. + */ + void UpdateGameList(QStandardItemModel* list); + void RetranslateUi(); + +private: + void Host(); + std::unique_ptr CreateVerifyBackend(bool use_validation) const; + + std::unique_ptr ui; + std::weak_ptr announce_multiplayer_session; + QStandardItemModel* game_list; + ComboBoxProxyModel* proxy; + Validation validation; +}; + +/** + * Proxy Model for the game list combo box so we can reuse the game list model while still + * displaying the fields slightly differently + */ +class ComboBoxProxyModel : public QSortFilterProxyModel { + Q_OBJECT + +public: + int columnCount(const QModelIndex& idx) const override { + return 1; + } + + QVariant data(const QModelIndex& idx, int role) const override; + + bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; +}; diff --git a/src/yuzu/multiplayer/host_room.ui b/src/yuzu/multiplayer/host_room.ui new file mode 100644 index 0000000000..d54cf49c65 --- /dev/null +++ b/src/yuzu/multiplayer/host_room.ui @@ -0,0 +1,207 @@ + + + HostRoom + + + + 0 + 0 + 607 + 211 + + + + Create Room + + + + + + + 0 + + + 0 + + + 0 + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Room Name + + + + + + + 50 + + + + + + + Preferred Game + + + + + + + + + + Max Players + + + + + + + 2 + + + 16 + + + 8 + + + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + Username + + + + + + + QLineEdit::PasswordEchoOnEdit + + + (Leave blank for open game) + + + + + + + Qt::ImhDigitsOnly + + + 5 + + + + + + + Password + + + + + + + Port + + + + + + + + + + + + + + Room Description + + + + + + + + + + + + + + Load Previous Ban List + + + true + + + + + + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + Public + + + + + Unlisted + + + + + + + + Host Room + + + + + + + + + + diff --git a/src/yuzu/multiplayer/lobby.cpp b/src/yuzu/multiplayer/lobby.cpp new file mode 100644 index 0000000000..fcaa7b517f --- /dev/null +++ b/src/yuzu/multiplayer/lobby.cpp @@ -0,0 +1,360 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "common/logging/log.h" +#include "common/settings.h" +#include "network/network.h" +#include "ui_lobby.h" +#include "yuzu/game_list_p.h" +#include "yuzu/main.h" +#include "yuzu/multiplayer/client_room.h" +#include "yuzu/multiplayer/lobby.h" +#include "yuzu/multiplayer/lobby_p.h" +#include "yuzu/multiplayer/message.h" +#include "yuzu/multiplayer/state.h" +#include "yuzu/multiplayer/validation.h" +#include "yuzu/uisettings.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/web_backend.h" +#endif + +Lobby::Lobby(QWidget* parent, QStandardItemModel* list, + std::shared_ptr session) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(std::make_unique()), announce_multiplayer_session(session) { + ui->setupUi(this); + + // setup the watcher for background connections + watcher = new QFutureWatcher; + + model = new QStandardItemModel(ui->room_list); + + // Create a proxy to the game list to get the list of games owned + game_list = new QStandardItemModel; + UpdateGameList(list); + + proxy = new LobbyFilterProxyModel(this, game_list); + proxy->setSourceModel(model); + proxy->setDynamicSortFilter(true); + proxy->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxy->setSortLocaleAware(true); + ui->room_list->setModel(proxy); + ui->room_list->header()->setSectionResizeMode(QHeaderView::Interactive); + ui->room_list->header()->stretchLastSection(); + ui->room_list->setAlternatingRowColors(true); + ui->room_list->setSelectionMode(QHeaderView::SingleSelection); + ui->room_list->setSelectionBehavior(QHeaderView::SelectRows); + ui->room_list->setVerticalScrollMode(QHeaderView::ScrollPerPixel); + ui->room_list->setHorizontalScrollMode(QHeaderView::ScrollPerPixel); + ui->room_list->setSortingEnabled(true); + ui->room_list->setEditTriggers(QHeaderView::NoEditTriggers); + ui->room_list->setExpandsOnDoubleClick(false); + ui->room_list->setContextMenuPolicy(Qt::CustomContextMenu); + + ui->nickname->setValidator(validation.GetNickname()); + ui->nickname->setText(UISettings::values.nickname); + if (ui->nickname->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) { + // Use yuzu Web Service user name as nickname by default + ui->nickname->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue())); + } + + // UI Buttons + connect(ui->refresh_list, &QPushButton::clicked, this, &Lobby::RefreshLobby); + connect(ui->games_owned, &QCheckBox::toggled, proxy, &LobbyFilterProxyModel::SetFilterOwned); + connect(ui->hide_full, &QCheckBox::toggled, proxy, &LobbyFilterProxyModel::SetFilterFull); + connect(ui->search, &QLineEdit::textChanged, proxy, &LobbyFilterProxyModel::SetFilterSearch); + connect(ui->room_list, &QTreeView::doubleClicked, this, &Lobby::OnJoinRoom); + connect(ui->room_list, &QTreeView::clicked, this, &Lobby::OnExpandRoom); + + // Actions + connect(&room_list_watcher, &QFutureWatcher::finished, this, + &Lobby::OnRefreshLobby); + + // manually start a refresh when the window is opening + // TODO(jroweboy): if this refresh is slow for people with bad internet, then don't do it as + // part of the constructor, but offload the refresh until after the window shown. perhaps emit a + // refreshroomlist signal from places that open the lobby + RefreshLobby(); +} + +Lobby::~Lobby() = default; + +void Lobby::UpdateGameList(QStandardItemModel* list) { + game_list->clear(); + for (int i = 0; i < list->rowCount(); i++) { + auto parent = list->item(i, 0); + for (int j = 0; j < parent->rowCount(); j++) { + game_list->appendRow(parent->child(j)->clone()); + } + } + if (proxy) + proxy->UpdateGameList(game_list); +} + +void Lobby::RetranslateUi() { + ui->retranslateUi(this); +} + +QString Lobby::PasswordPrompt() { + bool ok; + const QString text = + QInputDialog::getText(this, tr("Password Required to Join"), tr("Password:"), + QLineEdit::Password, QString(), &ok); + return ok ? text : QString(); +} + +void Lobby::OnExpandRoom(const QModelIndex& index) { + QModelIndex member_index = proxy->index(index.row(), Column::MEMBER); + auto member_list = proxy->data(member_index, LobbyItemMemberList::MemberListRole).toList(); +} + +void Lobby::OnJoinRoom(const QModelIndex& source) { + if (const auto member = Network::GetRoomMember().lock()) { + // Prevent the user from trying to join a room while they are already joining. + if (member->GetState() == Network::RoomMember::State::Joining) { + return; + } else if (member->IsConnected()) { + // And ask if they want to leave the room if they are already in one. + if (!NetworkMessage::WarnDisconnect()) { + return; + } + } + } + QModelIndex index = source; + // If the user double clicks on a child row (aka the player list) then use the parent instead + if (source.parent() != QModelIndex()) { + index = source.parent(); + } + if (!ui->nickname->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID); + return; + } + + // Get a password to pass if the room is password protected + QModelIndex password_index = proxy->index(index.row(), Column::ROOM_NAME); + bool has_password = proxy->data(password_index, LobbyItemName::PasswordRole).toBool(); + const std::string password = has_password ? PasswordPrompt().toStdString() : ""; + if (has_password && password.empty()) { + return; + } + + QModelIndex connection_index = proxy->index(index.row(), Column::HOST); + const std::string nickname = ui->nickname->text().toStdString(); + const std::string ip = + proxy->data(connection_index, LobbyItemHost::HostIPRole).toString().toStdString(); + int port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt(); + const std::string verify_UID = + proxy->data(connection_index, LobbyItemHost::HostVerifyUIDRole).toString().toStdString(); + + // attempt to connect in a different thread + QFuture f = QtConcurrent::run([nickname, ip, port, password, verify_UID] { + std::string token; +#ifdef ENABLE_WEB_SERVICE + if (!Settings::values.yuzu_username.empty() && !Settings::values.yuzu_token.empty()) { + WebService::Client client(Settings::values.web_api_url, Settings::values.yuzu_username, + Settings::values.yuzu_token); + token = client.GetExternalJWT(verify_UID).returned_data; + if (token.empty()) { + LOG_ERROR(WebService, "Could not get external JWT, verification may fail"); + } else { + LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size()); + } + } +#endif + if (auto room_member = Network::GetRoomMember().lock()) { + room_member->Join(nickname, "", ip.c_str(), port, 0, Network::NoPreferredMac, password, + token); + } + }); + watcher->setFuture(f); + + // TODO(jroweboy): disable widgets and display a connecting while we wait + + // Save settings + UISettings::values.nickname = ui->nickname->text(); + UISettings::values.ip = proxy->data(connection_index, LobbyItemHost::HostIPRole).toString(); + UISettings::values.port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toString(); +} + +void Lobby::ResetModel() { + model->clear(); + model->insertColumns(0, Column::TOTAL); + model->setHeaderData(Column::EXPAND, Qt::Horizontal, QString(), Qt::DisplayRole); + model->setHeaderData(Column::ROOM_NAME, Qt::Horizontal, tr("Room Name"), Qt::DisplayRole); + model->setHeaderData(Column::GAME_NAME, Qt::Horizontal, tr("Preferred Game"), Qt::DisplayRole); + model->setHeaderData(Column::HOST, Qt::Horizontal, tr("Host"), Qt::DisplayRole); + model->setHeaderData(Column::MEMBER, Qt::Horizontal, tr("Players"), Qt::DisplayRole); +} + +void Lobby::RefreshLobby() { + if (auto session = announce_multiplayer_session.lock()) { + ResetModel(); + ui->refresh_list->setEnabled(false); + ui->refresh_list->setText(tr("Refreshing")); + room_list_watcher.setFuture( + QtConcurrent::run([session]() { return session->GetRoomList(); })); + } else { + // TODO(jroweboy): Display an error box about announce couldn't be started + } +} + +void Lobby::OnRefreshLobby() { + AnnounceMultiplayerRoom::RoomList new_room_list = room_list_watcher.result(); + for (auto room : new_room_list) { + // find the icon for the game if this person owns that game. + QPixmap smdh_icon; + for (int r = 0; r < game_list->rowCount(); ++r) { + auto index = game_list->index(r, 0); + auto game_id = game_list->data(index, GameListItemPath::ProgramIdRole).toULongLong(); + if (game_id != 0 && room.preferred_game_id == game_id) { + smdh_icon = game_list->data(index, Qt::DecorationRole).value(); + } + } + + QList members; + for (auto member : room.members) { + QVariant var; + var.setValue(LobbyMember{QString::fromStdString(member.username), + QString::fromStdString(member.nickname), member.game_id, + QString::fromStdString(member.game_name)}); + members.append(var); + } + + auto first_item = new LobbyItem(); + auto row = QList({ + first_item, + new LobbyItemName(room.has_password, QString::fromStdString(room.name)), + new LobbyItemGame(room.preferred_game_id, QString::fromStdString(room.preferred_game), + smdh_icon), + new LobbyItemHost(QString::fromStdString(room.owner), QString::fromStdString(room.ip), + room.port, QString::fromStdString(room.verify_UID)), + new LobbyItemMemberList(members, room.max_player), + }); + model->appendRow(row); + // To make the rows expandable, add the member data as a child of the first column of the + // rows with people in them and have qt set them to colspan after the model is finished + // resetting + if (!room.description.empty()) { + first_item->appendRow( + new LobbyItemDescription(QString::fromStdString(room.description))); + } + if (!room.members.empty()) { + first_item->appendRow(new LobbyItemExpandedMemberList(members)); + } + } + + // Reenable the refresh button and resize the columns + ui->refresh_list->setEnabled(true); + ui->refresh_list->setText(tr("Refresh List")); + ui->room_list->header()->stretchLastSection(); + for (int i = 0; i < Column::TOTAL - 1; ++i) { + ui->room_list->resizeColumnToContents(i); + } + + // Set the member list child items to span all columns + for (int i = 0; i < proxy->rowCount(); i++) { + auto parent = model->item(i, 0); + for (int j = 0; j < parent->rowCount(); j++) { + ui->room_list->setFirstColumnSpanned(j, proxy->index(i, 0), true); + } + } +} + +LobbyFilterProxyModel::LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list) + : QSortFilterProxyModel(parent), game_list(list) {} + +void LobbyFilterProxyModel::UpdateGameList(QStandardItemModel* list) { + game_list = list; +} + +bool LobbyFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { + // Prioritize filters by fastest to compute + + // pass over any child rows (aka row that shows the players in the room) + if (sourceParent != QModelIndex()) { + return true; + } + + // filter by filled rooms + if (filter_full) { + QModelIndex member_list = sourceModel()->index(sourceRow, Column::MEMBER, sourceParent); + int player_count = + sourceModel()->data(member_list, LobbyItemMemberList::MemberListRole).toList().size(); + int max_players = + sourceModel()->data(member_list, LobbyItemMemberList::MaxPlayerRole).toInt(); + if (player_count >= max_players) { + return false; + } + } + + // filter by search parameters + if (!filter_search.isEmpty()) { + QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent); + QModelIndex room_name = sourceModel()->index(sourceRow, Column::ROOM_NAME, sourceParent); + QModelIndex host_name = sourceModel()->index(sourceRow, Column::HOST, sourceParent); + bool preferred_game_match = sourceModel() + ->data(game_name, LobbyItemGame::GameNameRole) + .toString() + .contains(filter_search, filterCaseSensitivity()); + bool room_name_match = sourceModel() + ->data(room_name, LobbyItemName::NameRole) + .toString() + .contains(filter_search, filterCaseSensitivity()); + bool username_match = sourceModel() + ->data(host_name, LobbyItemHost::HostUsernameRole) + .toString() + .contains(filter_search, filterCaseSensitivity()); + if (!preferred_game_match && !room_name_match && !username_match) { + return false; + } + } + + // filter by game owned + if (filter_owned) { + QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent); + QList owned_games; + for (int r = 0; r < game_list->rowCount(); ++r) { + owned_games.append(QModelIndex(game_list->index(r, 0))); + } + auto current_id = sourceModel()->data(game_name, LobbyItemGame::TitleIDRole).toLongLong(); + if (current_id == 0) { + // TODO(jroweboy): homebrew often doesn't have a game id and this hides them + return false; + } + bool owned = false; + for (const auto& game : owned_games) { + auto game_id = game_list->data(game, GameListItemPath::ProgramIdRole).toLongLong(); + if (current_id == game_id) { + owned = true; + } + } + if (!owned) { + return false; + } + } + + return true; +} + +void LobbyFilterProxyModel::sort(int column, Qt::SortOrder order) { + sourceModel()->sort(column, order); +} + +void LobbyFilterProxyModel::SetFilterOwned(bool filter) { + filter_owned = filter; + invalidate(); +} + +void LobbyFilterProxyModel::SetFilterFull(bool filter) { + filter_full = filter; + invalidate(); +} + +void LobbyFilterProxyModel::SetFilterSearch(const QString& filter) { + filter_search = filter; + invalidate(); +} diff --git a/src/yuzu/multiplayer/lobby.h b/src/yuzu/multiplayer/lobby.h new file mode 100644 index 0000000000..aea4a0e4e4 --- /dev/null +++ b/src/yuzu/multiplayer/lobby.h @@ -0,0 +1,127 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include "common/announce_multiplayer_room.h" +#include "core/announce_multiplayer_session.h" +#include "network/network.h" +#include "yuzu/multiplayer/validation.h" + +namespace Ui { +class Lobby; +} + +class LobbyModel; +class LobbyFilterProxyModel; + +/** + * Listing of all public games pulled from services. The lobby should be simple enough for users to + * find the game they want to play, and join it. + */ +class Lobby : public QDialog { + Q_OBJECT + +public: + explicit Lobby(QWidget* parent, QStandardItemModel* list, + std::shared_ptr session); + ~Lobby() override; + + /** + * Updates the lobby with a new game list model. + * This model should be the original model of the game list. + */ + void UpdateGameList(QStandardItemModel* list); + void RetranslateUi(); + +public slots: + /** + * Begin the process to pull the latest room list from web services. After the listing is + * returned from web services, `LobbyRefreshed` will be signalled + */ + void RefreshLobby(); + +private slots: + /** + * Pulls the list of rooms from network and fills out the lobby model with the results + */ + void OnRefreshLobby(); + + /** + * Handler for single clicking on a room in the list. Expands the treeitem to show player + * information for the people in the room + * + * index - The row of the proxy model that the user wants to join. + */ + void OnExpandRoom(const QModelIndex&); + + /** + * Handler for double clicking on a room in the list. Gathers the host ip and port and attempts + * to connect. Will also prompt for a password in case one is required. + * + * index - The row of the proxy model that the user wants to join. + */ + void OnJoinRoom(const QModelIndex&); + +signals: + void StateChanged(const Network::RoomMember::State&); + +private: + /** + * Removes all entries in the Lobby before refreshing. + */ + void ResetModel(); + + /** + * Prompts for a password. Returns an empty QString if the user either did not provide a + * password or if the user closed the window. + */ + QString PasswordPrompt(); + + std::unique_ptr ui; + + QStandardItemModel* model{}; + QStandardItemModel* game_list{}; + LobbyFilterProxyModel* proxy{}; + + QFutureWatcher room_list_watcher; + std::weak_ptr announce_multiplayer_session; + QFutureWatcher* watcher; + Validation validation; +}; + +/** + * Proxy Model for filtering the lobby + */ +class LobbyFilterProxyModel : public QSortFilterProxyModel { + Q_OBJECT; + +public: + explicit LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list); + + /** + * Updates the filter with a new game list model. + * This model should be the processed one created by the Lobby. + */ + void UpdateGameList(QStandardItemModel* list); + + bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; + void sort(int column, Qt::SortOrder order) override; + +public slots: + void SetFilterOwned(bool); + void SetFilterFull(bool); + void SetFilterSearch(const QString&); + +private: + QStandardItemModel* game_list; + bool filter_owned = false; + bool filter_full = false; + QString filter_search; +}; diff --git a/src/yuzu/multiplayer/lobby.ui b/src/yuzu/multiplayer/lobby.ui new file mode 100644 index 0000000000..4c9901c9af --- /dev/null +++ b/src/yuzu/multiplayer/lobby.ui @@ -0,0 +1,123 @@ + + + Lobby + + + + 0 + 0 + 903 + 487 + + + + Public Room Browser + + + + + + 3 + + + + + 6 + + + + + + + Nickname + + + + + + + Nickname + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Filters + + + + + + + Search + + + true + + + + + + + Games I Own + + + + + + + Hide Full Rooms + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Refresh Lobby + + + + + + + + + + + + + + + + + + + + diff --git a/src/yuzu/multiplayer/lobby_p.h b/src/yuzu/multiplayer/lobby_p.h new file mode 100644 index 0000000000..749ca627f2 --- /dev/null +++ b/src/yuzu/multiplayer/lobby_p.h @@ -0,0 +1,239 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include "common/common_types.h" + +namespace Column { +enum List { + EXPAND, + ROOM_NAME, + GAME_NAME, + HOST, + MEMBER, + TOTAL, +}; +} + +class LobbyItem : public QStandardItem { +public: + LobbyItem() = default; + explicit LobbyItem(const QString& string) : QStandardItem(string) {} + virtual ~LobbyItem() override = default; +}; + +class LobbyItemName : public LobbyItem { +public: + static const int NameRole = Qt::UserRole + 1; + static const int PasswordRole = Qt::UserRole + 2; + + LobbyItemName() = default; + explicit LobbyItemName(bool has_password, QString name) : LobbyItem() { + setData(name, NameRole); + setData(has_password, PasswordRole); + } + + QVariant data(int role) const override { + if (role == Qt::DecorationRole) { + bool has_password = data(PasswordRole).toBool(); + return has_password ? QIcon::fromTheme(QStringLiteral("lock")).pixmap(16) : QIcon(); + } + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + return data(NameRole).toString(); + } + + bool operator<(const QStandardItem& other) const override { + return data(NameRole).toString().localeAwareCompare(other.data(NameRole).toString()) < 0; + } +}; + +class LobbyItemDescription : public LobbyItem { +public: + static const int DescriptionRole = Qt::UserRole + 1; + + LobbyItemDescription() = default; + explicit LobbyItemDescription(QString description) { + setData(description, DescriptionRole); + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + auto description = data(DescriptionRole).toString(); + description.prepend(QStringLiteral("Description: ")); + return description; + } + + bool operator<(const QStandardItem& other) const override { + return data(DescriptionRole) + .toString() + .localeAwareCompare(other.data(DescriptionRole).toString()) < 0; + } +}; + +class LobbyItemGame : public LobbyItem { +public: + static const int TitleIDRole = Qt::UserRole + 1; + static const int GameNameRole = Qt::UserRole + 2; + static const int GameIconRole = Qt::UserRole + 3; + + LobbyItemGame() = default; + explicit LobbyItemGame(u64 title_id, QString game_name, QPixmap smdh_icon) { + setData(static_cast(title_id), TitleIDRole); + setData(game_name, GameNameRole); + if (!smdh_icon.isNull()) { + setData(smdh_icon, GameIconRole); + } + } + + QVariant data(int role) const override { + if (role == Qt::DecorationRole) { + auto val = data(GameIconRole); + if (val.isValid()) { + val = val.value().scaled(16, 16, Qt::KeepAspectRatio); + } + return val; + } else if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + return data(GameNameRole).toString(); + } + + bool operator<(const QStandardItem& other) const override { + return data(GameNameRole) + .toString() + .localeAwareCompare(other.data(GameNameRole).toString()) < 0; + } +}; + +class LobbyItemHost : public LobbyItem { +public: + static const int HostUsernameRole = Qt::UserRole + 1; + static const int HostIPRole = Qt::UserRole + 2; + static const int HostPortRole = Qt::UserRole + 3; + static const int HostVerifyUIDRole = Qt::UserRole + 4; + + LobbyItemHost() = default; + explicit LobbyItemHost(QString username, QString ip, u16 port, QString verify_UID) { + setData(username, HostUsernameRole); + setData(ip, HostIPRole); + setData(port, HostPortRole); + setData(verify_UID, HostVerifyUIDRole); + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + return data(HostUsernameRole).toString(); + } + + bool operator<(const QStandardItem& other) const override { + return data(HostUsernameRole) + .toString() + .localeAwareCompare(other.data(HostUsernameRole).toString()) < 0; + } +}; + +class LobbyMember { +public: + LobbyMember() = default; + LobbyMember(const LobbyMember& other) = default; + explicit LobbyMember(QString username, QString nickname, u64 title_id, QString game_name) + : username(std::move(username)), nickname(std::move(nickname)), title_id(title_id), + game_name(std::move(game_name)) {} + ~LobbyMember() = default; + + QString GetName() const { + if (username.isEmpty() || username == nickname) { + return nickname; + } else { + return QStringLiteral("%1 (%2)").arg(nickname, username); + } + } + u64 GetTitleId() const { + return title_id; + } + QString GetGameName() const { + return game_name; + } + +private: + QString username; + QString nickname; + u64 title_id; + QString game_name; +}; + +Q_DECLARE_METATYPE(LobbyMember); + +class LobbyItemMemberList : public LobbyItem { +public: + static const int MemberListRole = Qt::UserRole + 1; + static const int MaxPlayerRole = Qt::UserRole + 2; + + LobbyItemMemberList() = default; + explicit LobbyItemMemberList(QList members, u32 max_players) { + setData(members, MemberListRole); + setData(max_players, MaxPlayerRole); + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + auto members = data(MemberListRole).toList(); + return QStringLiteral("%1 / %2").arg(QString::number(members.size()), + data(MaxPlayerRole).toString()); + } + + bool operator<(const QStandardItem& other) const override { + // sort by rooms that have the most players + int left_members = data(MemberListRole).toList().size(); + int right_members = other.data(MemberListRole).toList().size(); + return left_members < right_members; + } +}; + +/** + * Member information for when a lobby is expanded in the UI + */ +class LobbyItemExpandedMemberList : public LobbyItem { +public: + static const int MemberListRole = Qt::UserRole + 1; + + LobbyItemExpandedMemberList() = default; + explicit LobbyItemExpandedMemberList(QList members) { + setData(members, MemberListRole); + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + auto members = data(MemberListRole).toList(); + QString out; + bool first = true; + for (const auto& member : members) { + if (!first) + out.append(QStringLiteral("\n")); + const auto& m = member.value(); + if (m.GetGameName().isEmpty()) { + out += QString(QObject::tr("%1 is not playing a game")).arg(m.GetName()); + } else { + out += QString(QObject::tr("%1 is playing %2")).arg(m.GetName(), m.GetGameName()); + } + first = false; + } + return out; + } +}; diff --git a/src/yuzu/multiplayer/message.cpp b/src/yuzu/multiplayer/message.cpp new file mode 100644 index 0000000000..458f1e7d1f --- /dev/null +++ b/src/yuzu/multiplayer/message.cpp @@ -0,0 +1,79 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include + +#include "yuzu/multiplayer/message.h" + +namespace NetworkMessage { +const ConnectionError ErrorManager::USERNAME_NOT_VALID( + QT_TR_NOOP("Username is not valid. Must be 4 to 20 alphanumeric characters.")); +const ConnectionError ErrorManager::ROOMNAME_NOT_VALID( + QT_TR_NOOP("Room name is not valid. Must be 4 to 20 alphanumeric characters.")); +const ConnectionError ErrorManager::USERNAME_NOT_VALID_SERVER( + QT_TR_NOOP("Username is already in use or not valid. Please choose another.")); +const ConnectionError ErrorManager::IP_ADDRESS_NOT_VALID( + QT_TR_NOOP("IP is not a valid IPv4 address.")); +const ConnectionError ErrorManager::PORT_NOT_VALID( + QT_TR_NOOP("Port must be a number between 0 to 65535.")); +const ConnectionError ErrorManager::GAME_NOT_SELECTED(QT_TR_NOOP( + "You must choose a Preferred Game to host a room. If you do not have any games in your game " + "list yet, add a game folder by clicking on the plus icon in the game list.")); +const ConnectionError ErrorManager::NO_INTERNET( + QT_TR_NOOP("Unable to find an internet connection. Check your internet settings.")); +const ConnectionError ErrorManager::UNABLE_TO_CONNECT( + QT_TR_NOOP("Unable to connect to the host. Verify that the connection settings are correct. If " + "you still cannot connect, contact the room host and verify that the host is " + "properly configured with the external port forwarded.")); +const ConnectionError ErrorManager::ROOM_IS_FULL( + QT_TR_NOOP("Unable to connect to the room because it is already full.")); +const ConnectionError ErrorManager::COULD_NOT_CREATE_ROOM( + QT_TR_NOOP("Creating a room failed. Please retry. Restarting yuzu might be necessary.")); +const ConnectionError ErrorManager::HOST_BANNED( + QT_TR_NOOP("The host of the room has banned you. Speak with the host to unban you " + "or try a different room.")); +const ConnectionError ErrorManager::WRONG_VERSION( + QT_TR_NOOP("Version mismatch! Please update to the latest version of yuzu. If the problem " + "persists, contact the room host and ask them to update the server.")); +const ConnectionError ErrorManager::WRONG_PASSWORD(QT_TR_NOOP("Incorrect password.")); +const ConnectionError ErrorManager::GENERIC_ERROR(QT_TR_NOOP( + "An unknown error occurred. If this error continues to occur, please open an issue")); +const ConnectionError ErrorManager::LOST_CONNECTION( + QT_TR_NOOP("Connection to room lost. Try to reconnect.")); +const ConnectionError ErrorManager::HOST_KICKED( + QT_TR_NOOP("You have been kicked by the room host.")); +const ConnectionError ErrorManager::MAC_COLLISION( + QT_TR_NOOP("MAC address is already in use. Please choose another.")); +const ConnectionError ErrorManager::CONSOLE_ID_COLLISION(QT_TR_NOOP( + "Your Console ID conflicted with someone else's in the room.\n\nPlease go to Emulation " + "> Configure > System to regenerate your Console ID.")); +const ConnectionError ErrorManager::PERMISSION_DENIED( + QT_TR_NOOP("You do not have enough permission to perform this action.")); +const ConnectionError ErrorManager::NO_SUCH_USER(QT_TR_NOOP( + "The user you are trying to kick/ban could not be found.\nThey may have left the room.")); + +static bool WarnMessage(const std::string& title, const std::string& text) { + return QMessageBox::Ok == QMessageBox::warning(nullptr, QObject::tr(title.c_str()), + QObject::tr(text.c_str()), + QMessageBox::Ok | QMessageBox::Cancel); +} + +void ErrorManager::ShowError(const ConnectionError& e) { + QMessageBox::critical(nullptr, tr("Error"), tr(e.GetString().c_str())); +} + +bool WarnCloseRoom() { + return WarnMessage( + QT_TR_NOOP("Leave Room"), + QT_TR_NOOP("You are about to close the room. Any network connections will be closed.")); +} + +bool WarnDisconnect() { + return WarnMessage( + QT_TR_NOOP("Disconnect"), + QT_TR_NOOP("You are about to leave the room. Any network connections will be closed.")); +} + +} // namespace NetworkMessage diff --git a/src/yuzu/multiplayer/message.h b/src/yuzu/multiplayer/message.h new file mode 100644 index 0000000000..49a31997dd --- /dev/null +++ b/src/yuzu/multiplayer/message.h @@ -0,0 +1,65 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include + +namespace NetworkMessage { + +class ConnectionError { + +public: + explicit ConnectionError(std::string str) : err(std::move(str)) {} + const std::string& GetString() const { + return err; + } + +private: + std::string err; +}; + +class ErrorManager : QObject { + Q_OBJECT +public: + /// When the nickname is considered invalid by the client + static const ConnectionError USERNAME_NOT_VALID; + static const ConnectionError ROOMNAME_NOT_VALID; + /// When the nickname is considered invalid by the room server + static const ConnectionError USERNAME_NOT_VALID_SERVER; + static const ConnectionError IP_ADDRESS_NOT_VALID; + static const ConnectionError PORT_NOT_VALID; + static const ConnectionError GAME_NOT_SELECTED; + static const ConnectionError NO_INTERNET; + static const ConnectionError UNABLE_TO_CONNECT; + static const ConnectionError ROOM_IS_FULL; + static const ConnectionError COULD_NOT_CREATE_ROOM; + static const ConnectionError HOST_BANNED; + static const ConnectionError WRONG_VERSION; + static const ConnectionError WRONG_PASSWORD; + static const ConnectionError GENERIC_ERROR; + static const ConnectionError LOST_CONNECTION; + static const ConnectionError HOST_KICKED; + static const ConnectionError MAC_COLLISION; + static const ConnectionError CONSOLE_ID_COLLISION; + static const ConnectionError PERMISSION_DENIED; + static const ConnectionError NO_SUCH_USER; + /** + * Shows a standard QMessageBox with a error message + */ + static void ShowError(const ConnectionError& e); +}; +/** + * Show a standard QMessageBox with a warning message about leaving the room + * return true if the user wants to close the network connection + */ +bool WarnCloseRoom(); + +/** + * Show a standard QMessageBox with a warning message about disconnecting from the room + * return true if the user wants to disconnect + */ +bool WarnDisconnect(); + +} // namespace NetworkMessage diff --git a/src/yuzu/multiplayer/moderation_dialog.cpp b/src/yuzu/multiplayer/moderation_dialog.cpp new file mode 100644 index 0000000000..e97f30ee5a --- /dev/null +++ b/src/yuzu/multiplayer/moderation_dialog.cpp @@ -0,0 +1,113 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "network/network.h" +#include "network/room_member.h" +#include "ui_moderation_dialog.h" +#include "yuzu/multiplayer/moderation_dialog.h" + +namespace Column { +enum { + SUBJECT, + TYPE, + COUNT, +}; +} + +ModerationDialog::ModerationDialog(QWidget* parent) + : QDialog(parent), ui(std::make_unique()) { + ui->setupUi(this); + + qRegisterMetaType(); + + if (auto member = Network::GetRoomMember().lock()) { + callback_handle_status_message = member->BindOnStatusMessageReceived( + [this](const Network::StatusMessageEntry& status_message) { + emit StatusMessageReceived(status_message); + }); + connect(this, &ModerationDialog::StatusMessageReceived, this, + &ModerationDialog::OnStatusMessageReceived); + callback_handle_ban_list = member->BindOnBanListReceived( + [this](const Network::Room::BanList& ban_list) { emit BanListReceived(ban_list); }); + connect(this, &ModerationDialog::BanListReceived, this, &ModerationDialog::PopulateBanList); + } + + // Initialize the UI + model = new QStandardItemModel(ui->ban_list_view); + model->insertColumns(0, Column::COUNT); + model->setHeaderData(Column::SUBJECT, Qt::Horizontal, tr("Subject")); + model->setHeaderData(Column::TYPE, Qt::Horizontal, tr("Type")); + + ui->ban_list_view->setModel(model); + + // Load the ban list in background + LoadBanList(); + + connect(ui->refresh, &QPushButton::clicked, this, [this] { LoadBanList(); }); + connect(ui->unban, &QPushButton::clicked, this, [this] { + auto index = ui->ban_list_view->currentIndex(); + SendUnbanRequest(model->item(index.row(), 0)->text()); + }); + connect(ui->ban_list_view, &QTreeView::clicked, [this] { ui->unban->setEnabled(true); }); +} + +ModerationDialog::~ModerationDialog() { + if (callback_handle_status_message) { + if (auto room = Network::GetRoomMember().lock()) { + room->Unbind(callback_handle_status_message); + } + } + + if (callback_handle_ban_list) { + if (auto room = Network::GetRoomMember().lock()) { + room->Unbind(callback_handle_ban_list); + } + } +} + +void ModerationDialog::LoadBanList() { + if (auto room = Network::GetRoomMember().lock()) { + ui->refresh->setEnabled(false); + ui->refresh->setText(tr("Refreshing")); + ui->unban->setEnabled(false); + room->RequestBanList(); + } +} + +void ModerationDialog::PopulateBanList(const Network::Room::BanList& ban_list) { + model->removeRows(0, model->rowCount()); + for (const auto& username : ban_list.first) { + QStandardItem* subject_item = new QStandardItem(QString::fromStdString(username)); + QStandardItem* type_item = new QStandardItem(tr("Forum Username")); + model->invisibleRootItem()->appendRow({subject_item, type_item}); + } + for (const auto& ip : ban_list.second) { + QStandardItem* subject_item = new QStandardItem(QString::fromStdString(ip)); + QStandardItem* type_item = new QStandardItem(tr("IP Address")); + model->invisibleRootItem()->appendRow({subject_item, type_item}); + } + for (int i = 0; i < Column::COUNT - 1; ++i) { + ui->ban_list_view->resizeColumnToContents(i); + } + ui->refresh->setEnabled(true); + ui->refresh->setText(tr("Refresh")); + ui->unban->setEnabled(false); +} + +void ModerationDialog::SendUnbanRequest(const QString& subject) { + if (auto room = Network::GetRoomMember().lock()) { + room->SendModerationRequest(Network::IdModUnban, subject.toStdString()); + } +} + +void ModerationDialog::OnStatusMessageReceived(const Network::StatusMessageEntry& status_message) { + if (status_message.type != Network::IdMemberBanned && + status_message.type != Network::IdAddressUnbanned) + return; + + // Update the ban list for ban/unban + LoadBanList(); +} diff --git a/src/yuzu/multiplayer/moderation_dialog.h b/src/yuzu/multiplayer/moderation_dialog.h new file mode 100644 index 0000000000..d10083d5b8 --- /dev/null +++ b/src/yuzu/multiplayer/moderation_dialog.h @@ -0,0 +1,42 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "network/room.h" +#include "network/room_member.h" + +namespace Ui { +class ModerationDialog; +} + +class QStandardItemModel; + +class ModerationDialog : public QDialog { + Q_OBJECT + +public: + explicit ModerationDialog(QWidget* parent = nullptr); + ~ModerationDialog(); + +signals: + void StatusMessageReceived(const Network::StatusMessageEntry&); + void BanListReceived(const Network::Room::BanList&); + +private: + void LoadBanList(); + void PopulateBanList(const Network::Room::BanList& ban_list); + void SendUnbanRequest(const QString& subject); + void OnStatusMessageReceived(const Network::StatusMessageEntry& status_message); + + std::unique_ptr ui; + QStandardItemModel* model; + Network::RoomMember::CallbackHandle callback_handle_status_message; + Network::RoomMember::CallbackHandle callback_handle_ban_list; +}; + +Q_DECLARE_METATYPE(Network::Room::BanList); diff --git a/src/yuzu/multiplayer/moderation_dialog.ui b/src/yuzu/multiplayer/moderation_dialog.ui new file mode 100644 index 0000000000..808d994142 --- /dev/null +++ b/src/yuzu/multiplayer/moderation_dialog.ui @@ -0,0 +1,84 @@ + + + ModerationDialog + + + Moderation + + + + 0 + 0 + 500 + 300 + + + + + + + Ban List + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Refreshing + + + false + + + + + + + Unban + + + false + + + + + + + + + + + + + + + QDialogButtonBox::Ok + + + + + + + + buttonBox + accepted() + ModerationDialog + accept() + + + + diff --git a/src/yuzu/multiplayer/state.cpp b/src/yuzu/multiplayer/state.cpp new file mode 100644 index 0000000000..b2aaed9828 --- /dev/null +++ b/src/yuzu/multiplayer/state.cpp @@ -0,0 +1,299 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include "common/announce_multiplayer_room.h" +#include "common/logging/log.h" +#include "yuzu/game_list.h" +#include "yuzu/multiplayer/client_room.h" +#include "yuzu/multiplayer/direct_connect.h" +#include "yuzu/multiplayer/host_room.h" +#include "yuzu/multiplayer/lobby.h" +#include "yuzu/multiplayer/message.h" +#include "yuzu/multiplayer/state.h" +#include "yuzu/uisettings.h" +#include "yuzu/util/clickable_label.h" + +MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_list_model, + QAction* leave_room, QAction* show_room) + : QWidget(parent), game_list_model(game_list_model), leave_room(leave_room), + show_room(show_room) { + if (auto member = Network::GetRoomMember().lock()) { + // register the network structs to use in slots and signals + state_callback_handle = member->BindOnStateChanged( + [this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); }); + connect(this, &MultiplayerState::NetworkStateChanged, this, + &MultiplayerState::OnNetworkStateChanged); + error_callback_handle = member->BindOnError( + [this](const Network::RoomMember::Error& error) { emit NetworkError(error); }); + connect(this, &MultiplayerState::NetworkError, this, &MultiplayerState::OnNetworkError); + } + + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + announce_multiplayer_session = std::make_shared(); + announce_multiplayer_session->BindErrorCallback( + [this](const WebService::WebResult& result) { emit AnnounceFailed(result); }); + connect(this, &MultiplayerState::AnnounceFailed, this, &MultiplayerState::OnAnnounceFailed); + + status_text = new ClickableLabel(this); + status_icon = new ClickableLabel(this); + status_text->setToolTip(tr("Current connection status")); + status_text->setText(tr("Not Connected. Click here to find a room!")); + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16)); + + connect(status_text, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom); + connect(status_icon, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom); + + connect(static_cast(QApplication::instance()), &QApplication::focusChanged, this, + [this](QWidget* /*old*/, QWidget* now) { + if (client_room && client_room->isAncestorOf(now)) { + HideNotification(); + } + }); +} + +MultiplayerState::~MultiplayerState() { + if (state_callback_handle) { + if (auto member = Network::GetRoomMember().lock()) { + member->Unbind(state_callback_handle); + } + } + + if (error_callback_handle) { + if (auto member = Network::GetRoomMember().lock()) { + member->Unbind(error_callback_handle); + } + } +} + +void MultiplayerState::Close() { + if (host_room) + host_room->close(); + if (direct_connect) + direct_connect->close(); + if (client_room) + client_room->close(); + if (lobby) + lobby->close(); +} + +void MultiplayerState::retranslateUi() { + status_text->setToolTip(tr("Current connection status")); + + if (current_state == Network::RoomMember::State::Uninitialized) { + status_text->setText(tr("Not Connected. Click here to find a room!")); + } else if (current_state == Network::RoomMember::State::Joined || + current_state == Network::RoomMember::State::Moderator) { + + status_text->setText(tr("Connected")); + } else { + status_text->setText(tr("Not Connected")); + } + + if (lobby) + lobby->RetranslateUi(); + if (host_room) + host_room->RetranslateUi(); + if (client_room) + client_room->RetranslateUi(); + if (direct_connect) + direct_connect->RetranslateUi(); +} + +void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& state) { + LOG_DEBUG(Frontend, "Network State: {}", Network::GetStateStr(state)); + if (state == Network::RoomMember::State::Joined || + state == Network::RoomMember::State::Moderator) { + + OnOpenNetworkRoom(); + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected")).pixmap(16)); + status_text->setText(tr("Connected")); + leave_room->setEnabled(true); + show_room->setEnabled(true); + } else { + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16)); + status_text->setText(tr("Not Connected")); + leave_room->setEnabled(false); + show_room->setEnabled(false); + } + + current_state = state; +} + +void MultiplayerState::OnNetworkError(const Network::RoomMember::Error& error) { + LOG_DEBUG(Frontend, "Network Error: {}", Network::GetErrorStr(error)); + switch (error) { + case Network::RoomMember::Error::LostConnection: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::LOST_CONNECTION); + break; + case Network::RoomMember::Error::HostKicked: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::HOST_KICKED); + break; + case Network::RoomMember::Error::CouldNotConnect: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::UNABLE_TO_CONNECT); + break; + case Network::RoomMember::Error::NameCollision: + NetworkMessage::ErrorManager::ShowError( + NetworkMessage::ErrorManager::USERNAME_NOT_VALID_SERVER); + break; + case Network::RoomMember::Error::MacCollision: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::MAC_COLLISION); + break; + case Network::RoomMember::Error::ConsoleIdCollision: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::CONSOLE_ID_COLLISION); + break; + case Network::RoomMember::Error::RoomIsFull: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::ROOM_IS_FULL); + break; + case Network::RoomMember::Error::WrongPassword: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::WRONG_PASSWORD); + break; + case Network::RoomMember::Error::WrongVersion: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::WRONG_VERSION); + break; + case Network::RoomMember::Error::HostBanned: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::HOST_BANNED); + break; + case Network::RoomMember::Error::UnknownError: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::UNABLE_TO_CONNECT); + break; + case Network::RoomMember::Error::PermissionDenied: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::PERMISSION_DENIED); + break; + case Network::RoomMember::Error::NoSuchUser: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::NO_SUCH_USER); + break; + } +} + +void MultiplayerState::OnAnnounceFailed(const WebService::WebResult& result) { + announce_multiplayer_session->Stop(); + QMessageBox::warning(this, tr("Error"), + tr("Failed to update the room information. Please check your Internet " + "connection and try hosting the room again.\nDebug Message: ") + + QString::fromStdString(result.result_string), + QMessageBox::Ok); +} + +void MultiplayerState::UpdateThemedIcons() { + if (show_notification) { + status_icon->setPixmap( + QIcon::fromTheme(QStringLiteral("connected_notification")).pixmap(16)); + } else if (current_state == Network::RoomMember::State::Joined || + current_state == Network::RoomMember::State::Moderator) { + + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected")).pixmap(16)); + } else { + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16)); + } + if (client_room) + client_room->UpdateIconDisplay(); +} + +static void BringWidgetToFront(QWidget* widget) { + widget->show(); + widget->activateWindow(); + widget->raise(); +} + +void MultiplayerState::OnViewLobby() { + if (lobby == nullptr) { + lobby = new Lobby(this, game_list_model, announce_multiplayer_session); + } + BringWidgetToFront(lobby); +} + +void MultiplayerState::OnCreateRoom() { + if (host_room == nullptr) { + host_room = new HostRoomWindow(this, game_list_model, announce_multiplayer_session); + } + BringWidgetToFront(host_room); +} + +bool MultiplayerState::OnCloseRoom() { + if (!NetworkMessage::WarnCloseRoom()) + return false; + if (auto room = Network::GetRoom().lock()) { + // if you are in a room, leave it + if (auto member = Network::GetRoomMember().lock()) { + member->Leave(); + LOG_DEBUG(Frontend, "Left the room (as a client)"); + } + + // if you are hosting a room, also stop hosting + if (room->GetState() != Network::Room::State::Open) { + return true; + } + // Save ban list + UISettings::values.ban_list = std::move(room->GetBanList()); + + room->Destroy(); + announce_multiplayer_session->Stop(); + LOG_DEBUG(Frontend, "Closed the room (as a server)"); + } + return true; +} + +void MultiplayerState::ShowNotification() { + if (client_room && client_room->isAncestorOf(QApplication::focusWidget())) + return; // Do not show notification if the chat window currently has focus + show_notification = true; + QApplication::alert(nullptr); + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected_notification")).pixmap(16)); + status_text->setText(tr("New Messages Received")); +} + +void MultiplayerState::HideNotification() { + show_notification = false; + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected")).pixmap(16)); + status_text->setText(tr("Connected")); +} + +void MultiplayerState::OnOpenNetworkRoom() { + if (auto member = Network::GetRoomMember().lock()) { + if (member->IsConnected()) { + if (client_room == nullptr) { + client_room = new ClientRoomWindow(this); + connect(client_room, &ClientRoomWindow::ShowNotification, this, + &MultiplayerState::ShowNotification); + } + BringWidgetToFront(client_room); + return; + } + } + // If the user is not a member of a room, show the lobby instead. + // This is currently only used on the clickable label in the status bar + OnViewLobby(); +} + +void MultiplayerState::OnDirectConnectToRoom() { + if (direct_connect == nullptr) { + direct_connect = new DirectConnectWindow(this); + } + BringWidgetToFront(direct_connect); +} + +bool MultiplayerState::IsHostingPublicRoom() const { + return announce_multiplayer_session->IsRunning(); +} + +void MultiplayerState::UpdateCredentials() { + announce_multiplayer_session->UpdateCredentials(); +} + +void MultiplayerState::UpdateGameList(QStandardItemModel* game_list) { + game_list_model = game_list; + if (lobby) { + lobby->UpdateGameList(game_list); + } + if (host_room) { + host_room->UpdateGameList(game_list); + } +} diff --git a/src/yuzu/multiplayer/state.h b/src/yuzu/multiplayer/state.h new file mode 100644 index 0000000000..414454acb9 --- /dev/null +++ b/src/yuzu/multiplayer/state.h @@ -0,0 +1,92 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "core/announce_multiplayer_session.h" +#include "network/network.h" + +class QStandardItemModel; +class Lobby; +class HostRoomWindow; +class ClientRoomWindow; +class DirectConnectWindow; +class ClickableLabel; + +class MultiplayerState : public QWidget { + Q_OBJECT; + +public: + explicit MultiplayerState(QWidget* parent, QStandardItemModel* game_list, QAction* leave_room, + QAction* show_room); + ~MultiplayerState(); + + /** + * Close all open multiplayer related dialogs + */ + void Close(); + + ClickableLabel* GetStatusText() const { + return status_text; + } + + ClickableLabel* GetStatusIcon() const { + return status_icon; + } + + void retranslateUi(); + + /** + * Whether a public room is being hosted or not. + * When this is true, Web Services configuration should be disabled. + */ + bool IsHostingPublicRoom() const; + + void UpdateCredentials(); + + /** + * Updates the multiplayer dialogs with a new game list model. + * This model should be the original model of the game list. + */ + void UpdateGameList(QStandardItemModel* game_list); + +public slots: + void OnNetworkStateChanged(const Network::RoomMember::State& state); + void OnNetworkError(const Network::RoomMember::Error& error); + void OnViewLobby(); + void OnCreateRoom(); + bool OnCloseRoom(); + void OnOpenNetworkRoom(); + void OnDirectConnectToRoom(); + void OnAnnounceFailed(const WebService::WebResult&); + void UpdateThemedIcons(); + void ShowNotification(); + void HideNotification(); + +signals: + void NetworkStateChanged(const Network::RoomMember::State&); + void NetworkError(const Network::RoomMember::Error&); + void AnnounceFailed(const WebService::WebResult&); + +private: + Lobby* lobby = nullptr; + HostRoomWindow* host_room = nullptr; + ClientRoomWindow* client_room = nullptr; + DirectConnectWindow* direct_connect = nullptr; + ClickableLabel* status_icon = nullptr; + ClickableLabel* status_text = nullptr; + QStandardItemModel* game_list_model = nullptr; + QAction* leave_room; + QAction* show_room; + std::shared_ptr announce_multiplayer_session; + Network::RoomMember::State current_state = Network::RoomMember::State::Uninitialized; + bool has_mod_perms = false; + Network::RoomMember::CallbackHandle state_callback_handle; + Network::RoomMember::CallbackHandle error_callback_handle; + + bool show_notification = false; +}; + +Q_DECLARE_METATYPE(WebService::WebResult); diff --git a/src/yuzu/multiplayer/validation.h b/src/yuzu/multiplayer/validation.h new file mode 100644 index 0000000000..1c215a1909 --- /dev/null +++ b/src/yuzu/multiplayer/validation.h @@ -0,0 +1,49 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include + +class Validation { +public: + Validation() + : room_name(room_name_regex), nickname(nickname_regex), ip(ip_regex), port(0, 65535) {} + + ~Validation() = default; + + const QValidator* GetRoomName() const { + return &room_name; + } + const QValidator* GetNickname() const { + return &nickname; + } + const QValidator* GetIP() const { + return &ip; + } + const QValidator* GetPort() const { + return &port; + } + +private: + /// room name can be alphanumeric and " " "_" "." and "-" and must have a size of 4-20 + QRegExp room_name_regex = QRegExp(QStringLiteral("^[a-zA-Z0-9._- ]{4,20}$")); + QRegExpValidator room_name; + + /// nickname can be alphanumeric and " " "_" "." and "-" and must have a size of 4-20 + QRegExp nickname_regex = QRegExp(QStringLiteral("^[a-zA-Z0-9._- ]{4,20}$")); + QRegExpValidator nickname; + + /// ipv4 address only + // TODO remove this when we support hostnames in direct connect + QRegExp ip_regex = QRegExp(QStringLiteral( + "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|" + "2[0-4][0-9]|25[0-5])")); + QRegExpValidator ip; + + /// port must be between 0 and 65535 + QIntValidator port; +}; diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h index 2f6948243d..aca1a28e1d 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h @@ -102,6 +102,19 @@ struct Values { Settings::Setting callout_flags{0, "calloutFlags"}; + // multiplayer settings + QString nickname; + QString ip; + QString port; + QString room_nickname; + QString room_name; + quint32 max_player; + QString room_port; + uint host_type; + qulonglong game_id; + QString room_description; + std::pair, std::vector> ban_list; + // logging Settings::Setting show_console{false, "showConsole"}; diff --git a/src/yuzu/util/clickable_label.cpp b/src/yuzu/util/clickable_label.cpp new file mode 100644 index 0000000000..5bde838ca4 --- /dev/null +++ b/src/yuzu/util/clickable_label.cpp @@ -0,0 +1,12 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "yuzu/util/clickable_label.h" + +ClickableLabel::ClickableLabel(QWidget* parent, [[maybe_unused]] Qt::WindowFlags f) + : QLabel(parent) {} + +void ClickableLabel::mouseReleaseEvent([[maybe_unused]] QMouseEvent* event) { + emit clicked(); +} diff --git a/src/yuzu/util/clickable_label.h b/src/yuzu/util/clickable_label.h new file mode 100644 index 0000000000..3c65a74bec --- /dev/null +++ b/src/yuzu/util/clickable_label.h @@ -0,0 +1,22 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +class ClickableLabel : public QLabel { + Q_OBJECT + +public: + explicit ClickableLabel(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); + ~ClickableLabel() = default; + +signals: + void clicked(); + +protected: + void mouseReleaseEvent(QMouseEvent* event); +}; diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index cb301e78b1..0194940be4 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -29,6 +30,7 @@ #include "core/loader/loader.h" #include "core/telemetry_session.h" #include "input_common/main.h" +#include "network/network.h" #include "video_core/renderer_base.h" #include "yuzu_cmd/config.h" #include "yuzu_cmd/emu_window/emu_window_sdl2.h" @@ -60,6 +62,8 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; static void PrintHelp(const char* argv0) { std::cout << "Usage: " << argv0 << " [options] \n" + "-m, --multiplayer=nick:password@address:port" + " Nickname, password, address and port for multiplayer\n" "-f, --fullscreen Start in fullscreen mode\n" "-h, --help Display this help and exit\n" "-v, --version Output version information and exit\n" @@ -71,6 +75,107 @@ static void PrintVersion() { std::cout << "yuzu " << Common::g_scm_branch << " " << Common::g_scm_desc << std::endl; } +static void OnStateChanged(const Network::RoomMember::State& state) { + switch (state) { + case Network::RoomMember::State::Idle: + LOG_DEBUG(Network, "Network is idle"); + break; + case Network::RoomMember::State::Joining: + LOG_DEBUG(Network, "Connection sequence to room started"); + break; + case Network::RoomMember::State::Joined: + LOG_DEBUG(Network, "Successfully joined to the room"); + break; + case Network::RoomMember::State::Moderator: + LOG_DEBUG(Network, "Successfully joined the room as a moderator"); + break; + default: + break; + } +} + +static void OnNetworkError(const Network::RoomMember::Error& error) { + switch (error) { + case Network::RoomMember::Error::LostConnection: + LOG_DEBUG(Network, "Lost connection to the room"); + break; + case Network::RoomMember::Error::CouldNotConnect: + LOG_ERROR(Network, "Error: Could not connect"); + exit(1); + break; + case Network::RoomMember::Error::NameCollision: + LOG_ERROR( + Network, + "You tried to use the same nickname as another user that is connected to the Room"); + exit(1); + break; + case Network::RoomMember::Error::MacCollision: + LOG_ERROR(Network, "You tried to use the same MAC-Address as another user that is " + "connected to the Room"); + exit(1); + break; + case Network::RoomMember::Error::ConsoleIdCollision: + LOG_ERROR(Network, "Your Console ID conflicted with someone else in the Room"); + exit(1); + break; + case Network::RoomMember::Error::WrongPassword: + LOG_ERROR(Network, "Room replied with: Wrong password"); + exit(1); + break; + case Network::RoomMember::Error::WrongVersion: + LOG_ERROR(Network, + "You are using a different version than the room you are trying to connect to"); + exit(1); + break; + case Network::RoomMember::Error::RoomIsFull: + LOG_ERROR(Network, "The room is full"); + exit(1); + break; + case Network::RoomMember::Error::HostKicked: + LOG_ERROR(Network, "You have been kicked by the host"); + break; + case Network::RoomMember::Error::HostBanned: + LOG_ERROR(Network, "You have been banned by the host"); + break; + case Network::RoomMember::Error::UnknownError: + LOG_ERROR(Network, "UnknownError"); + break; + case Network::RoomMember::Error::PermissionDenied: + LOG_ERROR(Network, "PermissionDenied"); + break; + case Network::RoomMember::Error::NoSuchUser: + LOG_ERROR(Network, "NoSuchUser"); + break; + } +} + +static void OnMessageReceived(const Network::ChatEntry& msg) { + std::cout << std::endl << msg.nickname << ": " << msg.message << std::endl << std::endl; +} + +static void OnStatusMessageReceived(const Network::StatusMessageEntry& msg) { + std::string message; + switch (msg.type) { + case Network::IdMemberJoin: + message = fmt::format("{} has joined", msg.nickname); + break; + case Network::IdMemberLeave: + message = fmt::format("{} has left", msg.nickname); + break; + case Network::IdMemberKicked: + message = fmt::format("{} has been kicked", msg.nickname); + break; + case Network::IdMemberBanned: + message = fmt::format("{} has been banned", msg.nickname); + break; + case Network::IdAddressUnbanned: + message = fmt::format("{} has been unbanned", msg.nickname); + break; + } + if (!message.empty()) + std::cout << std::endl << "* " << message << std::endl << std::endl; +} + /// Application entry point int main(int argc, char** argv) { Common::Log::Initialize(); @@ -92,10 +197,16 @@ int main(int argc, char** argv) { std::optional config_path; std::string program_args; + bool use_multiplayer = false; bool fullscreen = false; + std::string nickname{}; + std::string password{}; + std::string address{}; + u16 port = Network::DefaultRoomPort; static struct option long_options[] = { // clang-format off + {"multiplayer", required_argument, 0, 'm'}, {"fullscreen", no_argument, 0, 'f'}, {"help", no_argument, 0, 'h'}, {"version", no_argument, 0, 'v'}, @@ -109,6 +220,38 @@ int main(int argc, char** argv) { int arg = getopt_long(argc, argv, "g:fhvp::c:", long_options, &option_index); if (arg != -1) { switch (static_cast(arg)) { + case 'm': { + use_multiplayer = true; + const std::string str_arg(optarg); + // regex to check if the format is nickname:password@ip:port + // with optional :password + const std::regex re("^([^:]+)(?::(.+))?@([^:]+)(?::([0-9]+))?$"); + if (!std::regex_match(str_arg, re)) { + std::cout << "Wrong format for option --multiplayer\n"; + PrintHelp(argv[0]); + return 0; + } + + std::smatch match; + std::regex_search(str_arg, match, re); + ASSERT(match.size() == 5); + nickname = match[1]; + password = match[2]; + address = match[3]; + if (!match[4].str().empty()) + port = std::stoi(match[4]); + std::regex nickname_re("^[a-zA-Z0-9._\\- ]+$"); + if (!std::regex_match(nickname, nickname_re)) { + std::cout + << "Nickname is not valid. Must be 4 to 20 alphanumeric characters.\n"; + return 0; + } + if (address.empty()) { + std::cout << "Address to room must not be empty.\n"; + return 0; + } + break; + } case 'f': fullscreen = true; LOG_INFO(Frontend, "Starting in fullscreen mode..."); @@ -215,6 +358,21 @@ int main(int argc, char** argv) { system.TelemetrySession().AddField(Common::Telemetry::FieldType::App, "Frontend", "SDL"); + if (use_multiplayer) { + if (auto member = Network::GetRoomMember().lock()) { + member->BindOnChatMessageRecieved(OnMessageReceived); + member->BindOnStatusMessageReceived(OnStatusMessageReceived); + member->BindOnStateChanged(OnStateChanged); + member->BindOnError(OnNetworkError); + LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", address, port, + nickname); + member->Join(nickname, "", address.c_str(), port, 0, Network::NoPreferredMac, password); + } else { + LOG_ERROR(Network, "Could not access RoomMember"); + return 0; + } + } + // Core is loaded, start the GPU (makes the GPU contexts current to this thread) system.GPU().Start(); system.GetCpuManager().OnGpuReady(); From 1b36542be23ad6c09864d862ad7b61d5db3185c6 Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Wed, 6 Jul 2022 04:19:18 +0200 Subject: [PATCH 38/50] yuzu: Hide multiplayer button and room status --- src/yuzu/main.cpp | 5 +++-- src/yuzu/main.ui | 14 -------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 5d81326739..f1cc910c04 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -863,8 +863,9 @@ void GMainWindow::InitializeWidgets() { statusBar()->addPermanentWidget(label); } - statusBar()->addPermanentWidget(multiplayer_state->GetStatusText(), 0); - statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon(), 0); + // TODO (flTobi): Add the widget when multiplayer is fully implemented + // statusBar()->addPermanentWidget(multiplayer_state->GetStatusText(), 0); + // statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon(), 0); tas_label = new QLabel(); tas_label->setObjectName(QStringLiteral("TASlabel")); diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 60a8deab1c..cdf31b417d 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -120,20 +120,6 @@ - - - true - - - Multiplayer - - - - - - - - &Tools From 7c3d241f0d03304df2c4d4449c2c8f1f9c7a16d3 Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Thu, 7 Jul 2022 04:12:12 +0200 Subject: [PATCH 39/50] common, core: fix -Wmissing-field-initializers --- src/common/announce_multiplayer_room.h | 4 ++-- src/core/announce_multiplayer_session.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h index 5ca5893ef0..8773ce4dbb 100644 --- a/src/common/announce_multiplayer_room.h +++ b/src/common/announce_multiplayer_room.h @@ -121,11 +121,11 @@ public: const u64 /*game_id*/, const std::string& /*game_name*/) override {} WebService::WebResult Update() override { return WebService::WebResult{WebService::WebResult::Code::NoWebservice, - "WebService is missing"}; + "WebService is missing", ""}; } WebService::WebResult Register() override { return WebService::WebResult{WebService::WebResult::Code::NoWebservice, - "WebService is missing"}; + "WebService is missing", ""}; } void ClearPlayers() override {} RoomList GetRoomList() override { diff --git a/src/core/announce_multiplayer_session.cpp b/src/core/announce_multiplayer_session.cpp index a680ad202e..aeca87aaca 100644 --- a/src/core/announce_multiplayer_session.cpp +++ b/src/core/announce_multiplayer_session.cpp @@ -34,10 +34,10 @@ WebService::WebResult AnnounceMultiplayerSession::Register() { std::shared_ptr room = Network::GetRoom().lock(); if (!room) { return WebService::WebResult{WebService::WebResult::Code::LibError, - "Network is not initialized"}; + "Network is not initialized", ""}; } if (room->GetState() != Network::Room::State::Open) { - return WebService::WebResult{WebService::WebResult::Code::LibError, "Room is not open"}; + return WebService::WebResult{WebService::WebResult::Code::LibError, "Room is not open", ""}; } UpdateBackendData(room); WebService::WebResult result = backend->Register(); @@ -47,7 +47,7 @@ WebService::WebResult AnnounceMultiplayerSession::Register() { LOG_INFO(WebService, "Room has been registered"); room->SetVerifyUID(result.returned_data); registered = true; - return WebService::WebResult{WebService::WebResult::Code::Success}; + return WebService::WebResult{WebService::WebResult::Code::Success, "", ""}; } void AnnounceMultiplayerSession::Start() { From 7fbd2916a17763b93a8401ba0d560000d56b48cf Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Thu, 7 Jul 2022 16:54:20 +0200 Subject: [PATCH 40/50] core: Fix -Wunused-variable --- src/core/core.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/core.cpp b/src/core/core.cpp index 5df32c1e7d..98fe6d39cc 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -317,7 +317,9 @@ struct System::Impl { perf_stats->BeginSystemFrame(); std::string name = "Unknown Game"; - const Loader::ResultStatus res{app_loader->ReadTitle(name)}; + if (app_loader->ReadTitle(name) != Loader::ResultStatus::Success) { + LOG_ERROR(Core, "Failed to read title for ROM (Error {})", load_result); + } if (auto room_member = Network::GetRoomMember().lock()) { Network::GameInfo game_info; game_info.name = name; From ee5cb9c7b9a5416ae30cdf1e7ecb492ae9b75fc9 Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Fri, 8 Jul 2022 04:23:16 +0200 Subject: [PATCH 41/50] web_service: Fix -Wmissing-field-initializers --- src/web_service/announce_room_json.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web_service/announce_room_json.cpp b/src/web_service/announce_room_json.cpp index 31804d2b1c..32249f28e9 100644 --- a/src/web_service/announce_room_json.cpp +++ b/src/web_service/announce_room_json.cpp @@ -112,7 +112,7 @@ WebService::WebResult RoomJson::Update() { if (room_id.empty()) { LOG_ERROR(WebService, "Room must be registered to be updated"); return WebService::WebResult{WebService::WebResult::Code::LibError, - "Room is not registered"}; + "Room is not registered", ""}; } nlohmann::json json{{"players", room.members}}; return client.PostJson(fmt::format("/lobby/{}", room_id), json.dump(), false); From ec407bd3f1988c6f5d147307c662703c7bc94751 Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Mon, 11 Jul 2022 20:40:57 +0200 Subject: [PATCH 42/50] Fix compilation on linux gcc --- src/network/packet.cpp | 18 +++++++++--------- src/network/room.cpp | 19 ++++++++++--------- src/web_service/announce_room_json.cpp | 8 ++++---- src/web_service/announce_room_json.h | 4 ++-- src/yuzu/multiplayer/lobby_p.h | 6 +++--- src/yuzu/multiplayer/state.cpp | 8 ++++---- 6 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/network/packet.cpp b/src/network/packet.cpp index 8fc5feabd4..a3c1e16440 100644 --- a/src/network/packet.cpp +++ b/src/network/packet.cpp @@ -14,13 +14,13 @@ namespace Network { #ifndef htonll -u64 htonll(u64 x) { +static u64 htonll(u64 x) { return ((1 == htonl(1)) ? (x) : ((uint64_t)htonl((x)&0xFFFFFFFF) << 32) | htonl((x) >> 32)); } #endif #ifndef ntohll -u64 ntohll(u64 x) { +static u64 ntohll(u64 x) { return ((1 == ntohl(1)) ? (x) : ((uint64_t)ntohl((x)&0xFFFFFFFF) << 32) | ntohl((x) >> 32)); } #endif @@ -67,7 +67,7 @@ Packet::operator bool() const { } Packet& Packet::operator>>(bool& out_data) { - u8 value; + u8 value{}; if (*this >> value) { out_data = (value != 0); } @@ -85,42 +85,42 @@ Packet& Packet::operator>>(u8& out_data) { } Packet& Packet::operator>>(s16& out_data) { - s16 value; + s16 value{}; Read(&value, sizeof(value)); out_data = ntohs(value); return *this; } Packet& Packet::operator>>(u16& out_data) { - u16 value; + u16 value{}; Read(&value, sizeof(value)); out_data = ntohs(value); return *this; } Packet& Packet::operator>>(s32& out_data) { - s32 value; + s32 value{}; Read(&value, sizeof(value)); out_data = ntohl(value); return *this; } Packet& Packet::operator>>(u32& out_data) { - u32 value; + u32 value{}; Read(&value, sizeof(value)); out_data = ntohl(value); return *this; } Packet& Packet::operator>>(s64& out_data) { - s64 value; + s64 value{}; Read(&value, sizeof(value)); out_data = ntohll(value); return *this; } Packet& Packet::operator>>(u64& out_data) { - u64 value; + u64 value{}; Read(&value, sizeof(value)); out_data = ntohll(value); return *this; diff --git a/src/network/room.cpp b/src/network/room.cpp index 5288671469..b82a757491 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -877,8 +877,8 @@ void Room::RoomImpl::HandleWifiPacket(const ENetEvent* event) { } else { // Send the data only to the destination client std::lock_guard lock(member_mutex); auto member = std::find_if(members.begin(), members.end(), - [destination_address](const Member& member) -> bool { - return member.mac_address == destination_address; + [destination_address](const Member& member_entry) -> bool { + return member_entry.mac_address == destination_address; }); if (member != members.end()) { enet_peer_send(member->peer, 0, enet_packet); @@ -955,10 +955,10 @@ void Room::RoomImpl::HandleGameNamePacket(const ENetEvent* event) { { std::lock_guard lock(member_mutex); - auto member = - std::find_if(members.begin(), members.end(), [event](const Member& member) -> bool { - return member.peer == event->peer; - }); + auto member = std::find_if(members.begin(), members.end(), + [event](const Member& member_entry) -> bool { + return member_entry.peer == event->peer; + }); if (member != members.end()) { member->game_info = game_info; @@ -982,9 +982,10 @@ void Room::RoomImpl::HandleClientDisconnection(ENetPeer* client) { std::string nickname, username, ip; { std::lock_guard lock(member_mutex); - auto member = std::find_if(members.begin(), members.end(), [client](const Member& member) { - return member.peer == client; - }); + auto member = + std::find_if(members.begin(), members.end(), [client](const Member& member_entry) { + return member_entry.peer == client; + }); if (member != members.end()) { nickname = member->nickname; username = member->user_data.username; diff --git a/src/web_service/announce_room_json.cpp b/src/web_service/announce_room_json.cpp index 32249f28e9..984a59f777 100644 --- a/src/web_service/announce_room_json.cpp +++ b/src/web_service/announce_room_json.cpp @@ -11,7 +11,7 @@ namespace AnnounceMultiplayerRoom { -void to_json(nlohmann::json& json, const Room::Member& member) { +static void to_json(nlohmann::json& json, const Room::Member& member) { if (!member.username.empty()) { json["username"] = member.username; } @@ -23,7 +23,7 @@ void to_json(nlohmann::json& json, const Room::Member& member) { json["gameId"] = member.game_id; } -void from_json(const nlohmann::json& json, Room::Member& member) { +static void from_json(const nlohmann::json& json, Room::Member& member) { member.nickname = json.at("nickname").get(); member.game_name = json.at("gameName").get(); member.game_id = json.at("gameId").get(); @@ -36,7 +36,7 @@ void from_json(const nlohmann::json& json, Room::Member& member) { } } -void to_json(nlohmann::json& json, const Room& room) { +static void to_json(nlohmann::json& json, const Room& room) { json["port"] = room.port; json["name"] = room.name; if (!room.description.empty()) { @@ -53,7 +53,7 @@ void to_json(nlohmann::json& json, const Room& room) { } } -void from_json(const nlohmann::json& json, Room& room) { +static void from_json(const nlohmann::json& json, Room& room) { room.verify_UID = json.at("externalGuid").get(); room.ip = json.at("address").get(); room.name = json.at("name").get(); diff --git a/src/web_service/announce_room_json.h b/src/web_service/announce_room_json.h index ac02af5b17..f65c93214a 100644 --- a/src/web_service/announce_room_json.h +++ b/src/web_service/announce_room_json.h @@ -17,8 +17,8 @@ namespace WebService { */ class RoomJson : public AnnounceMultiplayerRoom::Backend { public: - RoomJson(const std::string& host, const std::string& username, const std::string& token) - : client(host, username, token), host(host), username(username), token(token) {} + RoomJson(const std::string& host_, const std::string& username_, const std::string& token_) + : client(host_, username_, token_), host(host_), username(username_), token(token_) {} ~RoomJson() = default; void SetRoomInformation(const std::string& name, const std::string& description, const u16 port, const u32 max_player, const u32 net_version, const bool has_password, diff --git a/src/yuzu/multiplayer/lobby_p.h b/src/yuzu/multiplayer/lobby_p.h index 749ca627f2..afb8b99dc0 100644 --- a/src/yuzu/multiplayer/lobby_p.h +++ b/src/yuzu/multiplayer/lobby_p.h @@ -148,9 +148,9 @@ class LobbyMember { public: LobbyMember() = default; LobbyMember(const LobbyMember& other) = default; - explicit LobbyMember(QString username, QString nickname, u64 title_id, QString game_name) - : username(std::move(username)), nickname(std::move(nickname)), title_id(title_id), - game_name(std::move(game_name)) {} + explicit LobbyMember(QString username_, QString nickname_, u64 title_id_, QString game_name_) + : username(std::move(username_)), nickname(std::move(nickname_)), title_id(title_id_), + game_name(std::move(game_name_)) {} ~LobbyMember() = default; QString GetName() const { diff --git a/src/yuzu/multiplayer/state.cpp b/src/yuzu/multiplayer/state.cpp index b2aaed9828..e96b03e5df 100644 --- a/src/yuzu/multiplayer/state.cpp +++ b/src/yuzu/multiplayer/state.cpp @@ -19,10 +19,10 @@ #include "yuzu/uisettings.h" #include "yuzu/util/clickable_label.h" -MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_list_model, - QAction* leave_room, QAction* show_room) - : QWidget(parent), game_list_model(game_list_model), leave_room(leave_room), - show_room(show_room) { +MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_list_model_, + QAction* leave_room_, QAction* show_room_) + : QWidget(parent), game_list_model(game_list_model_), leave_room(leave_room_), + show_room(show_room_) { if (auto member = Network::GetRoomMember().lock()) { // register the network structs to use in slots and signals state_callback_handle = member->BindOnStateChanged( From 6c8e45618578de1406c0bf4a7f976bd4903c339c Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Fri, 15 Jul 2022 19:45:35 +0200 Subject: [PATCH 43/50] Address first part of review comments --- .gitmodules | 3 + externals/CMakeLists.txt | 5 + src/core/CMakeLists.txt | 10 +- src/input_common/helpers/udp_protocol.h | 2 +- src/network/room.h | 2 +- src/web_service/CMakeLists.txt | 4 +- src/web_service/verify_user_jwt.cpp | 60 ++++++++++ src/web_service/verify_user_jwt.h | 25 ++++ src/yuzu/CMakeLists.txt | 4 + src/yuzu/configuration/config.cpp | 148 ++++++++++++------------ src/yuzu/multiplayer/chat_room.cpp | 2 +- src/yuzu/multiplayer/direct_connect.cpp | 20 ++-- src/yuzu/multiplayer/host_room.cpp | 45 +++---- src/yuzu/multiplayer/lobby.cpp | 18 +-- src/yuzu/multiplayer/state.cpp | 2 +- src/yuzu/uisettings.h | 22 ++-- 16 files changed, 239 insertions(+), 133 deletions(-) create mode 100644 src/web_service/verify_user_jwt.cpp create mode 100644 src/web_service/verify_user_jwt.h diff --git a/.gitmodules b/.gitmodules index d8e04923ad..26c6735cd9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -46,3 +46,6 @@ [submodule "vcpkg"] path = externals/vcpkg url = https://github.com/Microsoft/vcpkg.git +[submodule "cpp-jwt"] + path = externals/cpp-jwt + url = https://github.com/arun11299/cpp-jwt.git diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index b4570bb697..5b74a17731 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -116,6 +116,11 @@ if (ENABLE_WEB_SERVICE) if (WIN32) target_link_libraries(httplib INTERFACE crypt32 cryptui ws2_32) endif() + + # cpp-jwt + add_library(cpp-jwt INTERFACE) + target_include_directories(cpp-jwt INTERFACE ./cpp-jwt/include) + target_compile_definitions(cpp-jwt INTERFACE CPP_JWT_USE_VENDORED_NLOHMANN_JSON) endif() # Opus diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 48f5c1ee0a..c1cc62a456 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -716,6 +716,11 @@ add_library(core STATIC hle/service/vi/vi_u.h hle/service/wlan/wlan.cpp hle/service/wlan/wlan.h + internal_network/network.cpp + internal_network/network.h + internal_network/network_interface.cpp + internal_network/network_interface.h + internal_network/sockets.h loader/deconstructed_rom_directory.cpp loader/deconstructed_rom_directory.h loader/elf.cpp @@ -743,11 +748,6 @@ add_library(core STATIC memory/dmnt_cheat_vm.h memory.cpp memory.h - internal_network/network.cpp - internal_network/network.h - internal_network/network_interface.cpp - internal_network/network_interface.h - internal_network/sockets.h perf_stats.cpp perf_stats.h reporter.cpp diff --git a/src/input_common/helpers/udp_protocol.h b/src/input_common/helpers/udp_protocol.h index 597f51cd37..889693e731 100644 --- a/src/input_common/helpers/udp_protocol.h +++ b/src/input_common/helpers/udp_protocol.h @@ -85,7 +85,7 @@ enum RegisterFlags : u8 { struct Version {}; /** * Requests the server to send information about what controllers are plugged into the ports - * In citra's case, we only have one controller, so for simplicity's sake, we can just send a + * In yuzu's case, we only have one controller, so for simplicity's sake, we can just send a * request explicitly for the first controller port and leave it at that. In the future it would be * nice to make this configurable */ diff --git a/src/network/room.h b/src/network/room.h index 5d4371c166..df2253bf81 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -13,7 +13,7 @@ namespace Network { -constexpr u32 network_version = 4; ///< The version of this Room and RoomMember +constexpr u32 network_version = 1; ///< The version of this Room and RoomMember constexpr u16 DefaultRoomPort = 24872; diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt index aff81f26d8..753fb6e7ac 100644 --- a/src/web_service/CMakeLists.txt +++ b/src/web_service/CMakeLists.txt @@ -5,10 +5,12 @@ add_library(web_service STATIC telemetry_json.h verify_login.cpp verify_login.h + verify_user_jwt.cpp + verify_user_jwt.h web_backend.cpp web_backend.h web_result.h ) create_target_directory_groups(web_service) -target_link_libraries(web_service PRIVATE common network nlohmann_json::nlohmann_json httplib) +target_link_libraries(web_service PRIVATE common network nlohmann_json::nlohmann_json httplib cpp-jwt) diff --git a/src/web_service/verify_user_jwt.cpp b/src/web_service/verify_user_jwt.cpp new file mode 100644 index 0000000000..78fe7bed5a --- /dev/null +++ b/src/web_service/verify_user_jwt.cpp @@ -0,0 +1,60 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "common/logging/log.h" +#include "web_service/verify_user_jwt.h" +#include "web_service/web_backend.h" +#include "web_service/web_result.h" + +namespace WebService { + +static std::string public_key; +std::string GetPublicKey(const std::string& host) { + if (public_key.empty()) { + Client client(host, "", ""); // no need for credentials here + public_key = client.GetPlain("/jwt/external/key.pem", true).returned_data; + if (public_key.empty()) { + LOG_ERROR(WebService, "Could not fetch external JWT public key, verification may fail"); + } else { + LOG_INFO(WebService, "Fetched external JWT public key (size={})", public_key.size()); + } + } + return public_key; +} + +VerifyUserJWT::VerifyUserJWT(const std::string& host) : pub_key(GetPublicKey(host)) {} + +Network::VerifyUser::UserData VerifyUserJWT::LoadUserData(const std::string& verify_UID, + const std::string& token) { + const std::string audience = fmt::format("external-{}", verify_UID); + using namespace jwt::params; + std::error_code error; + auto decoded = + jwt::decode(token, algorithms({"rs256"}), error, secret(pub_key), issuer("yuzu-core"), + aud(audience), validate_iat(true), validate_jti(true)); + if (error) { + LOG_INFO(WebService, "Verification failed: category={}, code={}, message={}", + error.category().name(), error.value(), error.message()); + return {}; + } + Network::VerifyUser::UserData user_data{}; + if (decoded.payload().has_claim("username")) { + user_data.username = decoded.payload().get_claim_value("username"); + } + if (decoded.payload().has_claim("displayName")) { + user_data.display_name = decoded.payload().get_claim_value("displayName"); + } + if (decoded.payload().has_claim("avatarUrl")) { + user_data.avatar_url = decoded.payload().get_claim_value("avatarUrl"); + } + if (decoded.payload().has_claim("roles")) { + auto roles = decoded.payload().get_claim_value>("roles"); + user_data.moderator = std::find(roles.begin(), roles.end(), "moderator") != roles.end(); + } + return user_data; +} + +} // namespace WebService diff --git a/src/web_service/verify_user_jwt.h b/src/web_service/verify_user_jwt.h new file mode 100644 index 0000000000..826e01eed7 --- /dev/null +++ b/src/web_service/verify_user_jwt.h @@ -0,0 +1,25 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "network/verify_user.h" +#include "web_service/web_backend.h" + +namespace WebService { + +class VerifyUserJWT final : public Network::VerifyUser::Backend { +public: + VerifyUserJWT(const std::string& host); + ~VerifyUserJWT() = default; + + Network::VerifyUser::UserData LoadUserData(const std::string& verify_UID, + const std::string& token) override; + +private: + std::string pub_key; +}; + +} // namespace WebService diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index f3cad8f311..66873143e3 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -326,6 +326,10 @@ if (USE_DISCORD_PRESENCE) target_compile_definitions(yuzu PRIVATE -DUSE_DISCORD_PRESENCE) endif() +if (ENABLE_WEB_SERVICE) + target_compile_definitions(yuzu PRIVATE -DENABLE_WEB_SERVICE) +endif() + if (YUZU_USE_QT_WEB_ENGINE) target_link_libraries(yuzu PRIVATE Qt::WebEngineCore Qt::WebEngineWidgets) target_compile_definitions(yuzu PRIVATE -DYUZU_USE_QT_WEB_ENGINE) diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 7c11e2c034..3b22102a80 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -585,48 +585,6 @@ void Config::ReadMiscellaneousValues() { qt_config->endGroup(); } -void Config::ReadMultiplayerValues() { - qt_config->beginGroup(QStringLiteral("Multiplayer")); - - UISettings::values.nickname = ReadSetting(QStringLiteral("nickname"), QString{}).toString(); - UISettings::values.ip = ReadSetting(QStringLiteral("ip"), QString{}).toString(); - UISettings::values.port = - ReadSetting(QStringLiteral("port"), Network::DefaultRoomPort).toString(); - UISettings::values.room_nickname = - ReadSetting(QStringLiteral("room_nickname"), QString{}).toString(); - UISettings::values.room_name = ReadSetting(QStringLiteral("room_name"), QString{}).toString(); - UISettings::values.room_port = - ReadSetting(QStringLiteral("room_port"), QStringLiteral("24872")).toString(); - bool ok; - UISettings::values.host_type = ReadSetting(QStringLiteral("host_type"), 0).toUInt(&ok); - if (!ok) { - UISettings::values.host_type = 0; - } - UISettings::values.max_player = ReadSetting(QStringLiteral("max_player"), 8).toUInt(); - UISettings::values.game_id = ReadSetting(QStringLiteral("game_id"), 0).toULongLong(); - UISettings::values.room_description = - ReadSetting(QStringLiteral("room_description"), QString{}).toString(); - // Read ban list back - int size = qt_config->beginReadArray(QStringLiteral("username_ban_list")); - UISettings::values.ban_list.first.resize(size); - for (int i = 0; i < size; ++i) { - qt_config->setArrayIndex(i); - UISettings::values.ban_list.first[i] = - ReadSetting(QStringLiteral("username")).toString().toStdString(); - } - qt_config->endArray(); - size = qt_config->beginReadArray(QStringLiteral("ip_ban_list")); - UISettings::values.ban_list.second.resize(size); - for (int i = 0; i < size; ++i) { - qt_config->setArrayIndex(i); - UISettings::values.ban_list.second[i] = - ReadSetting(QStringLiteral("ip")).toString().toStdString(); - } - qt_config->endArray(); - - qt_config->endGroup(); -} - void Config::ReadPathValues() { qt_config->beginGroup(QStringLiteral("Paths")); @@ -904,6 +862,42 @@ void Config::ReadWebServiceValues() { qt_config->endGroup(); } +void Config::ReadMultiplayerValues() { + qt_config->beginGroup(QStringLiteral("Multiplayer")); + + ReadBasicSetting(UISettings::values.multiplayer_nickname); + ReadBasicSetting(UISettings::values.multiplayer_ip); + ReadBasicSetting(UISettings::values.multiplayer_port); + ReadBasicSetting(UISettings::values.multiplayer_room_nickname); + ReadBasicSetting(UISettings::values.multiplayer_room_name); + ReadBasicSetting(UISettings::values.multiplayer_room_port); + ReadBasicSetting(UISettings::values.multiplayer_host_type); + ReadBasicSetting(UISettings::values.multiplayer_port); + ReadBasicSetting(UISettings::values.multiplayer_max_player); + ReadBasicSetting(UISettings::values.multiplayer_game_id); + ReadBasicSetting(UISettings::values.multiplayer_room_description); + + // Read ban list back + int size = qt_config->beginReadArray(QStringLiteral("username_ban_list")); + UISettings::values.multiplayer_ban_list.first.resize(size); + for (int i = 0; i < size; ++i) { + qt_config->setArrayIndex(i); + UISettings::values.multiplayer_ban_list.first[i] = + ReadSetting(QStringLiteral("username")).toString().toStdString(); + } + qt_config->endArray(); + size = qt_config->beginReadArray(QStringLiteral("ip_ban_list")); + UISettings::values.multiplayer_ban_list.second.resize(size); + for (int i = 0; i < size; ++i) { + qt_config->setArrayIndex(i); + UISettings::values.multiplayer_ban_list.second[i] = + ReadSetting(QStringLiteral("ip")).toString().toStdString(); + } + qt_config->endArray(); + + qt_config->endGroup(); +} + void Config::ReadValues() { if (global) { ReadControlValues(); @@ -920,6 +914,7 @@ void Config::ReadValues() { ReadRendererValues(); ReadAudioValues(); ReadSystemValues(); + ReadMultiplayerValues(); } void Config::SavePlayerValue(std::size_t player_index) { @@ -1069,6 +1064,7 @@ void Config::SaveValues() { SaveRendererValues(); SaveAudioValues(); SaveSystemValues(); + SaveMultiplayerValues(); } void Config::SaveAudioValues() { @@ -1205,40 +1201,6 @@ void Config::SaveMiscellaneousValues() { qt_config->endGroup(); } -void Config::SaveMultiplayerValues() { - qt_config->beginGroup(QStringLiteral("Multiplayer")); - - WriteSetting(QStringLiteral("nickname"), UISettings::values.nickname, QString{}); - WriteSetting(QStringLiteral("ip"), UISettings::values.ip, QString{}); - WriteSetting(QStringLiteral("port"), UISettings::values.port, Network::DefaultRoomPort); - WriteSetting(QStringLiteral("room_nickname"), UISettings::values.room_nickname, QString{}); - WriteSetting(QStringLiteral("room_name"), UISettings::values.room_name, QString{}); - WriteSetting(QStringLiteral("room_port"), UISettings::values.room_port, - QStringLiteral("24872")); - WriteSetting(QStringLiteral("host_type"), UISettings::values.host_type, 0); - WriteSetting(QStringLiteral("max_player"), UISettings::values.max_player, 8); - WriteSetting(QStringLiteral("game_id"), UISettings::values.game_id, 0); - WriteSetting(QStringLiteral("room_description"), UISettings::values.room_description, - QString{}); - // Write ban list - qt_config->beginWriteArray(QStringLiteral("username_ban_list")); - for (std::size_t i = 0; i < UISettings::values.ban_list.first.size(); ++i) { - qt_config->setArrayIndex(static_cast(i)); - WriteSetting(QStringLiteral("username"), - QString::fromStdString(UISettings::values.ban_list.first[i])); - } - qt_config->endArray(); - qt_config->beginWriteArray(QStringLiteral("ip_ban_list")); - for (std::size_t i = 0; i < UISettings::values.ban_list.second.size(); ++i) { - qt_config->setArrayIndex(static_cast(i)); - WriteSetting(QStringLiteral("ip"), - QString::fromStdString(UISettings::values.ban_list.second[i])); - } - qt_config->endArray(); - - qt_config->endGroup(); -} - void Config::SavePathValues() { qt_config->beginGroup(QStringLiteral("Paths")); @@ -1490,6 +1452,40 @@ void Config::SaveWebServiceValues() { qt_config->endGroup(); } +void Config::SaveMultiplayerValues() { + qt_config->beginGroup(QStringLiteral("Multiplayer")); + + WriteBasicSetting(UISettings::values.multiplayer_nickname); + WriteBasicSetting(UISettings::values.multiplayer_ip); + WriteBasicSetting(UISettings::values.multiplayer_port); + WriteBasicSetting(UISettings::values.multiplayer_room_nickname); + WriteBasicSetting(UISettings::values.multiplayer_room_name); + WriteBasicSetting(UISettings::values.multiplayer_room_port); + WriteBasicSetting(UISettings::values.multiplayer_host_type); + WriteBasicSetting(UISettings::values.multiplayer_port); + WriteBasicSetting(UISettings::values.multiplayer_max_player); + WriteBasicSetting(UISettings::values.multiplayer_game_id); + WriteBasicSetting(UISettings::values.multiplayer_room_description); + + // Write ban list + qt_config->beginWriteArray(QStringLiteral("username_ban_list")); + for (std::size_t i = 0; i < UISettings::values.multiplayer_ban_list.first.size(); ++i) { + qt_config->setArrayIndex(static_cast(i)); + WriteSetting(QStringLiteral("username"), + QString::fromStdString(UISettings::values.multiplayer_ban_list.first[i])); + } + qt_config->endArray(); + qt_config->beginWriteArray(QStringLiteral("ip_ban_list")); + for (std::size_t i = 0; i < UISettings::values.multiplayer_ban_list.second.size(); ++i) { + qt_config->setArrayIndex(static_cast(i)); + WriteSetting(QStringLiteral("ip"), + QString::fromStdString(UISettings::values.multiplayer_ban_list.second[i])); + } + qt_config->endArray(); + + qt_config->endGroup(); +} + QVariant Config::ReadSetting(const QString& name) const { return qt_config->value(name); } diff --git a/src/yuzu/multiplayer/chat_room.cpp b/src/yuzu/multiplayer/chat_room.cpp index 6dd4bc36d6..7048ee1fce 100644 --- a/src/yuzu/multiplayer/chat_room.cpp +++ b/src/yuzu/multiplayer/chat_room.cpp @@ -390,7 +390,7 @@ void ChatRoom::SetPlayerList(const Network::RoomMember::MemberList& member_list) return; QPixmap pixmap; if (!pixmap.loadFromData(reinterpret_cast(result.data()), - result.size())) + static_cast(result.size()))) return; icon_cache[avatar_url] = pixmap.scaled(48, 48, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); diff --git a/src/yuzu/multiplayer/direct_connect.cpp b/src/yuzu/multiplayer/direct_connect.cpp index 27741d657f..837baf85cc 100644 --- a/src/yuzu/multiplayer/direct_connect.cpp +++ b/src/yuzu/multiplayer/direct_connect.cpp @@ -32,15 +32,15 @@ DirectConnectWindow::DirectConnectWindow(QWidget* parent) connect(watcher, &QFutureWatcher::finished, this, &DirectConnectWindow::OnConnection); ui->nickname->setValidator(validation.GetNickname()); - ui->nickname->setText(UISettings::values.nickname); + ui->nickname->setText(UISettings::values.multiplayer_nickname.GetValue()); if (ui->nickname->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) { // Use yuzu Web Service user name as nickname by default ui->nickname->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue())); } ui->ip->setValidator(validation.GetIP()); - ui->ip->setText(UISettings::values.ip); + ui->ip->setText(UISettings::values.multiplayer_ip.GetValue()); ui->port->setValidator(validation.GetPort()); - ui->port->setText(UISettings::values.port); + ui->port->setText(QString::number(UISettings::values.multiplayer_port.GetValue())); // TODO(jroweboy): Show or hide the connection options based on the current value of the combo // box. Add this back in when the traversal server support is added. @@ -86,16 +86,18 @@ void DirectConnectWindow::Connect() { } // Store settings - UISettings::values.nickname = ui->nickname->text(); - UISettings::values.ip = ui->ip->text(); - UISettings::values.port = (ui->port->isModified() && !ui->port->text().isEmpty()) - ? ui->port->text() - : UISettings::values.port; + UISettings::values.multiplayer_nickname = ui->nickname->text(); + UISettings::values.multiplayer_ip = ui->ip->text(); + if (ui->port->isModified() && !ui->port->text().isEmpty()) { + UISettings::values.multiplayer_port = ui->port->text().toInt(); + } else { + UISettings::values.multiplayer_port = UISettings::values.multiplayer_port.GetDefault(); + } // attempt to connect in a different thread QFuture f = QtConcurrent::run([&] { if (auto room_member = Network::GetRoomMember().lock()) { - auto port = UISettings::values.port.toUInt(); + auto port = UISettings::values.multiplayer_port.GetValue(); room_member->Join(ui->nickname->text().toStdString(), "", ui->ip->text().toStdString().c_str(), port, 0, Network::NoPreferredMac, ui->password->text().toStdString().c_str()); diff --git a/src/yuzu/multiplayer/host_room.cpp b/src/yuzu/multiplayer/host_room.cpp index 1b73e2bec5..5470b8b86e 100644 --- a/src/yuzu/multiplayer/host_room.cpp +++ b/src/yuzu/multiplayer/host_room.cpp @@ -51,23 +51,24 @@ HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, connect(ui->host, &QPushButton::clicked, this, &HostRoomWindow::Host); // Restore the settings: - ui->username->setText(UISettings::values.room_nickname); + ui->username->setText(UISettings::values.multiplayer_room_nickname.GetValue()); if (ui->username->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) { // Use yuzu Web Service user name as nickname by default ui->username->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue())); } - ui->room_name->setText(UISettings::values.room_name); - ui->port->setText(UISettings::values.room_port); - ui->max_player->setValue(UISettings::values.max_player); - int index = UISettings::values.host_type; + ui->room_name->setText(UISettings::values.multiplayer_room_name.GetValue()); + ui->port->setText(QString::number(UISettings::values.multiplayer_room_port.GetValue())); + ui->max_player->setValue(UISettings::values.multiplayer_max_player.GetValue()); + int index = UISettings::values.multiplayer_host_type.GetValue(); if (index < ui->host_type->count()) { ui->host_type->setCurrentIndex(index); } - index = ui->game_list->findData(UISettings::values.game_id, GameListItemPath::ProgramIdRole); + index = ui->game_list->findData(UISettings::values.multiplayer_game_id.GetValue(), + GameListItemPath::ProgramIdRole); if (index != -1) { ui->game_list->setCurrentIndex(index); } - ui->room_description->setText(UISettings::values.room_description); + ui->room_description->setText(UISettings::values.multiplayer_room_description.GetValue()); } HostRoomWindow::~HostRoomWindow() = default; @@ -91,7 +92,8 @@ std::unique_ptr HostRoomWindow::CreateVerifyBacken std::unique_ptr verify_backend; if (use_validation) { #ifdef ENABLE_WEB_SERVICE - verify_backend = std::make_unique(Settings::values.web_api_url); + verify_backend = + std::make_unique(Settings::values.web_api_url.GetValue()); #else verify_backend = std::make_unique(); #endif @@ -137,7 +139,7 @@ void HostRoomWindow::Host() { const bool is_public = ui->host_type->currentIndex() == 0; Network::Room::BanList ban_list{}; if (ui->load_ban_list->isChecked()) { - ban_list = UISettings::values.ban_list; + ban_list = UISettings::values.multiplayer_ban_list; } if (auto room = Network::GetRoom().lock()) { bool created = room->Create( @@ -181,8 +183,9 @@ void HostRoomWindow::Host() { std::string token; #ifdef ENABLE_WEB_SERVICE if (is_public) { - WebService::Client client(Settings::values.web_api_url, Settings::values.yuzu_username, - Settings::values.yuzu_token); + WebService::Client client(Settings::values.web_api_url.GetValue(), + Settings::values.yuzu_username.GetValue(), + Settings::values.yuzu_token.GetValue()); if (auto room = Network::GetRoom().lock()) { token = client.GetExternalJWT(room->GetVerifyUID()).returned_data; } @@ -198,17 +201,19 @@ void HostRoomWindow::Host() { Network::NoPreferredMac, password, token); // Store settings - UISettings::values.room_nickname = ui->username->text(); - UISettings::values.room_name = ui->room_name->text(); - UISettings::values.game_id = + UISettings::values.multiplayer_room_nickname = ui->username->text(); + UISettings::values.multiplayer_room_name = ui->room_name->text(); + UISettings::values.multiplayer_game_id = ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong(); - UISettings::values.max_player = ui->max_player->value(); + UISettings::values.multiplayer_max_player = ui->max_player->value(); - UISettings::values.host_type = ui->host_type->currentIndex(); - UISettings::values.room_port = (ui->port->isModified() && !ui->port->text().isEmpty()) - ? ui->port->text() - : QString::number(Network::DefaultRoomPort); - UISettings::values.room_description = ui->room_description->toPlainText(); + UISettings::values.multiplayer_host_type = ui->host_type->currentIndex(); + if (ui->port->isModified() && !ui->port->text().isEmpty()) { + UISettings::values.multiplayer_room_port = ui->port->text().toInt(); + } else { + UISettings::values.multiplayer_room_port = Network::DefaultRoomPort; + } + UISettings::values.multiplayer_room_description = ui->room_description->toPlainText(); ui->host->setEnabled(true); close(); } diff --git a/src/yuzu/multiplayer/lobby.cpp b/src/yuzu/multiplayer/lobby.cpp index fcaa7b517f..364b4605e6 100644 --- a/src/yuzu/multiplayer/lobby.cpp +++ b/src/yuzu/multiplayer/lobby.cpp @@ -56,7 +56,7 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, ui->room_list->setContextMenuPolicy(Qt::CustomContextMenu); ui->nickname->setValidator(validation.GetNickname()); - ui->nickname->setText(UISettings::values.nickname); + ui->nickname->setText(UISettings::values.multiplayer_nickname.GetValue()); if (ui->nickname->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) { // Use yuzu Web Service user name as nickname by default ui->nickname->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue())); @@ -154,9 +154,11 @@ void Lobby::OnJoinRoom(const QModelIndex& source) { QFuture f = QtConcurrent::run([nickname, ip, port, password, verify_UID] { std::string token; #ifdef ENABLE_WEB_SERVICE - if (!Settings::values.yuzu_username.empty() && !Settings::values.yuzu_token.empty()) { - WebService::Client client(Settings::values.web_api_url, Settings::values.yuzu_username, - Settings::values.yuzu_token); + if (!Settings::values.yuzu_username.GetValue().empty() && + !Settings::values.yuzu_token.GetValue().empty()) { + WebService::Client client(Settings::values.web_api_url.GetValue(), + Settings::values.yuzu_username.GetValue(), + Settings::values.yuzu_token.GetValue()); token = client.GetExternalJWT(verify_UID).returned_data; if (token.empty()) { LOG_ERROR(WebService, "Could not get external JWT, verification may fail"); @@ -175,9 +177,11 @@ void Lobby::OnJoinRoom(const QModelIndex& source) { // TODO(jroweboy): disable widgets and display a connecting while we wait // Save settings - UISettings::values.nickname = ui->nickname->text(); - UISettings::values.ip = proxy->data(connection_index, LobbyItemHost::HostIPRole).toString(); - UISettings::values.port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toString(); + UISettings::values.multiplayer_nickname = ui->nickname->text(); + UISettings::values.multiplayer_ip = + proxy->data(connection_index, LobbyItemHost::HostIPRole).toString(); + UISettings::values.multiplayer_port = + proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt(); } void Lobby::ResetModel() { diff --git a/src/yuzu/multiplayer/state.cpp b/src/yuzu/multiplayer/state.cpp index e96b03e5df..ee138739b2 100644 --- a/src/yuzu/multiplayer/state.cpp +++ b/src/yuzu/multiplayer/state.cpp @@ -232,7 +232,7 @@ bool MultiplayerState::OnCloseRoom() { return true; } // Save ban list - UISettings::values.ban_list = std::move(room->GetBanList()); + UISettings::values.multiplayer_ban_list = std::move(room->GetBanList()); room->Destroy(); announce_multiplayer_session->Stop(); diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h index aca1a28e1d..83a6cffa3e 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h @@ -103,17 +103,17 @@ struct Values { Settings::Setting callout_flags{0, "calloutFlags"}; // multiplayer settings - QString nickname; - QString ip; - QString port; - QString room_nickname; - QString room_name; - quint32 max_player; - QString room_port; - uint host_type; - qulonglong game_id; - QString room_description; - std::pair, std::vector> ban_list; + Settings::Setting multiplayer_nickname{QStringLiteral("yuzu"), "nickname"}; + Settings::Setting multiplayer_ip{{}, "ip"}; + Settings::SwitchableSetting multiplayer_port{24872, 0, 65535, "port"}; + Settings::Setting multiplayer_room_nickname{{}, "room_nickname"}; + Settings::Setting multiplayer_room_name{{}, "room_name"}; + Settings::SwitchableSetting multiplayer_max_player{8, 0, 8, "max_player"}; + Settings::SwitchableSetting multiplayer_room_port{24872, 0, 65535, "room_port"}; + Settings::SwitchableSetting multiplayer_host_type{0, 0, 1, "host_type"}; + Settings::Setting multiplayer_game_id{{}, "game_id"}; + Settings::Setting multiplayer_room_description{{}, "room_description"}; + std::pair, std::vector> multiplayer_ban_list; // logging Settings::Setting show_console{false, "showConsole"}; From 4b404191cf054ec3206676f1fccc452bc0a0e223 Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Fri, 15 Jul 2022 21:11:09 +0200 Subject: [PATCH 44/50] Address second part of review comments --- src/common/announce_multiplayer_room.h | 51 +++++++++++-------- src/core/announce_multiplayer_session.cpp | 5 +- src/network/room.cpp | 7 +-- src/network/room.h | 32 +++--------- src/network/room_member.h | 4 ++ src/web_service/announce_room_json.cpp | 62 ++++++++++------------- src/web_service/announce_room_json.h | 5 +- src/web_service/verify_user_jwt.cpp | 10 +++- src/yuzu/multiplayer/lobby.cpp | 19 +++---- 9 files changed, 92 insertions(+), 103 deletions(-) diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h index 8773ce4dbb..2ff38b6cfc 100644 --- a/src/common/announce_multiplayer_room.h +++ b/src/common/announce_multiplayer_room.h @@ -15,27 +15,40 @@ namespace AnnounceMultiplayerRoom { using MacAddress = std::array; +struct Member { + std::string username; + std::string nickname; + std::string display_name; + std::string avatar_url; + MacAddress mac_address; + std::string game_name; + u64 game_id; +}; + +struct RoomInformation { + std::string name; ///< Name of the server + std::string description; ///< Server description + u32 member_slots; ///< Maximum number of members in this room + u16 port; ///< The port of this room + std::string preferred_game; ///< Game to advertise that you want to play + u64 preferred_game_id; ///< Title ID for the advertised game + std::string host_username; ///< Forum username of the host + bool enable_yuzu_mods; ///< Allow yuzu Moderators to moderate on this room +}; + +struct GameInfo { + std::string name{""}; + u64 id{0}; +}; + struct Room { - struct Member { - std::string username; - std::string nickname; - std::string avatar_url; - MacAddress mac_address; - std::string game_name; - u64 game_id; - }; + RoomInformation information; + std::string id; std::string verify_UID; ///< UID used for verification - std::string name; - std::string description; - std::string owner; std::string ip; - u16 port; - u32 max_player; u32 net_version; bool has_password; - std::string preferred_game; - u64 preferred_game_id; std::vector members; }; @@ -71,9 +84,7 @@ public: * @param game_id The title id of the game the player plays * @param game_name The name of the game the player plays */ - virtual void AddPlayer(const std::string& username, const std::string& nickname, - const std::string& avatar_url, const MacAddress& mac_address, - const u64 game_id, const std::string& game_name) = 0; + virtual void AddPlayer(const Member& member) = 0; /** * Updates the data in the announce service. Re-register the room when required. @@ -116,9 +127,7 @@ public: const u16 /*port*/, const u32 /*max_player*/, const u32 /*net_version*/, const bool /*has_password*/, const std::string& /*preferred_game*/, const u64 /*preferred_game_id*/) override {} - void AddPlayer(const std::string& /*username*/, const std::string& /*nickname*/, - const std::string& /*avatar_url*/, const MacAddress& /*mac_address*/, - const u64 /*game_id*/, const std::string& /*game_name*/) override {} + void AddPlayer(const Member& /*member*/) override {} WebService::WebResult Update() override { return WebService::WebResult{WebService::WebResult::Code::NoWebservice, "WebService is missing", ""}; diff --git a/src/core/announce_multiplayer_session.cpp b/src/core/announce_multiplayer_session.cpp index aeca87aaca..f8aa9bb0b2 100644 --- a/src/core/announce_multiplayer_session.cpp +++ b/src/core/announce_multiplayer_session.cpp @@ -88,15 +88,14 @@ AnnounceMultiplayerSession::~AnnounceMultiplayerSession() { void AnnounceMultiplayerSession::UpdateBackendData(std::shared_ptr room) { Network::RoomInformation room_information = room->GetRoomInformation(); - std::vector memberlist = room->GetRoomMemberList(); + std::vector memberlist = room->GetRoomMemberList(); backend->SetRoomInformation( room_information.name, room_information.description, room_information.port, room_information.member_slots, Network::network_version, room->HasPassword(), room_information.preferred_game, room_information.preferred_game_id); backend->ClearPlayers(); for (const auto& member : memberlist) { - backend->AddPlayer(member.username, member.nickname, member.avatar_url, member.mac_address, - member.game_info.id, member.game_info.name); + backend->AddPlayer(member); } } diff --git a/src/network/room.cpp b/src/network/room.cpp index b82a757491..fe55d194c4 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -1066,8 +1066,8 @@ Room::BanList Room::GetBanList() const { return {room_impl->username_ban_list, room_impl->ip_ban_list}; } -std::vector Room::GetRoomMemberList() const { - std::vector member_list; +std::vector Room::GetRoomMemberList() const { + std::vector member_list; std::lock_guard lock(room_impl->member_mutex); for (const auto& member_impl : room_impl->members) { Member member; @@ -1076,7 +1076,8 @@ std::vector Room::GetRoomMemberList() const { member.display_name = member_impl.user_data.display_name; member.avatar_url = member_impl.user_data.avatar_url; member.mac_address = member_impl.mac_address; - member.game_info = member_impl.game_info; + member.game_name = member_impl.game_info.name; + member.game_id = member_impl.game_info.id; member_list.push_back(member); } return member_list; diff --git a/src/network/room.h b/src/network/room.h index df2253bf81..f282a5ac32 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -8,11 +8,17 @@ #include #include #include +#include "common/announce_multiplayer_room.h" #include "common/common_types.h" #include "network/verify_user.h" namespace Network { +using AnnounceMultiplayerRoom::GameInfo; +using AnnounceMultiplayerRoom::MacAddress; +using AnnounceMultiplayerRoom::Member; +using AnnounceMultiplayerRoom::RoomInformation; + constexpr u32 network_version = 1; ///< The version of this Room and RoomMember constexpr u16 DefaultRoomPort = 24872; @@ -24,23 +30,6 @@ static constexpr u32 MaxConcurrentConnections = 254; constexpr std::size_t NumChannels = 1; // Number of channels used for the connection -struct RoomInformation { - std::string name; ///< Name of the server - std::string description; ///< Server description - u32 member_slots; ///< Maximum number of members in this room - u16 port; ///< The port of this room - std::string preferred_game; ///< Game to advertise that you want to play - u64 preferred_game_id; ///< Title ID for the advertised game - std::string host_username; ///< Forum username of the host - bool enable_yuzu_mods; ///< Allow yuzu Moderators to moderate on this room -}; - -struct GameInfo { - std::string name{""}; - u64 id{0}; -}; - -using MacAddress = std::array; /// A special MAC address that tells the room we're joining to assign us a MAC address /// automatically. constexpr MacAddress NoPreferredMac = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; @@ -95,15 +84,6 @@ public: Closed, ///< The room is not opened and can not accept connections. }; - struct Member { - std::string nickname; ///< The nickname of the member. - std::string username; ///< The web services username of the member. Can be empty. - std::string display_name; ///< The web services display name of the member. Can be empty. - std::string avatar_url; ///< Url to the member's avatar. Can be empty. - GameInfo game_info; ///< The current game of the member - MacAddress mac_address; ///< The assigned mac address of the member. - }; - Room(); ~Room(); diff --git a/src/network/room_member.h b/src/network/room_member.h index ee1c921d46..c835ba8635 100644 --- a/src/network/room_member.h +++ b/src/network/room_member.h @@ -9,11 +9,15 @@ #include #include #include +#include "common/announce_multiplayer_room.h" #include "common/common_types.h" #include "network/room.h" namespace Network { +using AnnounceMultiplayerRoom::GameInfo; +using AnnounceMultiplayerRoom::RoomInformation; + /// Information about the received WiFi packets. /// Acts as our own 802.11 header. struct WifiPacket { diff --git a/src/web_service/announce_room_json.cpp b/src/web_service/announce_room_json.cpp index 984a59f777..84220b851a 100644 --- a/src/web_service/announce_room_json.cpp +++ b/src/web_service/announce_room_json.cpp @@ -11,7 +11,7 @@ namespace AnnounceMultiplayerRoom { -static void to_json(nlohmann::json& json, const Room::Member& member) { +static void to_json(nlohmann::json& json, const Member& member) { if (!member.username.empty()) { json["username"] = member.username; } @@ -23,7 +23,7 @@ static void to_json(nlohmann::json& json, const Room::Member& member) { json["gameId"] = member.game_id; } -static void from_json(const nlohmann::json& json, Room::Member& member) { +static void from_json(const nlohmann::json& json, Member& member) { member.nickname = json.at("nickname").get(); member.game_name = json.at("gameName").get(); member.game_id = json.at("gameId").get(); @@ -37,14 +37,14 @@ static void from_json(const nlohmann::json& json, Room::Member& member) { } static void to_json(nlohmann::json& json, const Room& room) { - json["port"] = room.port; - json["name"] = room.name; - if (!room.description.empty()) { - json["description"] = room.description; + json["port"] = room.information.port; + json["name"] = room.information.name; + if (!room.information.description.empty()) { + json["description"] = room.information.description; } - json["preferredGameName"] = room.preferred_game; - json["preferredGameId"] = room.preferred_game_id; - json["maxPlayers"] = room.max_player; + json["preferredGameName"] = room.information.preferred_game; + json["preferredGameId"] = room.information.preferred_game_id; + json["maxPlayers"] = room.information.member_slots; json["netVersion"] = room.net_version; json["hasPassword"] = room.has_password; if (room.members.size() > 0) { @@ -56,22 +56,22 @@ static void to_json(nlohmann::json& json, const Room& room) { static void from_json(const nlohmann::json& json, Room& room) { room.verify_UID = json.at("externalGuid").get(); room.ip = json.at("address").get(); - room.name = json.at("name").get(); + room.information.name = json.at("name").get(); try { - room.description = json.at("description").get(); + room.information.description = json.at("description").get(); } catch (const nlohmann::detail::out_of_range&) { - room.description = ""; - LOG_DEBUG(Network, "Room \'{}\' doesn't contain a description", room.name); + room.information.description = ""; + LOG_DEBUG(Network, "Room \'{}\' doesn't contain a description", room.information.name); } - room.owner = json.at("owner").get(); - room.port = json.at("port").get(); - room.preferred_game = json.at("preferredGameName").get(); - room.preferred_game_id = json.at("preferredGameId").get(); - room.max_player = json.at("maxPlayers").get(); + room.information.host_username = json.at("owner").get(); + room.information.port = json.at("port").get(); + room.information.preferred_game = json.at("preferredGameName").get(); + room.information.preferred_game_id = json.at("preferredGameId").get(); + room.information.member_slots = json.at("maxPlayers").get(); room.net_version = json.at("netVersion").get(); room.has_password = json.at("hasPassword").get(); try { - room.members = json.at("players").get>(); + room.members = json.at("players").get>(); } catch (const nlohmann::detail::out_of_range& e) { LOG_DEBUG(Network, "Out of range {}", e.what()); } @@ -85,26 +85,16 @@ void RoomJson::SetRoomInformation(const std::string& name, const std::string& de const u16 port, const u32 max_player, const u32 net_version, const bool has_password, const std::string& preferred_game, const u64 preferred_game_id) { - room.name = name; - room.description = description; - room.port = port; - room.max_player = max_player; + room.information.name = name; + room.information.description = description; + room.information.port = port; + room.information.member_slots = max_player; room.net_version = net_version; room.has_password = has_password; - room.preferred_game = preferred_game; - room.preferred_game_id = preferred_game_id; + room.information.preferred_game = preferred_game; + room.information.preferred_game_id = preferred_game_id; } -void RoomJson::AddPlayer(const std::string& username_, const std::string& nickname_, - const std::string& avatar_url, - const AnnounceMultiplayerRoom::MacAddress& mac_address, const u64 game_id, - const std::string& game_name) { - AnnounceMultiplayerRoom::Room::Member member; - member.username = username_; - member.nickname = nickname_; - member.avatar_url = avatar_url; - member.mac_address = mac_address; - member.game_id = game_id; - member.game_name = game_name; +void RoomJson::AddPlayer(const AnnounceMultiplayerRoom::Member& member) { room.members.push_back(member); } diff --git a/src/web_service/announce_room_json.h b/src/web_service/announce_room_json.h index f65c93214a..811c76fbd2 100644 --- a/src/web_service/announce_room_json.h +++ b/src/web_service/announce_room_json.h @@ -24,10 +24,7 @@ public: const u32 max_player, const u32 net_version, const bool has_password, const std::string& preferred_game, const u64 preferred_game_id) override; - void AddPlayer(const std::string& username_, const std::string& nickname_, - const std::string& avatar_url, - const AnnounceMultiplayerRoom::MacAddress& mac_address, const u64 game_id, - const std::string& game_name) override; + void AddPlayer(const AnnounceMultiplayerRoom::Member& member) override; WebResult Update() override; WebResult Register() override; void ClearPlayers() override; diff --git a/src/web_service/verify_user_jwt.cpp b/src/web_service/verify_user_jwt.cpp index 78fe7bed5a..3133dcbe26 100644 --- a/src/web_service/verify_user_jwt.cpp +++ b/src/web_service/verify_user_jwt.cpp @@ -2,8 +2,16 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wimplicit-fallthrough" +#endif #include +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif + +#include #include "common/logging/log.h" #include "web_service/verify_user_jwt.h" #include "web_service/web_backend.h" diff --git a/src/yuzu/multiplayer/lobby.cpp b/src/yuzu/multiplayer/lobby.cpp index 364b4605e6..6cc5f8f7eb 100644 --- a/src/yuzu/multiplayer/lobby.cpp +++ b/src/yuzu/multiplayer/lobby.cpp @@ -214,7 +214,7 @@ void Lobby::OnRefreshLobby() { for (int r = 0; r < game_list->rowCount(); ++r) { auto index = game_list->index(r, 0); auto game_id = game_list->data(index, GameListItemPath::ProgramIdRole).toULongLong(); - if (game_id != 0 && room.preferred_game_id == game_id) { + if (game_id != 0 && room.information.preferred_game_id == game_id) { smdh_icon = game_list->data(index, Qt::DecorationRole).value(); } } @@ -231,20 +231,21 @@ void Lobby::OnRefreshLobby() { auto first_item = new LobbyItem(); auto row = QList({ first_item, - new LobbyItemName(room.has_password, QString::fromStdString(room.name)), - new LobbyItemGame(room.preferred_game_id, QString::fromStdString(room.preferred_game), - smdh_icon), - new LobbyItemHost(QString::fromStdString(room.owner), QString::fromStdString(room.ip), - room.port, QString::fromStdString(room.verify_UID)), - new LobbyItemMemberList(members, room.max_player), + new LobbyItemName(room.has_password, QString::fromStdString(room.information.name)), + new LobbyItemGame(room.information.preferred_game_id, + QString::fromStdString(room.information.preferred_game), smdh_icon), + new LobbyItemHost(QString::fromStdString(room.information.host_username), + QString::fromStdString(room.ip), room.information.port, + QString::fromStdString(room.verify_UID)), + new LobbyItemMemberList(members, room.information.member_slots), }); model->appendRow(row); // To make the rows expandable, add the member data as a child of the first column of the // rows with people in them and have qt set them to colspan after the model is finished // resetting - if (!room.description.empty()) { + if (!room.information.description.empty()) { first_item->appendRow( - new LobbyItemDescription(QString::fromStdString(room.description))); + new LobbyItemDescription(QString::fromStdString(room.information.description))); } if (!room.members.empty()) { first_item->appendRow(new LobbyItemExpandedMemberList(members)); From 899c8bb33094f43fbd8df9afb4ca84718ebac87e Mon Sep 17 00:00:00 2001 From: german77 Date: Sun, 17 Jul 2022 22:53:44 -0500 Subject: [PATCH 45/50] common: multiplayer: Use GameInfo type --- src/common/announce_multiplayer_room.h | 35 +++++++++++------------ src/core/announce_multiplayer_session.cpp | 8 +++--- src/network/room.cpp | 8 ++---- src/network/room.h | 3 +- src/network/room_member.cpp | 2 +- src/web_service/announce_room_json.cpp | 21 +++++++------- src/web_service/announce_room_json.h | 3 +- src/web_service/verify_user_jwt.h | 2 ++ src/yuzu/multiplayer/host_room.cpp | 21 ++++++++------ src/yuzu/multiplayer/lobby.cpp | 11 +++---- src/yuzu/uisettings.h | 8 +++--- 11 files changed, 60 insertions(+), 62 deletions(-) diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h index 2ff38b6cfc..a9e2f89b7a 100644 --- a/src/common/announce_multiplayer_room.h +++ b/src/common/announce_multiplayer_room.h @@ -15,30 +15,28 @@ namespace AnnounceMultiplayerRoom { using MacAddress = std::array; +struct GameInfo { + std::string name{""}; + u64 id{0}; +}; + struct Member { std::string username; std::string nickname; std::string display_name; std::string avatar_url; MacAddress mac_address; - std::string game_name; - u64 game_id; + GameInfo game; }; struct RoomInformation { - std::string name; ///< Name of the server - std::string description; ///< Server description - u32 member_slots; ///< Maximum number of members in this room - u16 port; ///< The port of this room - std::string preferred_game; ///< Game to advertise that you want to play - u64 preferred_game_id; ///< Title ID for the advertised game - std::string host_username; ///< Forum username of the host - bool enable_yuzu_mods; ///< Allow yuzu Moderators to moderate on this room -}; - -struct GameInfo { - std::string name{""}; - u64 id{0}; + std::string name; ///< Name of the server + std::string description; ///< Server description + u32 member_slots; ///< Maximum number of members in this room + u16 port; ///< The port of this room + GameInfo preferred_game; ///< Game to advertise that you want to play + std::string host_username; ///< Forum username of the host + bool enable_yuzu_mods; ///< Allow yuzu Moderators to moderate on this room }; struct Room { @@ -75,8 +73,7 @@ public: */ virtual void SetRoomInformation(const std::string& name, const std::string& description, const u16 port, const u32 max_player, const u32 net_version, - const bool has_password, const std::string& preferred_game, - const u64 preferred_game_id) = 0; + const bool has_password, const GameInfo& preferred_game) = 0; /** * Adds a player information to the data that gets announced * @param nickname The nickname of the player @@ -125,8 +122,8 @@ public: ~NullBackend() = default; void SetRoomInformation(const std::string& /*name*/, const std::string& /*description*/, const u16 /*port*/, const u32 /*max_player*/, const u32 /*net_version*/, - const bool /*has_password*/, const std::string& /*preferred_game*/, - const u64 /*preferred_game_id*/) override {} + const bool /*has_password*/, + const GameInfo& /*preferred_game*/) override {} void AddPlayer(const Member& /*member*/) override {} WebService::WebResult Update() override { return WebService::WebResult{WebService::WebResult::Code::NoWebservice, diff --git a/src/core/announce_multiplayer_session.cpp b/src/core/announce_multiplayer_session.cpp index f8aa9bb0b2..db9eaeac83 100644 --- a/src/core/announce_multiplayer_session.cpp +++ b/src/core/announce_multiplayer_session.cpp @@ -89,10 +89,10 @@ AnnounceMultiplayerSession::~AnnounceMultiplayerSession() { void AnnounceMultiplayerSession::UpdateBackendData(std::shared_ptr room) { Network::RoomInformation room_information = room->GetRoomInformation(); std::vector memberlist = room->GetRoomMemberList(); - backend->SetRoomInformation( - room_information.name, room_information.description, room_information.port, - room_information.member_slots, Network::network_version, room->HasPassword(), - room_information.preferred_game, room_information.preferred_game_id); + backend->SetRoomInformation(room_information.name, room_information.description, + room_information.port, room_information.member_slots, + Network::network_version, room->HasPassword(), + room_information.preferred_game); backend->ClearPlayers(); for (const auto& member : memberlist) { backend->AddPlayer(member); diff --git a/src/network/room.cpp b/src/network/room.cpp index fe55d194c4..22491b2996 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -811,7 +811,7 @@ void Room::RoomImpl::BroadcastRoomInformation() { packet << room_information.description; packet << room_information.member_slots; packet << room_information.port; - packet << room_information.preferred_game; + packet << room_information.preferred_game.name; packet << room_information.host_username; packet << static_cast(members.size()); @@ -1013,7 +1013,7 @@ Room::~Room() = default; bool Room::Create(const std::string& name, const std::string& description, const std::string& server_address, u16 server_port, const std::string& password, const u32 max_connections, const std::string& host_username, - const std::string& preferred_game, u64 preferred_game_id, + const GameInfo preferred_game, std::unique_ptr verify_backend, const Room::BanList& ban_list, bool enable_yuzu_mods) { ENetAddress address; @@ -1036,7 +1036,6 @@ bool Room::Create(const std::string& name, const std::string& description, room_impl->room_information.member_slots = max_connections; room_impl->room_information.port = server_port; room_impl->room_information.preferred_game = preferred_game; - room_impl->room_information.preferred_game_id = preferred_game_id; room_impl->room_information.host_username = host_username; room_impl->room_information.enable_yuzu_mods = enable_yuzu_mods; room_impl->password = password; @@ -1076,8 +1075,7 @@ std::vector Room::GetRoomMemberList() const { member.display_name = member_impl.user_data.display_name; member.avatar_url = member_impl.user_data.avatar_url; member.mac_address = member_impl.mac_address; - member.game_name = member_impl.game_info.name; - member.game_id = member_impl.game_info.id; + member.game = member_impl.game_info; member_list.push_back(member); } return member_list; diff --git a/src/network/room.h b/src/network/room.h index f282a5ac32..90a82563f2 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -125,8 +125,7 @@ public: const std::string& server = "", u16 server_port = DefaultRoomPort, const std::string& password = "", const u32 max_connections = MaxConcurrentConnections, - const std::string& host_username = "", const std::string& preferred_game = "", - u64 preferred_game_id = 0, + const std::string& host_username = "", const GameInfo = {}, std::unique_ptr verify_backend = nullptr, const BanList& ban_list = {}, bool enable_yuzu_mods = false); diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp index d6ace9b39d..11a2e276ea 100644 --- a/src/network/room_member.cpp +++ b/src/network/room_member.cpp @@ -303,7 +303,7 @@ void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* ev packet >> info.description; packet >> info.member_slots; packet >> info.port; - packet >> info.preferred_game; + packet >> info.preferred_game.name; packet >> info.host_username; room_information.name = info.name; room_information.description = info.description; diff --git a/src/web_service/announce_room_json.cpp b/src/web_service/announce_room_json.cpp index 84220b851a..082bebaa92 100644 --- a/src/web_service/announce_room_json.cpp +++ b/src/web_service/announce_room_json.cpp @@ -19,14 +19,14 @@ static void to_json(nlohmann::json& json, const Member& member) { if (!member.avatar_url.empty()) { json["avatarUrl"] = member.avatar_url; } - json["gameName"] = member.game_name; - json["gameId"] = member.game_id; + json["gameName"] = member.game.name; + json["gameId"] = member.game.id; } static void from_json(const nlohmann::json& json, Member& member) { member.nickname = json.at("nickname").get(); - member.game_name = json.at("gameName").get(); - member.game_id = json.at("gameId").get(); + member.game.name = json.at("gameName").get(); + member.game.id = json.at("gameId").get(); try { member.username = json.at("username").get(); member.avatar_url = json.at("avatarUrl").get(); @@ -42,8 +42,8 @@ static void to_json(nlohmann::json& json, const Room& room) { if (!room.information.description.empty()) { json["description"] = room.information.description; } - json["preferredGameName"] = room.information.preferred_game; - json["preferredGameId"] = room.information.preferred_game_id; + json["preferredGameName"] = room.information.preferred_game.name; + json["preferredGameId"] = room.information.preferred_game.id; json["maxPlayers"] = room.information.member_slots; json["netVersion"] = room.net_version; json["hasPassword"] = room.has_password; @@ -65,8 +65,8 @@ static void from_json(const nlohmann::json& json, Room& room) { } room.information.host_username = json.at("owner").get(); room.information.port = json.at("port").get(); - room.information.preferred_game = json.at("preferredGameName").get(); - room.information.preferred_game_id = json.at("preferredGameId").get(); + room.information.preferred_game.name = json.at("preferredGameName").get(); + room.information.preferred_game.id = json.at("preferredGameId").get(); room.information.member_slots = json.at("maxPlayers").get(); room.net_version = json.at("netVersion").get(); room.has_password = json.at("hasPassword").get(); @@ -83,8 +83,8 @@ namespace WebService { void RoomJson::SetRoomInformation(const std::string& name, const std::string& description, const u16 port, const u32 max_player, const u32 net_version, - const bool has_password, const std::string& preferred_game, - const u64 preferred_game_id) { + const bool has_password, + const AnnounceMultiplayerRoom::GameInfo& preferred_game) { room.information.name = name; room.information.description = description; room.information.port = port; @@ -92,7 +92,6 @@ void RoomJson::SetRoomInformation(const std::string& name, const std::string& de room.net_version = net_version; room.has_password = has_password; room.information.preferred_game = preferred_game; - room.information.preferred_game_id = preferred_game_id; } void RoomJson::AddPlayer(const AnnounceMultiplayerRoom::Member& member) { room.members.push_back(member); diff --git a/src/web_service/announce_room_json.h b/src/web_service/announce_room_json.h index 811c76fbd2..24ec29c65e 100644 --- a/src/web_service/announce_room_json.h +++ b/src/web_service/announce_room_json.h @@ -22,8 +22,7 @@ public: ~RoomJson() = default; void SetRoomInformation(const std::string& name, const std::string& description, const u16 port, const u32 max_player, const u32 net_version, const bool has_password, - const std::string& preferred_game, - const u64 preferred_game_id) override; + const AnnounceMultiplayerRoom::GameInfo& preferred_game) override; void AddPlayer(const AnnounceMultiplayerRoom::Member& member) override; WebResult Update() override; WebResult Register() override; diff --git a/src/web_service/verify_user_jwt.h b/src/web_service/verify_user_jwt.h index 826e01eed7..6db74c2089 100644 --- a/src/web_service/verify_user_jwt.h +++ b/src/web_service/verify_user_jwt.h @@ -10,6 +10,8 @@ namespace WebService { +std::string GetPublicKey(const std::string& host); + class VerifyUserJWT final : public Network::VerifyUser::Backend { public: VerifyUserJWT(const std::string& host); diff --git a/src/yuzu/multiplayer/host_room.cpp b/src/yuzu/multiplayer/host_room.cpp index 5470b8b86e..053e22fe4b 100644 --- a/src/yuzu/multiplayer/host_room.cpp +++ b/src/yuzu/multiplayer/host_room.cpp @@ -132,21 +132,24 @@ void HostRoomWindow::Host() { } ui->host->setDisabled(true); - auto game_name = ui->game_list->currentData(Qt::DisplayRole).toString(); - auto game_id = ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong(); - auto port = ui->port->isModified() ? ui->port->text().toInt() : Network::DefaultRoomPort; - auto password = ui->password->text().toStdString(); + const AnnounceMultiplayerRoom::GameInfo game{ + .name = ui->game_list->currentData(Qt::DisplayRole).toString().toStdString(), + .id = ui->game_list->currentData(GameListItemPath::ProgramIdRole).toULongLong(), + }; + const auto port = + ui->port->isModified() ? ui->port->text().toInt() : Network::DefaultRoomPort; + const auto password = ui->password->text().toStdString(); const bool is_public = ui->host_type->currentIndex() == 0; Network::Room::BanList ban_list{}; if (ui->load_ban_list->isChecked()) { ban_list = UISettings::values.multiplayer_ban_list; } if (auto room = Network::GetRoom().lock()) { - bool created = room->Create( - ui->room_name->text().toStdString(), - ui->room_description->toPlainText().toStdString(), "", port, password, - ui->max_player->value(), Settings::values.yuzu_username.GetValue(), - game_name.toStdString(), game_id, CreateVerifyBackend(is_public), ban_list); + const bool created = + room->Create(ui->room_name->text().toStdString(), + ui->room_description->toPlainText().toStdString(), "", port, password, + ui->max_player->value(), Settings::values.yuzu_username.GetValue(), + game, CreateVerifyBackend(is_public), ban_list); if (!created) { NetworkMessage::ErrorManager::ShowError( NetworkMessage::ErrorManager::COULD_NOT_CREATE_ROOM); diff --git a/src/yuzu/multiplayer/lobby.cpp b/src/yuzu/multiplayer/lobby.cpp index 6cc5f8f7eb..1b6b782d99 100644 --- a/src/yuzu/multiplayer/lobby.cpp +++ b/src/yuzu/multiplayer/lobby.cpp @@ -214,7 +214,7 @@ void Lobby::OnRefreshLobby() { for (int r = 0; r < game_list->rowCount(); ++r) { auto index = game_list->index(r, 0); auto game_id = game_list->data(index, GameListItemPath::ProgramIdRole).toULongLong(); - if (game_id != 0 && room.information.preferred_game_id == game_id) { + if (game_id != 0 && room.information.preferred_game.id == game_id) { smdh_icon = game_list->data(index, Qt::DecorationRole).value(); } } @@ -223,8 +223,8 @@ void Lobby::OnRefreshLobby() { for (auto member : room.members) { QVariant var; var.setValue(LobbyMember{QString::fromStdString(member.username), - QString::fromStdString(member.nickname), member.game_id, - QString::fromStdString(member.game_name)}); + QString::fromStdString(member.nickname), member.game.id, + QString::fromStdString(member.game.name)}); members.append(var); } @@ -232,8 +232,9 @@ void Lobby::OnRefreshLobby() { auto row = QList({ first_item, new LobbyItemName(room.has_password, QString::fromStdString(room.information.name)), - new LobbyItemGame(room.information.preferred_game_id, - QString::fromStdString(room.information.preferred_game), smdh_icon), + new LobbyItemGame(room.information.preferred_game.id, + QString::fromStdString(room.information.preferred_game.name), + smdh_icon), new LobbyItemHost(QString::fromStdString(room.information.host_username), QString::fromStdString(room.ip), room.information.port, QString::fromStdString(room.verify_UID)), diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h index 83a6cffa3e..6cd4d6cb2e 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h @@ -105,12 +105,12 @@ struct Values { // multiplayer settings Settings::Setting multiplayer_nickname{QStringLiteral("yuzu"), "nickname"}; Settings::Setting multiplayer_ip{{}, "ip"}; - Settings::SwitchableSetting multiplayer_port{24872, 0, 65535, "port"}; + Settings::SwitchableSetting multiplayer_port{24872, 0, 65535, "port"}; Settings::Setting multiplayer_room_nickname{{}, "room_nickname"}; Settings::Setting multiplayer_room_name{{}, "room_name"}; - Settings::SwitchableSetting multiplayer_max_player{8, 0, 8, "max_player"}; - Settings::SwitchableSetting multiplayer_room_port{24872, 0, 65535, "room_port"}; - Settings::SwitchableSetting multiplayer_host_type{0, 0, 1, "host_type"}; + Settings::SwitchableSetting multiplayer_max_player{8, 0, 8, "max_player"}; + Settings::SwitchableSetting multiplayer_room_port{24872, 0, 65535, "room_port"}; + Settings::SwitchableSetting multiplayer_host_type{0, 0, 1, "host_type"}; Settings::Setting multiplayer_game_id{{}, "game_id"}; Settings::Setting multiplayer_room_description{{}, "room_description"}; std::pair, std::vector> multiplayer_ban_list; From 7d82e57b91dee30e0fe6fed36550ea7cc9eb778e Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Fri, 22 Jul 2022 16:31:13 +0200 Subject: [PATCH 46/50] network: Move global state into a seperate class Co-Authored-By: Narr the Reg <5944268+german77@users.noreply.github.com> --- src/core/announce_multiplayer_session.cpp | 7 ++-- src/core/announce_multiplayer_session.h | 11 +++-- src/core/core.cpp | 16 ++++++-- src/core/core.h | 10 +++++ src/network/network.cpp | 15 +++---- src/network/network.h | 25 +++++++---- src/yuzu/main.cpp | 6 +-- src/yuzu/multiplayer/chat_room.cpp | 48 +++++++++++----------- src/yuzu/multiplayer/chat_room.h | 2 + src/yuzu/multiplayer/client_room.cpp | 11 ++--- src/yuzu/multiplayer/client_room.h | 3 +- src/yuzu/multiplayer/direct_connect.cpp | 10 ++--- src/yuzu/multiplayer/direct_connect.h | 3 +- src/yuzu/multiplayer/host_room.cpp | 14 ++++--- src/yuzu/multiplayer/host_room.h | 4 +- src/yuzu/multiplayer/lobby.cpp | 12 +++--- src/yuzu/multiplayer/lobby.h | 4 +- src/yuzu/multiplayer/moderation_dialog.cpp | 14 +++---- src/yuzu/multiplayer/moderation_dialog.h | 4 +- src/yuzu/multiplayer/state.cpp | 28 +++++++------ src/yuzu/multiplayer/state.h | 3 +- 21 files changed, 152 insertions(+), 98 deletions(-) diff --git a/src/core/announce_multiplayer_session.cpp b/src/core/announce_multiplayer_session.cpp index db9eaeac83..8f96b4ee87 100644 --- a/src/core/announce_multiplayer_session.cpp +++ b/src/core/announce_multiplayer_session.cpp @@ -20,7 +20,8 @@ namespace Core { // Time between room is announced to web_service static constexpr std::chrono::seconds announce_time_interval(15); -AnnounceMultiplayerSession::AnnounceMultiplayerSession() { +AnnounceMultiplayerSession::AnnounceMultiplayerSession(Network::RoomNetwork& room_network_) + : room_network{room_network_} { #ifdef ENABLE_WEB_SERVICE backend = std::make_unique(Settings::values.web_api_url.GetValue(), Settings::values.yuzu_username.GetValue(), @@ -31,7 +32,7 @@ AnnounceMultiplayerSession::AnnounceMultiplayerSession() { } WebService::WebResult AnnounceMultiplayerSession::Register() { - std::shared_ptr room = Network::GetRoom().lock(); + std::shared_ptr room = room_network.GetRoom().lock(); if (!room) { return WebService::WebResult{WebService::WebResult::Code::LibError, "Network is not initialized", ""}; @@ -120,7 +121,7 @@ void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { std::future future; while (!shutdown_event.WaitUntil(update_time)) { update_time += announce_time_interval; - std::shared_ptr room = Network::GetRoom().lock(); + std::shared_ptr room = room_network.GetRoom().lock(); if (!room) { break; } diff --git a/src/core/announce_multiplayer_session.h b/src/core/announce_multiplayer_session.h index 2aaf550177..5da3c1f8d0 100644 --- a/src/core/announce_multiplayer_session.h +++ b/src/core/announce_multiplayer_session.h @@ -16,7 +16,8 @@ namespace Network { class Room; -} +class RoomNetwork; +} // namespace Network namespace Core { @@ -28,7 +29,7 @@ namespace Core { class AnnounceMultiplayerSession { public: using CallbackHandle = std::shared_ptr>; - AnnounceMultiplayerSession(); + AnnounceMultiplayerSession(Network::RoomNetwork& room_network_); ~AnnounceMultiplayerSession(); /** @@ -79,6 +80,9 @@ public: void UpdateCredentials(); private: + void UpdateBackendData(std::shared_ptr room); + void AnnounceMultiplayerLoop(); + Common::Event shutdown_event; std::mutex callback_mutex; std::set error_callbacks; @@ -89,8 +93,7 @@ private: std::atomic_bool registered = false; ///< Whether the room has been registered - void UpdateBackendData(std::shared_ptr room); - void AnnounceMultiplayerLoop(); + Network::RoomNetwork& room_network; }; } // namespace Core diff --git a/src/core/core.cpp b/src/core/core.cpp index 98fe6d39cc..95791a07f1 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -131,7 +131,7 @@ FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs, struct System::Impl { explicit Impl(System& system) - : kernel{system}, fs_controller{system}, memory{system}, hid_core{}, + : kernel{system}, fs_controller{system}, memory{system}, hid_core{}, room_network{}, cpu_manager{system}, reporter{system}, applet_manager{system}, time_manager{system} {} SystemResultStatus Run() { @@ -320,7 +320,7 @@ struct System::Impl { if (app_loader->ReadTitle(name) != Loader::ResultStatus::Success) { LOG_ERROR(Core, "Failed to read title for ROM (Error {})", load_result); } - if (auto room_member = Network::GetRoomMember().lock()) { + if (auto room_member = room_network.GetRoomMember().lock()) { Network::GameInfo game_info; game_info.name = name; game_info.id = program_id; @@ -374,7 +374,7 @@ struct System::Impl { memory.Reset(); applet_manager.ClearAll(); - if (auto room_member = Network::GetRoomMember().lock()) { + if (auto room_member = room_network.GetRoomMember().lock()) { Network::GameInfo game_info{}; room_member->SendGameInfo(game_info); } @@ -451,6 +451,8 @@ struct System::Impl { std::unique_ptr audio_core; Core::Memory::Memory memory; Core::HID::HIDCore hid_core; + Network::RoomNetwork room_network; + CpuManager cpu_manager; std::atomic_bool is_powered_on{}; bool exit_lock = false; @@ -896,6 +898,14 @@ const Core::Debugger& System::GetDebugger() const { return *impl->debugger; } +Network::RoomNetwork& System::GetRoomNetwork() { + return impl->room_network; +} + +const Network::RoomNetwork& System::GetRoomNetwork() const { + return impl->room_network; +} + void System::RegisterExecuteProgramCallback(ExecuteProgramCallback&& callback) { impl->execute_program_callback = std::move(callback); } diff --git a/src/core/core.h b/src/core/core.h index a49d1214b7..13122dd619 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -97,6 +97,10 @@ namespace Core::HID { class HIDCore; } +namespace Network { +class RoomNetwork; +} + namespace Core { class ARM_Interface; @@ -379,6 +383,12 @@ public: [[nodiscard]] Core::Debugger& GetDebugger(); [[nodiscard]] const Core::Debugger& GetDebugger() const; + /// Gets a mutable reference to the Room Network. + [[nodiscard]] Network::RoomNetwork& GetRoomNetwork(); + + /// Gets an immutable reference to the Room Network. + [[nodiscard]] const Network::RoomNetwork& GetRoomNetwork() const; + void SetExitLock(bool locked); [[nodiscard]] bool GetExitLock() const; diff --git a/src/network/network.cpp b/src/network/network.cpp index 51b5d6a9fa..e1401a4031 100644 --- a/src/network/network.cpp +++ b/src/network/network.cpp @@ -9,11 +9,12 @@ namespace Network { -static std::shared_ptr g_room_member; ///< RoomMember (Client) for network games -static std::shared_ptr g_room; ///< Room (Server) for network games -// TODO(B3N30): Put these globals into a networking class +RoomNetwork::RoomNetwork() { + g_room = std::make_shared(); + g_room_member = std::make_shared(); +} -bool Init() { +bool RoomNetwork::Init() { if (enet_initialize() != 0) { LOG_ERROR(Network, "Error initalizing ENet"); return false; @@ -24,15 +25,15 @@ bool Init() { return true; } -std::weak_ptr GetRoom() { +std::weak_ptr RoomNetwork::GetRoom() { return g_room; } -std::weak_ptr GetRoomMember() { +std::weak_ptr RoomNetwork::GetRoomMember() { return g_room_member; } -void Shutdown() { +void RoomNetwork::Shutdown() { if (g_room_member) { if (g_room_member->IsConnected()) g_room_member->Leave(); diff --git a/src/network/network.h b/src/network/network.h index 6d002d693f..74eb42bf5c 100644 --- a/src/network/network.h +++ b/src/network/network.h @@ -10,16 +10,25 @@ namespace Network { -/// Initializes and registers the network device, the room, and the room member. -bool Init(); +class RoomNetwork { +public: + RoomNetwork(); -/// Returns a pointer to the room handle -std::weak_ptr GetRoom(); + /// Initializes and registers the network device, the room, and the room member. + bool Init(); -/// Returns a pointer to the room member handle -std::weak_ptr GetRoomMember(); + /// Returns a pointer to the room handle + std::weak_ptr GetRoom(); -/// Unregisters the network device, the room, and the room member and shut them down. -void Shutdown(); + /// Returns a pointer to the room member handle + std::weak_ptr GetRoomMember(); + + /// Unregisters the network device, the room, and the room member and shut them down. + void Shutdown(); + +private: + std::shared_ptr g_room_member; ///< RoomMember (Client) for network games + std::shared_ptr g_room; ///< Room (Server) for network games +}; } // namespace Network diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index f1cc910c04..e56fcabffb 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -273,7 +273,7 @@ GMainWindow::GMainWindow(bool has_broken_vulkan) SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); discord_rpc->Update(); - Network::Init(); + system->GetRoomNetwork().Init(); RegisterMetaTypes(); @@ -463,7 +463,7 @@ GMainWindow::~GMainWindow() { if (render_window->parent() == nullptr) { delete render_window; } - Network::Shutdown(); + system->GetRoomNetwork().Shutdown(); } void GMainWindow::RegisterMetaTypes() { @@ -828,7 +828,7 @@ void GMainWindow::InitializeWidgets() { }); multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui->action_Leave_Room, - ui->action_Show_Room); + ui->action_Show_Room, system->GetRoomNetwork()); multiplayer_state->setVisible(false); // Create status bar diff --git a/src/yuzu/multiplayer/chat_room.cpp b/src/yuzu/multiplayer/chat_room.cpp index 7048ee1fce..9f2c57eee3 100644 --- a/src/yuzu/multiplayer/chat_room.cpp +++ b/src/yuzu/multiplayer/chat_room.cpp @@ -28,7 +28,8 @@ class ChatMessage { public: - explicit ChatMessage(const Network::ChatEntry& chat, QTime ts = {}) { + explicit ChatMessage(const Network::ChatEntry& chat, Network::RoomNetwork& room_network, + QTime ts = {}) { /// Convert the time to their default locale defined format QLocale locale; timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat); @@ -38,7 +39,7 @@ public: // Check for user pings QString cur_nickname, cur_username; - if (auto room = Network::GetRoomMember().lock()) { + if (auto room = room_network.GetRoomMember().lock()) { cur_nickname = QString::fromStdString(room->GetNickname()); cur_username = QString::fromStdString(room->GetUsername()); } @@ -173,20 +174,6 @@ ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique(); qRegisterMetaType(); - // setup the callbacks for network updates - if (auto member = Network::GetRoomMember().lock()) { - member->BindOnChatMessageRecieved( - [this](const Network::ChatEntry& chat) { emit ChatReceived(chat); }); - member->BindOnStatusMessageReceived( - [this](const Network::StatusMessageEntry& status_message) { - emit StatusMessageReceived(status_message); - }); - connect(this, &ChatRoom::ChatReceived, this, &ChatRoom::OnChatReceive); - connect(this, &ChatRoom::StatusMessageReceived, this, &ChatRoom::OnStatusMessageReceive); - } else { - // TODO (jroweboy) network was not initialized? - } - // Connect all the widgets to the appropriate events connect(ui->player_view, &QTreeView::customContextMenuRequested, this, &ChatRoom::PopupContextMenu); @@ -197,6 +184,21 @@ ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_uniqueGetRoomMember().lock()) { + member->BindOnChatMessageRecieved( + [this](const Network::ChatEntry& chat) { emit ChatReceived(chat); }); + member->BindOnStatusMessageReceived( + [this](const Network::StatusMessageEntry& status_message) { + emit StatusMessageReceived(status_message); + }); + connect(this, &ChatRoom::ChatReceived, this, &ChatRoom::OnChatReceive); + connect(this, &ChatRoom::StatusMessageReceived, this, &ChatRoom::OnStatusMessageReceive); + } +} + void ChatRoom::SetModPerms(bool is_mod) { has_mod_perms = is_mod; } @@ -219,7 +221,7 @@ void ChatRoom::AppendChatMessage(const QString& msg) { } void ChatRoom::SendModerationRequest(Network::RoomMessageTypes type, const std::string& nickname) { - if (auto room = Network::GetRoomMember().lock()) { + if (auto room = room_network->GetRoomMember().lock()) { auto members = room->GetMemberInformation(); auto it = std::find_if(members.begin(), members.end(), [&nickname](const Network::RoomMember::MemberInformation& member) { @@ -239,7 +241,7 @@ bool ChatRoom::ValidateMessage(const std::string& msg) { void ChatRoom::OnRoomUpdate(const Network::RoomInformation& info) { // TODO(B3N30): change title - if (auto room_member = Network::GetRoomMember().lock()) { + if (auto room_member = room_network->GetRoomMember().lock()) { SetPlayerList(room_member->GetMemberInformation()); } } @@ -258,7 +260,7 @@ void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) { if (!ValidateMessage(chat.message)) { return; } - if (auto room = Network::GetRoomMember().lock()) { + if (auto room = room_network->GetRoomMember().lock()) { // get the id of the player auto members = room->GetMemberInformation(); auto it = std::find_if(members.begin(), members.end(), @@ -276,7 +278,7 @@ void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) { return; } auto player = std::distance(members.begin(), it); - ChatMessage m(chat); + ChatMessage m(chat, *room_network); if (m.ContainsPing()) { emit UserPinged(); } @@ -315,7 +317,7 @@ void ChatRoom::OnStatusMessageReceive(const Network::StatusMessageEntry& status_ } void ChatRoom::OnSendChat() { - if (auto room = Network::GetRoomMember().lock()) { + if (auto room = room_network->GetRoomMember().lock()) { if (room->GetState() != Network::RoomMember::State::Joined && room->GetState() != Network::RoomMember::State::Moderator) { @@ -339,7 +341,7 @@ void ChatRoom::OnSendChat() { LOG_INFO(Network, "Cannot find self in the player list when sending a message."); } auto player = std::distance(members.begin(), it); - ChatMessage m(chat); + ChatMessage m(chat, *room_network); room->SendChatMessage(message); AppendChatMessage(m.GetPlayerChatMessage(player)); ui->chat_message->clear(); @@ -433,7 +435,7 @@ void ChatRoom::PopupContextMenu(const QPoint& menu_location) { } std::string cur_nickname; - if (auto room = Network::GetRoomMember().lock()) { + if (auto room = room_network->GetRoomMember().lock()) { cur_nickname = room->GetNickname(); } diff --git a/src/yuzu/multiplayer/chat_room.h b/src/yuzu/multiplayer/chat_room.h index a810377f7a..9179d16fbb 100644 --- a/src/yuzu/multiplayer/chat_room.h +++ b/src/yuzu/multiplayer/chat_room.h @@ -30,6 +30,7 @@ class ChatRoom : public QWidget { public: explicit ChatRoom(QWidget* parent); + void Initialize(Network::RoomNetwork* room_network); void RetranslateUi(); void SetPlayerList(const Network::RoomMember::MemberList& member_list); void Clear(); @@ -65,6 +66,7 @@ private: std::unique_ptr ui; std::unordered_set block_list; std::unordered_map icon_cache; + Network::RoomNetwork* room_network; }; Q_DECLARE_METATYPE(Network::ChatEntry); diff --git a/src/yuzu/multiplayer/client_room.cpp b/src/yuzu/multiplayer/client_room.cpp index 7b2e16e06e..9bef9bdfcd 100644 --- a/src/yuzu/multiplayer/client_room.cpp +++ b/src/yuzu/multiplayer/client_room.cpp @@ -19,13 +19,14 @@ #include "yuzu/multiplayer/moderation_dialog.h" #include "yuzu/multiplayer/state.h" -ClientRoomWindow::ClientRoomWindow(QWidget* parent) +ClientRoomWindow::ClientRoomWindow(QWidget* parent, Network::RoomNetwork& room_network_) : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), - ui(std::make_unique()) { + ui(std::make_unique()), room_network{room_network_} { ui->setupUi(this); + ui->chat->Initialize(&room_network); // setup the callbacks for network updates - if (auto member = Network::GetRoomMember().lock()) { + if (auto member = room_network.GetRoomMember().lock()) { member->BindOnRoomInformationChanged( [this](const Network::RoomInformation& info) { emit RoomInformationChanged(info); }); member->BindOnStateChanged( @@ -44,7 +45,7 @@ ClientRoomWindow::ClientRoomWindow(QWidget* parent) ui->disconnect->setDefault(false); ui->disconnect->setAutoDefault(false); connect(ui->moderation, &QPushButton::clicked, [this] { - ModerationDialog dialog(this); + ModerationDialog dialog(room_network, this); dialog.exec(); }); ui->moderation->setDefault(false); @@ -91,7 +92,7 @@ void ClientRoomWindow::Disconnect() { } void ClientRoomWindow::UpdateView() { - if (auto member = Network::GetRoomMember().lock()) { + if (auto member = room_network.GetRoomMember().lock()) { if (member->IsConnected()) { ui->chat->Enable(); ui->disconnect->setEnabled(true); diff --git a/src/yuzu/multiplayer/client_room.h b/src/yuzu/multiplayer/client_room.h index 607b4073d0..6303b2595f 100644 --- a/src/yuzu/multiplayer/client_room.h +++ b/src/yuzu/multiplayer/client_room.h @@ -14,7 +14,7 @@ class ClientRoomWindow : public QDialog { Q_OBJECT public: - explicit ClientRoomWindow(QWidget* parent); + explicit ClientRoomWindow(QWidget* parent, Network::RoomNetwork& room_network_); ~ClientRoomWindow(); void RetranslateUi(); @@ -36,4 +36,5 @@ private: QStandardItemModel* player_list; std::unique_ptr ui; + Network::RoomNetwork& room_network; }; diff --git a/src/yuzu/multiplayer/direct_connect.cpp b/src/yuzu/multiplayer/direct_connect.cpp index 837baf85cc..360d66beae 100644 --- a/src/yuzu/multiplayer/direct_connect.cpp +++ b/src/yuzu/multiplayer/direct_connect.cpp @@ -21,9 +21,9 @@ enum class ConnectionType : u8 { TraversalServer, IP }; -DirectConnectWindow::DirectConnectWindow(QWidget* parent) +DirectConnectWindow::DirectConnectWindow(Network::RoomNetwork& room_network_, QWidget* parent) : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), - ui(std::make_unique()) { + ui(std::make_unique()), room_network{room_network_} { ui->setupUi(this); @@ -58,7 +58,7 @@ void DirectConnectWindow::Connect() { NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID); return; } - if (const auto member = Network::GetRoomMember().lock()) { + if (const auto member = room_network.GetRoomMember().lock()) { // Prevent the user from trying to join a room while they are already joining. if (member->GetState() == Network::RoomMember::State::Joining) { return; @@ -96,7 +96,7 @@ void DirectConnectWindow::Connect() { // attempt to connect in a different thread QFuture f = QtConcurrent::run([&] { - if (auto room_member = Network::GetRoomMember().lock()) { + if (auto room_member = room_network.GetRoomMember().lock()) { auto port = UISettings::values.multiplayer_port.GetValue(); room_member->Join(ui->nickname->text().toStdString(), "", ui->ip->text().toStdString().c_str(), port, 0, @@ -121,7 +121,7 @@ void DirectConnectWindow::EndConnecting() { void DirectConnectWindow::OnConnection() { EndConnecting(); - if (auto room_member = Network::GetRoomMember().lock()) { + if (auto room_member = room_network.GetRoomMember().lock()) { if (room_member->GetState() == Network::RoomMember::State::Joined || room_member->GetState() == Network::RoomMember::State::Moderator) { diff --git a/src/yuzu/multiplayer/direct_connect.h b/src/yuzu/multiplayer/direct_connect.h index e38961ed04..719030d29c 100644 --- a/src/yuzu/multiplayer/direct_connect.h +++ b/src/yuzu/multiplayer/direct_connect.h @@ -17,7 +17,7 @@ class DirectConnectWindow : public QDialog { Q_OBJECT public: - explicit DirectConnectWindow(QWidget* parent = nullptr); + explicit DirectConnectWindow(Network::RoomNetwork& room_network_, QWidget* parent = nullptr); ~DirectConnectWindow(); void RetranslateUi(); @@ -40,4 +40,5 @@ private: QFutureWatcher* watcher; std::unique_ptr ui; Validation validation; + Network::RoomNetwork& room_network; }; diff --git a/src/yuzu/multiplayer/host_room.cpp b/src/yuzu/multiplayer/host_room.cpp index 053e22fe4b..a48077544d 100644 --- a/src/yuzu/multiplayer/host_room.cpp +++ b/src/yuzu/multiplayer/host_room.cpp @@ -27,9 +27,11 @@ #endif HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, - std::shared_ptr session) + std::shared_ptr session, + Network::RoomNetwork& room_network_) : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), - ui(std::make_unique()), announce_multiplayer_session(session) { + ui(std::make_unique()), + announce_multiplayer_session(session), room_network{room_network_} { ui->setupUi(this); // set up validation for all of the fields @@ -120,7 +122,7 @@ void HostRoomWindow::Host() { NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::GAME_NOT_SELECTED); return; } - if (auto member = Network::GetRoomMember().lock()) { + if (auto member = room_network.GetRoomMember().lock()) { if (member->GetState() == Network::RoomMember::State::Joining) { return; } else if (member->IsConnected()) { @@ -144,7 +146,7 @@ void HostRoomWindow::Host() { if (ui->load_ban_list->isChecked()) { ban_list = UISettings::values.multiplayer_ban_list; } - if (auto room = Network::GetRoom().lock()) { + if (auto room = room_network.GetRoom().lock()) { const bool created = room->Create(ui->room_name->text().toStdString(), ui->room_description->toPlainText().toStdString(), "", port, password, @@ -173,7 +175,7 @@ void HostRoomWindow::Host() { QString::fromStdString(result.result_string), QMessageBox::Ok); ui->host->setEnabled(true); - if (auto room = Network::GetRoom().lock()) { + if (auto room = room_network.GetRoom().lock()) { room->Destroy(); } return; @@ -189,7 +191,7 @@ void HostRoomWindow::Host() { WebService::Client client(Settings::values.web_api_url.GetValue(), Settings::values.yuzu_username.GetValue(), Settings::values.yuzu_token.GetValue()); - if (auto room = Network::GetRoom().lock()) { + if (auto room = room_network.GetRoom().lock()) { token = client.GetExternalJWT(room->GetVerifyUID()).returned_data; } if (token.empty()) { diff --git a/src/yuzu/multiplayer/host_room.h b/src/yuzu/multiplayer/host_room.h index d84f93ffde..98a56458f9 100644 --- a/src/yuzu/multiplayer/host_room.h +++ b/src/yuzu/multiplayer/host_room.h @@ -35,7 +35,8 @@ class HostRoomWindow : public QDialog { public: explicit HostRoomWindow(QWidget* parent, QStandardItemModel* list, - std::shared_ptr session); + std::shared_ptr session, + Network::RoomNetwork& room_network_); ~HostRoomWindow(); /** @@ -54,6 +55,7 @@ private: QStandardItemModel* game_list; ComboBoxProxyModel* proxy; Validation validation; + Network::RoomNetwork& room_network; }; /** diff --git a/src/yuzu/multiplayer/lobby.cpp b/src/yuzu/multiplayer/lobby.cpp index 1b6b782d99..0c6648ab5d 100644 --- a/src/yuzu/multiplayer/lobby.cpp +++ b/src/yuzu/multiplayer/lobby.cpp @@ -23,9 +23,11 @@ #endif Lobby::Lobby(QWidget* parent, QStandardItemModel* list, - std::shared_ptr session) + std::shared_ptr session, + Network::RoomNetwork& room_network_) : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), - ui(std::make_unique()), announce_multiplayer_session(session) { + ui(std::make_unique()), + announce_multiplayer_session(session), room_network{room_network_} { ui->setupUi(this); // setup the watcher for background connections @@ -113,7 +115,7 @@ void Lobby::OnExpandRoom(const QModelIndex& index) { } void Lobby::OnJoinRoom(const QModelIndex& source) { - if (const auto member = Network::GetRoomMember().lock()) { + if (const auto member = room_network.GetRoomMember().lock()) { // Prevent the user from trying to join a room while they are already joining. if (member->GetState() == Network::RoomMember::State::Joining) { return; @@ -151,7 +153,7 @@ void Lobby::OnJoinRoom(const QModelIndex& source) { proxy->data(connection_index, LobbyItemHost::HostVerifyUIDRole).toString().toStdString(); // attempt to connect in a different thread - QFuture f = QtConcurrent::run([nickname, ip, port, password, verify_UID] { + QFuture f = QtConcurrent::run([nickname, ip, port, password, verify_UID, this] { std::string token; #ifdef ENABLE_WEB_SERVICE if (!Settings::values.yuzu_username.GetValue().empty() && @@ -167,7 +169,7 @@ void Lobby::OnJoinRoom(const QModelIndex& source) { } } #endif - if (auto room_member = Network::GetRoomMember().lock()) { + if (auto room_member = room_network.GetRoomMember().lock()) { room_member->Join(nickname, "", ip.c_str(), port, 0, Network::NoPreferredMac, password, token); } diff --git a/src/yuzu/multiplayer/lobby.h b/src/yuzu/multiplayer/lobby.h index aea4a0e4e4..ec6ec26628 100644 --- a/src/yuzu/multiplayer/lobby.h +++ b/src/yuzu/multiplayer/lobby.h @@ -30,7 +30,8 @@ class Lobby : public QDialog { public: explicit Lobby(QWidget* parent, QStandardItemModel* list, - std::shared_ptr session); + std::shared_ptr session, + Network::RoomNetwork& room_network_); ~Lobby() override; /** @@ -94,6 +95,7 @@ private: std::weak_ptr announce_multiplayer_session; QFutureWatcher* watcher; Validation validation; + Network::RoomNetwork& room_network; }; /** diff --git a/src/yuzu/multiplayer/moderation_dialog.cpp b/src/yuzu/multiplayer/moderation_dialog.cpp index e97f30ee5a..fc3f36c57e 100644 --- a/src/yuzu/multiplayer/moderation_dialog.cpp +++ b/src/yuzu/multiplayer/moderation_dialog.cpp @@ -17,13 +17,13 @@ enum { }; } -ModerationDialog::ModerationDialog(QWidget* parent) - : QDialog(parent), ui(std::make_unique()) { +ModerationDialog::ModerationDialog(Network::RoomNetwork& room_network_, QWidget* parent) + : QDialog(parent), ui(std::make_unique()), room_network{room_network_} { ui->setupUi(this); qRegisterMetaType(); - if (auto member = Network::GetRoomMember().lock()) { + if (auto member = room_network.GetRoomMember().lock()) { callback_handle_status_message = member->BindOnStatusMessageReceived( [this](const Network::StatusMessageEntry& status_message) { emit StatusMessageReceived(status_message); @@ -56,20 +56,20 @@ ModerationDialog::ModerationDialog(QWidget* parent) ModerationDialog::~ModerationDialog() { if (callback_handle_status_message) { - if (auto room = Network::GetRoomMember().lock()) { + if (auto room = room_network.GetRoomMember().lock()) { room->Unbind(callback_handle_status_message); } } if (callback_handle_ban_list) { - if (auto room = Network::GetRoomMember().lock()) { + if (auto room = room_network.GetRoomMember().lock()) { room->Unbind(callback_handle_ban_list); } } } void ModerationDialog::LoadBanList() { - if (auto room = Network::GetRoomMember().lock()) { + if (auto room = room_network.GetRoomMember().lock()) { ui->refresh->setEnabled(false); ui->refresh->setText(tr("Refreshing")); ui->unban->setEnabled(false); @@ -98,7 +98,7 @@ void ModerationDialog::PopulateBanList(const Network::Room::BanList& ban_list) { } void ModerationDialog::SendUnbanRequest(const QString& subject) { - if (auto room = Network::GetRoomMember().lock()) { + if (auto room = room_network.GetRoomMember().lock()) { room->SendModerationRequest(Network::IdModUnban, subject.toStdString()); } } diff --git a/src/yuzu/multiplayer/moderation_dialog.h b/src/yuzu/multiplayer/moderation_dialog.h index d10083d5b8..8adec0cd89 100644 --- a/src/yuzu/multiplayer/moderation_dialog.h +++ b/src/yuzu/multiplayer/moderation_dialog.h @@ -20,7 +20,7 @@ class ModerationDialog : public QDialog { Q_OBJECT public: - explicit ModerationDialog(QWidget* parent = nullptr); + explicit ModerationDialog(Network::RoomNetwork& room_network_, QWidget* parent = nullptr); ~ModerationDialog(); signals: @@ -37,6 +37,8 @@ private: QStandardItemModel* model; Network::RoomMember::CallbackHandle callback_handle_status_message; Network::RoomMember::CallbackHandle callback_handle_ban_list; + + Network::RoomNetwork& room_network; }; Q_DECLARE_METATYPE(Network::Room::BanList); diff --git a/src/yuzu/multiplayer/state.cpp b/src/yuzu/multiplayer/state.cpp index ee138739b2..de25225dda 100644 --- a/src/yuzu/multiplayer/state.cpp +++ b/src/yuzu/multiplayer/state.cpp @@ -20,10 +20,11 @@ #include "yuzu/util/clickable_label.h" MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_list_model_, - QAction* leave_room_, QAction* show_room_) + QAction* leave_room_, QAction* show_room_, + Network::RoomNetwork& room_network_) : QWidget(parent), game_list_model(game_list_model_), leave_room(leave_room_), - show_room(show_room_) { - if (auto member = Network::GetRoomMember().lock()) { + show_room(show_room_), room_network{room_network_} { + if (auto member = room_network.GetRoomMember().lock()) { // register the network structs to use in slots and signals state_callback_handle = member->BindOnStateChanged( [this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); }); @@ -37,7 +38,7 @@ MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_lis qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); - announce_multiplayer_session = std::make_shared(); + announce_multiplayer_session = std::make_shared(room_network); announce_multiplayer_session->BindErrorCallback( [this](const WebService::WebResult& result) { emit AnnounceFailed(result); }); connect(this, &MultiplayerState::AnnounceFailed, this, &MultiplayerState::OnAnnounceFailed); @@ -61,13 +62,13 @@ MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_lis MultiplayerState::~MultiplayerState() { if (state_callback_handle) { - if (auto member = Network::GetRoomMember().lock()) { + if (auto member = room_network.GetRoomMember().lock()) { member->Unbind(state_callback_handle); } } if (error_callback_handle) { - if (auto member = Network::GetRoomMember().lock()) { + if (auto member = room_network.GetRoomMember().lock()) { member->Unbind(error_callback_handle); } } @@ -205,14 +206,15 @@ static void BringWidgetToFront(QWidget* widget) { void MultiplayerState::OnViewLobby() { if (lobby == nullptr) { - lobby = new Lobby(this, game_list_model, announce_multiplayer_session); + lobby = new Lobby(this, game_list_model, announce_multiplayer_session, room_network); } BringWidgetToFront(lobby); } void MultiplayerState::OnCreateRoom() { if (host_room == nullptr) { - host_room = new HostRoomWindow(this, game_list_model, announce_multiplayer_session); + host_room = + new HostRoomWindow(this, game_list_model, announce_multiplayer_session, room_network); } BringWidgetToFront(host_room); } @@ -220,9 +222,9 @@ void MultiplayerState::OnCreateRoom() { bool MultiplayerState::OnCloseRoom() { if (!NetworkMessage::WarnCloseRoom()) return false; - if (auto room = Network::GetRoom().lock()) { + if (auto room = room_network.GetRoom().lock()) { // if you are in a room, leave it - if (auto member = Network::GetRoomMember().lock()) { + if (auto member = room_network.GetRoomMember().lock()) { member->Leave(); LOG_DEBUG(Frontend, "Left the room (as a client)"); } @@ -257,10 +259,10 @@ void MultiplayerState::HideNotification() { } void MultiplayerState::OnOpenNetworkRoom() { - if (auto member = Network::GetRoomMember().lock()) { + if (auto member = room_network.GetRoomMember().lock()) { if (member->IsConnected()) { if (client_room == nullptr) { - client_room = new ClientRoomWindow(this); + client_room = new ClientRoomWindow(this, room_network); connect(client_room, &ClientRoomWindow::ShowNotification, this, &MultiplayerState::ShowNotification); } @@ -275,7 +277,7 @@ void MultiplayerState::OnOpenNetworkRoom() { void MultiplayerState::OnDirectConnectToRoom() { if (direct_connect == nullptr) { - direct_connect = new DirectConnectWindow(this); + direct_connect = new DirectConnectWindow(room_network, this); } BringWidgetToFront(direct_connect); } diff --git a/src/yuzu/multiplayer/state.h b/src/yuzu/multiplayer/state.h index 414454acb9..bdd5cc954e 100644 --- a/src/yuzu/multiplayer/state.h +++ b/src/yuzu/multiplayer/state.h @@ -20,7 +20,7 @@ class MultiplayerState : public QWidget { public: explicit MultiplayerState(QWidget* parent, QStandardItemModel* game_list, QAction* leave_room, - QAction* show_room); + QAction* show_room, Network::RoomNetwork& room_network_); ~MultiplayerState(); /** @@ -87,6 +87,7 @@ private: Network::RoomMember::CallbackHandle error_callback_handle; bool show_notification = false; + Network::RoomNetwork& room_network; }; Q_DECLARE_METATYPE(WebService::WebResult); From 6b5667dfa55c2c0c9537a2f46537158e316d0508 Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Sat, 23 Jul 2022 19:28:19 +0200 Subject: [PATCH 47/50] yuzu_cmd: Fix compilation --- src/network/room_member.h | 12 ------------ src/yuzu_cmd/yuzu.cpp | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/network/room_member.h b/src/network/room_member.h index c835ba8635..8d6254023e 100644 --- a/src/network/room_member.h +++ b/src/network/room_member.h @@ -8,7 +8,6 @@ #include #include #include -#include #include "common/announce_multiplayer_room.h" #include "common/common_types.h" #include "network/room.h" @@ -35,17 +34,6 @@ struct WifiPacket { MacAddress transmitter_address; ///< Mac address of the transmitter. MacAddress destination_address; ///< Mac address of the receiver. u8 channel; ///< WiFi channel where this frame was transmitted. - -private: - template - void serialize(Archive& ar, const unsigned int) { - ar& type; - ar& data; - ar& transmitter_address; - ar& destination_address; - ar& channel; - } - friend class boost::serialization::access; }; /// Represents a chat message. diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index 0194940be4..e10d3f5b43 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -359,7 +359,7 @@ int main(int argc, char** argv) { system.TelemetrySession().AddField(Common::Telemetry::FieldType::App, "Frontend", "SDL"); if (use_multiplayer) { - if (auto member = Network::GetRoomMember().lock()) { + if (auto member = system.GetRoomNetwork().GetRoomMember().lock()) { member->BindOnChatMessageRecieved(OnMessageReceived); member->BindOnStatusMessageReceived(OnStatusMessageReceived); member->BindOnStateChanged(OnStateChanged); From 6a2dcc8b3d4ed0940e33d60fee701bcdb063eb6b Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Mon, 25 Jul 2022 17:08:20 +0200 Subject: [PATCH 48/50] network, yuzu: Improve variable naming and style consistency --- src/common/announce_multiplayer_room.h | 2 +- src/network/network.cpp | 28 +++++++++++++------------- src/network/network.h | 4 ++-- src/network/room.cpp | 16 +++++++-------- src/network/room_member.cpp | 6 ++++-- src/network/verify_user.cpp | 2 +- src/network/verify_user.h | 6 +++--- src/web_service/announce_room_json.cpp | 4 ++-- src/web_service/verify_user_jwt.cpp | 4 ++-- src/web_service/verify_user_jwt.h | 2 +- src/yuzu/multiplayer/host_room.cpp | 2 +- src/yuzu/multiplayer/lobby.cpp | 8 ++++---- src/yuzu/multiplayer/lobby_p.h | 4 ++-- src/yuzu/multiplayer/state.cpp | 12 +++++++---- 14 files changed, 53 insertions(+), 47 deletions(-) diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h index a9e2f89b7a..11a80aa8ee 100644 --- a/src/common/announce_multiplayer_room.h +++ b/src/common/announce_multiplayer_room.h @@ -43,7 +43,7 @@ struct Room { RoomInformation information; std::string id; - std::string verify_UID; ///< UID used for verification + std::string verify_uid; ///< UID used for verification std::string ip; u32 net_version; bool has_password; diff --git a/src/network/network.cpp b/src/network/network.cpp index e1401a4031..36b70c36f2 100644 --- a/src/network/network.cpp +++ b/src/network/network.cpp @@ -10,8 +10,8 @@ namespace Network { RoomNetwork::RoomNetwork() { - g_room = std::make_shared(); - g_room_member = std::make_shared(); + m_room = std::make_shared(); + m_room_member = std::make_shared(); } bool RoomNetwork::Init() { @@ -19,30 +19,30 @@ bool RoomNetwork::Init() { LOG_ERROR(Network, "Error initalizing ENet"); return false; } - g_room = std::make_shared(); - g_room_member = std::make_shared(); + m_room = std::make_shared(); + m_room_member = std::make_shared(); LOG_DEBUG(Network, "initialized OK"); return true; } std::weak_ptr RoomNetwork::GetRoom() { - return g_room; + return m_room; } std::weak_ptr RoomNetwork::GetRoomMember() { - return g_room_member; + return m_room_member; } void RoomNetwork::Shutdown() { - if (g_room_member) { - if (g_room_member->IsConnected()) - g_room_member->Leave(); - g_room_member.reset(); + if (m_room_member) { + if (m_room_member->IsConnected()) + m_room_member->Leave(); + m_room_member.reset(); } - if (g_room) { - if (g_room->GetState() == Room::State::Open) - g_room->Destroy(); - g_room.reset(); + if (m_room) { + if (m_room->GetState() == Room::State::Open) + m_room->Destroy(); + m_room.reset(); } enet_deinitialize(); LOG_DEBUG(Network, "shutdown OK"); diff --git a/src/network/network.h b/src/network/network.h index 74eb42bf5c..a38f040293 100644 --- a/src/network/network.h +++ b/src/network/network.h @@ -27,8 +27,8 @@ public: void Shutdown(); private: - std::shared_ptr g_room_member; ///< RoomMember (Client) for network games - std::shared_ptr g_room; ///< Room (Server) for network games + std::shared_ptr m_room_member; ///< RoomMember (Client) for network games + std::shared_ptr m_room; ///< Room (Server) for network games }; } // namespace Network diff --git a/src/network/room.cpp b/src/network/room.cpp index 22491b2996..b22c5fb89c 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -29,8 +29,8 @@ public: std::atomic state{State::Closed}; ///< Current state of the room. RoomInformation room_information; ///< Information about this room. - std::string verify_UID; ///< A GUID which may be used for verfication. - mutable std::mutex verify_UID_mutex; ///< Mutex for verify_UID + std::string verify_uid; ///< A GUID which may be used for verfication. + mutable std::mutex verify_uid_mutex; ///< Mutex for verify_uid std::string password; ///< The password required to connect to this room. @@ -369,8 +369,8 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { std::string uid; { - std::lock_guard lock(verify_UID_mutex); - uid = verify_UID; + std::lock_guard lock(verify_uid_mutex); + uid = verify_uid; } member.user_data = verify_backend->LoadUserData(uid, token); @@ -1056,8 +1056,8 @@ const RoomInformation& Room::GetRoomInformation() const { } std::string Room::GetVerifyUID() const { - std::lock_guard lock(room_impl->verify_UID_mutex); - return room_impl->verify_UID; + std::lock_guard lock(room_impl->verify_uid_mutex); + return room_impl->verify_uid; } Room::BanList Room::GetBanList() const { @@ -1086,8 +1086,8 @@ bool Room::HasPassword() const { } void Room::SetVerifyUID(const std::string& uid) { - std::lock_guard lock(room_impl->verify_UID_mutex); - room_impl->verify_UID = uid; + std::lock_guard lock(room_impl->verify_uid_mutex); + room_impl->verify_uid = uid; } void Room::Destroy() { diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp index 11a2e276ea..d8cb327210 100644 --- a/src/network/room_member.cpp +++ b/src/network/room_member.cpp @@ -416,8 +416,9 @@ void RoomMember::RoomMemberImpl::Disconnect() { room_information.member_slots = 0; room_information.name.clear(); - if (!server) + if (!server) { return; + } enet_peer_disconnect(server, 0); ENetEvent event; @@ -483,8 +484,9 @@ template void RoomMember::RoomMemberImpl::Invoke(const T& data) { std::lock_guard lock(callback_mutex); CallbackSet callback_set = callbacks.Get(); - for (auto const& callback : callback_set) + for (auto const& callback : callback_set) { (*callback)(data); + } } template diff --git a/src/network/verify_user.cpp b/src/network/verify_user.cpp index d9d98e4955..51094e9bc4 100644 --- a/src/network/verify_user.cpp +++ b/src/network/verify_user.cpp @@ -10,7 +10,7 @@ Backend::~Backend() = default; NullBackend::~NullBackend() = default; -UserData NullBackend::LoadUserData([[maybe_unused]] const std::string& verify_UID, +UserData NullBackend::LoadUserData([[maybe_unused]] const std::string& verify_uid, [[maybe_unused]] const std::string& token) { return {}; } diff --git a/src/network/verify_user.h b/src/network/verify_user.h index 5c3852d4aa..ddae67e99a 100644 --- a/src/network/verify_user.h +++ b/src/network/verify_user.h @@ -25,11 +25,11 @@ public: /** * Verifies the given token and loads the information into a UserData struct. - * @param verify_UID A GUID that may be used for verification. + * @param verify_uid A GUID that may be used for verification. * @param token A token that contains user data and verification data. The format and content is * decided by backends. */ - virtual UserData LoadUserData(const std::string& verify_UID, const std::string& token) = 0; + virtual UserData LoadUserData(const std::string& verify_uid, const std::string& token) = 0; }; /** @@ -40,7 +40,7 @@ class NullBackend final : public Backend { public: ~NullBackend(); - UserData LoadUserData(const std::string& verify_UID, const std::string& token) override; + UserData LoadUserData(const std::string& verify_uid, const std::string& token) override; }; } // namespace Network::VerifyUser diff --git a/src/web_service/announce_room_json.cpp b/src/web_service/announce_room_json.cpp index 082bebaa92..0aae8e2155 100644 --- a/src/web_service/announce_room_json.cpp +++ b/src/web_service/announce_room_json.cpp @@ -54,7 +54,7 @@ static void to_json(nlohmann::json& json, const Room& room) { } static void from_json(const nlohmann::json& json, Room& room) { - room.verify_UID = json.at("externalGuid").get(); + room.verify_uid = json.at("externalGuid").get(); room.ip = json.at("address").get(); room.information.name = json.at("name").get(); try { @@ -116,7 +116,7 @@ WebService::WebResult RoomJson::Register() { auto reply_json = nlohmann::json::parse(result.returned_data); room = reply_json.get(); room_id = reply_json.at("id").get(); - return WebService::WebResult{WebService::WebResult::Code::Success, "", room.verify_UID}; + return WebService::WebResult{WebService::WebResult::Code::Success, "", room.verify_uid}; } void RoomJson::ClearPlayers() { diff --git a/src/web_service/verify_user_jwt.cpp b/src/web_service/verify_user_jwt.cpp index 3133dcbe26..2f294d378a 100644 --- a/src/web_service/verify_user_jwt.cpp +++ b/src/web_service/verify_user_jwt.cpp @@ -35,9 +35,9 @@ std::string GetPublicKey(const std::string& host) { VerifyUserJWT::VerifyUserJWT(const std::string& host) : pub_key(GetPublicKey(host)) {} -Network::VerifyUser::UserData VerifyUserJWT::LoadUserData(const std::string& verify_UID, +Network::VerifyUser::UserData VerifyUserJWT::LoadUserData(const std::string& verify_uid, const std::string& token) { - const std::string audience = fmt::format("external-{}", verify_UID); + const std::string audience = fmt::format("external-{}", verify_uid); using namespace jwt::params; std::error_code error; auto decoded = diff --git a/src/web_service/verify_user_jwt.h b/src/web_service/verify_user_jwt.h index 6db74c2089..ec3cc29043 100644 --- a/src/web_service/verify_user_jwt.h +++ b/src/web_service/verify_user_jwt.h @@ -17,7 +17,7 @@ public: VerifyUserJWT(const std::string& host); ~VerifyUserJWT() = default; - Network::VerifyUser::UserData LoadUserData(const std::string& verify_UID, + Network::VerifyUser::UserData LoadUserData(const std::string& verify_uid, const std::string& token) override; private: diff --git a/src/yuzu/multiplayer/host_room.cpp b/src/yuzu/multiplayer/host_room.cpp index a48077544d..f59c6a28d5 100644 --- a/src/yuzu/multiplayer/host_room.cpp +++ b/src/yuzu/multiplayer/host_room.cpp @@ -163,7 +163,7 @@ void HostRoomWindow::Host() { // Start the announce session if they chose Public if (is_public) { if (auto session = announce_multiplayer_session.lock()) { - // Register the room first to ensure verify_UID is present when we connect + // Register the room first to ensure verify_uid is present when we connect WebService::WebResult result = session->Register(); if (result.result_code != WebService::WebResult::Code::Success) { QMessageBox::warning( diff --git a/src/yuzu/multiplayer/lobby.cpp b/src/yuzu/multiplayer/lobby.cpp index 0c6648ab5d..6daef97129 100644 --- a/src/yuzu/multiplayer/lobby.cpp +++ b/src/yuzu/multiplayer/lobby.cpp @@ -149,11 +149,11 @@ void Lobby::OnJoinRoom(const QModelIndex& source) { const std::string ip = proxy->data(connection_index, LobbyItemHost::HostIPRole).toString().toStdString(); int port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt(); - const std::string verify_UID = + const std::string verify_uid = proxy->data(connection_index, LobbyItemHost::HostVerifyUIDRole).toString().toStdString(); // attempt to connect in a different thread - QFuture f = QtConcurrent::run([nickname, ip, port, password, verify_UID, this] { + QFuture f = QtConcurrent::run([nickname, ip, port, password, verify_uid, this] { std::string token; #ifdef ENABLE_WEB_SERVICE if (!Settings::values.yuzu_username.GetValue().empty() && @@ -161,7 +161,7 @@ void Lobby::OnJoinRoom(const QModelIndex& source) { WebService::Client client(Settings::values.web_api_url.GetValue(), Settings::values.yuzu_username.GetValue(), Settings::values.yuzu_token.GetValue()); - token = client.GetExternalJWT(verify_UID).returned_data; + token = client.GetExternalJWT(verify_uid).returned_data; if (token.empty()) { LOG_ERROR(WebService, "Could not get external JWT, verification may fail"); } else { @@ -239,7 +239,7 @@ void Lobby::OnRefreshLobby() { smdh_icon), new LobbyItemHost(QString::fromStdString(room.information.host_username), QString::fromStdString(room.ip), room.information.port, - QString::fromStdString(room.verify_UID)), + QString::fromStdString(room.verify_uid)), new LobbyItemMemberList(members, room.information.member_slots), }); model->appendRow(row); diff --git a/src/yuzu/multiplayer/lobby_p.h b/src/yuzu/multiplayer/lobby_p.h index afb8b99dc0..bb2de4af31 100644 --- a/src/yuzu/multiplayer/lobby_p.h +++ b/src/yuzu/multiplayer/lobby_p.h @@ -123,11 +123,11 @@ public: static const int HostVerifyUIDRole = Qt::UserRole + 4; LobbyItemHost() = default; - explicit LobbyItemHost(QString username, QString ip, u16 port, QString verify_UID) { + explicit LobbyItemHost(QString username, QString ip, u16 port, QString verify_uid) { setData(username, HostUsernameRole); setData(ip, HostIPRole); setData(port, HostPortRole); - setData(verify_UID, HostVerifyUIDRole); + setData(verify_uid, HostVerifyUIDRole); } QVariant data(int role) const override { diff --git a/src/yuzu/multiplayer/state.cpp b/src/yuzu/multiplayer/state.cpp index de25225dda..661a32b3e5 100644 --- a/src/yuzu/multiplayer/state.cpp +++ b/src/yuzu/multiplayer/state.cpp @@ -98,14 +98,18 @@ void MultiplayerState::retranslateUi() { status_text->setText(tr("Not Connected")); } - if (lobby) + if (lobby) { lobby->RetranslateUi(); - if (host_room) + } + if (host_room) { host_room->RetranslateUi(); - if (client_room) + } + if (client_room) { client_room->RetranslateUi(); - if (direct_connect) + } + if (direct_connect) { direct_connect->RetranslateUi(); + } } void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& state) { From 61ce57b5242984c297283de5868ea4938391a911 Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Mon, 25 Jul 2022 17:18:30 +0200 Subject: [PATCH 49/50] network, yuzu: Make copyright headers SPDX-compliant --- src/common/announce_multiplayer_room.h | 5 ++--- src/core/announce_multiplayer_session.cpp | 5 ++--- src/core/announce_multiplayer_session.h | 5 ++--- src/network/network.cpp | 5 ++--- src/network/network.h | 5 ++--- src/network/packet.cpp | 5 ++--- src/network/packet.h | 5 ++--- src/network/room.cpp | 5 ++--- src/network/room.h | 5 ++--- src/network/room_member.cpp | 5 ++--- src/network/room_member.h | 5 ++--- src/network/verify_user.cpp | 5 ++--- src/network/verify_user.h | 5 ++--- src/web_service/announce_room_json.cpp | 5 ++--- src/web_service/announce_room_json.h | 5 ++--- src/web_service/verify_user_jwt.cpp | 5 ++--- src/web_service/verify_user_jwt.h | 5 ++--- src/yuzu/multiplayer/chat_room.cpp | 5 ++--- src/yuzu/multiplayer/chat_room.h | 5 ++--- src/yuzu/multiplayer/client_room.cpp | 5 ++--- src/yuzu/multiplayer/client_room.h | 5 ++--- src/yuzu/multiplayer/direct_connect.cpp | 5 ++--- src/yuzu/multiplayer/direct_connect.h | 5 ++--- src/yuzu/multiplayer/host_room.cpp | 5 ++--- src/yuzu/multiplayer/host_room.h | 5 ++--- src/yuzu/multiplayer/lobby.cpp | 5 ++--- src/yuzu/multiplayer/lobby.h | 5 ++--- src/yuzu/multiplayer/lobby_p.h | 5 ++--- src/yuzu/multiplayer/message.cpp | 5 ++--- src/yuzu/multiplayer/message.h | 5 ++--- src/yuzu/multiplayer/moderation_dialog.cpp | 5 ++--- src/yuzu/multiplayer/moderation_dialog.h | 5 ++--- src/yuzu/multiplayer/state.cpp | 5 ++--- src/yuzu/multiplayer/state.h | 5 ++--- src/yuzu/multiplayer/validation.h | 5 ++--- src/yuzu/util/clickable_label.cpp | 5 ++--- src/yuzu/util/clickable_label.h | 5 ++--- 37 files changed, 74 insertions(+), 111 deletions(-) diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h index 11a80aa8ee..0ad9da2be9 100644 --- a/src/common/announce_multiplayer_room.h +++ b/src/common/announce_multiplayer_room.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/core/announce_multiplayer_session.cpp b/src/core/announce_multiplayer_session.cpp index 8f96b4ee87..d73a488cf2 100644 --- a/src/core/announce_multiplayer_session.cpp +++ b/src/core/announce_multiplayer_session.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include #include diff --git a/src/core/announce_multiplayer_session.h b/src/core/announce_multiplayer_session.h index 5da3c1f8d0..db790f7d2d 100644 --- a/src/core/announce_multiplayer_session.h +++ b/src/core/announce_multiplayer_session.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/network/network.cpp b/src/network/network.cpp index 36b70c36f2..0841e41347 100644 --- a/src/network/network.cpp +++ b/src/network/network.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include "common/assert.h" #include "common/logging/log.h" diff --git a/src/network/network.h b/src/network/network.h index a38f040293..e4de207b27 100644 --- a/src/network/network.h +++ b/src/network/network.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/network/packet.cpp b/src/network/packet.cpp index a3c1e16440..d6c8dc6463 100644 --- a/src/network/packet.cpp +++ b/src/network/packet.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #ifdef _WIN32 #include diff --git a/src/network/packet.h b/src/network/packet.h index 7bdc3da958..aa9fa39e2d 100644 --- a/src/network/packet.h +++ b/src/network/packet.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/network/room.cpp b/src/network/room.cpp index b22c5fb89c..d5f0bb723d 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include #include diff --git a/src/network/room.h b/src/network/room.h index 90a82563f2..6f7e3b5b53 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp index d8cb327210..38a6f6bfd7 100644 --- a/src/network/room_member.cpp +++ b/src/network/room_member.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include #include diff --git a/src/network/room_member.h b/src/network/room_member.h index 8d6254023e..bbb7d13d4c 100644 --- a/src/network/room_member.h +++ b/src/network/room_member.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/network/verify_user.cpp b/src/network/verify_user.cpp index 51094e9bc4..f84cfe59b6 100644 --- a/src/network/verify_user.cpp +++ b/src/network/verify_user.cpp @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include "network/verify_user.h" diff --git a/src/network/verify_user.h b/src/network/verify_user.h index ddae67e99a..6fc64d8a3e 100644 --- a/src/network/verify_user.h +++ b/src/network/verify_user.h @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/web_service/announce_room_json.cpp b/src/web_service/announce_room_json.cpp index 0aae8e2155..4c3195efda 100644 --- a/src/web_service/announce_room_json.cpp +++ b/src/web_service/announce_room_json.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include #include diff --git a/src/web_service/announce_room_json.h b/src/web_service/announce_room_json.h index 24ec29c65e..32c08858dd 100644 --- a/src/web_service/announce_room_json.h +++ b/src/web_service/announce_room_json.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/web_service/verify_user_jwt.cpp b/src/web_service/verify_user_jwt.cpp index 2f294d378a..3bff46f0ac 100644 --- a/src/web_service/verify_user_jwt.cpp +++ b/src/web_service/verify_user_jwt.cpp @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic push diff --git a/src/web_service/verify_user_jwt.h b/src/web_service/verify_user_jwt.h index ec3cc29043..27b0a100c6 100644 --- a/src/web_service/verify_user_jwt.h +++ b/src/web_service/verify_user_jwt.h @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/multiplayer/chat_room.cpp b/src/yuzu/multiplayer/chat_room.cpp index 9f2c57eee3..5837b36ab5 100644 --- a/src/yuzu/multiplayer/chat_room.cpp +++ b/src/yuzu/multiplayer/chat_room.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include #include diff --git a/src/yuzu/multiplayer/chat_room.h b/src/yuzu/multiplayer/chat_room.h index 9179d16fbb..01c70fad07 100644 --- a/src/yuzu/multiplayer/chat_room.h +++ b/src/yuzu/multiplayer/chat_room.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/multiplayer/client_room.cpp b/src/yuzu/multiplayer/client_room.cpp index 9bef9bdfcd..a9859ed702 100644 --- a/src/yuzu/multiplayer/client_room.cpp +++ b/src/yuzu/multiplayer/client_room.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include #include diff --git a/src/yuzu/multiplayer/client_room.h b/src/yuzu/multiplayer/client_room.h index 6303b2595f..f338e3c592 100644 --- a/src/yuzu/multiplayer/client_room.h +++ b/src/yuzu/multiplayer/client_room.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/multiplayer/direct_connect.cpp b/src/yuzu/multiplayer/direct_connect.cpp index 360d66beae..9000c45312 100644 --- a/src/yuzu/multiplayer/direct_connect.cpp +++ b/src/yuzu/multiplayer/direct_connect.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include #include diff --git a/src/yuzu/multiplayer/direct_connect.h b/src/yuzu/multiplayer/direct_connect.h index 719030d29c..4e10430532 100644 --- a/src/yuzu/multiplayer/direct_connect.h +++ b/src/yuzu/multiplayer/direct_connect.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/multiplayer/host_room.cpp b/src/yuzu/multiplayer/host_room.cpp index f59c6a28d5..cb9464b2bc 100644 --- a/src/yuzu/multiplayer/host_room.cpp +++ b/src/yuzu/multiplayer/host_room.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include #include diff --git a/src/yuzu/multiplayer/host_room.h b/src/yuzu/multiplayer/host_room.h index 98a56458f9..a968042d03 100644 --- a/src/yuzu/multiplayer/host_room.h +++ b/src/yuzu/multiplayer/host_room.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/multiplayer/lobby.cpp b/src/yuzu/multiplayer/lobby.cpp index 6daef97129..23c2f21ab0 100644 --- a/src/yuzu/multiplayer/lobby.cpp +++ b/src/yuzu/multiplayer/lobby.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include #include diff --git a/src/yuzu/multiplayer/lobby.h b/src/yuzu/multiplayer/lobby.h index ec6ec26628..82744ca945 100644 --- a/src/yuzu/multiplayer/lobby.h +++ b/src/yuzu/multiplayer/lobby.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/multiplayer/lobby_p.h b/src/yuzu/multiplayer/lobby_p.h index bb2de4af31..8071cede4d 100644 --- a/src/yuzu/multiplayer/lobby_p.h +++ b/src/yuzu/multiplayer/lobby_p.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/multiplayer/message.cpp b/src/yuzu/multiplayer/message.cpp index 458f1e7d1f..76ec276ad7 100644 --- a/src/yuzu/multiplayer/message.cpp +++ b/src/yuzu/multiplayer/message.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include #include diff --git a/src/yuzu/multiplayer/message.h b/src/yuzu/multiplayer/message.h index 49a31997dd..eb5c8d1be5 100644 --- a/src/yuzu/multiplayer/message.h +++ b/src/yuzu/multiplayer/message.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/multiplayer/moderation_dialog.cpp b/src/yuzu/multiplayer/moderation_dialog.cpp index fc3f36c57e..c9b8ed397a 100644 --- a/src/yuzu/multiplayer/moderation_dialog.cpp +++ b/src/yuzu/multiplayer/moderation_dialog.cpp @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include #include diff --git a/src/yuzu/multiplayer/moderation_dialog.h b/src/yuzu/multiplayer/moderation_dialog.h index 8adec0cd89..e9e5daff79 100644 --- a/src/yuzu/multiplayer/moderation_dialog.h +++ b/src/yuzu/multiplayer/moderation_dialog.h @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/multiplayer/state.cpp b/src/yuzu/multiplayer/state.cpp index 661a32b3e5..015c59788a 100644 --- a/src/yuzu/multiplayer/state.cpp +++ b/src/yuzu/multiplayer/state.cpp @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include #include diff --git a/src/yuzu/multiplayer/state.h b/src/yuzu/multiplayer/state.h index bdd5cc954e..9c60712d51 100644 --- a/src/yuzu/multiplayer/state.h +++ b/src/yuzu/multiplayer/state.h @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/multiplayer/validation.h b/src/yuzu/multiplayer/validation.h index 1c215a1909..7d48e589d4 100644 --- a/src/yuzu/multiplayer/validation.h +++ b/src/yuzu/multiplayer/validation.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/util/clickable_label.cpp b/src/yuzu/util/clickable_label.cpp index 5bde838ca4..89d14190ac 100644 --- a/src/yuzu/util/clickable_label.cpp +++ b/src/yuzu/util/clickable_label.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include "yuzu/util/clickable_label.h" diff --git a/src/yuzu/util/clickable_label.h b/src/yuzu/util/clickable_label.h index 3c65a74bec..4fe7441506 100644 --- a/src/yuzu/util/clickable_label.h +++ b/src/yuzu/util/clickable_label.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once From a41baaa181f30229d3552caa69135be978c1ddb5 Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Mon, 25 Jul 2022 19:16:59 +0200 Subject: [PATCH 50/50] network: Address review comments --- src/network/packet.cpp | 64 +++++++-------- src/network/packet.h | 84 ++++++++++---------- src/network/room.cpp | 140 ++++++++++++++++----------------- src/network/room_member.cpp | 98 +++++++++++------------ src/yuzu/multiplayer/state.cpp | 12 ++- 5 files changed, 201 insertions(+), 197 deletions(-) diff --git a/src/network/packet.cpp b/src/network/packet.cpp index d6c8dc6463..0e22f1eb4d 100644 --- a/src/network/packet.cpp +++ b/src/network/packet.cpp @@ -65,80 +65,80 @@ Packet::operator bool() const { return is_valid; } -Packet& Packet::operator>>(bool& out_data) { +Packet& Packet::Read(bool& out_data) { u8 value{}; - if (*this >> value) { + if (Read(value)) { out_data = (value != 0); } return *this; } -Packet& Packet::operator>>(s8& out_data) { +Packet& Packet::Read(s8& out_data) { Read(&out_data, sizeof(out_data)); return *this; } -Packet& Packet::operator>>(u8& out_data) { +Packet& Packet::Read(u8& out_data) { Read(&out_data, sizeof(out_data)); return *this; } -Packet& Packet::operator>>(s16& out_data) { +Packet& Packet::Read(s16& out_data) { s16 value{}; Read(&value, sizeof(value)); out_data = ntohs(value); return *this; } -Packet& Packet::operator>>(u16& out_data) { +Packet& Packet::Read(u16& out_data) { u16 value{}; Read(&value, sizeof(value)); out_data = ntohs(value); return *this; } -Packet& Packet::operator>>(s32& out_data) { +Packet& Packet::Read(s32& out_data) { s32 value{}; Read(&value, sizeof(value)); out_data = ntohl(value); return *this; } -Packet& Packet::operator>>(u32& out_data) { +Packet& Packet::Read(u32& out_data) { u32 value{}; Read(&value, sizeof(value)); out_data = ntohl(value); return *this; } -Packet& Packet::operator>>(s64& out_data) { +Packet& Packet::Read(s64& out_data) { s64 value{}; Read(&value, sizeof(value)); out_data = ntohll(value); return *this; } -Packet& Packet::operator>>(u64& out_data) { +Packet& Packet::Read(u64& out_data) { u64 value{}; Read(&value, sizeof(value)); out_data = ntohll(value); return *this; } -Packet& Packet::operator>>(float& out_data) { +Packet& Packet::Read(float& out_data) { Read(&out_data, sizeof(out_data)); return *this; } -Packet& Packet::operator>>(double& out_data) { +Packet& Packet::Read(double& out_data) { Read(&out_data, sizeof(out_data)); return *this; } -Packet& Packet::operator>>(char* out_data) { +Packet& Packet::Read(char* out_data) { // First extract string length u32 length = 0; - *this >> length; + Read(length); if ((length > 0) && CheckSize(length)) { // Then extract characters @@ -152,10 +152,10 @@ Packet& Packet::operator>>(char* out_data) { return *this; } -Packet& Packet::operator>>(std::string& out_data) { +Packet& Packet::Read(std::string& out_data) { // First extract string length u32 length = 0; - *this >> length; + Read(length); out_data.clear(); if ((length > 0) && CheckSize(length)) { @@ -169,71 +169,71 @@ Packet& Packet::operator>>(std::string& out_data) { return *this; } -Packet& Packet::operator<<(bool in_data) { - *this << static_cast(in_data); +Packet& Packet::Write(bool in_data) { + Write(static_cast(in_data)); return *this; } -Packet& Packet::operator<<(s8 in_data) { +Packet& Packet::Write(s8 in_data) { Append(&in_data, sizeof(in_data)); return *this; } -Packet& Packet::operator<<(u8 in_data) { +Packet& Packet::Write(u8 in_data) { Append(&in_data, sizeof(in_data)); return *this; } -Packet& Packet::operator<<(s16 in_data) { +Packet& Packet::Write(s16 in_data) { s16 toWrite = htons(in_data); Append(&toWrite, sizeof(toWrite)); return *this; } -Packet& Packet::operator<<(u16 in_data) { +Packet& Packet::Write(u16 in_data) { u16 toWrite = htons(in_data); Append(&toWrite, sizeof(toWrite)); return *this; } -Packet& Packet::operator<<(s32 in_data) { +Packet& Packet::Write(s32 in_data) { s32 toWrite = htonl(in_data); Append(&toWrite, sizeof(toWrite)); return *this; } -Packet& Packet::operator<<(u32 in_data) { +Packet& Packet::Write(u32 in_data) { u32 toWrite = htonl(in_data); Append(&toWrite, sizeof(toWrite)); return *this; } -Packet& Packet::operator<<(s64 in_data) { +Packet& Packet::Write(s64 in_data) { s64 toWrite = htonll(in_data); Append(&toWrite, sizeof(toWrite)); return *this; } -Packet& Packet::operator<<(u64 in_data) { +Packet& Packet::Write(u64 in_data) { u64 toWrite = htonll(in_data); Append(&toWrite, sizeof(toWrite)); return *this; } -Packet& Packet::operator<<(float in_data) { +Packet& Packet::Write(float in_data) { Append(&in_data, sizeof(in_data)); return *this; } -Packet& Packet::operator<<(double in_data) { +Packet& Packet::Write(double in_data) { Append(&in_data, sizeof(in_data)); return *this; } -Packet& Packet::operator<<(const char* in_data) { +Packet& Packet::Write(const char* in_data) { // First insert string length u32 length = static_cast(std::strlen(in_data)); - *this << length; + Write(length); // Then insert characters Append(in_data, length * sizeof(char)); @@ -241,10 +241,10 @@ Packet& Packet::operator<<(const char* in_data) { return *this; } -Packet& Packet::operator<<(const std::string& in_data) { +Packet& Packet::Write(const std::string& in_data) { // First insert string length u32 length = static_cast(in_data.size()); - *this << length; + Write(length); // Then insert characters if (length > 0) diff --git a/src/network/packet.h b/src/network/packet.h index aa9fa39e2d..e69217488c 100644 --- a/src/network/packet.h +++ b/src/network/packet.h @@ -63,43 +63,43 @@ public: explicit operator bool() const; - /// Overloads of operator >> to read data from the packet - Packet& operator>>(bool& out_data); - Packet& operator>>(s8& out_data); - Packet& operator>>(u8& out_data); - Packet& operator>>(s16& out_data); - Packet& operator>>(u16& out_data); - Packet& operator>>(s32& out_data); - Packet& operator>>(u32& out_data); - Packet& operator>>(s64& out_data); - Packet& operator>>(u64& out_data); - Packet& operator>>(float& out_data); - Packet& operator>>(double& out_data); - Packet& operator>>(char* out_data); - Packet& operator>>(std::string& out_data); + /// Overloads of read function to read data from the packet + Packet& Read(bool& out_data); + Packet& Read(s8& out_data); + Packet& Read(u8& out_data); + Packet& Read(s16& out_data); + Packet& Read(u16& out_data); + Packet& Read(s32& out_data); + Packet& Read(u32& out_data); + Packet& Read(s64& out_data); + Packet& Read(u64& out_data); + Packet& Read(float& out_data); + Packet& Read(double& out_data); + Packet& Read(char* out_data); + Packet& Read(std::string& out_data); template - Packet& operator>>(std::vector& out_data); + Packet& Read(std::vector& out_data); template - Packet& operator>>(std::array& out_data); + Packet& Read(std::array& out_data); - /// Overloads of operator << to write data into the packet - Packet& operator<<(bool in_data); - Packet& operator<<(s8 in_data); - Packet& operator<<(u8 in_data); - Packet& operator<<(s16 in_data); - Packet& operator<<(u16 in_data); - Packet& operator<<(s32 in_data); - Packet& operator<<(u32 in_data); - Packet& operator<<(s64 in_data); - Packet& operator<<(u64 in_data); - Packet& operator<<(float in_data); - Packet& operator<<(double in_data); - Packet& operator<<(const char* in_data); - Packet& operator<<(const std::string& in_data); + /// Overloads of write function to write data into the packet + Packet& Write(bool in_data); + Packet& Write(s8 in_data); + Packet& Write(u8 in_data); + Packet& Write(s16 in_data); + Packet& Write(u16 in_data); + Packet& Write(s32 in_data); + Packet& Write(u32 in_data); + Packet& Write(s64 in_data); + Packet& Write(u64 in_data); + Packet& Write(float in_data); + Packet& Write(double in_data); + Packet& Write(const char* in_data); + Packet& Write(const std::string& in_data); template - Packet& operator<<(const std::vector& in_data); + Packet& Write(const std::vector& in_data); template - Packet& operator<<(const std::array& data); + Packet& Write(const std::array& data); private: /** @@ -117,47 +117,47 @@ private: }; template -Packet& Packet::operator>>(std::vector& out_data) { +Packet& Packet::Read(std::vector& out_data) { // First extract the size u32 size = 0; - *this >> size; + Read(size); out_data.resize(size); // Then extract the data for (std::size_t i = 0; i < out_data.size(); ++i) { T character; - *this >> character; + Read(character); out_data[i] = character; } return *this; } template -Packet& Packet::operator>>(std::array& out_data) { +Packet& Packet::Read(std::array& out_data) { for (std::size_t i = 0; i < out_data.size(); ++i) { T character; - *this >> character; + Read(character); out_data[i] = character; } return *this; } template -Packet& Packet::operator<<(const std::vector& in_data) { +Packet& Packet::Write(const std::vector& in_data) { // First insert the size - *this << static_cast(in_data.size()); + Write(static_cast(in_data.size())); // Then insert the data for (std::size_t i = 0; i < in_data.size(); ++i) { - *this << in_data[i]; + Write(in_data[i]); } return *this; } template -Packet& Packet::operator<<(const std::array& in_data) { +Packet& Packet::Write(const std::array& in_data) { for (std::size_t i = 0; i < in_data.size(); ++i) { - *this << in_data[i]; + Write(in_data[i]); } return *this; } diff --git a/src/network/room.cpp b/src/network/room.cpp index d5f0bb723d..3fc3a0383c 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include "common/logging/log.h" @@ -43,9 +44,8 @@ public: ENetPeer* peer; ///< The remote peer. }; using MemberList = std::vector; - MemberList members; ///< Information about the members of this room - mutable std::mutex member_mutex; ///< Mutex for locking the members list - /// This should be a std::shared_mutex as soon as C++17 is supported + MemberList members; ///< Information about the members of this room + mutable std::shared_mutex member_mutex; ///< Mutex for locking the members list UsernameBanList username_ban_list; ///< List of banned usernames IPBanList ip_ban_list; ///< List of banned IP addresses @@ -311,22 +311,22 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { packet.Append(event->packet->data, event->packet->dataLength); packet.IgnoreBytes(sizeof(u8)); // Ignore the message type std::string nickname; - packet >> nickname; + packet.Read(nickname); std::string console_id_hash; - packet >> console_id_hash; + packet.Read(console_id_hash); MacAddress preferred_mac; - packet >> preferred_mac; + packet.Read(preferred_mac); u32 client_version; - packet >> client_version; + packet.Read(client_version); std::string pass; - packet >> pass; + packet.Read(pass); std::string token; - packet >> token; + packet.Read(token); if (pass != password) { SendWrongPassword(event->peer); @@ -387,9 +387,9 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { } // Check IP ban - char ip_raw[256]; - enet_address_get_host_ip(&event->peer->address, ip_raw, sizeof(ip_raw) - 1); - ip = ip_raw; + std::array ip_raw{}; + enet_address_get_host_ip(&event->peer->address, ip_raw.data(), sizeof(ip_raw) - 1); + ip = ip_raw.data(); if (std::find(ip_ban_list.begin(), ip_ban_list.end(), ip) != ip_ban_list.end()) { SendUserBanned(event->peer); @@ -425,7 +425,7 @@ void Room::RoomImpl::HandleModKickPacket(const ENetEvent* event) { packet.IgnoreBytes(sizeof(u8)); // Ignore the message type std::string nickname; - packet >> nickname; + packet.Read(nickname); std::string username, ip; { @@ -443,9 +443,9 @@ void Room::RoomImpl::HandleModKickPacket(const ENetEvent* event) { username = target_member->user_data.username; - char ip_raw[256]; - enet_address_get_host_ip(&target_member->peer->address, ip_raw, sizeof(ip_raw) - 1); - ip = ip_raw; + std::array ip_raw{}; + enet_address_get_host_ip(&target_member->peer->address, ip_raw.data(), sizeof(ip_raw) - 1); + ip = ip_raw.data(); enet_peer_disconnect(target_member->peer, 0); members.erase(target_member); @@ -467,7 +467,7 @@ void Room::RoomImpl::HandleModBanPacket(const ENetEvent* event) { packet.IgnoreBytes(sizeof(u8)); // Ignore the message type std::string nickname; - packet >> nickname; + packet.Read(nickname); std::string username, ip; { @@ -486,9 +486,9 @@ void Room::RoomImpl::HandleModBanPacket(const ENetEvent* event) { nickname = target_member->nickname; username = target_member->user_data.username; - char ip_raw[256]; - enet_address_get_host_ip(&target_member->peer->address, ip_raw, sizeof(ip_raw) - 1); - ip = ip_raw; + std::array ip_raw{}; + enet_address_get_host_ip(&target_member->peer->address, ip_raw.data(), sizeof(ip_raw) - 1); + ip = ip_raw.data(); enet_peer_disconnect(target_member->peer, 0); members.erase(target_member); @@ -528,7 +528,7 @@ void Room::RoomImpl::HandleModUnbanPacket(const ENetEvent* event) { packet.IgnoreBytes(sizeof(u8)); // Ignore the message type std::string address; - packet >> address; + packet.Read(address); bool unbanned = false; { @@ -613,7 +613,7 @@ bool Room::RoomImpl::HasModPermission(const ENetPeer* client) const { void Room::RoomImpl::SendNameCollision(ENetPeer* client) { Packet packet; - packet << static_cast(IdNameCollision); + packet.Write(static_cast(IdNameCollision)); ENetPacket* enet_packet = enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); @@ -623,7 +623,7 @@ void Room::RoomImpl::SendNameCollision(ENetPeer* client) { void Room::RoomImpl::SendMacCollision(ENetPeer* client) { Packet packet; - packet << static_cast(IdMacCollision); + packet.Write(static_cast(IdMacCollision)); ENetPacket* enet_packet = enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); @@ -633,7 +633,7 @@ void Room::RoomImpl::SendMacCollision(ENetPeer* client) { void Room::RoomImpl::SendConsoleIdCollision(ENetPeer* client) { Packet packet; - packet << static_cast(IdConsoleIdCollision); + packet.Write(static_cast(IdConsoleIdCollision)); ENetPacket* enet_packet = enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); @@ -643,7 +643,7 @@ void Room::RoomImpl::SendConsoleIdCollision(ENetPeer* client) { void Room::RoomImpl::SendWrongPassword(ENetPeer* client) { Packet packet; - packet << static_cast(IdWrongPassword); + packet.Write(static_cast(IdWrongPassword)); ENetPacket* enet_packet = enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); @@ -653,7 +653,7 @@ void Room::RoomImpl::SendWrongPassword(ENetPeer* client) { void Room::RoomImpl::SendRoomIsFull(ENetPeer* client) { Packet packet; - packet << static_cast(IdRoomIsFull); + packet.Write(static_cast(IdRoomIsFull)); ENetPacket* enet_packet = enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); @@ -663,8 +663,8 @@ void Room::RoomImpl::SendRoomIsFull(ENetPeer* client) { void Room::RoomImpl::SendVersionMismatch(ENetPeer* client) { Packet packet; - packet << static_cast(IdVersionMismatch); - packet << network_version; + packet.Write(static_cast(IdVersionMismatch)); + packet.Write(network_version); ENetPacket* enet_packet = enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); @@ -674,8 +674,8 @@ void Room::RoomImpl::SendVersionMismatch(ENetPeer* client) { void Room::RoomImpl::SendJoinSuccess(ENetPeer* client, MacAddress mac_address) { Packet packet; - packet << static_cast(IdJoinSuccess); - packet << mac_address; + packet.Write(static_cast(IdJoinSuccess)); + packet.Write(mac_address); ENetPacket* enet_packet = enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); enet_peer_send(client, 0, enet_packet); @@ -684,8 +684,8 @@ void Room::RoomImpl::SendJoinSuccess(ENetPeer* client, MacAddress mac_address) { void Room::RoomImpl::SendJoinSuccessAsMod(ENetPeer* client, MacAddress mac_address) { Packet packet; - packet << static_cast(IdJoinSuccessAsMod); - packet << mac_address; + packet.Write(static_cast(IdJoinSuccessAsMod)); + packet.Write(mac_address); ENetPacket* enet_packet = enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); enet_peer_send(client, 0, enet_packet); @@ -694,7 +694,7 @@ void Room::RoomImpl::SendJoinSuccessAsMod(ENetPeer* client, MacAddress mac_addre void Room::RoomImpl::SendUserKicked(ENetPeer* client) { Packet packet; - packet << static_cast(IdHostKicked); + packet.Write(static_cast(IdHostKicked)); ENetPacket* enet_packet = enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); @@ -704,7 +704,7 @@ void Room::RoomImpl::SendUserKicked(ENetPeer* client) { void Room::RoomImpl::SendUserBanned(ENetPeer* client) { Packet packet; - packet << static_cast(IdHostBanned); + packet.Write(static_cast(IdHostBanned)); ENetPacket* enet_packet = enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); @@ -714,7 +714,7 @@ void Room::RoomImpl::SendUserBanned(ENetPeer* client) { void Room::RoomImpl::SendModPermissionDenied(ENetPeer* client) { Packet packet; - packet << static_cast(IdModPermissionDenied); + packet.Write(static_cast(IdModPermissionDenied)); ENetPacket* enet_packet = enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); @@ -724,7 +724,7 @@ void Room::RoomImpl::SendModPermissionDenied(ENetPeer* client) { void Room::RoomImpl::SendModNoSuchUser(ENetPeer* client) { Packet packet; - packet << static_cast(IdModNoSuchUser); + packet.Write(static_cast(IdModNoSuchUser)); ENetPacket* enet_packet = enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); @@ -734,11 +734,11 @@ void Room::RoomImpl::SendModNoSuchUser(ENetPeer* client) { void Room::RoomImpl::SendModBanListResponse(ENetPeer* client) { Packet packet; - packet << static_cast(IdModBanListResponse); + packet.Write(static_cast(IdModBanListResponse)); { std::lock_guard lock(ban_list_mutex); - packet << username_ban_list; - packet << ip_ban_list; + packet.Write(username_ban_list); + packet.Write(ip_ban_list); } ENetPacket* enet_packet = @@ -749,7 +749,7 @@ void Room::RoomImpl::SendModBanListResponse(ENetPeer* client) { void Room::RoomImpl::SendCloseMessage() { Packet packet; - packet << static_cast(IdCloseRoom); + packet.Write(static_cast(IdCloseRoom)); std::lock_guard lock(member_mutex); if (!members.empty()) { ENetPacket* enet_packet = @@ -767,10 +767,10 @@ void Room::RoomImpl::SendCloseMessage() { void Room::RoomImpl::SendStatusMessage(StatusMessageTypes type, const std::string& nickname, const std::string& username, const std::string& ip) { Packet packet; - packet << static_cast(IdStatusMessage); - packet << static_cast(type); - packet << nickname; - packet << username; + packet.Write(static_cast(IdStatusMessage)); + packet.Write(static_cast(type)); + packet.Write(nickname); + packet.Write(username); std::lock_guard lock(member_mutex); if (!members.empty()) { ENetPacket* enet_packet = @@ -805,25 +805,25 @@ void Room::RoomImpl::SendStatusMessage(StatusMessageTypes type, const std::strin void Room::RoomImpl::BroadcastRoomInformation() { Packet packet; - packet << static_cast(IdRoomInformation); - packet << room_information.name; - packet << room_information.description; - packet << room_information.member_slots; - packet << room_information.port; - packet << room_information.preferred_game.name; - packet << room_information.host_username; + packet.Write(static_cast(IdRoomInformation)); + packet.Write(room_information.name); + packet.Write(room_information.description); + packet.Write(room_information.member_slots); + packet.Write(room_information.port); + packet.Write(room_information.preferred_game.name); + packet.Write(room_information.host_username); - packet << static_cast(members.size()); + packet.Write(static_cast(members.size())); { std::lock_guard lock(member_mutex); for (const auto& member : members) { - packet << member.nickname; - packet << member.mac_address; - packet << member.game_info.name; - packet << member.game_info.id; - packet << member.user_data.username; - packet << member.user_data.display_name; - packet << member.user_data.avatar_url; + packet.Write(member.nickname); + packet.Write(member.mac_address); + packet.Write(member.game_info.name); + packet.Write(member.game_info.id); + packet.Write(member.user_data.username); + packet.Write(member.user_data.display_name); + packet.Write(member.user_data.avatar_url); } } @@ -853,7 +853,7 @@ void Room::RoomImpl::HandleWifiPacket(const ENetEvent* event) { in_packet.IgnoreBytes(sizeof(u8)); // WifiPacket Channel in_packet.IgnoreBytes(sizeof(MacAddress)); // WifiPacket Transmitter Address MacAddress destination_address; - in_packet >> destination_address; + in_packet.Read(destination_address); Packet out_packet; out_packet.Append(event->packet->data, event->packet->dataLength); @@ -899,7 +899,7 @@ void Room::RoomImpl::HandleChatPacket(const ENetEvent* event) { in_packet.IgnoreBytes(sizeof(u8)); // Ignore the message type std::string message; - in_packet >> message; + in_packet.Read(message); auto CompareNetworkAddress = [event](const Member member) -> bool { return member.peer == event->peer; }; @@ -914,10 +914,10 @@ void Room::RoomImpl::HandleChatPacket(const ENetEvent* event) { message.resize(std::min(static_cast(message.size()), MaxMessageSize)); Packet out_packet; - out_packet << static_cast(IdChatMessage); - out_packet << sending_member->nickname; - out_packet << sending_member->user_data.username; - out_packet << message; + out_packet.Write(static_cast(IdChatMessage)); + out_packet.Write(sending_member->nickname); + out_packet.Write(sending_member->user_data.username); + out_packet.Write(message); ENetPacket* enet_packet = enet_packet_create(out_packet.GetData(), out_packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); @@ -949,8 +949,8 @@ void Room::RoomImpl::HandleGameNamePacket(const ENetEvent* event) { in_packet.IgnoreBytes(sizeof(u8)); // Ignore the message type GameInfo game_info; - in_packet >> game_info.name; - in_packet >> game_info.id; + in_packet.Read(game_info.name); + in_packet.Read(game_info.id); { std::lock_guard lock(member_mutex); @@ -989,9 +989,9 @@ void Room::RoomImpl::HandleClientDisconnection(ENetPeer* client) { nickname = member->nickname; username = member->user_data.username; - char ip_raw[256]; - enet_address_get_host_ip(&member->peer->address, ip_raw, sizeof(ip_raw) - 1); - ip = ip_raw; + std::array ip_raw{}; + enet_address_get_host_ip(&member->peer->address, ip_raw.data(), sizeof(ip_raw) - 1); + ip = ip_raw.data(); members.erase(member); } diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp index 38a6f6bfd7..e4f823e981 100644 --- a/src/network/room_member.cpp +++ b/src/network/room_member.cpp @@ -280,13 +280,13 @@ void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname_, const std::string& password, const std::string& token) { Packet packet; - packet << static_cast(IdJoinRequest); - packet << nickname_; - packet << console_id_hash; - packet << preferred_mac; - packet << network_version; - packet << password; - packet << token; + packet.Write(static_cast(IdJoinRequest)); + packet.Write(nickname_); + packet.Write(console_id_hash); + packet.Write(preferred_mac); + packet.Write(network_version); + packet.Write(password); + packet.Write(token); Send(std::move(packet)); } @@ -298,12 +298,12 @@ void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* ev packet.IgnoreBytes(sizeof(u8)); // Ignore the message type RoomInformation info{}; - packet >> info.name; - packet >> info.description; - packet >> info.member_slots; - packet >> info.port; - packet >> info.preferred_game.name; - packet >> info.host_username; + packet.Read(info.name); + packet.Read(info.description); + packet.Read(info.member_slots); + packet.Read(info.port); + packet.Read(info.preferred_game.name); + packet.Read(info.host_username); room_information.name = info.name; room_information.description = info.description; room_information.member_slots = info.member_slots; @@ -312,17 +312,17 @@ void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* ev room_information.host_username = info.host_username; u32 num_members; - packet >> num_members; + packet.Read(num_members); member_information.resize(num_members); for (auto& member : member_information) { - packet >> member.nickname; - packet >> member.mac_address; - packet >> member.game_info.name; - packet >> member.game_info.id; - packet >> member.username; - packet >> member.display_name; - packet >> member.avatar_url; + packet.Read(member.nickname); + packet.Read(member.mac_address); + packet.Read(member.game_info.name); + packet.Read(member.game_info.id); + packet.Read(member.username); + packet.Read(member.display_name); + packet.Read(member.avatar_url); { std::lock_guard lock(username_mutex); @@ -342,7 +342,7 @@ void RoomMember::RoomMemberImpl::HandleJoinPacket(const ENetEvent* event) { packet.IgnoreBytes(sizeof(u8)); // Ignore the message type // Parse the MAC Address from the packet - packet >> mac_address; + packet.Read(mac_address); } void RoomMember::RoomMemberImpl::HandleWifiPackets(const ENetEvent* event) { @@ -355,14 +355,14 @@ void RoomMember::RoomMemberImpl::HandleWifiPackets(const ENetEvent* event) { // Parse the WifiPacket from the packet u8 frame_type; - packet >> frame_type; + packet.Read(frame_type); WifiPacket::PacketType type = static_cast(frame_type); wifi_packet.type = type; - packet >> wifi_packet.channel; - packet >> wifi_packet.transmitter_address; - packet >> wifi_packet.destination_address; - packet >> wifi_packet.data; + packet.Read(wifi_packet.channel); + packet.Read(wifi_packet.transmitter_address); + packet.Read(wifi_packet.destination_address); + packet.Read(wifi_packet.data); Invoke(wifi_packet); } @@ -375,9 +375,9 @@ void RoomMember::RoomMemberImpl::HandleChatPacket(const ENetEvent* event) { packet.IgnoreBytes(sizeof(u8)); ChatEntry chat_entry{}; - packet >> chat_entry.nickname; - packet >> chat_entry.username; - packet >> chat_entry.message; + packet.Read(chat_entry.nickname); + packet.Read(chat_entry.username); + packet.Read(chat_entry.message); Invoke(chat_entry); } @@ -390,10 +390,10 @@ void RoomMember::RoomMemberImpl::HandleStatusMessagePacket(const ENetEvent* even StatusMessageEntry status_message_entry{}; u8 type{}; - packet >> type; + packet.Read(type); status_message_entry.type = static_cast(type); - packet >> status_message_entry.nickname; - packet >> status_message_entry.username; + packet.Read(status_message_entry.nickname); + packet.Read(status_message_entry.username); Invoke(status_message_entry); } @@ -405,8 +405,8 @@ void RoomMember::RoomMemberImpl::HandleModBanListResponsePacket(const ENetEvent* packet.IgnoreBytes(sizeof(u8)); Room::BanList ban_list = {}; - packet >> ban_list.first; - packet >> ban_list.second; + packet.Read(ban_list.first); + packet.Read(ban_list.second); Invoke(ban_list); } @@ -586,19 +586,19 @@ bool RoomMember::IsConnected() const { void RoomMember::SendWifiPacket(const WifiPacket& wifi_packet) { Packet packet; - packet << static_cast(IdWifiPacket); - packet << static_cast(wifi_packet.type); - packet << wifi_packet.channel; - packet << wifi_packet.transmitter_address; - packet << wifi_packet.destination_address; - packet << wifi_packet.data; + packet.Write(static_cast(IdWifiPacket)); + packet.Write(static_cast(wifi_packet.type)); + packet.Write(wifi_packet.channel); + packet.Write(wifi_packet.transmitter_address); + packet.Write(wifi_packet.destination_address); + packet.Write(wifi_packet.data); room_member_impl->Send(std::move(packet)); } void RoomMember::SendChatMessage(const std::string& message) { Packet packet; - packet << static_cast(IdChatMessage); - packet << message; + packet.Write(static_cast(IdChatMessage)); + packet.Write(message); room_member_impl->Send(std::move(packet)); } @@ -608,9 +608,9 @@ void RoomMember::SendGameInfo(const GameInfo& game_info) { return; Packet packet; - packet << static_cast(IdSetGameInfo); - packet << game_info.name; - packet << game_info.id; + packet.Write(static_cast(IdSetGameInfo)); + packet.Write(game_info.name); + packet.Write(game_info.id); room_member_impl->Send(std::move(packet)); } @@ -621,8 +621,8 @@ void RoomMember::SendModerationRequest(RoomMessageTypes type, const std::string& return; Packet packet; - packet << static_cast(type); - packet << nickname; + packet.Write(static_cast(type)); + packet.Write(nickname); room_member_impl->Send(std::move(packet)); } @@ -631,7 +631,7 @@ void RoomMember::RequestBanList() { return; Packet packet; - packet << static_cast(IdModGetBanList); + packet.Write(static_cast(IdModGetBanList)); room_member_impl->Send(std::move(packet)); } diff --git a/src/yuzu/multiplayer/state.cpp b/src/yuzu/multiplayer/state.cpp index 015c59788a..4149b52321 100644 --- a/src/yuzu/multiplayer/state.cpp +++ b/src/yuzu/multiplayer/state.cpp @@ -74,14 +74,18 @@ MultiplayerState::~MultiplayerState() { } void MultiplayerState::Close() { - if (host_room) + if (host_room) { host_room->close(); - if (direct_connect) + } + if (direct_connect) { direct_connect->close(); - if (client_room) + } + if (client_room) { client_room->close(); - if (lobby) + } + if (lobby) { lobby->close(); + } } void MultiplayerState::retranslateUi() {