@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
@@ -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" />
@@ -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())
@@ -10,6 +10,7 @@
#include <vector>

#include "InputCommon/ControllerInterface/ControllerInterface.h"
#include "InputCommon/DynamicInputTextureManager.h"

namespace ControllerEmu
{
@@ -30,7 +31,8 @@ class InputConfig
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 @@ class InputConfig
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;
};
@@ -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,16 +488,15 @@ 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)
{
if (match_gameid(file))
{
// 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));
@@ -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 @@ class HiresTexture
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;
};
@@ -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)
{
@@ -259,6 +259,9 @@ class Renderer
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 @@ class Renderer
void FinishFrameData();

std::unique_ptr<NetPlayChatUI> m_netplay_chat_ui;

Common::Flag m_force_reload_textures;
};

extern std::unique_ptr<Renderer> g_renderer;
@@ -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 ||
@@ -205,6 +205,7 @@ class TextureCacheBase
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.
@@ -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"
}
}
}
}
}
```