Merge pull request #8318 from iwubcode/dynamic_input_textures

InputCommon: Dynamic Input Textures
This commit is contained in:
Léo Lam 2020-10-20 02:10:29 +02:00 committed by GitHub
commit fc3b474cce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1134 additions and 20 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

@ -79,6 +79,7 @@
#include "VideoCommon/AsyncRequests.h"
#include "VideoCommon/Fifo.h"
#include "VideoCommon/HiresTextures.h"
#include "VideoCommon/OnScreenDisplay.h"
#include "VideoCommon/RenderBase.h"
#include "VideoCommon/VideoBackendBase.h"
@ -547,6 +548,10 @@ static void EmuThread(std::unique_ptr<BootParameters> boot, WindowSystemInfo wsi
return;
}
// Inputs loading may have generated custom dynamic textures
// it's now ok to initialize any custom textures
HiresTexture::Update();
AudioCommon::InitSoundStream();
Common::ScopeGuard audio_guard{&AudioCommon::ShutdownSoundStream};

View File

@ -232,8 +232,9 @@ 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 "
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 "

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

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

@ -51,7 +51,7 @@ static std::thread s_prefetcher;
void HiresTexture::Init()
{
Update();
// Note: Update is not called here so that we handle dynamic textures on startup more gracefully
}
void HiresTexture::Shutdown()
@ -76,8 +76,7 @@ void HiresTexture::Update()
if (!g_ActiveConfig.bHiresTextures)
{
s_textureMap.clear();
s_textureCache.clear();
Clear();
return;
}
@ -87,7 +86,8 @@ void HiresTexture::Update()
}
const std::string& game_id = SConfig::GetInstance().GetGameID();
const std::set<std::string> texture_directories = GetTextureDirectories(game_id);
const std::set<std::string> texture_directories =
GetTextureDirectoriesWithGameId(File::GetUserPath(D_HIRESTEXTURES_IDX), game_id);
const std::vector<std::string> extensions{".png", ".dds"};
for (const auto& texture_directory : texture_directories)
@ -145,6 +145,12 @@ void HiresTexture::Update()
}
}
void HiresTexture::Clear()
{
s_textureMap.clear();
s_textureCache.clear();
}
void HiresTexture::Prefetch()
{
Common::SetCurrentThreadName("Prefetcher");
@ -454,10 +460,11 @@ bool HiresTexture::LoadTexture(Level& level, const std::vector<u8>& buffer)
return true;
}
std::set<std::string> HiresTexture::GetTextureDirectories(const std::string& game_id)
std::set<std::string> GetTextureDirectoriesWithGameId(const std::string& root_directory,
const std::string& game_id)
{
std::set<std::string> result;
const std::string texture_directory = File::GetUserPath(D_HIRESTEXTURES_IDX) + game_id;
const std::string texture_directory = root_directory + game_id;
if (File::Exists(texture_directory))
{
@ -466,8 +473,7 @@ std::set<std::string> HiresTexture::GetTextureDirectories(const std::string& gam
else
{
// If there's no directory with the region-specific ID, look for a 3-character region-free one
const std::string region_free_directory =
File::GetUserPath(D_HIRESTEXTURES_IDX) + game_id.substr(0, 3);
const std::string region_free_directory = root_directory + game_id.substr(0, 3);
if (File::Exists(region_free_directory))
{
@ -482,7 +488,6 @@ std::set<std::string> HiresTexture::GetTextureDirectories(const std::string& gam
};
// Look for any other directories that might be specific to the given gameid
const auto root_directory = File::GetUserPath(D_HIRESTEXTURES_IDX);
const auto files = Common::DoFileSearch({root_directory}, {".txt"}, true);
for (const auto& file : files)
{
@ -490,8 +495,8 @@ std::set<std::string> HiresTexture::GetTextureDirectories(const std::string& gam
{
// The following code is used to calculate the top directory
// of a found gameid.txt file
// ex: <dolphin dir>/Load/Textures/My folder/gameids/<gameid>.txt
// would insert "<dolphin dir>/Load/Textures/My folder"
// ex: <root directory>/My folder/gameids/<gameid>.txt
// would insert "<root directory>/My folder"
const auto directory_path = file.substr(root_directory.size());
const std::size_t first_path_separator_position = directory_path.find_first_of(DIR_SEP_CHR);
result.insert(root_directory + directory_path.substr(0, first_path_separator_position));

View File

@ -14,11 +14,15 @@
enum class TextureFormat;
std::set<std::string> GetTextureDirectoriesWithGameId(const std::string& root_directory,
const std::string& game_id);
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,
@ -54,8 +58,6 @@ private:
static bool LoadTexture(Level& level, const std::vector<u8>& buffer);
static void Prefetch();
static std::set<std::string> GetTextureDirectories(const std::string& game_id);
HiresTexture() {}
bool m_has_arbitrary_mipmaps;
};

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();
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.
// 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"
}
}
}
}
}
```