| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` |