diff options
| author | iwubcode <iwubcode@users.noreply.github.com> | 2019-08-17 14:40:58 -0500 |
|---|---|---|
| committer | iwubcode <iwubcode@users.noreply.github.com> | 2020-10-03 17:10:35 -0500 |
| commit | fd3af4c5d32a34b27c45b478c74fa1127b6438b9 (patch) | |
| tree | df55a2548a01e0cd0578abb41f2c9ad78d1db544 /Source/Core/InputCommon | |
| parent | 8a1539f9487cff0b5785e2d341663e4d7136e2a3 (diff) | |
InputCommon: Introducing the "Dynamic Input Texture". Configuration links an emulated input action to an image based on what host key is defined for that emulated input. Specific regions are called out in configuration that mark where to replace an input button with a host key image.
Diffstat (limited to 'Source/Core/InputCommon')
| -rw-r--r-- | Source/Core/InputCommon/CMakeLists.txt | 7 | ||||
| -rw-r--r-- | Source/Core/InputCommon/ControllerEmu/ControllerEmu.cpp | 24 | ||||
| -rw-r--r-- | Source/Core/InputCommon/ControllerEmu/ControllerEmu.h | 4 | ||||
| -rw-r--r-- | Source/Core/InputCommon/DynamicInputTextureConfiguration.cpp | 367 | ||||
| -rw-r--r-- | Source/Core/InputCommon/DynamicInputTextureConfiguration.h | 46 | ||||
| -rw-r--r-- | Source/Core/InputCommon/DynamicInputTextureManager.cpp | 49 | ||||
| -rw-r--r-- | Source/Core/InputCommon/DynamicInputTextureManager.h | 27 | ||||
| -rw-r--r-- | Source/Core/InputCommon/ImageOperations.cpp | 250 | ||||
| -rw-r--r-- | Source/Core/InputCommon/ImageOperations.h | 65 | ||||
| -rw-r--r-- | Source/Core/InputCommon/InputCommon.vcxproj | 6 | ||||
| -rw-r--r-- | Source/Core/InputCommon/InputCommon.vcxproj.filters | 4 | ||||
| -rw-r--r-- | Source/Core/InputCommon/InputConfig.cpp | 7 | ||||
| -rw-r--r-- | Source/Core/InputCommon/InputConfig.h | 6 |
13 files changed, 861 insertions, 1 deletions
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 <optional> +#include <sstream> +#include <string> + +#include <fmt/format.h> +#include <picojson.h> + +#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<picojson::object>()) + { + 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<bool>()) + { + preserve_aspect_ratio = preserve_aspect_ratio_json.get<bool>(); + } + + 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<std::string>()) + { + generated_folder_name = generated_folder_name_json.get<std::string>(); + } + + const picojson::value& default_host_controls_json = out.get("default_host_controls"); + picojson::object default_host_controls; + if (default_host_controls_json.is<picojson::object>()) + { + default_host_controls = default_host_controls_json.get<picojson::object>(); + } + + const auto output_textures = output_textures_json.get<picojson::object>(); + 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<std::string>() || !emulated_controls.is<picojson::object>()) + { + 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<picojson::object>(); + for (auto& [emulated_controller_name, map] : emulated_controls_json) + { + if (!map.is<picojson::object>()) + { + 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<picojson::object>()) + { + if (!regions_array.is<picojson::array>()) + { + 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<Rect> region_rects; + for (auto& region : regions_array.get<picojson::array>()) + { + Rect r; + if (!region.is<picojson::array>()) + { + 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<picojson::array>(); + + 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<double>(); })) + { + 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<u32>(region_offsets[0].get<double>()); + r.top = static_cast<u32>(region_offsets[1].get<double>()); + r.right = static_cast<u32>(region_offsets[2].get<double>()); + r.bottom = static_cast<u32>(region_offsets[3].get<double>()); + 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<picojson::object>()) + { + host_controls = host_controls_json.get<picojson::object>(); + } + + 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<picojson::object>()) + { + 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<picojson::object>()) + { + 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 <string> +#include <unordered_map> +#include <vector> + +#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::string, std::vector<Rect>>; + std::unordered_map<std::string, EmulatedKeyToRegionsMap> m_emulated_controllers; + + using HostKeyToImagePath = std::unordered_map<std::string, std::string>; + std::unordered_map<std::string, HostKeyToImagePath> 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<DynamicInputTextureData> 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 <set> + +#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<std::string> 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 <string> +#include <vector> + +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<DynamicInputTextureConfiguration> 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 <algorithm> +#include <cmath> +#include <limits> +#include <stack> + +#include <png.h> + +#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<u32>(u * src.width), 0u, src.width - 1); + const u32 y = std::clamp(static_cast<u32>(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<ImagePixelData> LoadImage(const std::string& path) +{ + File::IOFile file; + file.Open(path, "rb"); + std::vector<u8> buffer(file.GetSize()); + file.ReadBytes(buffer.data(), file.GetSize()); + + ImagePixelData image; + std::vector<u8> 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<u8> 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<u8*>(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<double>(new_width - 1); + for (u32 y = 0; y < new_height; y++) + { + const double v = y / static_cast<double>(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<double>(src.width)); + const double corrected_width = new_height * (src.width / static_cast<double>(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 <optional> +#include <string> +#include <vector> + +#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<u32>; +using Rect = MathUtil::Rectangle<u32>; + +struct ImagePixelData +{ + ImagePixelData() = default; + + explicit ImagePixelData(std::vector<Pixel> 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<Pixel> pixels; + u32 width = 0; + u32 height = 0; +}; + +void CopyImageRegion(const ImagePixelData& src, ImagePixelData& dst, const Rect& src_region, + const Rect& dst_region); + +std::optional<ImagePixelData> 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 @@ <ClCompile Include="ControllerInterface\Wiimote\Wiimote.cpp" /> <ClCompile Include="ControllerInterface\XInput\XInput.cpp" /> <ClCompile Include="ControlReference\FunctionExpression.cpp" /> + <ClCompile Include="DynamicInputTextureConfiguration.cpp" /> + <ClCompile Include="DynamicInputTextureManager.cpp" /> <ClCompile Include="GCAdapter.cpp" /> + <ClCompile Include="ImageOperations.cpp" /> <ClCompile Include="InputConfig.cpp" /> <ClCompile Include="InputProfile.cpp" /> </ItemGroup> @@ -91,8 +94,11 @@ <ClInclude Include="ControllerInterface\Win32\Win32.h" /> <ClInclude Include="ControllerInterface\Wiimote\Wiimote.h" /> <ClInclude Include="ControllerInterface\XInput\XInput.h" /> + <ClInclude Include="DynamicInputTextureConfiguration.h" /> + <ClInclude Include="DynamicInputTextureManager.h" /> <ClInclude Include="GCAdapter.h" /> <ClInclude Include="GCPadStatus.h" /> + <ClInclude Include="ImageOperations.h" /> <ClInclude Include="InputConfig.h" /> <ClInclude Include="InputProfile.h" /> </ItemGroup> 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 @@ <ClCompile Include="ControllerInterface\DualShockUDPClient\DualShockUDPClient.cpp"> <Filter>ControllerInterface\DualShockUDPClient</Filter> </ClCompile> + <ClCompile Include="DynamicInputTextureConfiguration.cpp" /> + <ClCompile Include="DynamicInputTextureManager.cpp" /> </ItemGroup> <ItemGroup> <ClInclude Include="GCAdapter.h" /> @@ -250,6 +252,8 @@ <ClInclude Include="ControllerInterface\DualShockUDPClient\DualShockUDPProto.h"> <Filter>ControllerInterface\DualShockUDPClient</Filter> </ClInclude> + <ClInclude Include="DynamicInputTextureConfiguration.h" /> + <ClInclude Include="DynamicInputTextureManager.h" /> </ItemGroup> <ItemGroup> <Text Include="CMakeLists.txt" /> 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<size_t>(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 <vector> #include "InputCommon/ControllerInterface/ControllerInterface.h" +#include "InputCommon/DynamicInputTextureManager.h" namespace ControllerEmu { @@ -30,7 +31,8 @@ public: template <typename T, typename... Args> void CreateController(Args&&... args) { - m_controllers.emplace_back(std::make_unique<T>(std::forward<Args>(args)...)); + OnControllerCreated( + *m_controllers.emplace_back(std::make_unique<T>(std::forward<Args>(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<std::unique_ptr<ControllerEmu::EmulatedController>> 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; }; |
