summaryrefslogtreecommitdiff
path: root/Source/Core/InputCommon
diff options
context:
space:
mode:
authorLéo Lam <leo@leolam.fr>2020-10-20 02:10:29 +0200
committerGitHub <noreply@github.com>2020-10-20 02:10:29 +0200
commitfc3b474cceb2a24df21640140be4beb95b9b9fd1 (patch)
tree84c89656ad79357c819b129a927db20cec1cdf9e /Source/Core/InputCommon
parentb26c2e7adb80436f131f1a50e462ee7c25792c48 (diff)
parent4fff04db3ce16c72d8b3b95df2285a129605fde4 (diff)
Merge pull request #8318 from iwubcode/dynamic_input_textures
InputCommon: Dynamic Input Textures
Diffstat (limited to 'Source/Core/InputCommon')
-rw-r--r--Source/Core/InputCommon/CMakeLists.txt7
-rw-r--r--Source/Core/InputCommon/ControllerEmu/ControllerEmu.cpp24
-rw-r--r--Source/Core/InputCommon/ControllerEmu/ControllerEmu.h4
-rw-r--r--Source/Core/InputCommon/DynamicInputTextureConfiguration.cpp370
-rw-r--r--Source/Core/InputCommon/DynamicInputTextureConfiguration.h46
-rw-r--r--Source/Core/InputCommon/DynamicInputTextureManager.cpp49
-rw-r--r--Source/Core/InputCommon/DynamicInputTextureManager.h27
-rw-r--r--Source/Core/InputCommon/ImageOperations.cpp250
-rw-r--r--Source/Core/InputCommon/ImageOperations.h65
-rw-r--r--Source/Core/InputCommon/InputCommon.vcxproj6
-rw-r--r--Source/Core/InputCommon/InputCommon.vcxproj.filters4
-rw-r--r--Source/Core/InputCommon/InputConfig.cpp7
-rw-r--r--Source/Core/InputCommon/InputConfig.h6
13 files changed, 864 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..58c2c84d18
--- /dev/null
+++ b/Source/Core/InputCommon/DynamicInputTextureConfiguration.cpp
@@ -0,0 +1,370 @@
+// 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 "Core/Core.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 (Core::GetState() == Core::State::Starting)
+ 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;
};