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.

This commit is contained in:
iwubcode 2019-08-17 14:40:58 -05:00
parent 8a1539f948
commit fd3af4c5d3
24 changed files with 1114 additions and 9 deletions

View file

@ -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"

View file

@ -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;
}
}

View file

@ -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,

View file

@ -232,9 +232,10 @@ void AdvancedWidget::AddDescriptions()
"User/Dump/Textures/<game_id>/. 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/<game_id>/.\n\nIf unsure, leave this "
"unchecked.");
static const char TR_LOAD_CUSTOM_TEXTURE_DESCRIPTION[] =
QT_TR_NOOP("Loads custom textures from User/Load/Textures/<game_id>/ and "
"User/Load/DynamicInputTextures/<game_id>/.\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.");

View file

@ -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)

View file

@ -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

View file

@ -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};
};

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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" />

View file

@ -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())

View file

@ -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;
};

View file

@ -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");

View file

@ -22,6 +22,7 @@ class HiresTexture
public:
static void Init();
static void Update();
static void Clear();
static void Shutdown();
static std::shared_ptr<HiresTexture> Search(const u8* texture, size_t texture_size,

View file

@ -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)
{

View file

@ -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<NetPlayChatUI> m_netplay_chat_ui;
Common::Flag m_force_reload_textures;
};
extern std::unique_ptr<Renderer> g_renderer;

View file

@ -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 ||

View file

@ -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.

View file

@ -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 ``<GAMEID>.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 '<gameid>_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"
}
}
}
}
}
```