diff --git a/Source/Core/Common/CommonPaths.h b/Source/Core/Common/CommonPaths.h index 46a201d823..a1d87800f4 100644 --- a/Source/Core/Common/CommonPaths.h +++ b/Source/Core/Common/CommonPaths.h @@ -69,6 +69,7 @@ #define WFSROOT_DIR "WFS" #define BACKUP_DIR "Backup" #define RESOURCEPACK_DIR "ResourcePacks" +#define DYNAMICINPUT_DIR "DynamicInputTextures" // This one is only used to remove it if it was present #define SHADERCACHE_LEGACY_DIR "ShaderCache" diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp index 3c524ed9d8..6b1cf779e5 100644 --- a/Source/Core/Common/FileUtil.cpp +++ b/Source/Core/Common/FileUtil.cpp @@ -813,6 +813,7 @@ static void RebuildUserDirectories(unsigned int dir_index) s_user_paths[D_WFSROOT_IDX] = s_user_paths[D_USER_IDX] + WFSROOT_DIR DIR_SEP; s_user_paths[D_BACKUP_IDX] = s_user_paths[D_USER_IDX] + BACKUP_DIR DIR_SEP; s_user_paths[D_RESOURCEPACK_IDX] = s_user_paths[D_USER_IDX] + RESOURCEPACK_DIR DIR_SEP; + s_user_paths[D_DYNAMICINPUT_IDX] = s_user_paths[D_LOAD_IDX] + DYNAMICINPUT_DIR DIR_SEP; s_user_paths[F_DOLPHINCONFIG_IDX] = s_user_paths[D_CONFIG_IDX] + DOLPHIN_CONFIG; s_user_paths[F_GCPADCONFIG_IDX] = s_user_paths[D_CONFIG_IDX] + GCPAD_CONFIG; s_user_paths[F_WIIPADCONFIG_IDX] = s_user_paths[D_CONFIG_IDX] + WIIPAD_CONFIG; @@ -880,6 +881,7 @@ static void RebuildUserDirectories(unsigned int dir_index) case D_LOAD_IDX: s_user_paths[D_HIRESTEXTURES_IDX] = s_user_paths[D_LOAD_IDX] + HIRES_TEXTURES_DIR DIR_SEP; + s_user_paths[D_DYNAMICINPUT_IDX] = s_user_paths[D_LOAD_IDX] + DYNAMICINPUT_DIR DIR_SEP; break; } } diff --git a/Source/Core/Common/FileUtil.h b/Source/Core/Common/FileUtil.h index 2c13ae07c0..e593a0541f 100644 --- a/Source/Core/Common/FileUtil.h +++ b/Source/Core/Common/FileUtil.h @@ -54,6 +54,7 @@ enum D_WFSROOT_IDX, D_BACKUP_IDX, D_RESOURCEPACK_IDX, + D_DYNAMICINPUT_IDX, F_DOLPHINCONFIG_IDX, F_GCPADCONFIG_IDX, F_WIIPADCONFIG_IDX, diff --git a/Source/Core/DolphinQt/Config/Graphics/AdvancedWidget.cpp b/Source/Core/DolphinQt/Config/Graphics/AdvancedWidget.cpp index 05ddd8406f..173d1f4b34 100644 --- a/Source/Core/DolphinQt/Config/Graphics/AdvancedWidget.cpp +++ b/Source/Core/DolphinQt/Config/Graphics/AdvancedWidget.cpp @@ -232,9 +232,10 @@ void AdvancedWidget::AddDescriptions() "User/Dump/Textures//. This includes arbitrary base textures if 'Arbitrary " "Mipmap Detection' is enabled in Enhancements.\n\nIf unsure, leave " "this checked."); - static const char TR_LOAD_CUSTOM_TEXTURE_DESCRIPTION[] = QT_TR_NOOP( - "Loads custom textures from User/Load/Textures//.\n\nIf unsure, leave this " - "unchecked."); + static const char TR_LOAD_CUSTOM_TEXTURE_DESCRIPTION[] = + QT_TR_NOOP("Loads custom textures from User/Load/Textures// and " + "User/Load/DynamicInputTextures//.\n\nIf unsure, leave this " + "unchecked."); static const char TR_CACHE_CUSTOM_TEXTURE_DESCRIPTION[] = QT_TR_NOOP( "Caches custom textures to system RAM on startup.\n\nThis can require exponentially " "more RAM but fixes possible stuttering.\n\nIf unsure, leave this unchecked."); diff --git a/Source/Core/InputCommon/CMakeLists.txt b/Source/Core/InputCommon/CMakeLists.txt index d5efe58248..e0b8664a06 100644 --- a/Source/Core/InputCommon/CMakeLists.txt +++ b/Source/Core/InputCommon/CMakeLists.txt @@ -1,4 +1,10 @@ add_library(inputcommon + DynamicInputTextureConfiguration.cpp + DynamicInputTextureConfiguration.h + DynamicInputTextureManager.cpp + DynamicInputTextureManager.h + ImageOperations.cpp + ImageOperations.h InputConfig.cpp InputConfig.h InputProfile.cpp @@ -66,6 +72,7 @@ PUBLIC PRIVATE fmt::fmt + png ) if(WIN32) diff --git a/Source/Core/InputCommon/ControllerEmu/ControllerEmu.cpp b/Source/Core/InputCommon/ControllerEmu/ControllerEmu.cpp index a9c5844b48..8fc54fde06 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControllerEmu.cpp +++ b/Source/Core/InputCommon/ControllerEmu/ControllerEmu.cpp @@ -112,6 +112,12 @@ void EmulatedController::SetDefaultDevice(ciface::Core::DeviceQualifier devq) } } +void EmulatedController::SetDynamicInputTextureManager( + InputCommon::DynamicInputTextureManager* dynamic_input_tex_config_manager) +{ + m_dynamic_input_tex_config_manager = dynamic_input_tex_config_manager; +} + void EmulatedController::LoadConfig(IniFile::Section* sec, const std::string& base) { std::string defdev = GetDefaultDevice().ToString(); @@ -123,6 +129,11 @@ void EmulatedController::LoadConfig(IniFile::Section* sec, const std::string& ba for (auto& cg : groups) cg->LoadConfig(sec, defdev, base); + + if (base.empty()) + { + GenerateTextures(sec); + } } void EmulatedController::SaveConfig(IniFile::Section* sec, const std::string& base) @@ -133,6 +144,11 @@ void EmulatedController::SaveConfig(IniFile::Section* sec, const std::string& ba for (auto& ctrlGroup : groups) ctrlGroup->SaveConfig(sec, defdev, base); + + if (base.empty()) + { + GenerateTextures(sec); + } } void EmulatedController::LoadDefaults(const ControllerInterface& ciface) @@ -147,4 +163,12 @@ void EmulatedController::LoadDefaults(const ControllerInterface& ciface) SetDefaultDevice(default_device_string); } } + +void EmulatedController::GenerateTextures(IniFile::Section* sec) +{ + if (m_dynamic_input_tex_config_manager) + { + m_dynamic_input_tex_config_manager->GenerateTextures(sec, GetName()); + } +} } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/ControllerEmu.h b/Source/Core/InputCommon/ControllerEmu/ControllerEmu.h index b6808f1c0b..bcc25886f3 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControllerEmu.h +++ b/Source/Core/InputCommon/ControllerEmu/ControllerEmu.h @@ -17,6 +17,7 @@ #include "Common/MathUtil.h" #include "InputCommon/ControlReference/ExpressionParser.h" #include "InputCommon/ControllerInterface/Device.h" +#include "InputCommon/DynamicInputTextureManager.h" class ControllerInterface; @@ -182,6 +183,7 @@ public: const ciface::Core::DeviceQualifier& GetDefaultDevice() const; void SetDefaultDevice(const std::string& device); void SetDefaultDevice(ciface::Core::DeviceQualifier devq); + void SetDynamicInputTextureManager(InputCommon::DynamicInputTextureManager*); void UpdateReferences(const ControllerInterface& devi); void UpdateSingleControlReference(const ControllerInterface& devi, ControlReference* ref); @@ -224,6 +226,8 @@ protected: void UpdateReferences(ciface::ExpressionParser::ControlEnvironment& env); private: + void GenerateTextures(IniFile::Section* sec); + InputCommon::DynamicInputTextureManager* m_dynamic_input_tex_config_manager = nullptr; ciface::Core::DeviceQualifier m_default_device; bool m_default_device_is_connected{false}; }; diff --git a/Source/Core/InputCommon/DynamicInputTextureConfiguration.cpp b/Source/Core/InputCommon/DynamicInputTextureConfiguration.cpp new file mode 100644 index 0000000000..ad200e70a4 --- /dev/null +++ b/Source/Core/InputCommon/DynamicInputTextureConfiguration.cpp @@ -0,0 +1,367 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "InputCommon/DynamicInputTextureConfiguration.h" + +#include +#include +#include + +#include +#include + +#include "Common/CommonPaths.h" +#include "Common/File.h" +#include "Common/FileUtil.h" +#include "Common/Logging/Log.h" +#include "Common/StringUtil.h" +#include "Core/ConfigManager.h" +#include "InputCommon/ControllerEmu/ControllerEmu.h" +#include "InputCommon/ImageOperations.h" +#include "VideoCommon/RenderBase.h" + +namespace +{ +std::string GetStreamAsString(std::ifstream& stream) +{ + std::stringstream ss; + ss << stream.rdbuf(); + return ss.str(); +} +} // namespace + +namespace InputCommon +{ +DynamicInputTextureConfiguration::DynamicInputTextureConfiguration(const std::string& json_file) +{ + std::ifstream json_stream; + File::OpenFStream(json_stream, json_file, std::ios_base::in); + if (!json_stream.is_open()) + { + ERROR_LOG(VIDEO, "Failed to load dynamic input json file '%s'", json_file.c_str()); + m_valid = false; + return; + } + + picojson::value out; + const auto error = picojson::parse(out, GetStreamAsString(json_stream)); + + if (!error.empty()) + { + ERROR_LOG(VIDEO, "Failed to load dynamic input json file '%s' due to parse error: %s", + json_file.c_str(), error.c_str()); + m_valid = false; + return; + } + + const picojson::value& output_textures_json = out.get("output_textures"); + if (!output_textures_json.is()) + { + ERROR_LOG(VIDEO, + "Failed to load dynamic input json file '%s' because 'output_textures' is missing or " + "was not of type object", + json_file.c_str()); + m_valid = false; + return; + } + + const picojson::value& preserve_aspect_ratio_json = out.get("preserve_aspect_ratio"); + + bool preserve_aspect_ratio = true; + if (preserve_aspect_ratio_json.is()) + { + preserve_aspect_ratio = preserve_aspect_ratio_json.get(); + } + + const picojson::value& generated_folder_name_json = out.get("generated_folder_name"); + + const std::string& game_id = SConfig::GetInstance().GetGameID(); + std::string generated_folder_name = fmt::format("{}_Generated", game_id); + if (generated_folder_name_json.is()) + { + generated_folder_name = generated_folder_name_json.get(); + } + + const picojson::value& default_host_controls_json = out.get("default_host_controls"); + picojson::object default_host_controls; + if (default_host_controls_json.is()) + { + default_host_controls = default_host_controls_json.get(); + } + + const auto output_textures = output_textures_json.get(); + for (auto& [name, data] : output_textures) + { + DynamicInputTextureData texture_data; + texture_data.m_hires_texture_name = name; + + // Required fields + const picojson::value& image = data.get("image"); + const picojson::value& emulated_controls = data.get("emulated_controls"); + + if (!image.is() || !emulated_controls.is()) + { + ERROR_LOG(VIDEO, + "Failed to load dynamic input json file '%s' because required fields " + "'image', or 'emulated_controls' are either " + "missing or the incorrect type", + json_file.c_str()); + m_valid = false; + return; + } + + texture_data.m_image_name = image.to_str(); + texture_data.m_preserve_aspect_ratio = preserve_aspect_ratio; + texture_data.m_generated_folder_name = generated_folder_name; + + SplitPath(json_file, &m_base_path, nullptr, nullptr); + + const std::string image_full_path = m_base_path + texture_data.m_image_name; + if (!File::Exists(image_full_path)) + { + ERROR_LOG(VIDEO, + "Failed to load dynamic input json file '%s' because the image '%s' " + "could not be loaded", + json_file.c_str(), image_full_path.c_str()); + m_valid = false; + return; + } + + const auto& emulated_controls_json = emulated_controls.get(); + for (auto& [emulated_controller_name, map] : emulated_controls_json) + { + if (!map.is()) + { + ERROR_LOG(VIDEO, + "Failed to load dynamic input json file '%s' because 'emulated_controls' " + "map key '%s' is incorrect type. Expected map ", + json_file.c_str(), emulated_controller_name.c_str()); + m_valid = false; + return; + } + + auto& key_to_regions = texture_data.m_emulated_controllers[emulated_controller_name]; + for (auto& [emulated_control, regions_array] : map.get()) + { + if (!regions_array.is()) + { + ERROR_LOG(VIDEO, + "Failed to load dynamic input json file '%s' because emulated controller '%s' " + "key '%s' has incorrect value type. Expected array ", + json_file.c_str(), emulated_controller_name.c_str(), emulated_control.c_str()); + m_valid = false; + return; + } + + std::vector region_rects; + for (auto& region : regions_array.get()) + { + Rect r; + if (!region.is()) + { + ERROR_LOG( + VIDEO, + "Failed to load dynamic input json file '%s' because emulated controller '%s' " + "key '%s' has a region with the incorrect type. Expected array ", + json_file.c_str(), emulated_controller_name.c_str(), emulated_control.c_str()); + m_valid = false; + return; + } + + auto region_offsets = region.get(); + + if (region_offsets.size() != 4) + { + ERROR_LOG( + VIDEO, + "Failed to load dynamic input json file '%s' because emulated controller '%s' " + "key '%s' has a region that does not have 4 offsets (left, top, right, " + "bottom).", + json_file.c_str(), emulated_controller_name.c_str(), emulated_control.c_str()); + m_valid = false; + return; + } + + if (!std::all_of(region_offsets.begin(), region_offsets.end(), + [](picojson::value val) { return val.is(); })) + { + ERROR_LOG( + VIDEO, + "Failed to load dynamic input json file '%s' because emulated controller '%s' " + "key '%s' has a region that has the incorrect offset type.", + json_file.c_str(), emulated_controller_name.c_str(), emulated_control.c_str()); + m_valid = false; + return; + } + + r.left = static_cast(region_offsets[0].get()); + r.top = static_cast(region_offsets[1].get()); + r.right = static_cast(region_offsets[2].get()); + r.bottom = static_cast(region_offsets[3].get()); + region_rects.push_back(r); + } + key_to_regions.insert_or_assign(emulated_control, std::move(region_rects)); + } + } + + // Default to the default controls but overwrite if the creator + // has provided something specific + picojson::object host_controls = default_host_controls; + const picojson::value& host_controls_json = data.get("host_controls"); + if (host_controls_json.is()) + { + host_controls = host_controls_json.get(); + } + + if (host_controls.empty()) + { + ERROR_LOG(VIDEO, + "Failed to load dynamic input json file '%s' because field " + "'host_controls' is missing ", + json_file.c_str()); + m_valid = false; + return; + } + + for (auto& [host_device, map] : host_controls) + { + if (!map.is()) + { + ERROR_LOG(VIDEO, + "Failed to load dynamic input json file '%s' because 'host_controls' " + "map key '%s' is incorrect type ", + json_file.c_str(), host_device.c_str()); + m_valid = false; + return; + } + auto& host_control_to_imagename = texture_data.m_host_devices[host_device]; + for (auto& [host_control, image_name] : map.get()) + { + host_control_to_imagename.insert_or_assign(host_control, image_name.to_str()); + } + } + + m_dynamic_input_textures.emplace_back(std::move(texture_data)); + } +} + +DynamicInputTextureConfiguration::~DynamicInputTextureConfiguration() = default; + +void DynamicInputTextureConfiguration::GenerateTextures(const IniFile::Section* sec, + const std::string& controller_name) const +{ + bool any_dirty = false; + for (const auto& texture_data : m_dynamic_input_textures) + { + any_dirty |= GenerateTexture(sec, controller_name, texture_data); + } + + if (!any_dirty) + return; + if (!g_renderer) + return; + g_renderer->ForceReloadTextures(); +} + +bool DynamicInputTextureConfiguration::GenerateTexture( + const IniFile::Section* sec, const std::string& controller_name, + const DynamicInputTextureData& texture_data) const +{ + std::string device_name; + if (!sec->Get("Device", &device_name)) + { + return false; + } + + auto emulated_controls_iter = texture_data.m_emulated_controllers.find(controller_name); + if (emulated_controls_iter == texture_data.m_emulated_controllers.end()) + { + return false; + } + + bool device_found = true; + auto host_devices_iter = texture_data.m_host_devices.find(device_name); + if (host_devices_iter == texture_data.m_host_devices.end()) + { + // If we fail to find our exact device, + // it's possible the creator doesn't care (single player game) + // and has used a wildcard for any device + host_devices_iter = texture_data.m_host_devices.find(""); + + if (host_devices_iter == texture_data.m_host_devices.end()) + { + device_found = false; + } + } + + // Load image copy + auto base_image = LoadImage(m_base_path + texture_data.m_image_name); + bool dirty = false; + + for (auto& [emulated_key, rects] : emulated_controls_iter->second) + { + std::string host_key = ""; + sec->Get(emulated_key, &host_key); + + if (!device_found) + { + // If we get here, that means the controller is set to a + // device not exposed to the pack + continue; + } + + const auto input_image_iter = host_devices_iter->second.find(host_key); + if (input_image_iter != host_devices_iter->second.end()) + { + const auto host_key_image = LoadImage(m_base_path + input_image_iter->second); + + for (const auto& rect : rects) + { + InputCommon::ImagePixelData pixel_data; + if (host_key_image->width == rect.GetWidth() && host_key_image->height == rect.GetHeight()) + { + pixel_data = *host_key_image; + } + else if (texture_data.m_preserve_aspect_ratio) + { + pixel_data = ResizeKeepAspectRatio(ResizeMode::Nearest, *host_key_image, rect.GetWidth(), + rect.GetHeight(), Pixel{0, 0, 0, 0}); + } + else + { + pixel_data = + Resize(ResizeMode::Nearest, *host_key_image, rect.GetWidth(), rect.GetHeight()); + } + + CopyImageRegion(pixel_data, *base_image, Rect{0, 0, rect.GetWidth(), rect.GetHeight()}, + rect); + dirty = true; + } + } + } + + if (dirty) + { + const std::string& game_id = SConfig::GetInstance().GetGameID(); + const auto hi_res_folder = + File::GetUserPath(D_HIRESTEXTURES_IDX) + texture_data.m_generated_folder_name; + if (!File::IsDirectory(hi_res_folder)) + { + File::CreateDir(hi_res_folder); + } + WriteImage(hi_res_folder + DIR_SEP + texture_data.m_hires_texture_name, *base_image); + + const auto game_id_folder = hi_res_folder + DIR_SEP + "gameids"; + if (!File::IsDirectory(game_id_folder)) + { + File::CreateDir(game_id_folder); + } + File::CreateEmptyFile(game_id_folder + DIR_SEP + game_id + ".txt"); + + return true; + } + + return false; +} +} // namespace InputCommon diff --git a/Source/Core/InputCommon/DynamicInputTextureConfiguration.h b/Source/Core/InputCommon/DynamicInputTextureConfiguration.h new file mode 100644 index 0000000000..15e4d37129 --- /dev/null +++ b/Source/Core/InputCommon/DynamicInputTextureConfiguration.h @@ -0,0 +1,46 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/IniFile.h" +#include "InputCommon/ImageOperations.h" + +namespace InputCommon +{ +class DynamicInputTextureConfiguration +{ +public: + explicit DynamicInputTextureConfiguration(const std::string& json_file); + ~DynamicInputTextureConfiguration(); + void GenerateTextures(const IniFile::Section* sec, const std::string& controller_name) const; + +private: + struct DynamicInputTextureData + { + std::string m_image_name; + std::string m_hires_texture_name; + std::string m_generated_folder_name; + + using EmulatedKeyToRegionsMap = std::unordered_map>; + std::unordered_map m_emulated_controllers; + + using HostKeyToImagePath = std::unordered_map; + std::unordered_map m_host_devices; + bool m_preserve_aspect_ratio = true; + }; + + bool GenerateTexture(const IniFile::Section* sec, const std::string& controller_name, + const DynamicInputTextureData& texture_data) const; + + std::vector m_dynamic_input_textures; + std::string m_base_path; + bool m_valid = true; +}; +} // namespace InputCommon diff --git a/Source/Core/InputCommon/DynamicInputTextureManager.cpp b/Source/Core/InputCommon/DynamicInputTextureManager.cpp new file mode 100644 index 0000000000..437d083ee9 --- /dev/null +++ b/Source/Core/InputCommon/DynamicInputTextureManager.cpp @@ -0,0 +1,49 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "InputCommon/DynamicInputTextureManager.h" + +#include + +#include "Common/CommonPaths.h" +#include "Common/FileSearch.h" +#include "Common/FileUtil.h" +#include "Core/ConfigManager.h" + +#include "InputCommon/DynamicInputTextureConfiguration.h" +#include "VideoCommon/HiresTextures.h" + +namespace InputCommon +{ +DynamicInputTextureManager::DynamicInputTextureManager() = default; + +DynamicInputTextureManager::~DynamicInputTextureManager() = default; + +void DynamicInputTextureManager::Load() +{ + m_configuration.clear(); + + const std::string& game_id = SConfig::GetInstance().GetGameID(); + const std::set dynamic_input_directories = + GetTextureDirectoriesWithGameId(File::GetUserPath(D_DYNAMICINPUT_IDX), game_id); + + for (const auto& dynamic_input_directory : dynamic_input_directories) + { + const auto json_files = Common::DoFileSearch({dynamic_input_directory}, {".json"}); + for (auto& file : json_files) + { + m_configuration.emplace_back(file); + } + } +} + +void DynamicInputTextureManager::GenerateTextures(const IniFile::Section* sec, + const std::string& controller_name) +{ + for (const auto& configuration : m_configuration) + { + configuration.GenerateTextures(sec, controller_name); + } +} +} // namespace InputCommon diff --git a/Source/Core/InputCommon/DynamicInputTextureManager.h b/Source/Core/InputCommon/DynamicInputTextureManager.h new file mode 100644 index 0000000000..cd07854928 --- /dev/null +++ b/Source/Core/InputCommon/DynamicInputTextureManager.h @@ -0,0 +1,27 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include "Common/IniFile.h" + +#include +#include + +namespace InputCommon +{ +class DynamicInputTextureConfiguration; +class DynamicInputTextureManager +{ +public: + DynamicInputTextureManager(); + ~DynamicInputTextureManager(); + void Load(); + void GenerateTextures(const IniFile::Section* sec, const std::string& controller_name); + +private: + std::vector m_configuration; + std::string m_config_type; +}; +} // namespace InputCommon diff --git a/Source/Core/InputCommon/ImageOperations.cpp b/Source/Core/InputCommon/ImageOperations.cpp new file mode 100644 index 0000000000..348fcc0a98 --- /dev/null +++ b/Source/Core/InputCommon/ImageOperations.cpp @@ -0,0 +1,250 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "InputCommon/ImageOperations.h" + +#include +#include +#include +#include + +#include + +#include "Common/File.h" +#include "Common/FileUtil.h" +#include "Common/Image.h" + +namespace InputCommon +{ +namespace +{ +Pixel SampleNearest(const ImagePixelData& src, double u, double v) +{ + const u32 x = std::clamp(static_cast(u * src.width), 0u, src.width - 1); + const u32 y = std::clamp(static_cast(v * src.height), 0u, src.height - 1); + return src.pixels[x + y * src.width]; +} +} // namespace + +void CopyImageRegion(const ImagePixelData& src, ImagePixelData& dst, const Rect& src_region, + const Rect& dst_region) +{ + if (src_region.GetWidth() != dst_region.GetWidth() || + src_region.GetHeight() != dst_region.GetHeight()) + { + return; + } + + for (u32 x = 0; x < dst_region.GetWidth(); x++) + { + for (u32 y = 0; y < dst_region.GetHeight(); y++) + { + dst.pixels[(y + dst_region.top) * dst.width + x + dst_region.left] = + src.pixels[(y + src_region.top) * src.width + x + src_region.left]; + } + } +} + +std::optional LoadImage(const std::string& path) +{ + File::IOFile file; + file.Open(path, "rb"); + std::vector buffer(file.GetSize()); + file.ReadBytes(buffer.data(), file.GetSize()); + + ImagePixelData image; + std::vector data; + if (!Common::LoadPNG(buffer, &data, &image.width, &image.height)) + return std::nullopt; + + image.pixels.resize(image.width * image.height); + for (u32 x = 0; x < image.width; x++) + { + for (u32 y = 0; y < image.height; y++) + { + const u32 index = y * image.width + x; + const auto pixel = + Pixel{data[index * 4], data[index * 4 + 1], data[index * 4 + 2], data[index * 4 + 3]}; + image.pixels[index] = pixel; + } + } + + return image; +} + +// For Visual Studio, ignore the error caused by the 'setjmp' call +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4611) +#endif + +bool WriteImage(const std::string& path, const ImagePixelData& image) +{ + bool success = false; + char title[] = "Dynamic Input Texture"; + char title_key[] = "Title"; + png_structp png_ptr = nullptr; + png_infop info_ptr = nullptr; + std::vector buffer; + + // Open file for writing (binary mode) + File::IOFile fp(path, "wb"); + if (!fp.IsOpen()) + { + goto finalise; + } + + // Initialize write structure + png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (png_ptr == nullptr) + { + goto finalise; + } + + // Initialize info structure + info_ptr = png_create_info_struct(png_ptr); + if (info_ptr == nullptr) + { + goto finalise; + } + + // Classical libpng error handling uses longjmp to do C-style unwind. + // Modern libpng does support a user callback, but it's required to operate + // in the same way (just gives a chance to do stuff before the longjmp). + // Instead of futzing with it, we use gotos specifically so the compiler + // will still generate proper destructor calls for us (hopefully). + // We also do not use any local variables outside the region longjmp may + // have been called from if they were modified inside that region (they + // would need to be volatile). + if (setjmp(png_jmpbuf(png_ptr))) + { + goto finalise; + } + + // Begin region which may call longjmp + + png_init_io(png_ptr, fp.GetHandle()); + + // Write header (8 bit color depth) + png_set_IHDR(png_ptr, info_ptr, image.width, image.height, 8, PNG_COLOR_TYPE_RGB_ALPHA, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); + + png_text title_text; + title_text.compression = PNG_TEXT_COMPRESSION_NONE; + title_text.key = title_key; + title_text.text = title; + png_set_text(png_ptr, info_ptr, &title_text, 1); + + png_write_info(png_ptr, info_ptr); + + buffer.resize(image.width * 4); + + // Write image data + for (u32 y = 0; y < image.height; ++y) + { + for (u32 x = 0; x < image.width; x++) + { + const auto index = x + y * image.width; + const auto pixel = image.pixels[index]; + + const auto buffer_index = 4 * x; + buffer[buffer_index] = pixel.r; + buffer[buffer_index + 1] = pixel.g; + buffer[buffer_index + 2] = pixel.b; + buffer[buffer_index + 3] = pixel.a; + } + + // The old API uses u8* instead of const u8*. It doesn't write + // to this pointer, but to fit the API, we have to drop the const qualifier. + png_write_row(png_ptr, const_cast(buffer.data())); + } + + // End write + png_write_end(png_ptr, nullptr); + + // End region which may call longjmp + + success = true; + +finalise: + if (info_ptr != nullptr) + png_free_data(png_ptr, info_ptr, PNG_FREE_ALL, -1); + if (png_ptr != nullptr) + png_destroy_write_struct(&png_ptr, nullptr); + + return success; +} + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +ImagePixelData Resize(ResizeMode mode, const ImagePixelData& src, u32 new_width, u32 new_height) +{ + ImagePixelData result(new_width, new_height); + + for (u32 x = 0; x < new_width; x++) + { + const double u = x / static_cast(new_width - 1); + for (u32 y = 0; y < new_height; y++) + { + const double v = y / static_cast(new_height - 1); + + switch (mode) + { + case ResizeMode::Nearest: + result.pixels[y * new_width + x] = SampleNearest(src, u, v); + break; + } + } + } + + return result; +} + +ImagePixelData ResizeKeepAspectRatio(ResizeMode mode, const ImagePixelData& src, u32 new_width, + u32 new_height, const Pixel& background_color) +{ + ImagePixelData result(new_width, new_height, background_color); + + const double corrected_height = new_width * (src.height / static_cast(src.width)); + const double corrected_width = new_height * (src.width / static_cast(src.height)); + // initially no borders + u32 top = 0; + u32 left = 0; + + ImagePixelData resized; + if (corrected_height <= new_height) + { + // Handle vertical padding + + const int diff = new_height - std::trunc(corrected_height); + top = diff / 2; + if (diff % 2 != 0) + { + // If the difference is odd, we need to have one side be slightly larger + top += 1; + } + resized = Resize(mode, src, new_width, corrected_height); + } + else + { + // Handle horizontal padding + + const int diff = new_width - std::trunc(corrected_width); + left = diff / 2; + if (diff % 2 != 0) + { + // If the difference is odd, we need to have one side be slightly larger + left += 1; + } + resized = Resize(mode, src, corrected_width, new_height); + } + CopyImageRegion(resized, result, Rect{0, 0, resized.width, resized.height}, + Rect{left, top, left + resized.width, top + resized.height}); + + return result; +} + +} // namespace InputCommon diff --git a/Source/Core/InputCommon/ImageOperations.h b/Source/Core/InputCommon/ImageOperations.h new file mode 100644 index 0000000000..28d3859ff5 --- /dev/null +++ b/Source/Core/InputCommon/ImageOperations.h @@ -0,0 +1,65 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/MathUtil.h" +#include "Common/Matrix.h" + +namespace InputCommon +{ +struct Pixel +{ + u8 r = 0; + u8 g = 0; + u8 b = 0; + u8 a = 0; + + bool operator==(const Pixel& o) const { return r == o.r && g == o.g && b == o.b && a == o.a; } + bool operator!=(const Pixel& o) const { return !(o == *this); } +}; + +using Point = Common::TVec2; +using Rect = MathUtil::Rectangle; + +struct ImagePixelData +{ + ImagePixelData() = default; + + explicit ImagePixelData(std::vector image_pixels, u32 width, u32 height) + : pixels(std::move(image_pixels)), width(width), height(height) + { + } + + explicit ImagePixelData(u32 width, u32 height, const Pixel& default_color = Pixel{0, 0, 0, 0}) + : pixels(width * height, default_color), width(width), height(height) + { + } + std::vector pixels; + u32 width = 0; + u32 height = 0; +}; + +void CopyImageRegion(const ImagePixelData& src, ImagePixelData& dst, const Rect& src_region, + const Rect& dst_region); + +std::optional LoadImage(const std::string& path); + +bool WriteImage(const std::string& path, const ImagePixelData& image); + +enum class ResizeMode +{ + Nearest, +}; + +ImagePixelData Resize(ResizeMode mode, const ImagePixelData& src, u32 new_width, u32 new_height); + +ImagePixelData ResizeKeepAspectRatio(ResizeMode mode, const ImagePixelData& src, u32 new_width, + u32 new_height, const Pixel& background_color); +} // namespace InputCommon diff --git a/Source/Core/InputCommon/InputCommon.vcxproj b/Source/Core/InputCommon/InputCommon.vcxproj index b451760354..8cd9f05963 100644 --- a/Source/Core/InputCommon/InputCommon.vcxproj +++ b/Source/Core/InputCommon/InputCommon.vcxproj @@ -50,7 +50,10 @@ + + + @@ -91,8 +94,11 @@ + + + diff --git a/Source/Core/InputCommon/InputCommon.vcxproj.filters b/Source/Core/InputCommon/InputCommon.vcxproj.filters index 671c2b077d..17f6cdf489 100644 --- a/Source/Core/InputCommon/InputCommon.vcxproj.filters +++ b/Source/Core/InputCommon/InputCommon.vcxproj.filters @@ -138,6 +138,8 @@ ControllerInterface\DualShockUDPClient + + @@ -250,6 +252,8 @@ ControllerInterface\DualShockUDPClient + + diff --git a/Source/Core/InputCommon/InputConfig.cpp b/Source/Core/InputCommon/InputConfig.cpp index 0a046211b0..501e75d958 100644 --- a/Source/Core/InputCommon/InputConfig.cpp +++ b/Source/Core/InputCommon/InputConfig.cpp @@ -39,6 +39,8 @@ bool InputConfig::LoadConfig(bool isGC) std::string ir_values[3]; #endif + m_dynamic_input_tex_config_manager.Load(); + if (SConfig::GetInstance().GetGameID() != "00000000") { std::string type; @@ -191,6 +193,11 @@ void InputConfig::UnregisterHotplugCallback() g_controller_interface.UnregisterDevicesChangedCallback(m_hotplug_callback_handle); } +void InputConfig::OnControllerCreated(ControllerEmu::EmulatedController& controller) +{ + controller.SetDynamicInputTextureManager(&m_dynamic_input_tex_config_manager); +} + bool InputConfig::IsControllerControlledByGamepadDevice(int index) const { if (static_cast(index) >= m_controllers.size()) diff --git a/Source/Core/InputCommon/InputConfig.h b/Source/Core/InputCommon/InputConfig.h index 37d6e06b96..3d0a60dd73 100644 --- a/Source/Core/InputCommon/InputConfig.h +++ b/Source/Core/InputCommon/InputConfig.h @@ -10,6 +10,7 @@ #include #include "InputCommon/ControllerInterface/ControllerInterface.h" +#include "InputCommon/DynamicInputTextureManager.h" namespace ControllerEmu { @@ -30,7 +31,8 @@ public: template void CreateController(Args&&... args) { - m_controllers.emplace_back(std::make_unique(std::forward(args)...)); + OnControllerCreated( + *m_controllers.emplace_back(std::make_unique(std::forward(args)...))); } ControllerEmu::EmulatedController* GetController(int index); @@ -47,9 +49,11 @@ public: void UnregisterHotplugCallback(); private: + void OnControllerCreated(ControllerEmu::EmulatedController& controller); ControllerInterface::HotplugCallbackHandle m_hotplug_callback_handle; std::vector> m_controllers; const std::string m_ini_name; const std::string m_gui_name; const std::string m_profile_name; + InputCommon::DynamicInputTextureManager m_dynamic_input_tex_config_manager; }; diff --git a/Source/Core/VideoCommon/HiresTextures.cpp b/Source/Core/VideoCommon/HiresTextures.cpp index 92ba7ff9e6..d1ced9e307 100644 --- a/Source/Core/VideoCommon/HiresTextures.cpp +++ b/Source/Core/VideoCommon/HiresTextures.cpp @@ -76,8 +76,7 @@ void HiresTexture::Update() if (!g_ActiveConfig.bHiresTextures) { - s_textureMap.clear(); - s_textureCache.clear(); + Clear(); return; } @@ -146,6 +145,12 @@ void HiresTexture::Update() } } +void HiresTexture::Clear() +{ + s_textureMap.clear(); + s_textureCache.clear(); +} + void HiresTexture::Prefetch() { Common::SetCurrentThreadName("Prefetcher"); diff --git a/Source/Core/VideoCommon/HiresTextures.h b/Source/Core/VideoCommon/HiresTextures.h index 2112218f98..7adabbf94e 100644 --- a/Source/Core/VideoCommon/HiresTextures.h +++ b/Source/Core/VideoCommon/HiresTextures.h @@ -22,6 +22,7 @@ class HiresTexture public: static void Init(); static void Update(); + static void Clear(); static void Shutdown(); static std::shared_ptr Search(const u8* texture, size_t texture_size, diff --git a/Source/Core/VideoCommon/RenderBase.cpp b/Source/Core/VideoCommon/RenderBase.cpp index a60f9b9f91..4777314194 100644 --- a/Source/Core/VideoCommon/RenderBase.cpp +++ b/Source/Core/VideoCommon/RenderBase.cpp @@ -1138,6 +1138,11 @@ void Renderer::EndUIFrame() BeginImGuiFrame(); } +void Renderer::ForceReloadTextures() +{ + m_force_reload_textures.Set(); +} + // Heuristic to detect if a GameCube game is in 16:9 anamorphic widescreen mode. void Renderer::UpdateWidescreenHeuristic() { @@ -1302,9 +1307,17 @@ void Renderer::Swap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_height, u6 // state changes the specialized shader will not take over. g_vertex_manager->InvalidatePipelineObject(); - // Flush any outstanding EFB copies to RAM, in case the game is running at an uncapped frame - // rate and not waiting for vblank. Otherwise, we'd end up with a huge list of pending copies. - g_texture_cache->FlushEFBCopies(); + if (m_force_reload_textures.TestAndClear()) + { + g_texture_cache->ForceReload(); + } + else + { + // Flush any outstanding EFB copies to RAM, in case the game is running at an uncapped frame + // rate and not waiting for vblank. Otherwise, we'd end up with a huge list of pending + // copies. + g_texture_cache->FlushEFBCopies(); + } if (!is_duplicate_frame) { diff --git a/Source/Core/VideoCommon/RenderBase.h b/Source/Core/VideoCommon/RenderBase.h index 3b6de06fd8..4044af8e0b 100644 --- a/Source/Core/VideoCommon/RenderBase.h +++ b/Source/Core/VideoCommon/RenderBase.h @@ -259,6 +259,9 @@ public: void BeginUIFrame(); void EndUIFrame(); + // Will forcibly reload all textures on the next swap + void ForceReloadTextures(); + protected: // Bitmask containing information about which configuration has changed for the backend. enum ConfigChangeBits : u32 @@ -410,6 +413,8 @@ private: void FinishFrameData(); std::unique_ptr m_netplay_chat_ui; + + Common::Flag m_force_reload_textures; }; extern std::unique_ptr g_renderer; diff --git a/Source/Core/VideoCommon/TextureCacheBase.cpp b/Source/Core/VideoCommon/TextureCacheBase.cpp index fdf3c0e6b3..ec54f59d9a 100644 --- a/Source/Core/VideoCommon/TextureCacheBase.cpp +++ b/Source/Core/VideoCommon/TextureCacheBase.cpp @@ -137,6 +137,17 @@ void TextureCacheBase::Invalidate() texture_pool.clear(); } +void TextureCacheBase::ForceReload() +{ + Invalidate(); + + // Clear all current hires textures, they are invalid + HiresTexture::Clear(); + + // Load fresh + HiresTexture::Update(); +} + void TextureCacheBase::OnConfigChanged(const VideoConfig& config) { if (config.bHiresTextures != backup_config.hires_textures || diff --git a/Source/Core/VideoCommon/TextureCacheBase.h b/Source/Core/VideoCommon/TextureCacheBase.h index e8ecf5b6fe..9245f7bf3f 100644 --- a/Source/Core/VideoCommon/TextureCacheBase.h +++ b/Source/Core/VideoCommon/TextureCacheBase.h @@ -205,6 +205,7 @@ public: bool Initialize(); void OnConfigChanged(const VideoConfig& config); + void ForceReload(); // Removes textures which aren't used for more than TEXTURE_KILL_THRESHOLD frames, // frameCount is the current frame number. diff --git a/docs/DynamicInputTextures.md b/docs/DynamicInputTextures.md new file mode 100644 index 0000000000..770bb1daba --- /dev/null +++ b/docs/DynamicInputTextures.md @@ -0,0 +1,204 @@ +# Dolphin Dynamic Input Textures Specification (v1) + +## Format +Dynamic Input Textures are generated textures based on a user's input formed from a group of png files and json files. + +``` +\__ Dolphin User Directory + \__ Load (Directory) + \__ DynamicInputTextures (Directory) + \__ FOLDER (Directory) + \__ PNG and JSON GO HERE +``` + +``FOLDER`` can be one or multiple directories which are named after: +* a complete Game ID (e.g. ``SMNE01`` for "New Super Mario Bros. Wii (NTSC)") +* one without a region (e.g. ``SMN`` for "New Super Mario Bros. Wii (All regions)"). +* Any folder name but with an empty ``.txt`` underneath it + +## How to enable + +Place the files in the format above and ensure that "Load Custom Textures" is enabled under the advanced tab of the graphics settings. + +### PNG files + +At a minimum two images are required to support the generation and any number of 'button' images. These need to be in PNG format. + +### JSON files + +You need at least a single json file that describes the generation parameters. You may have multiple JSON files if you prefer that from an organizational standpoint. + +#### Possible fields in the JSON for a texture + +In each json, one or more generated textures can be specified. Each of those textures can have the following fields: + +|Identifier |Required | Since | +|-------------------------|---------|-------| +|``image`` | **Yes** | v1 | +|``emulated_controls`` | **Yes** | v1 | +|``host_controls`` | No | v1 | + +*image* - the image that has the input buttons you wish to replace, can be upscaled/redrawn if desired. + +*emulated_controls* - a map of emulated devices (ex: ``Wiimote1``, ``GCPad2``) each with their own section of emulated buttons that map to an array of "regions". Each region is a rectangle defined as a json array of four entries. The rectangle bounds are offsets into the image where the replacement occurs (left-coordinate, top-coordinate, right-coordinate, bottom-coordinate). + +*host_controls* - a map of devices (ex: ``DInput/0/Keyboard Mouse``, ``XInput/1/Gamepad``, or blank for wildcard) each with their own section of host buttons (keyboard or gamepad values) that each map to an image. This image will act as a replacement in the original image if this key is mapped to one of the buttons under the ``emulated_controls`` section. Required if ``default_host_controls`` is not defined in the global section. + +#### Global fields in the JSON applied to all textures + +The following fields apply to all textures in the json file: + +|Identifier | Since | +|-------------------------|-------| +|``generated_folder_name``| v1 | +|``preserve_aspect_ratio``| v1 | +|``default_host_controls``| v1 | + +*generated_folder_name* - the folder name that the textures will be generated into. Optional, defaults to '_Generated' + +*preserve_aspect_ratio* - will preserve the aspect ratio when replacing the colored boxes with the image. Optional, defaults to on + +*default_host_controls* - a default map of devices to a map of host controls (keyboard or gamepad values) that each maps to an image. + +#### Examples + +Here's an example of generating a single image with the "A" and "B" Wiimote1 buttons replaced to either keyboard buttons or gamepad buttons depending on the user device and the input mapped: + +```js +{ + "generated_folder_name": "MyDynamicTexturePack", + "preserve_aspect_ratio": false, + "output_textures": + { + "tex1_128x128_02870c3b015d8b40_5.png": + { + "image": "icons.png", + "emulated_controls": { + "Wiimote1": + { + "Buttons/A": [ + [0, 0, 30, 30], + [500, 550, 530, 580], + ] + "Buttons/B": [ + [100, 342, 132, 374] + ] + } + }, + "host_controls": { + "DInput/0/Keyboard Mouse": { + "A": "keyboard/a.png", + "B": "keyboard/b.png" + }, + "XInput/0/Gamepad": { + "`Button A`": "gamepad/a.png", + "`Button B`": "gamepad/b.png" + } + } + } + } +} +``` + +As an example, you are writing a pack for a single-player game. You may want to provide DS4 controller icons but not care what device the user is using. You can use a wildcard for that: + +```js +{ + "preserve_aspect_ratio": false, + "output_textures": + { + "tex1_128x128_02870c3b015d8b40_5.png": + { + "image": "icons.png", + "emulated_controls": { + "Wiimote1": + { + "Buttons/A": [ + [0, 0, 30, 30], + [500, 550, 530, 580] + ] + "Buttons/B": [ + [100, 342, 132, 374] + ] + } + }, + "host_controls": { + "": { + "`Button X`": "ds4/x.png", + "`Button Y`": "ds4/y.png" + } + } + } + } +} +``` + +Here's an example of generating multiple images but using defaults from the global section except for one texture: + +```js +{ + "default_host_controls": { + "DInput/0/Keyboard Mouse": { + "A": "keyboard/a.png", + "B": "keyboard/b.png" + } + }, + "default_device": "DInput/0/Keyboard Mouse", + "output_textures": + { + "tex1_21x26_5cbc6943a74cb7ca_67a541879d5fe615_9.png": + { + "image": "icons1.png", + "emulated_controls": { + "Wiimote1": + { + "Buttons/A": [ + [62, 0, 102, 40] + ] + "Buttons/B": [ + [100, 342, 132, 374] + ] + } + } + }, + "tex1_21x26_5e71c27dad9cda76_3d76bd5d1e73c3b1_9.png": + { + "image": "icons2.png", + "emulated_controls": { + "Wiimote1": + { + "Buttons/A": [ + [857, 682, 907, 732] + ] + "Buttons/B": [ + [100, 342, 132, 374] + ] + } + } + }, + "tex1_24x24_003f3a17f66f1704_82848f47946caa41_9.png": + { + "image": "icons3.png", + "emulated_controls": { + "Wiimote1": + { + "Buttons/A": [ + [0, 0, 30, 30], + [500, 550, 530, 580] + ] + "Buttons/B": [ + [100, 342, 132, 374] + ] + } + }, + "host_controls": + { + "DInput/0/Keyboard Mouse": { + "A": "keyboard/a_special.png", + "B": "keyboard/b.png" + } + } + } + } +} +```