mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-02-10 22:49:00 +01:00
Merge pull request #8318 from iwubcode/dynamic_input_textures
InputCommon: Dynamic Input Textures
This commit is contained in:
commit
fc3b474cce
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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};
|
||||
|
||||
|
@ -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 "
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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};
|
||||
};
|
||||
|
370
Source/Core/InputCommon/DynamicInputTextureConfiguration.cpp
Normal file
370
Source/Core/InputCommon/DynamicInputTextureConfiguration.cpp
Normal 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
|
46
Source/Core/InputCommon/DynamicInputTextureConfiguration.h
Normal file
46
Source/Core/InputCommon/DynamicInputTextureConfiguration.h
Normal 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
|
49
Source/Core/InputCommon/DynamicInputTextureManager.cpp
Normal file
49
Source/Core/InputCommon/DynamicInputTextureManager.cpp
Normal 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
|
27
Source/Core/InputCommon/DynamicInputTextureManager.h
Normal file
27
Source/Core/InputCommon/DynamicInputTextureManager.h
Normal 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
|
250
Source/Core/InputCommon/ImageOperations.cpp
Normal file
250
Source/Core/InputCommon/ImageOperations.cpp
Normal 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
|
65
Source/Core/InputCommon/ImageOperations.h
Normal file
65
Source/Core/InputCommon/ImageOperations.h
Normal 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
|
@ -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 @@ 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;
|
||||
};
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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 @@ 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.
|
||||
|
204
docs/DynamicInputTextures.md
Normal file
204
docs/DynamicInputTextures.md
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
Loading…
x
Reference in New Issue
Block a user