mirror of
https://github.com/WinampDesktop/winamp.git
synced 2025-06-18 10:35:46 -04:00
UI: Massive revamp, new features and improvements
This commit is contained in:
@ -1,33 +1,33 @@
|
||||
add_library(frontend-common
|
||||
common_host_interface.cpp
|
||||
common_host_interface.h
|
||||
controller_interface.cpp
|
||||
controller_interface.h
|
||||
achievements.cpp
|
||||
achievements.h
|
||||
common_host.cpp
|
||||
common_host.h
|
||||
cubeb_audio_stream.cpp
|
||||
cubeb_audio_stream.h
|
||||
fullscreen_ui.cpp
|
||||
fullscreen_ui.h
|
||||
fullscreen_ui_progress_callback.cpp
|
||||
fullscreen_ui_progress_callback.h
|
||||
game_database.cpp
|
||||
game_database.h
|
||||
game_list.cpp
|
||||
game_list.h
|
||||
game_settings.cpp
|
||||
game_settings.h
|
||||
host_settings.cpp
|
||||
icon.cpp
|
||||
icon.h
|
||||
inhibit_screensaver.cpp
|
||||
inhibit_screensaver.h
|
||||
ini_settings_interface.cpp
|
||||
ini_settings_interface.h
|
||||
input_overlay_ui.cpp
|
||||
input_overlay_ui.h
|
||||
input_manager.cpp
|
||||
input_manager.h
|
||||
input_source.cpp
|
||||
input_source.h
|
||||
imgui_fullscreen.cpp
|
||||
imgui_fullscreen.h
|
||||
imgui_impl_opengl3.cpp
|
||||
imgui_impl_opengl3.h
|
||||
imgui_impl_vulkan.cpp
|
||||
imgui_impl_vulkan.h
|
||||
imgui_manager.cpp
|
||||
imgui_manager.h
|
||||
imgui_overlays.cpp
|
||||
imgui_overlays.h
|
||||
opengl_host_display.cpp
|
||||
opengl_host_display.h
|
||||
postprocessing_chain.cpp
|
||||
@ -36,13 +36,11 @@ add_library(frontend-common
|
||||
postprocessing_shader.h
|
||||
postprocessing_shadergen.cpp
|
||||
postprocessing_shadergen.h
|
||||
save_state_selector_ui.cpp
|
||||
save_state_selector_ui.h
|
||||
vulkan_host_display.cpp
|
||||
vulkan_host_display.h
|
||||
)
|
||||
|
||||
target_link_libraries(frontend-common PUBLIC core common glad cubeb imgui simpleini tinyxml2 rapidjson scmversion)
|
||||
target_link_libraries(frontend-common PUBLIC core common glad cubeb imgui tinyxml2 rapidjson scmversion)
|
||||
|
||||
if(WIN32)
|
||||
target_sources(frontend-common PRIVATE
|
||||
@ -50,16 +48,18 @@ if(WIN32)
|
||||
d3d11_host_display.h
|
||||
d3d12_host_display.cpp
|
||||
d3d12_host_display.h
|
||||
dinput_controller_interface.cpp
|
||||
dinput_controller_interface.h
|
||||
dinput_source.cpp
|
||||
dinput_source.h
|
||||
imgui_impl_dx11.cpp
|
||||
imgui_impl_dx11.h
|
||||
imgui_impl_dx12.cpp
|
||||
imgui_impl_dx12.h
|
||||
win32_raw_input_source.cpp
|
||||
win32_raw_input_source.h
|
||||
xaudio2_audio_stream.cpp
|
||||
xaudio2_audio_stream.h
|
||||
xinput_controller_interface.cpp
|
||||
xinput_controller_interface.h
|
||||
xinput_source.cpp
|
||||
xinput_source.h
|
||||
)
|
||||
target_link_libraries(frontend-common PRIVATE d3d11.lib dxgi.lib)
|
||||
endif()
|
||||
@ -73,8 +73,8 @@ if(SDL2_FOUND)
|
||||
target_sources(frontend-common PRIVATE
|
||||
sdl_audio_stream.cpp
|
||||
sdl_audio_stream.h
|
||||
sdl_controller_interface.cpp
|
||||
sdl_controller_interface.h
|
||||
sdl_input_source.cpp
|
||||
sdl_input_source.h
|
||||
sdl_initializer.cpp
|
||||
sdl_initializer.h
|
||||
)
|
||||
@ -109,6 +109,15 @@ if(ENABLE_DISCORD_PRESENCE)
|
||||
target_link_libraries(frontend-common PRIVATE discord-rpc)
|
||||
endif()
|
||||
|
||||
if(ENABLE_CHEEVOS)
|
||||
target_sources(frontend-common PRIVATE
|
||||
achievements.cpp
|
||||
achievements.h
|
||||
)
|
||||
target_compile_definitions(frontend-common PUBLIC -DWITH_CHEEVOS=1)
|
||||
target_link_libraries(frontend-common PRIVATE rcheevos rapidjson)
|
||||
endif()
|
||||
|
||||
# Copy the provided data directory to the output directory.
|
||||
add_custom_command(TARGET frontend-common POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/data" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}"
|
||||
|
1991
src/frontend-common/achievements.cpp
Normal file
1991
src/frontend-common/achievements.cpp
Normal file
File diff suppressed because it is too large
Load Diff
169
src/frontend-common/achievements.h
Normal file
169
src/frontend-common/achievements.h
Normal file
@ -0,0 +1,169 @@
|
||||
#pragma once
|
||||
#include "common/string.h"
|
||||
#include "core/achievements.h"
|
||||
#include "core/settings.h"
|
||||
#include "core/types.h"
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
class CDImage;
|
||||
class StateWrapper;
|
||||
|
||||
namespace Achievements {
|
||||
enum class AchievementCategory : u32
|
||||
{
|
||||
Local = 0,
|
||||
Core = 3,
|
||||
Unofficial = 5
|
||||
};
|
||||
|
||||
struct Achievement
|
||||
{
|
||||
u32 id;
|
||||
std::string title;
|
||||
std::string description;
|
||||
std::string memaddr;
|
||||
std::string badge_name;
|
||||
|
||||
// badge paths are mutable because they're resolved when they're needed.
|
||||
mutable std::string locked_badge_path;
|
||||
mutable std::string unlocked_badge_path;
|
||||
|
||||
u32 points;
|
||||
AchievementCategory category;
|
||||
bool locked;
|
||||
bool active;
|
||||
};
|
||||
|
||||
struct Leaderboard
|
||||
{
|
||||
u32 id;
|
||||
std::string title;
|
||||
std::string description;
|
||||
int format;
|
||||
};
|
||||
|
||||
struct LeaderboardEntry
|
||||
{
|
||||
std::string user;
|
||||
std::string formatted_score;
|
||||
u32 rank;
|
||||
bool is_self;
|
||||
};
|
||||
|
||||
// RAIntegration only exists for Windows, so no point checking it on other platforms.
|
||||
#ifdef WITH_RAINTEGRATION
|
||||
|
||||
bool IsUsingRAIntegration();
|
||||
|
||||
#else
|
||||
|
||||
static ALWAYS_INLINE bool IsUsingRAIntegration()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
bool IsActive();
|
||||
bool IsLoggedIn();
|
||||
bool ChallengeModeActive();
|
||||
bool IsTestModeActive();
|
||||
bool IsUnofficialTestModeActive();
|
||||
bool IsRichPresenceEnabled();
|
||||
bool HasActiveGame();
|
||||
|
||||
u32 GetGameID();
|
||||
|
||||
/// Acquires the achievements lock. Must be held when accessing any achievement state from another thread.
|
||||
std::unique_lock<std::recursive_mutex> GetLock();
|
||||
|
||||
void Initialize();
|
||||
void UpdateSettings(const Settings& old_config);
|
||||
|
||||
/// Called when the system is being reset. If it returns false, the reset should be aborted.
|
||||
bool Reset();
|
||||
|
||||
/// Called when the system is being shut down. If Shutdown() returns false, the shutdown should be aborted.
|
||||
bool Shutdown();
|
||||
|
||||
/// Called when the system is being paused and resumed.
|
||||
void OnPaused(bool paused);
|
||||
|
||||
/// Called once a frame at vsync time on the CPU thread.
|
||||
void FrameUpdate();
|
||||
|
||||
/// Called when the system is paused, because FrameUpdate() won't be getting called.
|
||||
void ProcessPendingHTTPRequests();
|
||||
|
||||
/// Saves/loads state.
|
||||
bool DoState(StateWrapper& sw);
|
||||
|
||||
/// Returns true if the current game has any achievements or leaderboards.
|
||||
/// Does not need to have the lock held.
|
||||
bool SafeHasAchievementsOrLeaderboards();
|
||||
|
||||
const std::string& GetUsername();
|
||||
const std::string& GetRichPresenceString();
|
||||
|
||||
bool LoginAsync(const char* username, const char* password);
|
||||
bool Login(const char* username, const char* password);
|
||||
void Logout();
|
||||
|
||||
bool HasActiveGame();
|
||||
void GameChanged(const std::string& path, CDImage* image);
|
||||
|
||||
/// Re-enables hardcode mode if it is enabled in the settings.
|
||||
void ResetChallengeMode();
|
||||
|
||||
/// Forces hardcore mode off until next reset.
|
||||
void DisableChallengeMode();
|
||||
|
||||
/// Prompts the user to disable hardcore mode, if they agree, returns true.
|
||||
bool ConfirmChallengeModeDisable(const char* trigger);
|
||||
|
||||
/// Returns true if features such as save states should be disabled.
|
||||
bool ChallengeModeActive();
|
||||
|
||||
const std::string& GetGameTitle();
|
||||
const std::string& GetGameIcon();
|
||||
|
||||
bool EnumerateAchievements(std::function<bool(const Achievement&)> callback);
|
||||
u32 GetUnlockedAchiementCount();
|
||||
u32 GetAchievementCount();
|
||||
u32 GetMaximumPointsForGame();
|
||||
u32 GetCurrentPointsForGame();
|
||||
|
||||
bool EnumerateLeaderboards(std::function<bool(const Leaderboard&)> callback);
|
||||
std::optional<bool> TryEnumerateLeaderboardEntries(u32 id, std::function<bool(const LeaderboardEntry&)> callback);
|
||||
const Leaderboard* GetLeaderboardByID(u32 id);
|
||||
u32 GetLeaderboardCount();
|
||||
bool IsLeaderboardTimeType(const Leaderboard& leaderboard);
|
||||
|
||||
std::pair<u32, u32> GetAchievementProgress(const Achievement& achievement);
|
||||
std::string GetAchievementProgressText(const Achievement& achievement);
|
||||
const std::string& GetAchievementBadgePath(const Achievement& achievement);
|
||||
|
||||
void UnlockAchievement(u32 achievement_id, bool add_notification = true);
|
||||
void SubmitLeaderboard(u32 leaderboard_id, int value);
|
||||
|
||||
#ifdef WITH_RAINTEGRATION
|
||||
void SwitchToRAIntegration();
|
||||
|
||||
namespace RAIntegration {
|
||||
void MainWindowChanged(void* new_handle);
|
||||
void GameChanged();
|
||||
std::vector<std::pair<int, const char*>> GetMenuItems();
|
||||
void ActivateMenuItem(int item);
|
||||
} // namespace RAIntegration
|
||||
#endif
|
||||
} // namespace Achievements
|
||||
|
||||
/// Functions implemented in the frontend.
|
||||
namespace Host {
|
||||
void OnAchievementsRefreshed();
|
||||
void OnAchievementsChallengeModeChanged();
|
||||
} // namespace Host
|
1065
src/frontend-common/common_host.cpp
Normal file
1065
src/frontend-common/common_host.cpp
Normal file
File diff suppressed because it is too large
Load Diff
37
src/frontend-common/common_host.h
Normal file
37
src/frontend-common/common_host.h
Normal file
@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#include "core/system.h"
|
||||
#include <mutex>
|
||||
|
||||
class SettingsInterface;
|
||||
|
||||
namespace CommonHost {
|
||||
/// Initializes configuration.
|
||||
void UpdateLogSettings();
|
||||
|
||||
void Initialize();
|
||||
void Shutdown();
|
||||
|
||||
void SetDefaultSettings(SettingsInterface& si);
|
||||
void SetDefaultControllerSettings(SettingsInterface& si);
|
||||
void SetDefaultHotkeyBindings(SettingsInterface& si);
|
||||
void LoadSettings(SettingsInterface& si, std::unique_lock<std::mutex>& lock);
|
||||
void CheckForSettingsChanges(const Settings& old_settings);
|
||||
void OnSystemStarting();
|
||||
void OnSystemStarted();
|
||||
void OnSystemDestroyed();
|
||||
void OnSystemPaused();
|
||||
void OnSystemResumed();
|
||||
void OnGameChanged(const std::string& disc_path, const std::string& game_serial, const std::string& game_name);
|
||||
void PumpMessagesOnCPUThread();
|
||||
bool CreateHostDisplayResources();
|
||||
void ReleaseHostDisplayResources();
|
||||
} // namespace CommonHost
|
||||
|
||||
namespace ImGuiManager {
|
||||
void RenderDebugWindows();
|
||||
}
|
||||
|
||||
namespace Host {
|
||||
/// Return the current window handle. Needed for DInput.
|
||||
void* GetTopLevelWindowHandle();
|
||||
} // namespace Host
|
File diff suppressed because it is too large
Load Diff
@ -1,586 +0,0 @@
|
||||
#pragma once
|
||||
#include "common/bitfield.h"
|
||||
#include "common/string.h"
|
||||
#include "core/controller.h"
|
||||
#include "core/host_interface.h"
|
||||
#include <atomic>
|
||||
#include <ctime>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
class HostDisplayTexture;
|
||||
|
||||
class GameList;
|
||||
struct GameDatabaseEntry;
|
||||
|
||||
class ControllerInterface;
|
||||
|
||||
namespace FrontendCommon {
|
||||
class SaveStateSelectorUI;
|
||||
|
||||
enum class ControllerNavigationButton : u32
|
||||
{
|
||||
Activate, // A on XBox Controller, Cross on PS Controller
|
||||
Cancel, // B on XBox Controller, Circle on PS Controller
|
||||
LeftShoulder, // LB on XBox Controller, L1 on PS Controller
|
||||
RightShoulder, // RB on XBox Controller, R1 on PS Controller
|
||||
DPadLeft,
|
||||
DPadRight,
|
||||
DPadUp,
|
||||
DPadDown,
|
||||
Count
|
||||
};
|
||||
|
||||
} // namespace FrontendCommon
|
||||
|
||||
class CommonHostInterface : public HostInterface
|
||||
{
|
||||
public:
|
||||
friend ControllerInterface;
|
||||
|
||||
enum : s32
|
||||
{
|
||||
PER_GAME_SAVE_STATE_SLOTS = 10,
|
||||
GLOBAL_SAVE_STATE_SLOTS = 10,
|
||||
NUM_CONTROLLER_AUTOFIRE_BUTTONS = 4,
|
||||
DEFAULT_AUTOFIRE_FREQUENCY = 2
|
||||
};
|
||||
|
||||
using HostKeyCode = s32;
|
||||
using HostMouseButton = s32;
|
||||
|
||||
using InputButtonHandler = std::function<void(bool)>;
|
||||
using InputAxisHandler = std::function<void(float)>;
|
||||
using ControllerRumbleCallback = std::function<void(const float*, u32)>;
|
||||
|
||||
struct HotkeyInfo
|
||||
{
|
||||
String category;
|
||||
String name;
|
||||
String display_name;
|
||||
InputButtonHandler handler;
|
||||
};
|
||||
|
||||
using HotkeyInfoList = std::vector<HotkeyInfo>;
|
||||
|
||||
struct InputProfileEntry
|
||||
{
|
||||
std::string name;
|
||||
std::string path;
|
||||
};
|
||||
using InputProfileList = std::vector<InputProfileEntry>;
|
||||
|
||||
struct SaveStateInfo
|
||||
{
|
||||
std::string path;
|
||||
std::time_t timestamp;
|
||||
s32 slot;
|
||||
bool global;
|
||||
};
|
||||
|
||||
struct ExtendedSaveStateInfo
|
||||
{
|
||||
std::string path;
|
||||
std::string title;
|
||||
std::string game_code;
|
||||
std::string media_path;
|
||||
std::time_t timestamp;
|
||||
s32 slot;
|
||||
bool global;
|
||||
|
||||
u32 screenshot_width;
|
||||
u32 screenshot_height;
|
||||
std::vector<u32> screenshot_data;
|
||||
};
|
||||
|
||||
using HostInterface::SaveState;
|
||||
|
||||
/// Returns the name of the frontend.
|
||||
virtual const char* GetFrontendName() const = 0;
|
||||
|
||||
/// Request the frontend to exit.
|
||||
virtual void RequestExit() = 0;
|
||||
|
||||
/// Runs an event next frame as part of the event loop.
|
||||
virtual void RunLater(std::function<void()> func) = 0;
|
||||
|
||||
/// Thread-safe settings access.
|
||||
std::string GetStringSettingValue(const char* section, const char* key, const char* default_value = "") override;
|
||||
bool GetBoolSettingValue(const char* section, const char* key, bool default_value = false) override;
|
||||
int GetIntSettingValue(const char* section, const char* key, int default_value = 0) override;
|
||||
float GetFloatSettingValue(const char* section, const char* key, float default_value = 0.0f) override;
|
||||
std::vector<std::string> GetSettingStringList(const char* section, const char* key) override;
|
||||
|
||||
/// Loads new settings and applies them.
|
||||
virtual void ApplySettings(bool display_osd_messages);
|
||||
|
||||
/// Reloads the input map from config. Callable from controller interface.
|
||||
void UpdateInputMap();
|
||||
|
||||
virtual bool IsFullscreen() const;
|
||||
virtual bool SetFullscreen(bool enabled);
|
||||
|
||||
virtual bool Initialize() override;
|
||||
|
||||
virtual void Shutdown() override;
|
||||
|
||||
virtual bool BootSystem(std::shared_ptr<SystemBootParameters> parameters) override;
|
||||
virtual void ResetSystem() override;
|
||||
virtual void DestroySystem() override;
|
||||
|
||||
/// Returns the settings interface.
|
||||
SettingsInterface* GetSettingsInterface() override;
|
||||
std::lock_guard<std::recursive_mutex> GetSettingsLock() override;
|
||||
|
||||
/// Returns the game list.
|
||||
ALWAYS_INLINE GameList* GetGameList() const { return m_game_list.get(); }
|
||||
|
||||
/// Returns a list of all available hotkeys.
|
||||
ALWAYS_INLINE const HotkeyInfoList& GetHotkeyInfoList() const { return m_hotkeys; }
|
||||
|
||||
/// Access to current controller interface.
|
||||
ALWAYS_INLINE ControllerInterface* GetControllerInterface() const { return m_controller_interface.get(); }
|
||||
|
||||
/// Returns true if running in batch mode, i.e. exit after emulation.
|
||||
ALWAYS_INLINE bool InBatchMode() const { return m_flags.batch_mode; }
|
||||
|
||||
/// Returns true if the fullscreen UI is enabled.
|
||||
ALWAYS_INLINE bool IsFullscreenUIEnabled() const { return m_fullscreen_ui_enabled; }
|
||||
|
||||
/// Returns true if an undo load state exists.
|
||||
ALWAYS_INLINE bool CanUndoLoadState() const { return static_cast<bool>(m_undo_load_state); }
|
||||
|
||||
/// Parses command line parameters for all frontends.
|
||||
bool ParseCommandLineParameters(int argc, char* argv[], std::unique_ptr<SystemBootParameters>* out_boot_params);
|
||||
|
||||
/// Returns a path where an input profile with the specified name would be saved.
|
||||
std::string GetSavePathForInputProfile(const char* name) const;
|
||||
|
||||
/// Returns a list of all input profiles. first - name, second - path
|
||||
InputProfileList GetInputProfileList() const;
|
||||
|
||||
/// Returns the path for an input profile.
|
||||
std::string GetInputProfilePath(const char* name) const;
|
||||
|
||||
/// Applies the specified input profile.
|
||||
bool ApplyInputProfile(const char* profile_path);
|
||||
|
||||
/// Saves the current input configuration to the specified profile name.
|
||||
bool SaveInputProfile(const char* profile_path);
|
||||
|
||||
/// Powers off the system, optionally saving the resume state.
|
||||
void PowerOffSystem(bool save_resume_state);
|
||||
|
||||
/// Undoes a load state, i.e. restores the state prior to the load.
|
||||
bool UndoLoadState();
|
||||
|
||||
/// Loads state from the specified filename.
|
||||
bool LoadState(const char* filename);
|
||||
|
||||
/// Loads the current emulation state from file. Specifying a slot of -1 loads the "resume" game state.
|
||||
bool LoadState(bool global, s32 slot);
|
||||
|
||||
/// Saves the current emulation state to a file. Specifying a slot of -1 saves the "resume" save state.
|
||||
bool SaveState(bool global, s32 slot);
|
||||
|
||||
/// Returns true if the specified file/disc image is resumable.
|
||||
bool CanResumeSystemFromFile(const char* filename);
|
||||
|
||||
/// Loads the resume save state for the given game. Optionally boots the game anyway if loading fails.
|
||||
bool ResumeSystemFromState(const char* filename, bool boot_on_failure);
|
||||
|
||||
/// Loads the most recent resume save state. This may be global or per-game.
|
||||
bool ResumeSystemFromMostRecentState();
|
||||
|
||||
/// Saves the resume save state, call when shutting down.
|
||||
bool SaveResumeSaveState();
|
||||
|
||||
/// Returns a list of save states for the specified game code.
|
||||
std::vector<SaveStateInfo> GetAvailableSaveStates(const char* game_code) const;
|
||||
|
||||
/// Returns save state info if present. If game_code is null or empty, assumes global state.
|
||||
std::optional<SaveStateInfo> GetSaveStateInfo(const char* game_code, s32 slot);
|
||||
|
||||
/// Returns save state info from opened save state stream.
|
||||
std::optional<ExtendedSaveStateInfo> GetExtendedSaveStateInfo(ByteStream* stream);
|
||||
|
||||
/// Returns save state info if present. If game_code is null or empty, assumes global state.
|
||||
std::optional<ExtendedSaveStateInfo> GetExtendedSaveStateInfo(const char* game_code, s32 slot);
|
||||
|
||||
/// Returns save state info for the undo slot, if present.
|
||||
std::optional<ExtendedSaveStateInfo> GetUndoSaveStateInfo();
|
||||
|
||||
/// Deletes save states for the specified game code. If resume is set, the resume state is deleted too.
|
||||
void DeleteSaveStates(const char* game_code, bool resume);
|
||||
|
||||
/// Adds OSD messages, duration is in seconds.
|
||||
void AddOSDMessage(std::string message, float duration = 2.0f) override;
|
||||
void AddKeyedOSDMessage(std::string key, std::string message, float duration = 2.0f) override;
|
||||
void RemoveKeyedOSDMessage(std::string key) override;
|
||||
void ClearOSDMessages();
|
||||
|
||||
/// async message queue bookeeping for. Should be called on UI thread.
|
||||
void AcquirePendingOSDMessages();
|
||||
|
||||
/// Displays a loading screen with the logo, rendered with ImGui. Use when executing possibly-time-consuming tasks
|
||||
/// such as compiling shaders when starting up.
|
||||
void DisplayLoadingScreen(const char* message, int progress_min = -1, int progress_max = -1,
|
||||
int progress_value = -1) override;
|
||||
|
||||
/// Retrieves information about specified game from game list.
|
||||
void GetGameInfo(const char* path, CDImage* image, std::string* code, std::string* title) override;
|
||||
|
||||
/// Returns true if currently dumping audio.
|
||||
bool IsDumpingAudio() const;
|
||||
|
||||
/// Starts dumping audio to a file. If no file name is provided, one will be generated automatically.
|
||||
bool StartDumpingAudio(const char* filename = nullptr);
|
||||
|
||||
/// Stops dumping audio to file if it has been started.
|
||||
void StopDumpingAudio();
|
||||
|
||||
/// Saves a screenshot to the specified file. IF no file name is provided, one will be generated automatically.
|
||||
bool SaveScreenshot(const char* filename = nullptr, bool full_resolution = true, bool apply_aspect_ratio = true,
|
||||
bool compress_on_thread = true);
|
||||
|
||||
/// Loads the cheat list from the specified file.
|
||||
bool LoadCheatList(const char* filename);
|
||||
|
||||
/// Loads the cheat list for the current game title from the user directory.
|
||||
bool LoadCheatListFromGameTitle();
|
||||
|
||||
/// Loads the cheat list for the current game code from the built-in code database.
|
||||
bool LoadCheatListFromDatabase();
|
||||
|
||||
/// Saves the current cheat list to the game title's file.
|
||||
bool SaveCheatList();
|
||||
|
||||
/// Saves the current cheat list to the specified file.
|
||||
bool SaveCheatList(const char* filename);
|
||||
|
||||
/// Deletes the cheat list, if present.
|
||||
bool DeleteCheatList();
|
||||
|
||||
/// Removes all cheats from the cheat list.
|
||||
void ClearCheatList(bool save_to_file);
|
||||
|
||||
/// Enables/disabled the specified cheat code.
|
||||
void SetCheatCodeState(u32 index, bool enabled, bool save_to_file);
|
||||
|
||||
/// Immediately applies the specified cheat code.
|
||||
void ApplyCheatCode(u32 index);
|
||||
|
||||
/// Temporarily toggles post-processing on/off.
|
||||
void TogglePostProcessing();
|
||||
|
||||
/// Reloads post processing shaders with the current configuration.
|
||||
void ReloadPostProcessingShaders();
|
||||
|
||||
/// Toggle Widescreen Hack and Aspect Ratio
|
||||
void ToggleWidescreen();
|
||||
|
||||
/// Swaps memory cards in slot 1/2.
|
||||
void SwapMemoryCards();
|
||||
|
||||
/// Parses a fullscreen mode into its components (width * height @ refresh hz)
|
||||
static bool ParseFullscreenMode(const std::string_view& mode, u32* width, u32* height, float* refresh_rate);
|
||||
|
||||
/// Converts a fullscreen mode to a string.
|
||||
static std::string GetFullscreenModeString(u32 width, u32 height, float refresh_rate);
|
||||
|
||||
/// Returns true if the state should be saved on shutdown.
|
||||
bool ShouldSaveResumeState() const;
|
||||
|
||||
/// Returns true if fast forwarding or slow motion is currently active.
|
||||
bool IsRunningAtNonStandardSpeed() const;
|
||||
|
||||
/// Requests the specified size for the render window. Not guaranteed to succeed (e.g. if in fullscreen).
|
||||
virtual bool RequestRenderWindowSize(s32 new_window_width, s32 new_window_height);
|
||||
|
||||
/// Requests a resize to a multiple of the render window size.
|
||||
bool RequestRenderWindowScale(float scale);
|
||||
|
||||
/// Returns a pointer to the top-level window, needed by some controller interfaces.
|
||||
virtual void* GetTopLevelWindowHandle() const;
|
||||
|
||||
/// Called when achievements data is loaded.
|
||||
virtual void OnAchievementsRefreshed() override;
|
||||
|
||||
/// Opens a file in the DuckStation "package".
|
||||
/// This is the APK for Android builds, or the program directory for standalone builds.
|
||||
virtual std::unique_ptr<ByteStream> OpenPackageFile(const char* path, u32 flags) override;
|
||||
|
||||
/// Returns true if the fullscreen UI is intercepting controller input.
|
||||
bool IsControllerNavigationActive() const;
|
||||
|
||||
/// Controller navigation, used by fullscreen mode.
|
||||
void SetControllerNavigationButtonState(FrontendCommon::ControllerNavigationButton button, bool pressed);
|
||||
|
||||
/// Alters autofire state for controllers (activates/deactivates).
|
||||
void SetControllerAutoFireSlotState(u32 controller_index, u32 slot_index, bool active);
|
||||
|
||||
/// Toggles fast forward state.
|
||||
bool IsFastForwardEnabled() const { return m_fast_forward_enabled; }
|
||||
void SetFastForwardEnabled(bool enabled);
|
||||
|
||||
/// Toggles turbo state.
|
||||
bool IsTurboEnabled() const { return m_turbo_enabled; }
|
||||
void SetTurboEnabled(bool enabled);
|
||||
|
||||
/// Toggles rewind state.
|
||||
void SetRewindState(bool enabled);
|
||||
|
||||
/// ImGui window drawing.
|
||||
void DrawStatsOverlay();
|
||||
void DrawEnhancementsOverlay();
|
||||
void DrawOSDMessages();
|
||||
void DrawDebugWindows();
|
||||
|
||||
/// Returns true if features such as save states should be disabled.
|
||||
bool IsCheevosChallengeModeActive() const;
|
||||
|
||||
protected:
|
||||
enum : u32
|
||||
{
|
||||
SETTINGS_VERSION = 3
|
||||
};
|
||||
|
||||
struct OSDMessage
|
||||
{
|
||||
std::string key;
|
||||
std::string text;
|
||||
Common::Timer time;
|
||||
float duration;
|
||||
};
|
||||
|
||||
CommonHostInterface();
|
||||
~CommonHostInterface();
|
||||
|
||||
/// Registers frontend-specific hotkeys.
|
||||
virtual void RegisterHotkeys();
|
||||
|
||||
/// Executes per-frame tasks such as controller polling.
|
||||
virtual void PollAndUpdate();
|
||||
|
||||
/// Saves the undo load state, so it can be restored.
|
||||
bool SaveUndoLoadState();
|
||||
|
||||
virtual std::unique_ptr<AudioStream> CreateAudioStream(AudioBackend backend) override;
|
||||
virtual s32 GetAudioOutputVolume() const override;
|
||||
virtual void UpdateControllerInterface();
|
||||
|
||||
virtual void OnSystemCreated() override;
|
||||
virtual void OnSystemPaused(bool paused) override;
|
||||
virtual void OnSystemDestroyed() override;
|
||||
virtual void OnRunningGameChanged(const std::string& path, CDImage* image, const std::string& game_code,
|
||||
const std::string& game_title) override;
|
||||
virtual void OnControllerTypeChanged(u32 slot) override;
|
||||
|
||||
virtual std::optional<HostKeyCode> GetHostKeyCode(const std::string_view key_code) const;
|
||||
|
||||
virtual bool AddButtonToInputMap(const std::string& binding, const std::string_view& device,
|
||||
const std::string_view& button, InputButtonHandler handler);
|
||||
virtual bool AddAxisToInputMap(const std::string& binding, const std::string_view& device,
|
||||
const std::string_view& axis, Controller::AxisType axis_type,
|
||||
InputAxisHandler handler);
|
||||
virtual bool AddRumbleToInputMap(const std::string& binding, u32 controller_index, u32 num_motors);
|
||||
|
||||
void RegisterHotkey(String category, String name, String display_name, InputButtonHandler handler);
|
||||
bool HandleHostKeyEvent(HostKeyCode code, HostKeyCode modifiers, bool pressed);
|
||||
bool HandleHostMouseEvent(HostMouseButton button, bool pressed);
|
||||
virtual void UpdateInputMap(SettingsInterface& si);
|
||||
void ClearInputMap();
|
||||
|
||||
/// Updates controller metastate, including turbo and rumble.
|
||||
void UpdateControllerMetaState();
|
||||
|
||||
void AddControllerRumble(u32 controller_index, u32 num_motors, ControllerRumbleCallback callback);
|
||||
void UpdateControllerRumble();
|
||||
void StopControllerRumble();
|
||||
|
||||
void SetControllerAutoFireState(u32 controller_index, s32 button_code, bool active);
|
||||
void StopControllerAutoFire();
|
||||
void UpdateControllerAutoFire();
|
||||
|
||||
/// Returns the path to a save state file. Specifying an index of -1 is the "resume" save state.
|
||||
std::string GetGameSaveStateFileName(const char* game_code, s32 slot) const;
|
||||
|
||||
/// Returns the path to a save state file. Specifying an index of -1 is the "resume" save state.
|
||||
std::string GetGlobalSaveStateFileName(s32 slot) const;
|
||||
|
||||
/// Moves the current save state file to a backup name, if it exists.
|
||||
void RenameCurrentSaveStateToBackup(const char* filename);
|
||||
|
||||
/// Sets the base path for the user directory. Can be overridden by platform/frontend/command line.
|
||||
virtual void SetUserDirectory();
|
||||
|
||||
/// Updates logging settings.
|
||||
virtual void UpdateLogSettings(LOGLEVEL level, const char* filter, bool log_to_console, bool log_to_debug,
|
||||
bool log_to_window, bool log_to_file);
|
||||
|
||||
/// Returns the path of the settings file.
|
||||
std::string GetSettingsFileName() const;
|
||||
|
||||
/// Returns the most recent resume save state.
|
||||
std::string GetMostRecentResumeSaveStatePath() const;
|
||||
|
||||
/// Returns the path to the cheat file for the specified game title.
|
||||
std::string GetCheatFileName() const;
|
||||
|
||||
/// Restores all settings to defaults.
|
||||
virtual void SetDefaultSettings(SettingsInterface& si) override;
|
||||
|
||||
/// Resets known settings to default.
|
||||
virtual void SetDefaultSettings();
|
||||
|
||||
/// Loads settings to m_settings and any frontend-specific parameters.
|
||||
virtual void LoadSettings(SettingsInterface& si) override;
|
||||
|
||||
/// Saves current settings variables to ini.
|
||||
virtual void SaveSettings(SettingsInterface& si) override;
|
||||
|
||||
/// Checks and fixes up any incompatible settings.
|
||||
virtual void FixIncompatibleSettings(bool display_osd_messages) override;
|
||||
|
||||
/// Checks for settings changes, std::move() the old settings away for comparing beforehand.
|
||||
virtual void CheckForSettingsChanges(const Settings& old_settings) override;
|
||||
|
||||
/// Increases timer resolution when supported by the host OS.
|
||||
void SetTimerResolutionIncreased(bool enabled);
|
||||
|
||||
void UpdateSpeedLimiterState();
|
||||
|
||||
void RecreateSystem() override;
|
||||
void OnHostDisplayResized() override;
|
||||
|
||||
void ApplyGameSettings(bool display_osd_messages);
|
||||
void ApplyRendererFromGameSettings(const std::string& boot_filename);
|
||||
void ApplyControllerCompatibilitySettings(u64 controller_mask, bool display_osd_messages);
|
||||
|
||||
bool CreateHostDisplayResources();
|
||||
void ReleaseHostDisplayResources();
|
||||
|
||||
virtual void DrawImGuiWindows();
|
||||
|
||||
void DoFrameStep();
|
||||
void DoToggleCheats();
|
||||
|
||||
std::unique_ptr<SettingsInterface> m_settings_interface;
|
||||
std::recursive_mutex m_settings_mutex;
|
||||
|
||||
std::unique_ptr<GameList> m_game_list;
|
||||
|
||||
std::unique_ptr<ControllerInterface> m_controller_interface;
|
||||
|
||||
std::unique_ptr<HostDisplayTexture> m_logo_texture;
|
||||
|
||||
std::deque<OSDMessage> m_osd_active_messages; // accessed only by GUI/OSD thread (no lock reqs)
|
||||
std::deque<OSDMessage> m_osd_posted_messages; // written to by multiple threads.
|
||||
std::mutex m_osd_messages_lock;
|
||||
|
||||
bool m_fullscreen_ui_enabled = false;
|
||||
bool m_frame_step_request = false;
|
||||
bool m_fast_forward_enabled = false;
|
||||
bool m_turbo_enabled = false;
|
||||
bool m_throttler_enabled = true;
|
||||
bool m_display_all_frames = true;
|
||||
|
||||
union
|
||||
{
|
||||
u8 bits;
|
||||
|
||||
// running in batch mode? i.e. exit after stopping emulation
|
||||
BitField<u8, bool, 0, 1> batch_mode;
|
||||
|
||||
// disable controller interface (buggy devices with SDL)
|
||||
BitField<u8, bool, 1, 1> disable_controller_interface;
|
||||
|
||||
// starting fullscreen (outside of boot options)
|
||||
BitField<u8, bool, 2, 1> start_fullscreen;
|
||||
|
||||
// force fullscreen UI enabled (nogui)
|
||||
BitField<u8, bool, 3, 1> force_fullscreen_ui;
|
||||
} m_flags = {};
|
||||
|
||||
private:
|
||||
void LoadSettings();
|
||||
void InitializeUserDirectory();
|
||||
void RegisterGeneralHotkeys();
|
||||
void RegisterSystemHotkeys();
|
||||
void RegisterGraphicsHotkeys();
|
||||
void RegisterSaveStateHotkeys();
|
||||
void RegisterAudioHotkeys();
|
||||
void FindInputProfiles(const std::string& base_path, InputProfileList* out_list) const;
|
||||
void UpdateControllerInputMap(SettingsInterface& si);
|
||||
bool UpdateControllerInputMapFromGameSettings();
|
||||
void UpdateHotkeyInputMap(SettingsInterface& si);
|
||||
void ClearAllControllerBindings();
|
||||
void CreateImGuiContext();
|
||||
|
||||
#ifdef WITH_DISCORD_PRESENCE
|
||||
void SetDiscordPresenceEnabled(bool enabled);
|
||||
void InitializeDiscordPresence();
|
||||
void ShutdownDiscordPresence();
|
||||
void UpdateDiscordPresence(bool rich_presence_only);
|
||||
void PollDiscordPresence();
|
||||
#endif
|
||||
|
||||
#ifdef WITH_CHEEVOS
|
||||
void UpdateCheevosActive();
|
||||
#endif
|
||||
|
||||
HotkeyInfoList m_hotkeys;
|
||||
|
||||
std::unique_ptr<FrontendCommon::SaveStateSelectorUI> m_save_state_selector_ui;
|
||||
|
||||
// input key maps
|
||||
std::map<HostKeyCode, InputButtonHandler> m_keyboard_input_handlers;
|
||||
std::map<HostMouseButton, InputButtonHandler> m_mouse_input_handlers;
|
||||
|
||||
// controller vibration motors/rumble
|
||||
struct ControllerRumbleState
|
||||
{
|
||||
enum : u32
|
||||
{
|
||||
MAX_MOTORS = 2
|
||||
};
|
||||
|
||||
u32 controller_index;
|
||||
u32 num_motors;
|
||||
std::array<float, MAX_MOTORS> last_strength;
|
||||
u64 last_update_time;
|
||||
ControllerRumbleCallback update_callback;
|
||||
};
|
||||
std::vector<ControllerRumbleState> m_controller_vibration_motors;
|
||||
|
||||
// controller turbo buttons
|
||||
struct ControllerAutoFireState
|
||||
{
|
||||
u32 controller_index;
|
||||
u32 slot_index;
|
||||
s32 button_code;
|
||||
u8 frequency;
|
||||
u8 countdown;
|
||||
bool active;
|
||||
bool state;
|
||||
};
|
||||
std::vector<ControllerAutoFireState> m_controller_autofires;
|
||||
|
||||
// temporary save state, created when loading, used to undo load state
|
||||
std::unique_ptr<ByteStream> m_undo_load_state;
|
||||
|
||||
#ifdef WITH_DISCORD_PRESENCE
|
||||
// discord rich presence
|
||||
bool m_discord_presence_enabled = false;
|
||||
bool m_discord_presence_active = false;
|
||||
#ifdef WITH_CHEEVOS
|
||||
std::string m_discord_presence_cheevos_string;
|
||||
#endif
|
||||
#endif
|
||||
};
|
@ -1,189 +0,0 @@
|
||||
#include "controller_interface.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/controller.h"
|
||||
#include "core/system.h"
|
||||
#include <cmath>
|
||||
Log_SetChannel(ControllerInterface);
|
||||
|
||||
ControllerInterface::ControllerInterface() = default;
|
||||
|
||||
ControllerInterface::~ControllerInterface() = default;
|
||||
|
||||
bool ControllerInterface::Initialize(CommonHostInterface* host_interface)
|
||||
{
|
||||
m_host_interface = host_interface;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ControllerInterface::Shutdown()
|
||||
{
|
||||
m_host_interface = nullptr;
|
||||
}
|
||||
|
||||
std::optional<int> ControllerInterface::GetControllerIndex(const std::string_view& device)
|
||||
{
|
||||
if (!StringUtil::StartsWith(device, "Controller"))
|
||||
return std::nullopt;
|
||||
|
||||
const std::optional<int> controller_index = StringUtil::FromChars<int>(device.substr(10));
|
||||
if (!controller_index || *controller_index < 0)
|
||||
{
|
||||
Log_WarningPrintf("Invalid controller index in button binding '%*s'", static_cast<int>(device.length()),
|
||||
device.data());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return controller_index;
|
||||
}
|
||||
|
||||
void ControllerInterface::SetHook(Hook::Callback callback)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
|
||||
Assert(!m_event_intercept_callback);
|
||||
m_event_intercept_callback = std::move(callback);
|
||||
}
|
||||
|
||||
void ControllerInterface::ClearHook()
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
|
||||
if (m_event_intercept_callback)
|
||||
m_event_intercept_callback = {};
|
||||
}
|
||||
|
||||
bool ControllerInterface::HasHook()
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
|
||||
return (bool)m_event_intercept_callback;
|
||||
}
|
||||
|
||||
bool ControllerInterface::DoEventHook(Hook::Type type, int controller_index, int button_or_axis_number,
|
||||
std::variant<float, std::string_view> value, bool track_history)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
|
||||
if (!m_event_intercept_callback)
|
||||
return false;
|
||||
|
||||
const Hook ei{type, controller_index, button_or_axis_number, std::move(value), track_history};
|
||||
const Hook::CallbackResult action = m_event_intercept_callback(ei);
|
||||
if (action == Hook::CallbackResult::StopMonitoring)
|
||||
m_event_intercept_callback = {};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ControllerInterface::OnControllerConnected(int host_id)
|
||||
{
|
||||
Log_InfoPrintf("Host controller %d connected, updating input map", host_id);
|
||||
m_host_interface->UpdateInputMap();
|
||||
}
|
||||
|
||||
void ControllerInterface::OnControllerDisconnected(int host_id)
|
||||
{
|
||||
Log_InfoPrintf("Host controller %d disconnected, updating input map", host_id);
|
||||
m_host_interface->UpdateInputMap();
|
||||
}
|
||||
|
||||
void ControllerInterface::ClearBindings() {}
|
||||
|
||||
bool ControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side,
|
||||
AxisCallback callback)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ControllerInterface::BindControllerButton(int controller_index, int button_number, ButtonCallback callback)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ControllerInterface::BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||
ButtonCallback callback)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
static constexpr std::array<const char*, static_cast<u32>(ControllerInterface::Backend::Count)> s_backend_names = {{
|
||||
TRANSLATABLE("ControllerInterface", "None"),
|
||||
#ifdef WITH_SDL2
|
||||
TRANSLATABLE("ControllerInterface", "SDL"),
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
TRANSLATABLE("ControllerInterface", "XInput"),
|
||||
#endif
|
||||
#ifdef WITH_DINPUT
|
||||
TRANSLATABLE("ControllerInterface", "DInput"),
|
||||
#endif
|
||||
#ifdef ANDROID
|
||||
// Deliberately not translated as it's not exposed to users.
|
||||
"Android",
|
||||
#endif
|
||||
#ifdef WITH_EVDEV
|
||||
TRANSLATABLE("ControllerInterface", "Evdev"),
|
||||
#endif
|
||||
}};
|
||||
|
||||
std::optional<ControllerInterface::Backend> ControllerInterface::ParseBackendName(const char* name)
|
||||
{
|
||||
for (u32 i = 0; i < static_cast<u32>(s_backend_names.size()); i++)
|
||||
{
|
||||
if (StringUtil::Strcasecmp(name, s_backend_names[i]) == 0)
|
||||
return static_cast<Backend>(i);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const char* ControllerInterface::GetBackendName(Backend type)
|
||||
{
|
||||
return s_backend_names[static_cast<u32>(type)];
|
||||
}
|
||||
|
||||
ControllerInterface::Backend ControllerInterface::GetDefaultBackend()
|
||||
{
|
||||
#ifdef WITH_SDL2
|
||||
return Backend::SDL;
|
||||
#else
|
||||
#ifdef _WIN32
|
||||
return Backend::XInput;
|
||||
#else
|
||||
return Backend::None;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef WITH_SDL2
|
||||
#include "sdl_controller_interface.h"
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
#include "xinput_controller_interface.h"
|
||||
#endif
|
||||
#ifdef WITH_DINPUT
|
||||
#include "dinput_controller_interface.h"
|
||||
#endif
|
||||
#ifdef WITH_EVDEV
|
||||
#include "evdev_controller_interface.h"
|
||||
#endif
|
||||
|
||||
std::unique_ptr<ControllerInterface> ControllerInterface::Create(Backend type)
|
||||
{
|
||||
#ifdef WITH_SDL2
|
||||
if (type == Backend::SDL)
|
||||
return std::make_unique<SDLControllerInterface>();
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
if (type == Backend::XInput)
|
||||
return std::make_unique<XInputControllerInterface>();
|
||||
#endif
|
||||
#ifdef WITH_DINPUT
|
||||
if (type == Backend::DInput)
|
||||
return std::make_unique<DInputControllerInterface>();
|
||||
#endif
|
||||
#ifdef WITH_EVDEV
|
||||
if (type == Backend::Evdev)
|
||||
return std::make_unique<EvdevControllerInterface>();
|
||||
#endif
|
||||
|
||||
return {};
|
||||
}
|
@ -1,131 +0,0 @@
|
||||
#pragma once
|
||||
#include "common_host_interface.h"
|
||||
#include "core/types.h"
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
|
||||
class HostInterface;
|
||||
class Controller;
|
||||
|
||||
class ControllerInterface
|
||||
{
|
||||
public:
|
||||
enum class Backend
|
||||
{
|
||||
None,
|
||||
#ifdef WITH_SDL2
|
||||
SDL,
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
XInput,
|
||||
#endif
|
||||
#ifdef WITH_DINPUT
|
||||
DInput,
|
||||
#endif
|
||||
#ifdef ANDROID
|
||||
Android,
|
||||
#endif
|
||||
#ifdef WITH_EVDEV
|
||||
Evdev,
|
||||
#endif
|
||||
Count
|
||||
};
|
||||
|
||||
enum : int
|
||||
{
|
||||
NUM_HAT_DIRECTIONS = 4,
|
||||
HAT_DIRECTION_UP = 0,
|
||||
HAT_DIRECTION_DOWN = 1,
|
||||
HAT_DIRECTION_LEFT = 2,
|
||||
HAT_DIRECTION_RIGHT = 3,
|
||||
};
|
||||
|
||||
enum AxisSide
|
||||
{
|
||||
Full,
|
||||
Positive,
|
||||
Negative
|
||||
};
|
||||
|
||||
using AxisCallback = CommonHostInterface::InputAxisHandler;
|
||||
using ButtonCallback = CommonHostInterface::InputButtonHandler;
|
||||
|
||||
ControllerInterface();
|
||||
virtual ~ControllerInterface();
|
||||
|
||||
static std::optional<Backend> ParseBackendName(const char* name);
|
||||
static const char* GetBackendName(Backend type);
|
||||
static Backend GetDefaultBackend();
|
||||
static std::unique_ptr<ControllerInterface> Create(Backend type);
|
||||
|
||||
virtual Backend GetBackend() const = 0;
|
||||
virtual bool Initialize(CommonHostInterface* host_interface);
|
||||
virtual void Shutdown();
|
||||
|
||||
// Removes all bindings. Call before setting new bindings.
|
||||
virtual void ClearBindings() = 0;
|
||||
|
||||
// Binding to events. If a binding for this axis/button already exists, returns false.
|
||||
virtual std::optional<int> GetControllerIndex(const std::string_view& device);
|
||||
virtual bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) = 0;
|
||||
virtual bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) = 0;
|
||||
virtual bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||
ButtonCallback callback) = 0;
|
||||
virtual bool BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position,
|
||||
ButtonCallback callback) = 0;
|
||||
virtual bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) = 0;
|
||||
|
||||
virtual void PollEvents() = 0;
|
||||
|
||||
// Changing rumble strength.
|
||||
virtual u32 GetControllerRumbleMotorCount(int controller_index) = 0;
|
||||
virtual void SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors) = 0;
|
||||
|
||||
// Set deadzone that will be applied on axis-to-button mappings
|
||||
virtual bool SetControllerDeadzone(int controller_index, float size) = 0;
|
||||
|
||||
// Input monitoring for external access.
|
||||
struct Hook
|
||||
{
|
||||
enum class Type
|
||||
{
|
||||
Axis,
|
||||
Button,
|
||||
Hat // Only for joysticks
|
||||
};
|
||||
|
||||
enum class CallbackResult
|
||||
{
|
||||
StopMonitoring,
|
||||
ContinueMonitoring
|
||||
};
|
||||
|
||||
using Callback = std::function<CallbackResult(const Hook& ei)>;
|
||||
|
||||
Type type;
|
||||
int controller_index;
|
||||
int button_or_axis_number;
|
||||
std::variant<float, std::string_view> value; // 0/1 for buttons, -1..1 for axes, hat direction name for hats
|
||||
bool track_history; // Track axis movement to spot inversion/half axes
|
||||
};
|
||||
void SetHook(Hook::Callback callback);
|
||||
void ClearHook();
|
||||
bool HasHook();
|
||||
|
||||
protected:
|
||||
bool DoEventHook(Hook::Type type, int controller_index, int button_or_axis_number,
|
||||
std::variant<float, std::string_view> value, bool track_history = false);
|
||||
|
||||
void OnControllerConnected(int host_id);
|
||||
void OnControllerDisconnected(int host_id);
|
||||
|
||||
CommonHostInterface* m_host_interface = nullptr;
|
||||
|
||||
std::mutex m_event_intercept_mutex;
|
||||
Hook::Callback m_event_intercept_callback;
|
||||
};
|
@ -4,8 +4,8 @@
|
||||
#include "common/d3d11/shader_compiler.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "common_host_interface.h"
|
||||
#include "core/host_interface.h"
|
||||
#include "common_host.h"
|
||||
#include "core/host_settings.h"
|
||||
#include "core/settings.h"
|
||||
#include "core/shader_cache_version.h"
|
||||
#include "display_ps.hlsl.h"
|
||||
@ -395,8 +395,7 @@ bool D3D11HostDisplay::CreateSwapChain(const DXGI_MODE_DESC* fullscreen_mode)
|
||||
if (m_window_info.type != WindowInfo::Type::Win32)
|
||||
return false;
|
||||
|
||||
m_using_flip_model_swap_chain =
|
||||
fullscreen_mode || !g_host_interface->GetBoolSettingValue("Display", "UseBlitSwapChain", false);
|
||||
m_using_flip_model_swap_chain = fullscreen_mode || !Host::GetBoolSettingValue("Display", "UseBlitSwapChain", false);
|
||||
|
||||
const HWND window_hwnd = reinterpret_cast<HWND>(m_window_info.window_handle);
|
||||
RECT client_rc{};
|
||||
@ -966,7 +965,7 @@ HostDisplay::AdapterAndModeList D3D11HostDisplay::GetAdapterAndModeList(IDXGIFac
|
||||
{
|
||||
for (const DXGI_MODE_DESC& mode : modes)
|
||||
{
|
||||
adapter_info.fullscreen_modes.push_back(CommonHostInterface::GetFullscreenModeString(
|
||||
adapter_info.fullscreen_modes.push_back(GetFullscreenModeString(
|
||||
mode.Width, mode.Height,
|
||||
static_cast<float>(mode.RefreshRate.Numerator) / static_cast<float>(mode.RefreshRate.Denominator)));
|
||||
}
|
||||
@ -1017,7 +1016,7 @@ bool D3D11HostDisplay::SetPostProcessingChain(const std::string_view& config)
|
||||
m_post_processing_stages.clear();
|
||||
|
||||
D3D11::ShaderCache shader_cache;
|
||||
shader_cache.Open(g_host_interface->GetShaderCacheBasePath(), m_device->GetFeatureLevel(), SHADER_CACHE_VERSION,
|
||||
shader_cache.Open(EmuFolders::Cache, m_device->GetFeatureLevel(), SHADER_CACHE_VERSION,
|
||||
g_settings.gpu_use_debug_device);
|
||||
|
||||
FrontendCommon::PostProcessingShaderGen shadergen(HostDisplay::RenderAPI::D3D11, true);
|
||||
|
@ -5,7 +5,6 @@
|
||||
#include "common/d3d12/util.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/host_interface.h"
|
||||
#include "core/settings.h"
|
||||
#include "display_ps.hlsl.h"
|
||||
#include "display_vs.hlsl.h"
|
||||
|
@ -1,463 +0,0 @@
|
||||
#define INITGUID
|
||||
|
||||
#include "dinput_controller_interface.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/file_system.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/controller.h"
|
||||
#include "core/host_interface.h"
|
||||
#include "core/system.h"
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
Log_SetChannel(DInputControllerInterface);
|
||||
|
||||
using PFNDIRECTINPUT8CREATE = HRESULT(WINAPI*)(HINSTANCE hinst, DWORD dwVersion, REFIID riidltf, LPVOID* ppvOut,
|
||||
LPUNKNOWN punkOuter);
|
||||
using PFNGETDFDIJOYSTICK = LPCDIDATAFORMAT(WINAPI*)();
|
||||
|
||||
DInputControllerInterface::DInputControllerInterface() = default;
|
||||
|
||||
DInputControllerInterface::~DInputControllerInterface()
|
||||
{
|
||||
m_controllers.clear();
|
||||
m_dinput.Reset();
|
||||
if (m_dinput_module)
|
||||
FreeLibrary(m_dinput_module);
|
||||
}
|
||||
|
||||
ControllerInterface::Backend DInputControllerInterface::GetBackend() const
|
||||
{
|
||||
return ControllerInterface::Backend::XInput;
|
||||
}
|
||||
|
||||
bool DInputControllerInterface::Initialize(CommonHostInterface* host_interface)
|
||||
{
|
||||
m_dinput_module = LoadLibraryW(L"dinput8");
|
||||
if (!m_dinput_module)
|
||||
{
|
||||
Log_ErrorPrintf("Failed to load DInput module.");
|
||||
return false;
|
||||
}
|
||||
|
||||
PFNDIRECTINPUT8CREATE create =
|
||||
reinterpret_cast<PFNDIRECTINPUT8CREATE>(GetProcAddress(m_dinput_module, "DirectInput8Create"));
|
||||
PFNGETDFDIJOYSTICK get_joystick_data_format =
|
||||
reinterpret_cast<PFNGETDFDIJOYSTICK>(GetProcAddress(m_dinput_module, "GetdfDIJoystick"));
|
||||
if (!create || !get_joystick_data_format)
|
||||
{
|
||||
Log_ErrorPrintf("Failed to get DInput function pointers.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ControllerInterface::Initialize(host_interface))
|
||||
return false;
|
||||
|
||||
HRESULT hr = create(GetModuleHandleA(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8A,
|
||||
reinterpret_cast<LPVOID*>(m_dinput.GetAddressOf()), nullptr);
|
||||
m_joystick_data_format = get_joystick_data_format();
|
||||
if (FAILED(hr) || !m_joystick_data_format)
|
||||
{
|
||||
Log_ErrorPrintf("DirectInput8Create() failed: %08X", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
AddDevices();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DInputControllerInterface::Shutdown()
|
||||
{
|
||||
ControllerInterface::Shutdown();
|
||||
}
|
||||
|
||||
static BOOL CALLBACK EnumCallback(LPCDIDEVICEINSTANCE lpddi, LPVOID pvRef)
|
||||
{
|
||||
static_cast<std::vector<DIDEVICEINSTANCE>*>(pvRef)->push_back(*lpddi);
|
||||
return DIENUM_CONTINUE;
|
||||
}
|
||||
|
||||
void DInputControllerInterface::AddDevices()
|
||||
{
|
||||
std::vector<DIDEVICEINSTANCE> devices;
|
||||
m_dinput->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumCallback, &devices, DIEDFL_ATTACHEDONLY);
|
||||
|
||||
Log_InfoPrintf("Enumerated %zud evices", devices.size());
|
||||
|
||||
for (DIDEVICEINSTANCE inst : devices)
|
||||
{
|
||||
ControllerData cd;
|
||||
HRESULT hr = m_dinput->CreateDevice(inst.guidInstance, cd.device.GetAddressOf(), nullptr);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Log_WarningPrintf("Failed to create instance of device [%s, %s]", inst.tszProductName, inst.tszInstanceName);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (AddDevice(cd, inst.tszProductName))
|
||||
m_controllers.push_back(std::move(cd));
|
||||
}
|
||||
}
|
||||
|
||||
bool DInputControllerInterface::AddDevice(ControllerData& cd, const char* name)
|
||||
{
|
||||
HRESULT hr = cd.device->SetCooperativeLevel(static_cast<HWND>(m_host_interface->GetTopLevelWindowHandle()),
|
||||
DISCL_BACKGROUND | DISCL_EXCLUSIVE);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
hr = cd.device->SetCooperativeLevel(static_cast<HWND>(m_host_interface->GetTopLevelWindowHandle()),
|
||||
DISCL_BACKGROUND | DISCL_NONEXCLUSIVE);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Log_ErrorPrintf("Failed to set cooperative level for '%s'", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
Log_WarningPrintf("Failed to set exclusive mode for '%s'", name);
|
||||
}
|
||||
|
||||
hr = cd.device->SetDataFormat(m_joystick_data_format);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Log_ErrorPrintf("Failed to set data format for '%s'", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = cd.device->Acquire();
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Log_ErrorPrintf("Failed to acquire device '%s'", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
DIDEVCAPS caps = {};
|
||||
caps.dwSize = sizeof(caps);
|
||||
hr = cd.device->GetCapabilities(&caps);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Log_ErrorPrintf("Failed to get capabilities for '%s'", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
cd.num_buttons = caps.dwButtons;
|
||||
if (cd.num_buttons > NUM_BUTTONS)
|
||||
{
|
||||
Log_WarningPrintf("Device '%s' has too many buttons (%u), using %u instead.", name, cd.num_buttons, NUM_BUTTONS);
|
||||
cd.num_buttons = NUM_BUTTONS;
|
||||
}
|
||||
|
||||
static constexpr std::array<u32, NUM_AXISES> axis_offsets = {
|
||||
{offsetof(DIJOYSTATE, lX), offsetof(DIJOYSTATE, lY), offsetof(DIJOYSTATE, lZ), offsetof(DIJOYSTATE, lRz),
|
||||
offsetof(DIJOYSTATE, lRx), offsetof(DIJOYSTATE, lRy), offsetof(DIJOYSTATE, rglSlider[0]),
|
||||
offsetof(DIJOYSTATE, rglSlider[1])}};
|
||||
for (u32 i = 0; i < NUM_AXISES; i++)
|
||||
{
|
||||
// ask for 16 bits of axis range
|
||||
DIPROPRANGE range = {};
|
||||
range.diph.dwSize = sizeof(range);
|
||||
range.diph.dwHeaderSize = sizeof(range.diph);
|
||||
range.diph.dwHow = DIPH_BYOFFSET;
|
||||
range.diph.dwObj = axis_offsets[i];
|
||||
range.lMin = std::numeric_limits<s16>::min();
|
||||
range.lMax = std::numeric_limits<s16>::max();
|
||||
hr = cd.device->SetProperty(DIPROP_RANGE, &range.diph);
|
||||
|
||||
// did it apply?
|
||||
if (SUCCEEDED(cd.device->GetProperty(DIPROP_RANGE, &range.diph)))
|
||||
{
|
||||
cd.axis_offsets[cd.num_axes] = axis_offsets[i];
|
||||
cd.num_axes++;
|
||||
}
|
||||
}
|
||||
|
||||
cd.has_hat = (caps.dwPOVs > 0);
|
||||
|
||||
hr = cd.device->Poll();
|
||||
if (hr == DI_NOEFFECT)
|
||||
cd.needs_poll = false;
|
||||
else if (hr != DI_OK)
|
||||
Log_WarningPrintf("Polling device '%s' failed: %08X", name, hr);
|
||||
|
||||
hr = cd.device->GetDeviceState(sizeof(cd.last_state), &cd.last_state);
|
||||
if (hr != DI_OK)
|
||||
Log_WarningPrintf("GetDeviceState() for '%s' failed: %08X", name, hr);
|
||||
|
||||
Log_InfoPrintf("%s has %u buttons, %u axes%s", name, cd.num_buttons, cd.num_axes, cd.has_hat ? ", and a hat" : "");
|
||||
|
||||
return (cd.num_buttons > 0 || cd.num_axes > 0 || cd.has_hat);
|
||||
}
|
||||
|
||||
void DInputControllerInterface::PollEvents()
|
||||
{
|
||||
for (u32 i = 0; i < static_cast<u32>(m_controllers.size()); i++)
|
||||
{
|
||||
ControllerData& cd = m_controllers[i];
|
||||
if (!cd.device)
|
||||
continue;
|
||||
|
||||
if (cd.needs_poll)
|
||||
cd.device->Poll();
|
||||
|
||||
DIJOYSTATE js;
|
||||
HRESULT hr = cd.device->GetDeviceState(sizeof(js), &js);
|
||||
if (hr == DIERR_INPUTLOST || hr == DIERR_NOTACQUIRED)
|
||||
{
|
||||
hr = cd.device->Acquire();
|
||||
if (hr == DI_OK)
|
||||
hr = cd.device->GetDeviceState(sizeof(js), &js);
|
||||
|
||||
if (hr != DI_OK)
|
||||
{
|
||||
cd = {};
|
||||
OnControllerDisconnected(static_cast<int>(i));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (hr != DI_OK)
|
||||
{
|
||||
Log_WarningPrintf("GetDeviceState() failed: %08X", hr);
|
||||
continue;
|
||||
}
|
||||
|
||||
CheckForStateChanges(i, js);
|
||||
}
|
||||
}
|
||||
|
||||
std::array<bool, ControllerInterface::NUM_HAT_DIRECTIONS> DInputControllerInterface::GetHatButtons(DWORD hat)
|
||||
{
|
||||
std::array<bool, NUM_HAT_DIRECTIONS> buttons = {};
|
||||
|
||||
const WORD hv = LOWORD(hat);
|
||||
if (hv != 0xFFFF)
|
||||
{
|
||||
if ((hv >= 0 && hv < 9000) || hv >= 31500)
|
||||
buttons[HAT_DIRECTION_UP] = true;
|
||||
if (hv >= 4500 && hv < 18000)
|
||||
buttons[HAT_DIRECTION_RIGHT] = true;
|
||||
if (hv >= 13500 && hv < 27000)
|
||||
buttons[HAT_DIRECTION_DOWN] = true;
|
||||
if (hv >= 22500)
|
||||
buttons[HAT_DIRECTION_LEFT] = true;
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
void DInputControllerInterface::CheckForStateChanges(u32 index, const DIJOYSTATE& new_state)
|
||||
{
|
||||
ControllerData& cd = m_controllers[index];
|
||||
DIJOYSTATE& last_state = cd.last_state;
|
||||
|
||||
for (u32 i = 0; i < cd.num_axes; i++)
|
||||
{
|
||||
LONG new_value;
|
||||
LONG old_value;
|
||||
std::memcpy(&old_value, reinterpret_cast<const u8*>(&cd.last_state) + cd.axis_offsets[i], sizeof(old_value));
|
||||
std::memcpy(&new_value, reinterpret_cast<const u8*>(&new_state) + cd.axis_offsets[i], sizeof(new_value));
|
||||
if (old_value != new_value)
|
||||
{
|
||||
HandleAxisEvent(index, i, new_value);
|
||||
std::memcpy(reinterpret_cast<u8*>(&cd.last_state) + cd.axis_offsets[i], &new_value, sizeof(new_value));
|
||||
}
|
||||
}
|
||||
|
||||
for (u32 i = 0; i < cd.num_buttons; i++)
|
||||
{
|
||||
if (last_state.rgbButtons[i] != new_state.rgbButtons[i])
|
||||
{
|
||||
HandleButtonEvent(index, i, new_state.rgbButtons[i] != 0);
|
||||
last_state.rgbButtons[i] = new_state.rgbButtons[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (cd.has_hat)
|
||||
{
|
||||
if (last_state.rgdwPOV[0] != new_state.rgdwPOV[0])
|
||||
{
|
||||
Log_InfoPrintf("Hat %u", LOWORD(new_state.rgdwPOV[0]));
|
||||
// map hats to the last buttons
|
||||
const std::array<bool, NUM_HAT_DIRECTIONS> old_buttons(GetHatButtons(last_state.rgdwPOV[0]));
|
||||
const std::array<bool, NUM_HAT_DIRECTIONS> new_buttons(GetHatButtons(new_state.rgdwPOV[0]));
|
||||
for (u32 i = 0; i < NUM_HAT_DIRECTIONS; i++)
|
||||
{
|
||||
if (old_buttons[i] != new_buttons[i])
|
||||
HandleButtonEvent(index, cd.num_buttons + i, new_buttons[i]);
|
||||
}
|
||||
|
||||
last_state.rgdwPOV[0] = new_state.rgdwPOV[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DInputControllerInterface::ClearBindings()
|
||||
{
|
||||
for (ControllerData& cd : m_controllers)
|
||||
{
|
||||
cd.axis_mapping.fill({});
|
||||
cd.button_mapping.fill({});
|
||||
cd.axis_button_mapping.fill({});
|
||||
cd.button_axis_mapping.fill({});
|
||||
}
|
||||
}
|
||||
|
||||
bool DInputControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side,
|
||||
AxisCallback callback)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||
return false;
|
||||
|
||||
if (axis_number < 0 || axis_number >= NUM_AXISES)
|
||||
return false;
|
||||
|
||||
m_controllers[controller_index].axis_mapping[axis_number][axis_side] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DInputControllerInterface::BindControllerButton(int controller_index, int button_number, ButtonCallback callback)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||
return false;
|
||||
|
||||
if (button_number < 0 || button_number >= TOTAL_NUM_BUTTONS)
|
||||
return false;
|
||||
|
||||
m_controllers[controller_index].button_mapping[button_number] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DInputControllerInterface::BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||
ButtonCallback callback)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||
return false;
|
||||
|
||||
if (axis_number < 0 || axis_number >= NUM_AXISES)
|
||||
return false;
|
||||
|
||||
m_controllers[controller_index].axis_button_mapping[axis_number][BoolToUInt8(direction)] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DInputControllerInterface::BindControllerHatToButton(int controller_index, int hat_number,
|
||||
std::string_view hat_position, ButtonCallback callback)
|
||||
{
|
||||
// Hats don't exist in XInput
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DInputControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number,
|
||||
AxisCallback callback)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||
return false;
|
||||
|
||||
if (button_number < 0 || button_number >= TOTAL_NUM_BUTTONS)
|
||||
return false;
|
||||
|
||||
m_controllers[controller_index].button_axis_mapping[button_number] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DInputControllerInterface::HandleAxisEvent(u32 index, u32 axis, s32 value)
|
||||
{
|
||||
const float f_value = static_cast<float>(value) / (value < 0 ? 32768.0f : 32767.0f);
|
||||
Log_DevPrintf("controller %u axis %u %d %f", index, static_cast<u32>(axis), value, f_value);
|
||||
DebugAssert(index < m_controllers.size());
|
||||
|
||||
if (DoEventHook(Hook::Type::Axis, index, static_cast<u32>(axis), f_value))
|
||||
return true;
|
||||
|
||||
const AxisCallback& cb = m_controllers[index].axis_mapping[static_cast<u32>(axis)][AxisSide::Full];
|
||||
if (cb)
|
||||
{
|
||||
cb(f_value);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
const AxisCallback& positive_cb = m_controllers[index].axis_mapping[static_cast<u32>(axis)][AxisSide::Positive];
|
||||
const AxisCallback& negative_cb = m_controllers[index].axis_mapping[static_cast<u32>(axis)][AxisSide::Negative];
|
||||
if (positive_cb || negative_cb)
|
||||
{
|
||||
if (positive_cb)
|
||||
positive_cb((f_value < 0.0f) ? 0.0f : f_value);
|
||||
if (negative_cb)
|
||||
negative_cb((f_value >= 0.0f) ? 0.0f : -f_value);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// set the other direction to false so large movements don't leave the opposite on
|
||||
const bool outside_deadzone = (std::abs(f_value) >= m_controllers[index].deadzone);
|
||||
const bool positive = (f_value >= 0.0f);
|
||||
const ButtonCallback& other_button_cb =
|
||||
m_controllers[index].axis_button_mapping[static_cast<u32>(axis)][BoolToUInt8(!positive)];
|
||||
const ButtonCallback& button_cb =
|
||||
m_controllers[index].axis_button_mapping[static_cast<u32>(axis)][BoolToUInt8(positive)];
|
||||
if (button_cb)
|
||||
{
|
||||
button_cb(outside_deadzone);
|
||||
if (other_button_cb)
|
||||
other_button_cb(false);
|
||||
return true;
|
||||
}
|
||||
else if (other_button_cb)
|
||||
{
|
||||
other_button_cb(false);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool DInputControllerInterface::HandleButtonEvent(u32 index, u32 button, bool pressed)
|
||||
{
|
||||
Log_DevPrintf("controller %u button %u %s", index, button, pressed ? "pressed" : "released");
|
||||
DebugAssert(index < m_controllers.size());
|
||||
|
||||
if (DoEventHook(Hook::Type::Button, index, button, pressed ? 1.0f : 0.0f))
|
||||
return true;
|
||||
|
||||
const ButtonCallback& cb = m_controllers[index].button_mapping[button];
|
||||
if (cb)
|
||||
{
|
||||
cb(pressed);
|
||||
return true;
|
||||
}
|
||||
|
||||
const AxisCallback& axis_cb = m_controllers[index].button_axis_mapping[button];
|
||||
if (axis_cb)
|
||||
{
|
||||
axis_cb(pressed ? 1.0f : -1.0f);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
u32 DInputControllerInterface::GetControllerRumbleMotorCount(int controller_index)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||
return 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void DInputControllerInterface::SetControllerRumbleStrength(int controller_index, const float* strengths,
|
||||
u32 num_motors)
|
||||
{
|
||||
DebugAssert(static_cast<u32>(controller_index) < m_controllers.size());
|
||||
}
|
||||
|
||||
bool DInputControllerInterface::SetControllerDeadzone(int controller_index, float size /* = 0.25f */)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||
return false;
|
||||
|
||||
m_controllers[static_cast<u32>(controller_index)].deadzone = std::clamp(std::abs(size), 0.01f, 0.99f);
|
||||
Log_InfoPrintf("Controller %d deadzone size set to %f", controller_index,
|
||||
m_controllers[static_cast<u32>(controller_index)].deadzone);
|
||||
return true;
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
#pragma once
|
||||
#define DIRECTINPUT_VERSION 0x0800
|
||||
#include "common/windows_headers.h"
|
||||
#include "controller_interface.h"
|
||||
#include "core/types.h"
|
||||
#include <array>
|
||||
#include <dinput.h>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <wrl/client.h>
|
||||
|
||||
class DInputControllerInterface final : public ControllerInterface
|
||||
{
|
||||
public:
|
||||
DInputControllerInterface();
|
||||
~DInputControllerInterface() override;
|
||||
|
||||
Backend GetBackend() const override;
|
||||
bool Initialize(CommonHostInterface* host_interface) override;
|
||||
void Shutdown() override;
|
||||
|
||||
// Removes all bindings. Call before setting new bindings.
|
||||
void ClearBindings() override;
|
||||
|
||||
// Binding to events. If a binding for this axis/button already exists, returns false.
|
||||
bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) override;
|
||||
bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override;
|
||||
bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||
ButtonCallback callback) override;
|
||||
bool BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position,
|
||||
ButtonCallback callback) override;
|
||||
bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override;
|
||||
|
||||
// Changing rumble strength.
|
||||
u32 GetControllerRumbleMotorCount(int controller_index) override;
|
||||
void SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors) override;
|
||||
|
||||
// Set deadzone that will be applied on axis-to-button mappings
|
||||
bool SetControllerDeadzone(int controller_index, float size = 0.25f) override;
|
||||
|
||||
void PollEvents() override;
|
||||
|
||||
private:
|
||||
template<typename T>
|
||||
using ComPtr = Microsoft::WRL::ComPtr<T>;
|
||||
|
||||
enum : u32
|
||||
{
|
||||
NUM_AXISES = 8,
|
||||
NUM_BUTTONS = 16,
|
||||
NUM_HATS = 1,
|
||||
|
||||
TOTAL_NUM_BUTTONS = NUM_BUTTONS + (NUM_HATS * NUM_HAT_DIRECTIONS),
|
||||
};
|
||||
|
||||
struct ControllerData
|
||||
{
|
||||
ComPtr<IDirectInputDevice8> device;
|
||||
DIJOYSTATE last_state = {};
|
||||
u32 num_buttons = 0;
|
||||
u32 num_axes = 0;
|
||||
|
||||
float deadzone = 0.25f;
|
||||
|
||||
std::array<u32, NUM_AXISES> axis_offsets;
|
||||
|
||||
std::array<std::array<AxisCallback, 3>, NUM_AXISES> axis_mapping;
|
||||
std::array<ButtonCallback, TOTAL_NUM_BUTTONS + NUM_HAT_DIRECTIONS> button_mapping;
|
||||
std::array<std::array<ButtonCallback, 2>, NUM_AXISES> axis_button_mapping;
|
||||
std::array<AxisCallback, TOTAL_NUM_BUTTONS + NUM_HAT_DIRECTIONS> button_axis_mapping;
|
||||
|
||||
bool has_hat = false;
|
||||
bool needs_poll = true;
|
||||
};
|
||||
|
||||
using ControllerDataArray = std::vector<ControllerData>;
|
||||
|
||||
void AddDevices();
|
||||
bool AddDevice(ControllerData& cd, const char* name);
|
||||
|
||||
static std::array<bool, NUM_HAT_DIRECTIONS> GetHatButtons(DWORD hat);
|
||||
|
||||
void CheckForStateChanges(u32 index, const DIJOYSTATE& new_state);
|
||||
|
||||
bool HandleAxisEvent(u32 index, u32 axis, s32 value);
|
||||
bool HandleButtonEvent(u32 index, u32 button, bool pressed);
|
||||
|
||||
ControllerDataArray m_controllers;
|
||||
|
||||
HMODULE m_dinput_module{};
|
||||
LPCDIDATAFORMAT m_joystick_data_format{};
|
||||
ComPtr<IDirectInput8> m_dinput;
|
||||
std::mutex m_event_intercept_mutex;
|
||||
Hook::Callback m_event_intercept_callback;
|
||||
};
|
441
src/frontend-common/dinput_source.cpp
Normal file
441
src/frontend-common/dinput_source.cpp
Normal file
@ -0,0 +1,441 @@
|
||||
#define INITGUID
|
||||
|
||||
#include "dinput_source.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/log.h"
|
||||
#include "common/make_array.h"
|
||||
#include "common/string_util.h"
|
||||
#include "common_host.h"
|
||||
#include "core/host.h"
|
||||
#include "fmt/format.h"
|
||||
#include "input_manager.h"
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
Log_SetChannel(DInputSource);
|
||||
|
||||
using PFNDIRECTINPUT8CREATE = HRESULT(WINAPI*)(HINSTANCE hinst, DWORD dwVersion, REFIID riidltf, LPVOID* ppvOut,
|
||||
LPUNKNOWN punkOuter);
|
||||
using PFNGETDFDIJOYSTICK = LPCDIDATAFORMAT(WINAPI*)();
|
||||
|
||||
DInputSource::DInputSource() = default;
|
||||
|
||||
DInputSource::~DInputSource()
|
||||
{
|
||||
m_controllers.clear();
|
||||
m_dinput.Reset();
|
||||
if (m_dinput_module)
|
||||
FreeLibrary(m_dinput_module);
|
||||
}
|
||||
|
||||
std::array<bool, DInputSource::NUM_HAT_DIRECTIONS> DInputSource::GetHatButtons(DWORD hat)
|
||||
{
|
||||
std::array<bool, NUM_HAT_DIRECTIONS> buttons = {};
|
||||
|
||||
const WORD hv = LOWORD(hat);
|
||||
if (hv != 0xFFFF)
|
||||
{
|
||||
if ((hv >= 0 && hv < 9000) || hv >= 31500)
|
||||
buttons[HAT_DIRECTION_UP] = true;
|
||||
if (hv >= 4500 && hv < 18000)
|
||||
buttons[HAT_DIRECTION_RIGHT] = true;
|
||||
if (hv >= 13500 && hv < 27000)
|
||||
buttons[HAT_DIRECTION_DOWN] = true;
|
||||
if (hv >= 22500)
|
||||
buttons[HAT_DIRECTION_LEFT] = true;
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
std::string DInputSource::GetDeviceIdentifier(u32 index)
|
||||
{
|
||||
return fmt::format("DInput-{}", index);
|
||||
}
|
||||
|
||||
static constexpr std::array<const char*, DInputSource::NUM_HAT_DIRECTIONS> s_hat_directions = {
|
||||
{"Up", "Right", "Down", "Left"}};
|
||||
|
||||
bool DInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
|
||||
{
|
||||
m_dinput_module = LoadLibraryW(L"dinput8");
|
||||
if (!m_dinput_module)
|
||||
{
|
||||
Log_ErrorPrintf("Failed to load DInput module.");
|
||||
return false;
|
||||
}
|
||||
|
||||
PFNDIRECTINPUT8CREATE create =
|
||||
reinterpret_cast<PFNDIRECTINPUT8CREATE>(GetProcAddress(m_dinput_module, "DirectInput8Create"));
|
||||
PFNGETDFDIJOYSTICK get_joystick_data_format =
|
||||
reinterpret_cast<PFNGETDFDIJOYSTICK>(GetProcAddress(m_dinput_module, "GetdfDIJoystick"));
|
||||
if (!create || !get_joystick_data_format)
|
||||
{
|
||||
Log_ErrorPrintf("Failed to get DInput function pointers.");
|
||||
return false;
|
||||
}
|
||||
|
||||
HRESULT hr = create(GetModuleHandleA(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8A,
|
||||
reinterpret_cast<LPVOID*>(m_dinput.GetAddressOf()), nullptr);
|
||||
m_joystick_data_format = get_joystick_data_format();
|
||||
if (FAILED(hr) || !m_joystick_data_format)
|
||||
{
|
||||
Log_ErrorPrintf("DirectInput8Create() failed: %08X", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
// need to release the lock while we're enumerating, because we call winId().
|
||||
settings_lock.unlock();
|
||||
HWND toplevel_window = static_cast<HWND>(Host::GetTopLevelWindowHandle());
|
||||
AddDevices(toplevel_window);
|
||||
settings_lock.lock();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
|
||||
{
|
||||
// noop
|
||||
}
|
||||
|
||||
void DInputSource::Shutdown()
|
||||
{
|
||||
while (!m_controllers.empty())
|
||||
{
|
||||
Host::OnInputDeviceDisconnected(GetDeviceIdentifier(static_cast<u32>(m_controllers.size() - 1)));
|
||||
m_controllers.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
static BOOL CALLBACK EnumCallback(LPCDIDEVICEINSTANCE lpddi, LPVOID pvRef)
|
||||
{
|
||||
static_cast<std::vector<DIDEVICEINSTANCE>*>(pvRef)->push_back(*lpddi);
|
||||
return DIENUM_CONTINUE;
|
||||
}
|
||||
|
||||
void DInputSource::AddDevices(HWND toplevel_window)
|
||||
{
|
||||
std::vector<DIDEVICEINSTANCE> devices;
|
||||
m_dinput->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumCallback, &devices, DIEDFL_ATTACHEDONLY);
|
||||
|
||||
Log_InfoPrintf("Enumerated %zu devices", devices.size());
|
||||
|
||||
for (DIDEVICEINSTANCE inst : devices)
|
||||
{
|
||||
ControllerData cd;
|
||||
HRESULT hr = m_dinput->CreateDevice(inst.guidInstance, cd.device.GetAddressOf(), nullptr);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Log_WarningPrintf("Failed to create instance of device [%s, %s]", inst.tszProductName, inst.tszInstanceName);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (AddDevice(cd, toplevel_window, inst.tszProductName))
|
||||
m_controllers.push_back(std::move(cd));
|
||||
}
|
||||
}
|
||||
|
||||
bool DInputSource::AddDevice(ControllerData& cd, HWND toplevel_window, const char* name)
|
||||
{
|
||||
HRESULT hr = cd.device->SetCooperativeLevel(toplevel_window, DISCL_BACKGROUND | DISCL_EXCLUSIVE);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
hr = cd.device->SetCooperativeLevel(toplevel_window, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Log_ErrorPrintf("Failed to set cooperative level for '%s'", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
Log_WarningPrintf("Failed to set exclusive mode for '%s'", name);
|
||||
}
|
||||
|
||||
hr = cd.device->SetDataFormat(m_joystick_data_format);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Log_ErrorPrintf("Failed to set data format for '%s'", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = cd.device->Acquire();
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Log_ErrorPrintf("Failed to acquire device '%s'", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
DIDEVCAPS caps = {};
|
||||
caps.dwSize = sizeof(caps);
|
||||
hr = cd.device->GetCapabilities(&caps);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Log_ErrorPrintf("Failed to get capabilities for '%s'", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
cd.num_buttons = caps.dwButtons;
|
||||
|
||||
static constexpr auto axis_offsets =
|
||||
make_array(offsetof(DIJOYSTATE, lX), offsetof(DIJOYSTATE, lY), offsetof(DIJOYSTATE, lZ), offsetof(DIJOYSTATE, lRz),
|
||||
offsetof(DIJOYSTATE, lRx), offsetof(DIJOYSTATE, lRy), offsetof(DIJOYSTATE, rglSlider[0]),
|
||||
offsetof(DIJOYSTATE, rglSlider[1]));
|
||||
for (const auto offset : axis_offsets)
|
||||
{
|
||||
// ask for 16 bits of axis range
|
||||
DIPROPRANGE range = {};
|
||||
range.diph.dwSize = sizeof(range);
|
||||
range.diph.dwHeaderSize = sizeof(range.diph);
|
||||
range.diph.dwHow = DIPH_BYOFFSET;
|
||||
range.diph.dwObj = static_cast<DWORD>(offset);
|
||||
range.lMin = std::numeric_limits<s16>::min();
|
||||
range.lMax = std::numeric_limits<s16>::max();
|
||||
hr = cd.device->SetProperty(DIPROP_RANGE, &range.diph);
|
||||
|
||||
// did it apply?
|
||||
if (SUCCEEDED(cd.device->GetProperty(DIPROP_RANGE, &range.diph)))
|
||||
{
|
||||
cd.axis_offsets.push_back(static_cast<u32>(offset));
|
||||
}
|
||||
}
|
||||
|
||||
cd.num_hats = caps.dwPOVs;
|
||||
|
||||
hr = cd.device->Poll();
|
||||
if (hr == DI_NOEFFECT)
|
||||
cd.needs_poll = false;
|
||||
else if (hr != DI_OK)
|
||||
Log_WarningPrintf("Polling device '%s' failed: %08X", name, hr);
|
||||
|
||||
hr = cd.device->GetDeviceState(sizeof(cd.last_state), &cd.last_state);
|
||||
if (hr != DI_OK)
|
||||
Log_WarningPrintf("GetDeviceState() for '%s' failed: %08X", name, hr);
|
||||
|
||||
Log_InfoPrintf("%s has %u buttons, %u axes, %u hats", name, cd.num_buttons, static_cast<u32>(cd.axis_offsets.size()),
|
||||
cd.num_hats);
|
||||
|
||||
return (cd.num_buttons > 0 || !cd.axis_offsets.empty() || cd.num_hats > 0);
|
||||
}
|
||||
|
||||
void DInputSource::PollEvents()
|
||||
{
|
||||
for (size_t i = 0; i < m_controllers.size(); i++)
|
||||
{
|
||||
ControllerData& cd = m_controllers[i];
|
||||
if (!cd.device)
|
||||
continue;
|
||||
|
||||
if (cd.needs_poll)
|
||||
cd.device->Poll();
|
||||
|
||||
DIJOYSTATE js;
|
||||
HRESULT hr = cd.device->GetDeviceState(sizeof(js), &js);
|
||||
if (hr == DIERR_INPUTLOST || hr == DIERR_NOTACQUIRED)
|
||||
{
|
||||
hr = cd.device->Acquire();
|
||||
if (hr == DI_OK)
|
||||
hr = cd.device->GetDeviceState(sizeof(js), &js);
|
||||
|
||||
if (hr != DI_OK)
|
||||
{
|
||||
// TODO: This should remove from the list instead.
|
||||
cd = {};
|
||||
Host::OnInputDeviceDisconnected(GetDeviceIdentifier(static_cast<u32>(i)));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (hr != DI_OK)
|
||||
{
|
||||
Log_WarningPrintf("GetDeviceState() failed: %08X", hr);
|
||||
continue;
|
||||
}
|
||||
|
||||
CheckForStateChanges(i, js);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> DInputSource::EnumerateDevices()
|
||||
{
|
||||
std::vector<std::pair<std::string, std::string>> ret;
|
||||
for (size_t i = 0; i < m_controllers.size(); i++)
|
||||
{
|
||||
DIDEVICEINSTANCEA dii = {sizeof(DIDEVICEINSTANCEA)};
|
||||
std::string name;
|
||||
if (SUCCEEDED(m_controllers[i].device->GetDeviceInfo(&dii)))
|
||||
name = dii.tszProductName;
|
||||
|
||||
if (name.empty())
|
||||
name = "Unknown";
|
||||
|
||||
ret.emplace_back(GetDeviceIdentifier(static_cast<u32>(i)), std::move(name));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<InputBindingKey> DInputSource::EnumerateMotors()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
bool DInputSource::GetGenericBindingMapping(const std::string_view& device, GenericInputBindingMapping* mapping)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void DInputSource::UpdateMotorState(InputBindingKey key, float intensity)
|
||||
{
|
||||
// not supported
|
||||
}
|
||||
|
||||
void DInputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
|
||||
float small_intensity)
|
||||
{
|
||||
// not supported
|
||||
}
|
||||
|
||||
std::optional<InputBindingKey> DInputSource::ParseKeyString(const std::string_view& device,
|
||||
const std::string_view& binding)
|
||||
{
|
||||
if (!StringUtil::StartsWith(device, "DInput-") || binding.empty())
|
||||
return std::nullopt;
|
||||
|
||||
const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(7));
|
||||
if (!player_id.has_value() || player_id.value() < 0)
|
||||
return std::nullopt;
|
||||
|
||||
InputBindingKey key = {};
|
||||
key.source_type = InputSourceType::DInput;
|
||||
key.source_index = static_cast<u32>(player_id.value());
|
||||
|
||||
if (StringUtil::StartsWith(binding, "+Axis") || StringUtil::StartsWith(binding, "-Axis"))
|
||||
{
|
||||
const std::optional<u32> axis_index = StringUtil::FromChars<u32>(binding.substr(5));
|
||||
if (!axis_index.has_value())
|
||||
return std::nullopt;
|
||||
|
||||
key.source_subtype = InputSubclass::ControllerAxis;
|
||||
key.data = axis_index.value();
|
||||
key.negative = (binding[0] == '-');
|
||||
return key;
|
||||
}
|
||||
else if (StringUtil::StartsWith(binding, "Hat"))
|
||||
{
|
||||
if (binding[3] < '0' || binding[3] > '9' || binding.length() < 5)
|
||||
return std::nullopt;
|
||||
|
||||
const u32 hat_index = binding[3] - '0';
|
||||
const std::string_view hat_dir(binding.substr(4));
|
||||
for (u32 i = 0; i < NUM_HAT_DIRECTIONS; i++)
|
||||
{
|
||||
if (hat_dir == s_hat_directions[i])
|
||||
{
|
||||
key.source_subtype = InputSubclass::ControllerButton;
|
||||
key.data = MAX_NUM_BUTTONS + hat_index * NUM_HAT_DIRECTIONS + i;
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
// bad direction
|
||||
return std::nullopt;
|
||||
}
|
||||
else if (StringUtil::StartsWith(binding, "Button"))
|
||||
{
|
||||
const std::optional<u32> button_index = StringUtil::FromChars<u32>(binding.substr(6));
|
||||
if (!button_index.has_value())
|
||||
return std::nullopt;
|
||||
|
||||
key.source_subtype = InputSubclass::ControllerButton;
|
||||
key.data = button_index.value();
|
||||
return key;
|
||||
}
|
||||
|
||||
// unknown axis/button
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string DInputSource::ConvertKeyToString(InputBindingKey key)
|
||||
{
|
||||
std::string ret;
|
||||
|
||||
if (key.source_type == InputSourceType::DInput)
|
||||
{
|
||||
if (key.source_subtype == InputSubclass::ControllerAxis)
|
||||
{
|
||||
ret = fmt::format("DInput-{}/{}Axis{}", u32(key.source_index), key.negative ? '-' : '+', u32(key.data));
|
||||
}
|
||||
else if (key.source_subtype == InputSubclass::ControllerButton && key.data >= MAX_NUM_BUTTONS)
|
||||
{
|
||||
const u32 hat_num = (key.data - MAX_NUM_BUTTONS) / NUM_HAT_DIRECTIONS;
|
||||
const u32 hat_dir = (key.data - MAX_NUM_BUTTONS) % NUM_HAT_DIRECTIONS;
|
||||
ret = fmt::format("DInput-{}/Hat{}{}", u32(key.source_index), hat_num, s_hat_directions[hat_dir]);
|
||||
}
|
||||
else if (key.source_subtype == InputSubclass::ControllerButton)
|
||||
{
|
||||
ret = fmt::format("DInput-{}/Button{}", u32(key.source_index), u32(key.data));
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void DInputSource::CheckForStateChanges(size_t index, const DIJOYSTATE& new_state)
|
||||
{
|
||||
ControllerData& cd = m_controllers[index];
|
||||
DIJOYSTATE& last_state = cd.last_state;
|
||||
|
||||
for (size_t i = 0; i < cd.axis_offsets.size(); i++)
|
||||
{
|
||||
LONG new_value;
|
||||
LONG old_value;
|
||||
std::memcpy(&old_value, reinterpret_cast<const u8*>(&cd.last_state) + cd.axis_offsets[i], sizeof(old_value));
|
||||
std::memcpy(&new_value, reinterpret_cast<const u8*>(&new_state) + cd.axis_offsets[i], sizeof(new_value));
|
||||
if (old_value != new_value)
|
||||
{
|
||||
std::memcpy(reinterpret_cast<u8*>(&cd.last_state) + cd.axis_offsets[i], &new_value, sizeof(new_value));
|
||||
|
||||
// TODO: Use the range from caps?
|
||||
const float value = static_cast<float>(new_value) / (new_value < 0 ? 32768.0f : 32767.0f);
|
||||
InputManager::InvokeEvents(
|
||||
MakeGenericControllerAxisKey(InputSourceType::DInput, static_cast<u32>(index), static_cast<u32>(i)), value,
|
||||
GenericInputBinding::Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
for (u32 i = 0; i < cd.num_buttons; i++)
|
||||
{
|
||||
if (last_state.rgbButtons[i] != new_state.rgbButtons[i])
|
||||
{
|
||||
last_state.rgbButtons[i] = new_state.rgbButtons[i];
|
||||
|
||||
const float value = (new_state.rgbButtons[i] != 0) ? 1.0f : 0.0f;
|
||||
InputManager::InvokeEvents(MakeGenericControllerButtonKey(InputSourceType::DInput, static_cast<u32>(index), i),
|
||||
value, GenericInputBinding::Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
for (u32 i = 0; i < cd.num_hats; i++)
|
||||
{
|
||||
if (last_state.rgdwPOV[i] != new_state.rgdwPOV[i])
|
||||
{
|
||||
// map hats to the last buttons
|
||||
const std::array<bool, NUM_HAT_DIRECTIONS> old_buttons(GetHatButtons(last_state.rgdwPOV[i]));
|
||||
const std::array<bool, NUM_HAT_DIRECTIONS> new_buttons(GetHatButtons(new_state.rgdwPOV[i]));
|
||||
last_state.rgdwPOV[i] = new_state.rgdwPOV[i];
|
||||
|
||||
for (u32 j = 0; j < NUM_HAT_DIRECTIONS; j++)
|
||||
{
|
||||
if (old_buttons[j] != new_buttons[j])
|
||||
{
|
||||
const float value = (new_buttons[j] ? 1.0f : 0.0f);
|
||||
InputManager::InvokeEvents(MakeGenericControllerButtonKey(InputSourceType::DInput, static_cast<u32>(index),
|
||||
cd.num_buttons + (i * NUM_HAT_DIRECTIONS) + j),
|
||||
value, GenericInputBinding::Unknown);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<InputSource> InputSource::CreateDInputSource()
|
||||
{
|
||||
return std::make_unique<DInputSource>();
|
||||
}
|
81
src/frontend-common/dinput_source.h
Normal file
81
src/frontend-common/dinput_source.h
Normal file
@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
#define DIRECTINPUT_VERSION 0x0800
|
||||
#include "common/windows_headers.h"
|
||||
#include "input_source.h"
|
||||
#include "core/types.h"
|
||||
#include <array>
|
||||
#include <dinput.h>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <wrl/client.h>
|
||||
|
||||
class DInputSource final : public InputSource
|
||||
{
|
||||
public:
|
||||
enum HAT_DIRECTION : u32
|
||||
{
|
||||
HAT_DIRECTION_UP = 0,
|
||||
HAT_DIRECTION_DOWN = 1,
|
||||
HAT_DIRECTION_LEFT = 2,
|
||||
HAT_DIRECTION_RIGHT = 3,
|
||||
NUM_HAT_DIRECTIONS = 4,
|
||||
};
|
||||
|
||||
enum : u32
|
||||
{
|
||||
MAX_NUM_BUTTONS = 32,
|
||||
};
|
||||
|
||||
DInputSource();
|
||||
~DInputSource() override;
|
||||
|
||||
bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
|
||||
void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
|
||||
void Shutdown() override;
|
||||
|
||||
void PollEvents() override;
|
||||
std::vector<std::pair<std::string, std::string>> EnumerateDevices() override;
|
||||
std::vector<InputBindingKey> EnumerateMotors() override;
|
||||
bool GetGenericBindingMapping(const std::string_view& device, GenericInputBindingMapping* mapping) override;
|
||||
void UpdateMotorState(InputBindingKey key, float intensity) override;
|
||||
void UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
|
||||
float small_intensity) override;
|
||||
|
||||
std::optional<InputBindingKey> ParseKeyString(const std::string_view& device,
|
||||
const std::string_view& binding) override;
|
||||
std::string ConvertKeyToString(InputBindingKey key) override;
|
||||
|
||||
private:
|
||||
template<typename T>
|
||||
using ComPtr = Microsoft::WRL::ComPtr<T>;
|
||||
|
||||
struct ControllerData
|
||||
{
|
||||
ComPtr<IDirectInputDevice8> device;
|
||||
DIJOYSTATE last_state = {};
|
||||
std::vector<u32> axis_offsets;
|
||||
u32 num_buttons = 0;
|
||||
|
||||
// NOTE: We expose hats as num_buttons + (hat_index * 4) + direction.
|
||||
u32 num_hats = 0;
|
||||
|
||||
bool needs_poll = true;
|
||||
};
|
||||
|
||||
using ControllerDataArray = std::vector<ControllerData>;
|
||||
|
||||
static std::array<bool, NUM_HAT_DIRECTIONS> GetHatButtons(DWORD hat);
|
||||
static std::string GetDeviceIdentifier(u32 index);
|
||||
|
||||
void AddDevices(HWND toplevel_window);
|
||||
bool AddDevice(ControllerData& cd, HWND toplevel_window, const char* name);
|
||||
|
||||
void CheckForStateChanges(size_t index, const DIJOYSTATE& new_state);
|
||||
|
||||
ControllerDataArray m_controllers;
|
||||
|
||||
HMODULE m_dinput_module{};
|
||||
LPCDIDATAFORMAT m_joystick_data_format{};
|
||||
ComPtr<IDirectInput8> m_dinput;
|
||||
};
|
@ -3,7 +3,6 @@
|
||||
#include "common/file_system.h"
|
||||
#include "common/log.h"
|
||||
#include "core/controller.h"
|
||||
#include "core/host_interface.h"
|
||||
#include "core/system.h"
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
@ -15,6 +14,8 @@
|
||||
#include <alloca.h>
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
|
||||
Log_SetChannel(EvdevControllerInterface);
|
||||
|
||||
EvdevControllerInterface::EvdevControllerInterface() = default;
|
||||
@ -442,3 +443,5 @@ bool EvdevControllerInterface::SetControllerDeadzone(int controller_index, float
|
||||
Log_InfoPrintf("Controller %d deadzone size set to %f", controller_index, cd->deadzone);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
@ -1,5 +1,5 @@
|
||||
#pragma once
|
||||
#include "controller_interface.h"
|
||||
#include "input_source.h"
|
||||
#include "core/types.h"
|
||||
#include <array>
|
||||
#include <functional>
|
||||
@ -7,6 +7,8 @@
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#if 0
|
||||
|
||||
class EvdevControllerInterface final : public ControllerInterface
|
||||
{
|
||||
public:
|
||||
@ -97,3 +99,5 @@ private:
|
||||
std::mutex m_event_intercept_mutex;
|
||||
Hook::Callback m_event_intercept_callback;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -4,10 +4,10 @@
|
||||
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>$(RootBuildDir)simpleini\simpleini.lib;$(RootBuildDir)tinyxml2\tinyxml2.lib;$(RootBuildDir)core\core.lib;$(RootBuildDir)scmversion\scmversion.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>$(RootBuildDir)core\core.lib;$(RootBuildDir)scmversion\scmversion.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
|
@ -2,89 +2,81 @@
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\dep\msvc\vsprops\Configurations.props" />
|
||||
<ItemGroup>
|
||||
<ClCompile Include="common_host_interface.cpp" />
|
||||
<ClCompile Include="controller_interface.cpp" />
|
||||
<ClCompile Include="achievements.cpp" />
|
||||
<ClCompile Include="common_host.cpp" />
|
||||
<ClCompile Include="cubeb_audio_stream.cpp">
|
||||
<ExcludedFromBuild Condition="'$(BuildingForUWP)'=='true'">true</ExcludedFromBuild>
|
||||
</ClCompile>
|
||||
<ClCompile Include="d3d11_host_display.cpp" />
|
||||
<ClCompile Include="dinput_controller_interface.cpp">
|
||||
<ExcludedFromBuild Condition="'$(BuildingForUWP)'=='true'">true</ExcludedFromBuild>
|
||||
</ClCompile>
|
||||
<ClCompile Include="dinput_source.cpp" />
|
||||
<ClCompile Include="fullscreen_ui.cpp" />
|
||||
<ClCompile Include="fullscreen_ui_progress_callback.cpp" />
|
||||
<ClCompile Include="game_database.cpp" />
|
||||
<ClCompile Include="d3d12_host_display.cpp" />
|
||||
<ClCompile Include="game_list.cpp" />
|
||||
<ClCompile Include="game_settings.cpp" />
|
||||
<ClCompile Include="host_settings.cpp" />
|
||||
<ClCompile Include="icon.cpp" />
|
||||
<ClCompile Include="imgui_fullscreen.cpp" />
|
||||
<ClCompile Include="imgui_impl_dx11.cpp" />
|
||||
<ClCompile Include="imgui_impl_dx12.cpp" />
|
||||
<ClCompile Include="imgui_impl_opengl3.cpp" />
|
||||
<ClCompile Include="imgui_impl_vulkan.cpp" />
|
||||
<ClCompile Include="imgui_manager.cpp" />
|
||||
<ClCompile Include="imgui_overlays.cpp" />
|
||||
<ClCompile Include="inhibit_screensaver.cpp" />
|
||||
<ClCompile Include="ini_settings_interface.cpp" />
|
||||
<ClCompile Include="input_overlay_ui.cpp" />
|
||||
<ClCompile Include="input_manager.cpp" />
|
||||
<ClCompile Include="input_source.cpp" />
|
||||
<ClCompile Include="opengl_host_display.cpp" />
|
||||
<ClCompile Include="postprocessing_chain.cpp" />
|
||||
<ClCompile Include="postprocessing_shader.cpp" />
|
||||
<ClCompile Include="postprocessing_shadergen.cpp" />
|
||||
<ClCompile Include="save_state_selector_ui.cpp" />
|
||||
<ClCompile Include="sdl_audio_stream.cpp">
|
||||
<ExcludedFromBuild Condition="'$(BuildingForUWP)'=='true'">true</ExcludedFromBuild>
|
||||
</ClCompile>
|
||||
<ClCompile Include="sdl_controller_interface.cpp">
|
||||
<ExcludedFromBuild Condition="'$(BuildingForUWP)'=='true'">true</ExcludedFromBuild>
|
||||
</ClCompile>
|
||||
<ClCompile Include="sdl_initializer.cpp">
|
||||
<ExcludedFromBuild Condition="'$(BuildingForUWP)'=='true'">true</ExcludedFromBuild>
|
||||
</ClCompile>
|
||||
<ClCompile Include="sdl_input_source.cpp" />
|
||||
<ClCompile Include="vulkan_host_display.cpp" />
|
||||
<ClCompile Include="win32_raw_input_source.cpp" />
|
||||
<ClCompile Include="xaudio2_audio_stream.cpp" />
|
||||
<ClCompile Include="xinput_controller_interface.cpp" />
|
||||
<ClCompile Include="xinput_source.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="common_host_interface.h" />
|
||||
<ClInclude Include="controller_interface.h" />
|
||||
<ClInclude Include="achievements.h" />
|
||||
<ClInclude Include="common_host.h" />
|
||||
<ClInclude Include="cubeb_audio_stream.h">
|
||||
<ExcludedFromBuild Condition="'$(BuildingForUWP)'=='true'">true</ExcludedFromBuild>
|
||||
</ClInclude>
|
||||
<ClInclude Include="d3d11_host_display.h" />
|
||||
<ClInclude Include="dinput_controller_interface.h">
|
||||
<ExcludedFromBuild Condition="'$(BuildingForUWP)'=='true'">true</ExcludedFromBuild>
|
||||
</ClInclude>
|
||||
<ClInclude Include="dinput_source.h" />
|
||||
<ClInclude Include="fullscreen_ui.h" />
|
||||
<ClInclude Include="fullscreen_ui_progress_callback.h" />
|
||||
<ClInclude Include="game_database.h" />
|
||||
<ClInclude Include="d3d12_host_display.h" />
|
||||
<ClInclude Include="game_list.h" />
|
||||
<ClInclude Include="game_settings.h" />
|
||||
<ClInclude Include="icon.h" />
|
||||
<ClInclude Include="imgui_fullscreen.h" />
|
||||
<ClInclude Include="imgui_impl_dx11.h" />
|
||||
<ClInclude Include="imgui_impl_dx12.h" />
|
||||
<ClInclude Include="imgui_impl_opengl3.h" />
|
||||
<ClInclude Include="imgui_impl_vulkan.h" />
|
||||
<ClInclude Include="imgui_manager.h" />
|
||||
<ClInclude Include="imgui_overlays.h" />
|
||||
<ClInclude Include="inhibit_screensaver.h" />
|
||||
<ClInclude Include="ini_settings_interface.h" />
|
||||
<ClInclude Include="input_overlay_ui.h" />
|
||||
<ClInclude Include="input_manager.h" />
|
||||
<ClInclude Include="input_source.h" />
|
||||
<ClInclude Include="opengl_host_display.h" />
|
||||
<ClInclude Include="postprocessing_chain.h" />
|
||||
<ClInclude Include="postprocessing_shader.h" />
|
||||
<ClInclude Include="postprocessing_shadergen.h" />
|
||||
<ClInclude Include="save_state_selector_ui.h" />
|
||||
<ClInclude Include="sdl_audio_stream.h">
|
||||
<ExcludedFromBuild Condition="'$(BuildingForUWP)'=='true'">true</ExcludedFromBuild>
|
||||
</ClInclude>
|
||||
<ClInclude Include="sdl_controller_interface.h">
|
||||
<ExcludedFromBuild Condition="'$(BuildingForUWP)'=='true'">true</ExcludedFromBuild>
|
||||
</ClInclude>
|
||||
<ClInclude Include="sdl_initializer.h">
|
||||
<ExcludedFromBuild Condition="'$(BuildingForUWP)'=='true'">true</ExcludedFromBuild>
|
||||
</ClInclude>
|
||||
<ClInclude Include="sdl_input_source.h" />
|
||||
<ClInclude Include="vulkan_host_display.h" />
|
||||
<ClInclude Include="win32_raw_input_source.h" />
|
||||
<ClInclude Include="xaudio2_audio_stream.h" />
|
||||
<ClInclude Include="xinput_controller_interface.h" />
|
||||
<ClInclude Include="xinput_source.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="font_roboto_regular.inl" />
|
||||
|
@ -3,17 +3,11 @@
|
||||
<ItemGroup>
|
||||
<ClCompile Include="icon.cpp" />
|
||||
<ClCompile Include="sdl_audio_stream.cpp" />
|
||||
<ClCompile Include="sdl_controller_interface.cpp" />
|
||||
<ClCompile Include="sdl_initializer.cpp" />
|
||||
<ClCompile Include="common_host_interface.cpp" />
|
||||
<ClCompile Include="ini_settings_interface.cpp" />
|
||||
<ClCompile Include="controller_interface.cpp" />
|
||||
<ClCompile Include="save_state_selector_ui.cpp" />
|
||||
<ClCompile Include="common_host.cpp" />
|
||||
<ClCompile Include="vulkan_host_display.cpp" />
|
||||
<ClCompile Include="d3d11_host_display.cpp" />
|
||||
<ClCompile Include="opengl_host_display.cpp" />
|
||||
<ClCompile Include="xinput_controller_interface.cpp" />
|
||||
<ClCompile Include="game_settings.cpp" />
|
||||
<ClCompile Include="game_list.cpp" />
|
||||
<ClCompile Include="imgui_impl_dx11.cpp" />
|
||||
<ClCompile Include="imgui_impl_opengl3.cpp" />
|
||||
@ -22,32 +16,32 @@
|
||||
<ClCompile Include="postprocessing_shadergen.cpp" />
|
||||
<ClCompile Include="postprocessing_chain.cpp" />
|
||||
<ClCompile Include="cubeb_audio_stream.cpp" />
|
||||
<ClCompile Include="dinput_controller_interface.cpp" />
|
||||
<ClCompile Include="fullscreen_ui.cpp" />
|
||||
<ClCompile Include="fullscreen_ui_progress_callback.cpp" />
|
||||
<ClCompile Include="input_overlay_ui.cpp" />
|
||||
<ClCompile Include="game_database.cpp" />
|
||||
<ClCompile Include="inhibit_screensaver.cpp" />
|
||||
<ClCompile Include="xaudio2_audio_stream.cpp" />
|
||||
<ClCompile Include="d3d12_host_display.cpp" />
|
||||
<ClCompile Include="imgui_impl_dx12.cpp" />
|
||||
<ClCompile Include="host_settings.cpp" />
|
||||
<ClCompile Include="input_source.cpp" />
|
||||
<ClCompile Include="sdl_input_source.cpp" />
|
||||
<ClCompile Include="xinput_source.cpp" />
|
||||
<ClCompile Include="input_manager.cpp" />
|
||||
<ClCompile Include="imgui_manager.cpp" />
|
||||
<ClCompile Include="imgui_fullscreen.cpp" />
|
||||
<ClCompile Include="achievements.cpp" />
|
||||
<ClCompile Include="win32_raw_input_source.cpp" />
|
||||
<ClCompile Include="dinput_source.cpp" />
|
||||
<ClCompile Include="imgui_overlays.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="icon.h" />
|
||||
<ClInclude Include="sdl_audio_stream.h" />
|
||||
<ClInclude Include="sdl_controller_interface.h" />
|
||||
<ClInclude Include="sdl_initializer.h" />
|
||||
<ClInclude Include="common_host_interface.h" />
|
||||
<ClInclude Include="ini_settings_interface.h" />
|
||||
<ClInclude Include="controller_interface.h" />
|
||||
<ClInclude Include="save_state_selector_ui.h" />
|
||||
<ClInclude Include="common_host.h" />
|
||||
<ClInclude Include="vulkan_host_display.h" />
|
||||
<ClInclude Include="d3d11_host_display.h" />
|
||||
<ClInclude Include="opengl_host_display.h" />
|
||||
<ClInclude Include="xinput_controller_interface.h" />
|
||||
<ClInclude Include="game_list.h" />
|
||||
<ClInclude Include="game_settings.h" />
|
||||
<ClInclude Include="imgui_impl_vulkan.h" />
|
||||
<ClInclude Include="imgui_impl_dx11.h" />
|
||||
<ClInclude Include="imgui_impl_opengl3.h" />
|
||||
@ -55,15 +49,21 @@
|
||||
<ClInclude Include="postprocessing_shadergen.h" />
|
||||
<ClInclude Include="postprocessing_chain.h" />
|
||||
<ClInclude Include="cubeb_audio_stream.h" />
|
||||
<ClInclude Include="dinput_controller_interface.h" />
|
||||
<ClInclude Include="fullscreen_ui.h" />
|
||||
<ClInclude Include="fullscreen_ui_progress_callback.h" />
|
||||
<ClInclude Include="input_overlay_ui.h" />
|
||||
<ClInclude Include="game_database.h" />
|
||||
<ClInclude Include="inhibit_screensaver.h" />
|
||||
<ClInclude Include="xaudio2_audio_stream.h" />
|
||||
<ClInclude Include="d3d12_host_display.h" />
|
||||
<ClInclude Include="imgui_impl_dx12.h" />
|
||||
<ClInclude Include="input_manager.h" />
|
||||
<ClInclude Include="input_source.h" />
|
||||
<ClInclude Include="sdl_input_source.h" />
|
||||
<ClInclude Include="xinput_source.h" />
|
||||
<ClInclude Include="imgui_manager.h" />
|
||||
<ClInclude Include="imgui_fullscreen.h" />
|
||||
<ClInclude Include="achievements.h" />
|
||||
<ClInclude Include="win32_raw_input_source.h" />
|
||||
<ClInclude Include="dinput_source.h" />
|
||||
<ClInclude Include="imgui_overlays.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="font_roboto_regular.inl" />
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,85 +1,61 @@
|
||||
#pragma once
|
||||
#include "common/progress_callback.h"
|
||||
#include "common/types.h"
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class HostDisplayTexture;
|
||||
class CommonHostInterface;
|
||||
class SettingsInterface;
|
||||
struct Settings;
|
||||
|
||||
namespace FrontendCommon {
|
||||
enum class ControllerNavigationButton : u32;
|
||||
}
|
||||
|
||||
namespace FullscreenUI {
|
||||
enum class MainWindowType
|
||||
{
|
||||
None,
|
||||
Landing,
|
||||
GameList,
|
||||
Settings,
|
||||
QuickMenu,
|
||||
Achievements,
|
||||
Leaderboards,
|
||||
};
|
||||
|
||||
enum class SettingsPage
|
||||
{
|
||||
InterfaceSettings,
|
||||
GameListSettings,
|
||||
ConsoleSettings,
|
||||
EmulationSettings,
|
||||
BIOSSettings,
|
||||
ControllerSettings,
|
||||
HotkeySettings,
|
||||
MemoryCardSettings,
|
||||
DisplaySettings,
|
||||
EnhancementSettings,
|
||||
AudioSettings,
|
||||
AchievementsSetings,
|
||||
AdvancedSettings,
|
||||
Count
|
||||
};
|
||||
|
||||
bool Initialize(CommonHostInterface* host_interface);
|
||||
bool Initialize();
|
||||
bool IsInitialized();
|
||||
bool HasActiveWindow();
|
||||
void UpdateSettings();
|
||||
void SystemCreated();
|
||||
void SystemDestroyed();
|
||||
void OpenQuickMenu();
|
||||
void CloseQuickMenu();
|
||||
|
||||
#ifdef WITH_CHEEVOS
|
||||
void OnSystemStarted();
|
||||
void OnSystemPaused();
|
||||
void OnSystemResumed();
|
||||
void OnSystemDestroyed();
|
||||
void OnRunningGameChanged();
|
||||
void OpenPauseMenu();
|
||||
bool OpenAchievementsWindow();
|
||||
bool OpenLeaderboardsWindow();
|
||||
#endif
|
||||
|
||||
void Shutdown();
|
||||
void Render();
|
||||
|
||||
bool IsBindingInput();
|
||||
bool HandleKeyboardBinding(const char* keyName, bool pressed);
|
||||
|
||||
std::unique_ptr<HostDisplayTexture> LoadTextureResource(const char* name, bool allow_fallback = true);
|
||||
|
||||
// Returns true if the message has been dismissed.
|
||||
bool DrawErrorWindow(const char* message);
|
||||
bool DrawConfirmWindow(const char* message, bool* result);
|
||||
|
||||
void QueueGameListRefresh();
|
||||
void EnsureGameListLoaded();
|
||||
class ProgressCallback final : public BaseProgressCallback
|
||||
{
|
||||
public:
|
||||
ProgressCallback(std::string name);
|
||||
~ProgressCallback() override;
|
||||
|
||||
Settings& GetSettingsCopy();
|
||||
void SaveAndApplySettings();
|
||||
void SetDebugMenuAllowed(bool allowed);
|
||||
void PushState() override;
|
||||
void PopState() override;
|
||||
|
||||
/// Only ImGuiNavInput_Activate, ImGuiNavInput_Cancel, and DPad should be forwarded.
|
||||
/// Returns true if the UI consumed the event, and it should not execute the normal handler.
|
||||
bool SetControllerNavInput(FrontendCommon::ControllerNavigationButton button, bool value);
|
||||
void SetCancellable(bool cancellable) override;
|
||||
void SetTitle(const char* title) override;
|
||||
void SetStatusText(const char* text) override;
|
||||
void SetProgressRange(u32 range) override;
|
||||
void SetProgressValue(u32 value) override;
|
||||
|
||||
/// Forwards the controller navigation to ImGui for fullscreen navigation. Call before NewFrame().
|
||||
void SetImGuiNavInputs();
|
||||
void DisplayError(const char* message) override;
|
||||
void DisplayWarning(const char* message) override;
|
||||
void DisplayInformation(const char* message) override;
|
||||
void DisplayDebugMessage(const char* message) override;
|
||||
|
||||
void ModalError(const char* message) override;
|
||||
bool ModalConfirmation(const char* message) override;
|
||||
void ModalInformation(const char* message) override;
|
||||
|
||||
void SetCancelled();
|
||||
|
||||
private:
|
||||
void Redraw(bool force);
|
||||
|
||||
std::string m_name;
|
||||
int m_last_progress_percent = -1;
|
||||
};
|
||||
} // namespace FullscreenUI
|
||||
|
@ -1,117 +0,0 @@
|
||||
#include "fullscreen_ui_progress_callback.h"
|
||||
#include "common/log.h"
|
||||
#include "core/host_interface.h"
|
||||
#include "core/imgui_fullscreen.h"
|
||||
Log_SetChannel(ProgressCallback);
|
||||
|
||||
namespace FullscreenUI {
|
||||
|
||||
ProgressCallback::ProgressCallback(String name) : BaseProgressCallback(), m_name(std::move(name))
|
||||
{
|
||||
ImGuiFullscreen::OpenBackgroundProgressDialog(m_name, "", 0, 100, 0);
|
||||
}
|
||||
|
||||
ProgressCallback::~ProgressCallback()
|
||||
{
|
||||
ImGuiFullscreen::CloseBackgroundProgressDialog(m_name);
|
||||
}
|
||||
|
||||
void ProgressCallback::PushState()
|
||||
{
|
||||
BaseProgressCallback::PushState();
|
||||
}
|
||||
|
||||
void ProgressCallback::PopState()
|
||||
{
|
||||
BaseProgressCallback::PopState();
|
||||
Redraw(true);
|
||||
}
|
||||
|
||||
void ProgressCallback::SetCancellable(bool cancellable)
|
||||
{
|
||||
BaseProgressCallback::SetCancellable(cancellable);
|
||||
Redraw(true);
|
||||
}
|
||||
|
||||
void ProgressCallback::SetTitle(const char* title)
|
||||
{
|
||||
// todo?
|
||||
}
|
||||
|
||||
void ProgressCallback::SetStatusText(const char* text)
|
||||
{
|
||||
BaseProgressCallback::SetStatusText(text);
|
||||
Redraw(true);
|
||||
}
|
||||
|
||||
void ProgressCallback::SetProgressRange(u32 range)
|
||||
{
|
||||
u32 last_range = m_progress_range;
|
||||
|
||||
BaseProgressCallback::SetProgressRange(range);
|
||||
|
||||
if (m_progress_range != last_range)
|
||||
Redraw(false);
|
||||
}
|
||||
|
||||
void ProgressCallback::SetProgressValue(u32 value)
|
||||
{
|
||||
u32 lastValue = m_progress_value;
|
||||
|
||||
BaseProgressCallback::SetProgressValue(value);
|
||||
|
||||
if (m_progress_value != lastValue)
|
||||
Redraw(false);
|
||||
}
|
||||
|
||||
void ProgressCallback::Redraw(bool force)
|
||||
{
|
||||
const int percent =
|
||||
static_cast<int>((static_cast<float>(m_progress_value) / static_cast<float>(m_progress_range)) * 100.0f);
|
||||
if (percent == m_last_progress_percent && !force)
|
||||
return;
|
||||
|
||||
m_last_progress_percent = percent;
|
||||
ImGuiFullscreen::UpdateBackgroundProgressDialog(
|
||||
m_name, m_status_text.GetCharArray(), 0, 100, percent);
|
||||
}
|
||||
|
||||
void ProgressCallback::DisplayError(const char* message)
|
||||
{
|
||||
Log_ErrorPrint(message);
|
||||
}
|
||||
|
||||
void ProgressCallback::DisplayWarning(const char* message)
|
||||
{
|
||||
Log_WarningPrint(message);
|
||||
}
|
||||
|
||||
void ProgressCallback::DisplayInformation(const char* message)
|
||||
{
|
||||
Log_InfoPrint(message);
|
||||
}
|
||||
|
||||
void ProgressCallback::DisplayDebugMessage(const char* message)
|
||||
{
|
||||
Log_DevPrint(message);
|
||||
}
|
||||
|
||||
void ProgressCallback::ModalError(const char* message)
|
||||
{
|
||||
Log_ErrorPrint(message);
|
||||
g_host_interface->ReportError(message);
|
||||
}
|
||||
|
||||
bool ProgressCallback::ModalConfirmation(const char* message)
|
||||
{
|
||||
Log_InfoPrint(message);
|
||||
return g_host_interface->ConfirmMessage(message);
|
||||
}
|
||||
|
||||
void ProgressCallback::ModalInformation(const char* message)
|
||||
{
|
||||
Log_InfoPrint(message);
|
||||
g_host_interface->ReportMessage(message);
|
||||
}
|
||||
|
||||
} // namespace FullscreenUI
|
@ -1,38 +0,0 @@
|
||||
#pragma once
|
||||
#include "common/progress_callback.h"
|
||||
#include "common/string.h"
|
||||
|
||||
namespace FullscreenUI {
|
||||
|
||||
class ProgressCallback final : public BaseProgressCallback
|
||||
{
|
||||
public:
|
||||
ProgressCallback(String name);
|
||||
~ProgressCallback() override;
|
||||
|
||||
void PushState() override;
|
||||
void PopState() override;
|
||||
|
||||
void SetCancellable(bool cancellable) override;
|
||||
void SetTitle(const char* title) override;
|
||||
void SetStatusText(const char* text) override;
|
||||
void SetProgressRange(u32 range) override;
|
||||
void SetProgressValue(u32 value) override;
|
||||
|
||||
void DisplayError(const char* message) override;
|
||||
void DisplayWarning(const char* message) override;
|
||||
void DisplayInformation(const char* message) override;
|
||||
void DisplayDebugMessage(const char* message) override;
|
||||
|
||||
void ModalError(const char* message) override;
|
||||
bool ModalConfirmation(const char* message) override;
|
||||
void ModalInformation(const char* message) override;
|
||||
|
||||
private:
|
||||
void Redraw(bool force);
|
||||
|
||||
String m_name;
|
||||
int m_last_progress_percent = -1;
|
||||
};
|
||||
|
||||
} // namespace FullscreenUI
|
@ -1,329 +0,0 @@
|
||||
#include "game_database.h"
|
||||
#include "common/byte_stream.h"
|
||||
#include "common/file_system.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/host_interface.h"
|
||||
#include "core/system.h"
|
||||
#include "rapidjson/document.h"
|
||||
#include "rapidjson/error/en.h"
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
Log_SetChannel(GameDatabase);
|
||||
|
||||
GameDatabase::GameDatabase() = default;
|
||||
|
||||
GameDatabase::~GameDatabase()
|
||||
{
|
||||
Unload();
|
||||
}
|
||||
|
||||
bool GameDatabase::Load()
|
||||
{
|
||||
// TODO: use stream directly
|
||||
std::unique_ptr<ByteStream> stream(
|
||||
g_host_interface->OpenPackageFile("database/gamedb.json", BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED));
|
||||
if (!stream)
|
||||
{
|
||||
Log_ErrorPrintf("Failed to open game database");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string gamedb_data(ByteStream::ReadStreamToString(stream.get(), false));
|
||||
if (gamedb_data.empty())
|
||||
{
|
||||
Log_ErrorPrintf("Failed to read game database");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<rapidjson::Document> json = std::make_unique<rapidjson::Document>();
|
||||
json->Parse(gamedb_data.c_str(), gamedb_data.size());
|
||||
if (json->HasParseError())
|
||||
{
|
||||
Log_ErrorPrintf("Failed to parse game database: %s at offset %zu",
|
||||
rapidjson::GetParseError_En(json->GetParseError()), json->GetErrorOffset());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!json->IsArray())
|
||||
{
|
||||
Log_ErrorPrintf("Document is not an array");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_json = json.release();
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameDatabase::Unload()
|
||||
{
|
||||
if (m_json)
|
||||
{
|
||||
delete static_cast<rapidjson::Document*>(m_json);
|
||||
m_json = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static bool GetStringFromObject(const rapidjson::Value& object, const char* key, std::string* dest)
|
||||
{
|
||||
dest->clear();
|
||||
auto member = object.FindMember(key);
|
||||
if (member == object.MemberEnd() || !member->value.IsString())
|
||||
return false;
|
||||
|
||||
dest->assign(member->value.GetString(), member->value.GetStringLength());
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool GetUIntFromObject(const rapidjson::Value& object, const char* key, u32* dest)
|
||||
{
|
||||
*dest = 0;
|
||||
|
||||
auto member = object.FindMember(key);
|
||||
if (member == object.MemberEnd() || !member->value.IsUint())
|
||||
return false;
|
||||
|
||||
*dest = member->value.GetUint();
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool GetArrayOfStringsFromObject(const rapidjson::Value& object, const char* key, std::vector<std::string>* dest)
|
||||
{
|
||||
dest->clear();
|
||||
auto member = object.FindMember(key);
|
||||
if (member == object.MemberEnd() || !member->value.IsArray())
|
||||
return false;
|
||||
|
||||
for (const rapidjson::Value& str : member->value.GetArray())
|
||||
{
|
||||
if (str.IsString())
|
||||
{
|
||||
dest->emplace_back(str.GetString(), str.GetStringLength());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static const rapidjson::Value* FindDatabaseEntry(const std::string_view& code, rapidjson::Document* json)
|
||||
{
|
||||
for (const rapidjson::Value& current : json->GetArray())
|
||||
{
|
||||
if (!current.IsObject())
|
||||
{
|
||||
Log_WarningPrintf("entry is not an object");
|
||||
continue;
|
||||
}
|
||||
|
||||
auto member = current.FindMember("codes");
|
||||
if (member == current.MemberEnd())
|
||||
{
|
||||
Log_WarningPrintf("codes member is missing");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!member->value.IsArray())
|
||||
{
|
||||
Log_WarningPrintf("codes is not an array");
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const rapidjson::Value& current_code : member->value.GetArray())
|
||||
{
|
||||
if (!current_code.IsString())
|
||||
{
|
||||
Log_WarningPrintf("code is not a string");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current_code.GetStringLength() == code.length() &&
|
||||
StringUtil::Strncasecmp(current_code.GetString(), code.data(), code.length()) == 0)
|
||||
{
|
||||
return ¤t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool GameDatabase::GetEntryForCode(const std::string_view& code, GameDatabaseEntry* entry) const
|
||||
{
|
||||
if (!m_json)
|
||||
return false;
|
||||
|
||||
const rapidjson::Value* object = FindDatabaseEntry(code, static_cast<rapidjson::Document*>(m_json));
|
||||
if (!object)
|
||||
return false;
|
||||
|
||||
if (!GetStringFromObject(*object, "serial", &entry->serial) || !GetStringFromObject(*object, "name", &entry->title))
|
||||
{
|
||||
Log_ErrorPrintf("Missing serial or title for entry");
|
||||
return false;
|
||||
}
|
||||
|
||||
GetStringFromObject(*object, "genre", &entry->genre);
|
||||
GetStringFromObject(*object, "developer", &entry->developer);
|
||||
GetStringFromObject(*object, "publisher", &entry->publisher);
|
||||
|
||||
GetUIntFromObject(*object, "minPlayers", &entry->min_players);
|
||||
GetUIntFromObject(*object, "maxPlayers", &entry->max_players);
|
||||
GetUIntFromObject(*object, "minBlocks", &entry->min_blocks);
|
||||
GetUIntFromObject(*object, "maxBlocks", &entry->max_blocks);
|
||||
|
||||
entry->release_date = 0;
|
||||
{
|
||||
std::string release_date;
|
||||
if (GetStringFromObject(*object, "releaseDate", &release_date))
|
||||
{
|
||||
std::istringstream iss(release_date);
|
||||
struct tm parsed_time = {};
|
||||
iss >> std::get_time(&parsed_time, "%Y-%m-%d");
|
||||
if (!iss.fail())
|
||||
{
|
||||
parsed_time.tm_isdst = 0;
|
||||
#ifdef _WIN32
|
||||
entry->release_date = _mkgmtime(&parsed_time);
|
||||
#else
|
||||
entry->release_date = timegm(&parsed_time);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entry->supported_controllers_mask = ~0u;
|
||||
auto controllers = object->FindMember("controllers");
|
||||
if (controllers != object->MemberEnd())
|
||||
{
|
||||
if (controllers->value.IsArray())
|
||||
{
|
||||
bool first = true;
|
||||
for (const rapidjson::Value& controller : controllers->value.GetArray())
|
||||
{
|
||||
if (!controller.IsString())
|
||||
{
|
||||
Log_WarningPrintf("controller is not a string");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<ControllerType> ctype = Settings::ParseControllerTypeName(controller.GetString());
|
||||
if (!ctype.has_value())
|
||||
{
|
||||
Log_WarningPrintf("Invalid controller type '%s'", controller.GetString());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (first)
|
||||
{
|
||||
entry->supported_controllers_mask = 0;
|
||||
first = false;
|
||||
}
|
||||
|
||||
entry->supported_controllers_mask |= (1u << static_cast<u32>(ctype.value()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log_WarningPrintf("controllers is not an array");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
GameDatabase::TrackHashesMap GameDatabase::GetTrackHashesMap() const
|
||||
{
|
||||
TrackHashesMap result;
|
||||
|
||||
auto json = static_cast<const rapidjson::Document*>(m_json);
|
||||
|
||||
for (const rapidjson::Value& current : json->GetArray())
|
||||
{
|
||||
if (!current.IsObject())
|
||||
{
|
||||
Log_WarningPrintf("entry is not an object");
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<std::string> codes;
|
||||
if (!GetArrayOfStringsFromObject(current, "codes", &codes))
|
||||
{
|
||||
Log_WarningPrintf("codes member is missing");
|
||||
continue;
|
||||
}
|
||||
|
||||
auto track_data = current.FindMember("track_data");
|
||||
if (track_data == current.MemberEnd())
|
||||
{
|
||||
Log_WarningPrintf("track_data member is missing");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!track_data->value.IsArray())
|
||||
{
|
||||
Log_WarningPrintf("track_data is not an array");
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t revision = 0;
|
||||
for (const rapidjson::Value& track_revisions : track_data->value.GetArray())
|
||||
{
|
||||
if (!track_revisions.IsObject())
|
||||
{
|
||||
Log_WarningPrintf("track_data is not an array of object");
|
||||
continue;
|
||||
}
|
||||
|
||||
auto tracks = track_revisions.FindMember("tracks");
|
||||
if (tracks == track_revisions.MemberEnd())
|
||||
{
|
||||
Log_WarningPrintf("tracks member is missing");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!tracks->value.IsArray())
|
||||
{
|
||||
Log_WarningPrintf("tracks is not an array");
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string revisionString;
|
||||
GetStringFromObject(track_revisions, "version", &revisionString);
|
||||
|
||||
for (const rapidjson::Value& track : tracks->value.GetArray())
|
||||
{
|
||||
auto md5_field = track.FindMember("md5");
|
||||
if (md5_field == track.MemberEnd() || !md5_field->value.IsString())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto md5 = CDImageHasher::HashFromString(
|
||||
std::string_view(md5_field->value.GetString(), md5_field->value.GetStringLength()));
|
||||
if (md5)
|
||||
{
|
||||
result.emplace(std::piecewise_construct, std::forward_as_tuple(md5.value()),
|
||||
std::forward_as_tuple(codes, revisionString, revision));
|
||||
}
|
||||
}
|
||||
revision++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool GameDatabase::GetEntryForDisc(CDImage* image, GameDatabaseEntry* entry) const
|
||||
{
|
||||
std::string exe_name_code(System::GetGameCodeForImage(image, false));
|
||||
if (!exe_name_code.empty() && GetEntryForCode(exe_name_code, entry))
|
||||
return true;
|
||||
|
||||
std::string exe_hash_code(System::GetGameHashCodeForImage(image));
|
||||
if (!exe_hash_code.empty() && GetEntryForCode(exe_hash_code, entry))
|
||||
return true;
|
||||
|
||||
Log_WarningPrintf("No entry found for disc (exe code: '%s', hash code: '%s')", exe_name_code.c_str(),
|
||||
exe_hash_code.c_str());
|
||||
return false;
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
#pragma once
|
||||
#include "core/types.h"
|
||||
#include "util/cd_image_hasher.h"
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
class CDImage;
|
||||
|
||||
struct GameDatabaseEntry
|
||||
{
|
||||
std::string serial;
|
||||
std::string title;
|
||||
std::string genre;
|
||||
std::string developer;
|
||||
std::string publisher;
|
||||
u64 release_date;
|
||||
u32 min_players;
|
||||
u32 max_players;
|
||||
u32 min_blocks;
|
||||
u32 max_blocks;
|
||||
u32 supported_controllers_mask;
|
||||
};
|
||||
|
||||
class GameDatabase
|
||||
{
|
||||
public:
|
||||
GameDatabase();
|
||||
~GameDatabase();
|
||||
|
||||
bool Load();
|
||||
void Unload();
|
||||
|
||||
bool GetEntryForDisc(CDImage* image, GameDatabaseEntry* entry) const;
|
||||
|
||||
bool GetEntryForCode(const std::string_view& code, GameDatabaseEntry* entry) const;
|
||||
|
||||
// Map of track hashes for image verification
|
||||
struct TrackData
|
||||
{
|
||||
TrackData(std::vector<std::string> codes, std::string revisionString, uint32_t revision)
|
||||
: codes(codes), revisionString(revisionString), revision(revision)
|
||||
{
|
||||
}
|
||||
|
||||
friend bool operator==(const TrackData& left, const TrackData& right)
|
||||
{
|
||||
// 'revisionString' is deliberately ignored in comparisons as it's redundant with comparing 'revision'! Do not
|
||||
// change!
|
||||
return left.codes == right.codes && left.revision == right.revision;
|
||||
}
|
||||
|
||||
std::vector<std::string> codes;
|
||||
std::string revisionString;
|
||||
uint32_t revision;
|
||||
};
|
||||
using TrackHashesMap = std::multimap<CDImageHasher::Hash, TrackData>;
|
||||
TrackHashesMap GetTrackHashesMap() const;
|
||||
|
||||
private:
|
||||
void* m_json = nullptr;
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@ -1,22 +1,18 @@
|
||||
#pragma once
|
||||
#include "core/game_database.h"
|
||||
#include "core/types.h"
|
||||
#include "game_database.h"
|
||||
#include "game_settings.h"
|
||||
#include "util/cd_image.h"
|
||||
#include <ctime>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class ByteStream;
|
||||
class ProgressCallback;
|
||||
|
||||
class SettingsInterface;
|
||||
struct SystemBootParameters;
|
||||
|
||||
enum class GameListEntryType
|
||||
namespace GameList {
|
||||
enum class EntryType
|
||||
{
|
||||
Disc,
|
||||
PSExe,
|
||||
@ -25,30 +21,19 @@ enum class GameListEntryType
|
||||
Count
|
||||
};
|
||||
|
||||
enum class GameListCompatibilityRating
|
||||
struct Entry
|
||||
{
|
||||
Unknown = 0,
|
||||
DoesntBoot = 1,
|
||||
CrashesInIntro = 2,
|
||||
CrashesInGame = 3,
|
||||
GraphicalAudioIssues = 4,
|
||||
NoIssues = 5,
|
||||
Count,
|
||||
};
|
||||
|
||||
struct GameListEntry
|
||||
{
|
||||
GameListEntryType type = GameListEntryType::Disc;
|
||||
EntryType type = EntryType::Disc;
|
||||
DiscRegion region = DiscRegion::Other;
|
||||
|
||||
std::string path;
|
||||
std::string code;
|
||||
std::string serial;
|
||||
std::string title;
|
||||
std::string genre;
|
||||
std::string publisher;
|
||||
std::string developer;
|
||||
u64 total_size = 0;
|
||||
u64 last_modified_time = 0;
|
||||
std::time_t last_modified_time = 0;
|
||||
|
||||
u64 release_date = 0;
|
||||
u32 supported_controllers = ~static_cast<u32>(0);
|
||||
@ -57,137 +42,47 @@ struct GameListEntry
|
||||
u8 min_blocks = 0;
|
||||
u8 max_blocks = 0;
|
||||
|
||||
GameListCompatibilityRating compatibility_rating = GameListCompatibilityRating::Unknown;
|
||||
GameSettings::Entry settings;
|
||||
GameDatabase::CompatibilityRating compatibility = GameDatabase::CompatibilityRating::Unknown;
|
||||
|
||||
size_t GetReleaseDateString(char* buffer, size_t buffer_size) const;
|
||||
|
||||
ALWAYS_INLINE bool IsDisc() const { return (type == EntryType::Disc); }
|
||||
|
||||
static_assert(sizeof(std::time_t) == sizeof(u64));
|
||||
};
|
||||
|
||||
struct GameListCompatibilityEntry
|
||||
{
|
||||
std::string code;
|
||||
std::string title;
|
||||
std::string version_tested;
|
||||
std::string upscaling_issues;
|
||||
std::string comments;
|
||||
DiscRegion region = DiscRegion::Other;
|
||||
GameListCompatibilityRating compatibility_rating = GameListCompatibilityRating::Unknown;
|
||||
};
|
||||
const char* GetEntryTypeName(EntryType type);
|
||||
const char* GetEntryTypeDisplayName(EntryType type);
|
||||
|
||||
class GameList
|
||||
{
|
||||
public:
|
||||
using EntryList = std::vector<GameListEntry>;
|
||||
bool IsScannableFilename(const std::string_view& path);
|
||||
|
||||
struct DirectoryEntry
|
||||
{
|
||||
std::string path;
|
||||
bool recursive;
|
||||
};
|
||||
/// Populates a game list entry struct with information from the iso/elf.
|
||||
/// Do *not* call while the system is running, it will mess with CDVD state.
|
||||
bool PopulateEntryFromPath(const std::string& path, Entry* entry);
|
||||
|
||||
GameList();
|
||||
~GameList();
|
||||
// Game list access. It's the caller's responsibility to hold the lock while manipulating the entry in any way.
|
||||
std::unique_lock<std::recursive_mutex> GetLock();
|
||||
const Entry* GetEntryByIndex(u32 index);
|
||||
const Entry* GetEntryForPath(const char* path);
|
||||
const Entry* GetEntryBySerial(const std::string_view& serial);
|
||||
u32 GetEntryCount();
|
||||
|
||||
static const char* EntryTypeToString(GameListEntryType type);
|
||||
static const char* EntryCompatibilityRatingToString(GameListCompatibilityRating rating);
|
||||
bool IsGameListLoaded();
|
||||
|
||||
/// Returns a string representation of a compatibility level.
|
||||
static const char* GetGameListCompatibilityRatingString(GameListCompatibilityRating rating);
|
||||
/// Populates the game list with files in the configured directories.
|
||||
/// If invalidate_cache is set, all files will be re-scanned.
|
||||
/// If only_cache is set, no new files will be scanned, only those present in the cache.
|
||||
void Refresh(bool invalidate_cache, bool only_cache = false, ProgressCallback* progress = nullptr);
|
||||
|
||||
static bool IsScannableFilename(const std::string& path);
|
||||
std::string GetCoverImagePathForEntry(const Entry* entry);
|
||||
std::string GetCoverImagePath(const std::string& path, const std::string& serial, const std::string& title);
|
||||
std::string GetNewCoverImagePathForEntry(const Entry* entry, const char* new_filename);
|
||||
}; // namespace GameList
|
||||
|
||||
const EntryList& GetEntries() const { return m_entries; }
|
||||
const u32 GetEntryCount() const { return static_cast<u32>(m_entries.size()); }
|
||||
const std::vector<DirectoryEntry>& GetSearchDirectories() const { return m_search_directories; }
|
||||
const u32 GetSearchDirectoryCount() const { return static_cast<u32>(m_search_directories.size()); }
|
||||
const bool IsGameListLoaded() const { return m_game_list_loaded; }
|
||||
namespace Host {
|
||||
/// Asynchronously starts refreshing the game list.
|
||||
void RefreshGameListAsync(bool invalidate_cache);
|
||||
|
||||
const GameListEntry* GetEntryForPath(const char* path) const;
|
||||
const GameListCompatibilityEntry* GetCompatibilityEntryForCode(const std::string& code) const;
|
||||
bool GetDatabaseEntryForCode(const std::string_view& code, GameDatabaseEntry* entry);
|
||||
bool GetDatabaseEntryForDisc(CDImage* image, GameDatabaseEntry* entry);
|
||||
bool IsPathExcluded(const std::string& path) const;
|
||||
|
||||
void SetCacheFilename(std::string filename) { m_cache_filename = std::move(filename); }
|
||||
void SetUserCompatibilityListFilename(std::string filename)
|
||||
{
|
||||
m_user_compatibility_list_filename = std::move(filename);
|
||||
}
|
||||
void SetUserGameSettingsFilename(std::string filename) { m_user_game_settings_filename = std::move(filename); }
|
||||
void SetSearchDirectoriesFromSettings(SettingsInterface& si);
|
||||
|
||||
void AddDirectory(std::string path, bool recursive);
|
||||
void Refresh(bool invalidate_cache, bool invalidate_database, ProgressCallback* progress = nullptr);
|
||||
|
||||
void UpdateCompatibilityEntry(GameListCompatibilityEntry new_entry, bool save_to_list = true);
|
||||
|
||||
static std::string ExportCompatibilityEntry(const GameListCompatibilityEntry* entry);
|
||||
|
||||
const GameSettings::Entry* GetGameSettings(const std::string& filename, const std::string& game_code);
|
||||
const GameSettings::Entry* GetGameSettingsForCode(const std::string& game_code);
|
||||
void UpdateGameSettings(const std::string& filename, const std::string& game_code, const std::string& game_title,
|
||||
const GameSettings::Entry& new_entry, bool save_to_list = true);
|
||||
|
||||
std::string GetCoverImagePathForEntry(const GameListEntry* entry) const;
|
||||
std::string GetCoverImagePath(const std::string& path, const std::string& code, const std::string& title) const;
|
||||
std::string GetNewCoverImagePathForEntry(const GameListEntry* entry, const char* new_filename) const;
|
||||
|
||||
private:
|
||||
enum : u32
|
||||
{
|
||||
GAME_LIST_CACHE_SIGNATURE = 0x45434C47,
|
||||
GAME_LIST_CACHE_VERSION = 31
|
||||
};
|
||||
|
||||
using CacheMap = std::unordered_map<std::string, GameListEntry>;
|
||||
using CompatibilityMap = std::unordered_map<std::string, GameListCompatibilityEntry>;
|
||||
|
||||
class RedumpDatVisitor;
|
||||
class CompatibilityListVisitor;
|
||||
|
||||
GameListEntry* GetMutableEntryForPath(const char* path);
|
||||
|
||||
static bool GetExeListEntry(const std::string& path, GameListEntry* entry);
|
||||
static bool GetPsfListEntry(const std::string& path, GameListEntry* entry);
|
||||
|
||||
bool GetGameListEntry(const std::string& path, GameListEntry* entry);
|
||||
bool GetGameListEntryFromCache(const std::string& path, GameListEntry* entry);
|
||||
void ScanDirectory(const char* path, bool recursive, ProgressCallback* progress);
|
||||
bool AddFileFromCache(const std::string& path, std::time_t timestamp);
|
||||
bool ScanFile(std::string path, std::time_t timestamp);
|
||||
|
||||
void LoadCache();
|
||||
bool LoadEntriesFromCache(ByteStream* stream);
|
||||
bool OpenCacheForWriting();
|
||||
bool WriteEntryToCache(const GameListEntry* entry, ByteStream* stream);
|
||||
void FlushCacheFileStream();
|
||||
void CloseCacheFileStream();
|
||||
void RewriteCacheFile();
|
||||
void DeleteCacheFile();
|
||||
|
||||
void LoadDatabase();
|
||||
void ClearDatabase();
|
||||
|
||||
void LoadCompatibilityList();
|
||||
bool LoadCompatibilityListFromXML(const std::string& xml);
|
||||
bool SaveCompatibilityDatabaseForEntry(const GameListCompatibilityEntry* entry);
|
||||
|
||||
void LoadGameSettings();
|
||||
|
||||
EntryList m_entries;
|
||||
CacheMap m_cache_map;
|
||||
GameDatabase m_database;
|
||||
CompatibilityMap m_compatibility_list;
|
||||
GameSettings::Database m_game_settings;
|
||||
std::unique_ptr<ByteStream> m_cache_write_stream;
|
||||
|
||||
std::vector<DirectoryEntry> m_search_directories;
|
||||
std::vector<std::string> m_excluded_paths;
|
||||
std::string m_cache_filename;
|
||||
std::string m_user_compatibility_list_filename;
|
||||
std::string m_user_game_settings_filename;
|
||||
bool m_database_load_tried = false;
|
||||
bool m_compatibility_list_load_tried = false;
|
||||
bool m_game_settings_load_tried = false;
|
||||
bool m_game_list_loaded = false;
|
||||
};
|
||||
/// Cancels game list refresh, if there is one in progress.
|
||||
void CancelGameListRefresh();
|
||||
} // namespace Host
|
File diff suppressed because it is too large
Load Diff
@ -1,122 +0,0 @@
|
||||
#pragma once
|
||||
#include "core/types.h"
|
||||
#include <bitset>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
class ByteStream;
|
||||
|
||||
namespace GameSettings {
|
||||
enum class Trait : u32
|
||||
{
|
||||
ForceInterpreter,
|
||||
ForceSoftwareRenderer,
|
||||
ForceSoftwareRendererForReadbacks,
|
||||
ForceInterlacing,
|
||||
DisableTrueColor,
|
||||
DisableUpscaling,
|
||||
DisableScaledDithering,
|
||||
DisableForceNTSCTimings,
|
||||
DisableWidescreen,
|
||||
DisablePGXP,
|
||||
DisablePGXPCulling,
|
||||
DisablePGXPTextureCorrection,
|
||||
DisablePGXPDepthBuffer,
|
||||
ForcePGXPVertexCache,
|
||||
ForcePGXPCPUMode,
|
||||
ForceRecompilerMemoryExceptions,
|
||||
ForceRecompilerICache,
|
||||
ForceRecompilerLUTFastmem,
|
||||
|
||||
Count
|
||||
};
|
||||
|
||||
const char* GetTraitName(Trait trait);
|
||||
const char* GetTraitDisplayName(Trait trait);
|
||||
|
||||
struct Entry
|
||||
{
|
||||
std::bitset<static_cast<int>(Trait::Count)> traits{};
|
||||
std::optional<s16> display_active_start_offset;
|
||||
std::optional<s16> display_active_end_offset;
|
||||
std::optional<s8> display_line_start_offset;
|
||||
std::optional<s8> display_line_end_offset;
|
||||
std::optional<u32> dma_max_slice_ticks;
|
||||
std::optional<u32> dma_halt_ticks;
|
||||
std::optional<u32> gpu_fifo_size;
|
||||
std::optional<u32> gpu_max_run_ahead;
|
||||
std::optional<float> gpu_pgxp_tolerance;
|
||||
std::optional<float> gpu_pgxp_depth_threshold;
|
||||
|
||||
// user settings
|
||||
std::optional<u32> runahead_frames;
|
||||
std::optional<u32> cpu_overclock_numerator;
|
||||
std::optional<u32> cpu_overclock_denominator;
|
||||
std::optional<bool> cpu_overclock_enable;
|
||||
std::optional<bool> enable_8mb_ram;
|
||||
std::optional<u32> cdrom_read_speedup;
|
||||
std::optional<u32> cdrom_seek_speedup;
|
||||
std::optional<DisplayCropMode> display_crop_mode;
|
||||
std::optional<DisplayAspectRatio> display_aspect_ratio;
|
||||
std::optional<GPURenderer> gpu_renderer;
|
||||
std::optional<GPUDownsampleMode> gpu_downsample_mode;
|
||||
std::optional<bool> display_linear_upscaling;
|
||||
std::optional<bool> display_integer_upscaling;
|
||||
std::optional<bool> display_force_4_3_for_24bit;
|
||||
std::optional<u16> display_aspect_ratio_custom_numerator;
|
||||
std::optional<u16> display_aspect_ratio_custom_denominator;
|
||||
std::optional<u32> gpu_resolution_scale;
|
||||
std::optional<u32> gpu_multisamples;
|
||||
std::optional<bool> gpu_per_sample_shading;
|
||||
std::optional<bool> gpu_true_color;
|
||||
std::optional<bool> gpu_scaled_dithering;
|
||||
std::optional<bool> gpu_force_ntsc_timings;
|
||||
std::optional<GPUTextureFilter> gpu_texture_filter;
|
||||
std::optional<bool> gpu_widescreen_hack;
|
||||
std::optional<bool> gpu_pgxp;
|
||||
std::optional<bool> gpu_pgxp_projection_precision;
|
||||
std::optional<bool> gpu_pgxp_depth_buffer;
|
||||
std::optional<MultitapMode> multitap_mode;
|
||||
std::optional<ControllerType> controller_1_type;
|
||||
std::optional<ControllerType> controller_2_type;
|
||||
std::optional<MemoryCardType> memory_card_1_type;
|
||||
std::optional<MemoryCardType> memory_card_2_type;
|
||||
std::string memory_card_1_shared_path;
|
||||
std::string memory_card_2_shared_path;
|
||||
std::string input_profile_name;
|
||||
|
||||
ALWAYS_INLINE bool HasTrait(Trait trait) const { return traits[static_cast<int>(trait)]; }
|
||||
ALWAYS_INLINE void AddTrait(Trait trait) { traits[static_cast<int>(trait)] = true; }
|
||||
ALWAYS_INLINE void RemoveTrait(Trait trait) { traits[static_cast<int>(trait)] = false; }
|
||||
ALWAYS_INLINE void SetTrait(Trait trait, bool enabled) { traits[static_cast<int>(trait)] = enabled; }
|
||||
|
||||
bool LoadFromStream(ByteStream* stream);
|
||||
bool SaveToStream(ByteStream* stream) const;
|
||||
|
||||
u32 GetUserSettingsCount() const;
|
||||
|
||||
void ApplySettings(bool display_osd_messages) const;
|
||||
|
||||
// Key-based interface, used by Android.
|
||||
std::optional<std::string> GetValueForKey(const std::string_view& key) const;
|
||||
void SetValueForKey(const std::string_view& key, const std::optional<std::string>& value);
|
||||
};
|
||||
|
||||
class Database
|
||||
{
|
||||
public:
|
||||
Database();
|
||||
~Database();
|
||||
|
||||
const Entry* GetEntry(const std::string& code) const;
|
||||
void SetEntry(const std::string& code, const std::string& name, const Entry& entry, const char* save_path);
|
||||
|
||||
bool Load(const std::string_view& ini_data);
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, Entry> m_entries;
|
||||
};
|
||||
|
||||
}; // namespace GameSettings
|
@ -64,54 +64,6 @@ std::vector<std::string> Host::GetBaseStringListSetting(const char* section, con
|
||||
return s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE)->GetStringList(section, key);
|
||||
}
|
||||
|
||||
void Host::SetBaseBoolSettingValue(const char* section, const char* key, bool value)
|
||||
{
|
||||
std::unique_lock lock(s_settings_mutex);
|
||||
s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE)->SetBoolValue(section, key, value);
|
||||
}
|
||||
|
||||
void Host::SetBaseIntSettingValue(const char* section, const char* key, s32 value)
|
||||
{
|
||||
std::unique_lock lock(s_settings_mutex);
|
||||
s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE)->SetIntValue(section, key, value);
|
||||
}
|
||||
|
||||
void Host::SetBaseUIntSettingValue(const char* section, const char* key, u32 value)
|
||||
{
|
||||
std::unique_lock lock(s_settings_mutex);
|
||||
s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE)->SetUIntValue(section, key, value);
|
||||
}
|
||||
|
||||
void Host::SetBaseFloatSettingValue(const char* section, const char* key, float value)
|
||||
{
|
||||
std::unique_lock lock(s_settings_mutex);
|
||||
s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE)->SetFloatValue(section, key, value);
|
||||
}
|
||||
|
||||
void Host::SetBaseStringSettingValue(const char* section, const char* key, const char* value)
|
||||
{
|
||||
std::unique_lock lock(s_settings_mutex);
|
||||
s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE)->SetStringValue(section, key, value);
|
||||
}
|
||||
|
||||
void Host::SetBaseStringListSettingValue(const char* section, const char* key, const std::vector<std::string>& values)
|
||||
{
|
||||
std::unique_lock lock(s_settings_mutex);
|
||||
s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE)->SetStringList(section, key, values);
|
||||
}
|
||||
|
||||
void Host::DeleteBaseSettingValue(const char* section, const char* key)
|
||||
{
|
||||
std::unique_lock lock(s_settings_mutex);
|
||||
s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE)->DeleteValue(section, key);
|
||||
}
|
||||
|
||||
void Host::CommitBaseSettingChanges()
|
||||
{
|
||||
std::unique_lock lock(s_settings_mutex);
|
||||
s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE)->Save();
|
||||
}
|
||||
|
||||
std::string Host::GetStringSettingValue(const char* section, const char* key, const char* default_value /*= ""*/)
|
||||
{
|
||||
std::unique_lock lock(s_settings_mutex);
|
||||
|
2207
src/frontend-common/imgui_fullscreen.cpp
Normal file
2207
src/frontend-common/imgui_fullscreen.cpp
Normal file
File diff suppressed because it is too large
Load Diff
266
src/frontend-common/imgui_fullscreen.h
Normal file
266
src/frontend-common/imgui_fullscreen.h
Normal file
@ -0,0 +1,266 @@
|
||||
/* PCSX2 - PS2 Emulator for PCs
|
||||
* Copyright (C) 2002-2022 PCSX2 Dev Team
|
||||
*
|
||||
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
|
||||
* of the GNU Lesser General Public License as published by the Free Software Found-
|
||||
* ation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
* PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with PCSX2.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "common/types.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class HostDisplayTexture;
|
||||
|
||||
namespace ImGuiFullscreen {
|
||||
#define HEX_TO_IMVEC4(hex, alpha) \
|
||||
ImVec4(static_cast<float>((hex >> 16) & 0xFFu) / 255.0f, static_cast<float>((hex >> 8) & 0xFFu) / 255.0f, \
|
||||
static_cast<float>(hex & 0xFFu) / 255.0f, static_cast<float>(alpha) / 255.0f)
|
||||
|
||||
static constexpr float LAYOUT_SCREEN_WIDTH = 1280.0f;
|
||||
static constexpr float LAYOUT_SCREEN_HEIGHT = 720.0f;
|
||||
static constexpr float LAYOUT_LARGE_FONT_SIZE = 26.0f;
|
||||
static constexpr float LAYOUT_MEDIUM_FONT_SIZE = 16.0f;
|
||||
static constexpr float LAYOUT_SMALL_FONT_SIZE = 10.0f;
|
||||
static constexpr float LAYOUT_MENU_BUTTON_HEIGHT = 50.0f;
|
||||
static constexpr float LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY = 26.0f;
|
||||
static constexpr float LAYOUT_MENU_BUTTON_X_PADDING = 15.0f;
|
||||
static constexpr float LAYOUT_MENU_BUTTON_Y_PADDING = 10.0f;
|
||||
|
||||
extern ImFont* g_standard_font;
|
||||
extern ImFont* g_medium_font;
|
||||
extern ImFont* g_large_font;
|
||||
|
||||
extern float g_layout_scale;
|
||||
extern float g_layout_padding_left;
|
||||
extern float g_layout_padding_top;
|
||||
|
||||
extern ImVec4 UIBackgroundColor;
|
||||
extern ImVec4 UIBackgroundTextColor;
|
||||
extern ImVec4 UIBackgroundLineColor;
|
||||
extern ImVec4 UIBackgroundHighlightColor;
|
||||
extern ImVec4 UIDisabledColor;
|
||||
extern ImVec4 UIPrimaryColor;
|
||||
extern ImVec4 UIPrimaryLightColor;
|
||||
extern ImVec4 UIPrimaryDarkColor;
|
||||
extern ImVec4 UIPrimaryTextColor;
|
||||
extern ImVec4 UITextHighlightColor;
|
||||
extern ImVec4 UIPrimaryLineColor;
|
||||
extern ImVec4 UISecondaryColor;
|
||||
extern ImVec4 UISecondaryLightColor;
|
||||
extern ImVec4 UISecondaryDarkColor;
|
||||
extern ImVec4 UISecondaryTextColor;
|
||||
|
||||
static ALWAYS_INLINE float DPIScale(float v)
|
||||
{
|
||||
return ImGui::GetIO().DisplayFramebufferScale.x * v;
|
||||
}
|
||||
|
||||
static ALWAYS_INLINE float DPIScale(int v)
|
||||
{
|
||||
return ImGui::GetIO().DisplayFramebufferScale.x * static_cast<float>(v);
|
||||
}
|
||||
|
||||
static ALWAYS_INLINE ImVec2 DPIScale(const ImVec2& v)
|
||||
{
|
||||
const ImVec2& fbs = ImGui::GetIO().DisplayFramebufferScale;
|
||||
return ImVec2(v.x * fbs.x, v.y * fbs.y);
|
||||
}
|
||||
|
||||
static ALWAYS_INLINE float WindowWidthScale(float v)
|
||||
{
|
||||
return ImGui::GetWindowWidth() * v;
|
||||
}
|
||||
|
||||
static ALWAYS_INLINE float WindowHeightScale(float v)
|
||||
{
|
||||
return ImGui::GetWindowHeight() * v;
|
||||
}
|
||||
|
||||
static ALWAYS_INLINE float LayoutScale(float v)
|
||||
{
|
||||
return g_layout_scale * v;
|
||||
}
|
||||
|
||||
static ALWAYS_INLINE ImVec2 LayoutScale(const ImVec2& v)
|
||||
{
|
||||
return ImVec2(v.x * g_layout_scale, v.y * g_layout_scale);
|
||||
}
|
||||
|
||||
static ALWAYS_INLINE ImVec2 LayoutScale(float x, float y)
|
||||
{
|
||||
return ImVec2(x * g_layout_scale, y * g_layout_scale);
|
||||
}
|
||||
|
||||
static ALWAYS_INLINE ImVec2 LayoutScaleAndOffset(float x, float y)
|
||||
{
|
||||
return ImVec2(g_layout_padding_left + x * g_layout_scale, g_layout_padding_top + y * g_layout_scale);
|
||||
}
|
||||
|
||||
static ALWAYS_INLINE ImVec4 ModAlpha(const ImVec4& v, float a)
|
||||
{
|
||||
return ImVec4(v.x, v.y, v.z, a);
|
||||
}
|
||||
|
||||
/// Centers an image within the specified bounds, scaling up or down as needed.
|
||||
ImRect CenterImage(const ImVec2& fit_size, const ImVec2& image_size);
|
||||
ImRect CenterImage(const ImRect& fit_rect, const ImVec2& image_size);
|
||||
|
||||
/// Initializes, setting up any state.
|
||||
bool Initialize(const char* placeholder_image_path);
|
||||
|
||||
void SetTheme();
|
||||
void SetFonts(ImFont* standard_font, ImFont* medium_font, ImFont* large_font);
|
||||
bool UpdateLayoutScale();
|
||||
|
||||
/// Shuts down, clearing all state.
|
||||
void Shutdown();
|
||||
|
||||
/// Texture cache.
|
||||
const std::shared_ptr<HostDisplayTexture>& GetPlaceholderTexture();
|
||||
std::shared_ptr<HostDisplayTexture> LoadTexture(const std::string_view& path);
|
||||
HostDisplayTexture* GetCachedTexture(const std::string_view& name);
|
||||
HostDisplayTexture* GetCachedTextureAsync(const std::string_view& name);
|
||||
bool InvalidateCachedTexture(const std::string& path);
|
||||
void UploadAsyncTextures();
|
||||
|
||||
void BeginLayout();
|
||||
void EndLayout();
|
||||
|
||||
void QueueResetFocus();
|
||||
bool ResetFocusHere();
|
||||
bool WantsToCloseMenu();
|
||||
|
||||
void PushPrimaryColor();
|
||||
void PopPrimaryColor();
|
||||
void PushSecondaryColor();
|
||||
void PopSecondaryColor();
|
||||
|
||||
bool IsCancelButtonPressed();
|
||||
|
||||
void DrawWindowTitle(const char* title);
|
||||
|
||||
bool BeginFullscreenColumns(const char* title = nullptr);
|
||||
void EndFullscreenColumns();
|
||||
|
||||
bool BeginFullscreenColumnWindow(float start, float end, const char* name,
|
||||
const ImVec4& background = UIBackgroundColor);
|
||||
void EndFullscreenColumnWindow();
|
||||
|
||||
bool BeginFullscreenWindow(float left, float top, float width, float height, const char* name,
|
||||
const ImVec4& background = HEX_TO_IMVEC4(0x212121, 0xFF), float rounding = 0.0f,
|
||||
float padding = 0.0f, ImGuiWindowFlags flags = 0);
|
||||
bool BeginFullscreenWindow(const ImVec2& position, const ImVec2& size, const char* name,
|
||||
const ImVec4& background = HEX_TO_IMVEC4(0x212121, 0xFF), float rounding = 0.0f,
|
||||
float padding = 0.0f, ImGuiWindowFlags flags = 0);
|
||||
void EndFullscreenWindow();
|
||||
|
||||
void BeginMenuButtons(u32 num_items = 0, float y_align = 0.0f, float x_padding = LAYOUT_MENU_BUTTON_X_PADDING,
|
||||
float y_padding = LAYOUT_MENU_BUTTON_Y_PADDING, float item_height = LAYOUT_MENU_BUTTON_HEIGHT);
|
||||
void EndMenuButtons();
|
||||
bool MenuButtonFrame(const char* str_id, bool enabled, float height, bool* visible, bool* hovered, ImVec2* min,
|
||||
ImVec2* max, ImGuiButtonFlags flags = 0, float hover_alpha = 1.0f);
|
||||
void MenuHeading(const char* title, bool draw_line = true);
|
||||
bool MenuHeadingButton(const char* title, const char* value = nullptr, bool enabled = true, bool draw_line = true);
|
||||
bool ActiveButton(const char* title, bool is_active, bool enabled = true,
|
||||
float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, ImFont* font = g_large_font);
|
||||
bool MenuButton(const char* title, const char* summary, bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT,
|
||||
ImFont* font = g_large_font, ImFont* summary_font = g_medium_font);
|
||||
bool MenuButtonWithValue(const char* title, const char* summary, const char* value, bool enabled = true,
|
||||
float height = LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font,
|
||||
ImFont* summary_font = g_medium_font);
|
||||
bool MenuImageButton(const char* title, const char* summary, ImTextureID user_texture_id, const ImVec2& image_size,
|
||||
bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT,
|
||||
const ImVec2& uv0 = ImVec2(0.0f, 0.0f), const ImVec2& uv1 = ImVec2(1.0f, 1.0f),
|
||||
ImFont* font = g_large_font, ImFont* summary_font = g_medium_font);
|
||||
bool FloatingButton(const char* text, float x, float y, float width = -1.0f,
|
||||
float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, float anchor_x = 0.0f, float anchor_y = 0.0f,
|
||||
bool enabled = true, ImFont* font = g_large_font, ImVec2* out_position = nullptr);
|
||||
bool ToggleButton(const char* title, const char* summary, bool* v, bool enabled = true,
|
||||
float height = LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font,
|
||||
ImFont* summary_font = g_medium_font);
|
||||
bool ThreeWayToggleButton(const char* title, const char* summary, std::optional<bool>* v, bool enabled = true,
|
||||
float height = LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font,
|
||||
ImFont* summary_font = g_medium_font);
|
||||
bool RangeButton(const char* title, const char* summary, s32* value, s32 min, s32 max, s32 increment,
|
||||
const char* format = "%d", bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT,
|
||||
ImFont* font = g_large_font, ImFont* summary_font = g_medium_font);
|
||||
bool RangeButton(const char* title, const char* summary, float* value, float min, float max, float increment,
|
||||
const char* format = "%f", bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT,
|
||||
ImFont* font = g_large_font, ImFont* summary_font = g_medium_font);
|
||||
bool EnumChoiceButtonImpl(const char* title, const char* summary, s32* value_pointer,
|
||||
const char* (*to_display_name_function)(s32 value, void* opaque), void* opaque, u32 count,
|
||||
bool enabled, float height, ImFont* font, ImFont* summary_font);
|
||||
|
||||
template<typename DataType, typename CountType>
|
||||
ALWAYS_INLINE static bool EnumChoiceButton(const char* title, const char* summary, DataType* value_pointer,
|
||||
const char* (*to_display_name_function)(DataType value), CountType count,
|
||||
bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT,
|
||||
ImFont* font = g_large_font, ImFont* summary_font = g_medium_font)
|
||||
{
|
||||
s32 value = static_cast<s32>(*value_pointer);
|
||||
auto to_display_name_wrapper = [](s32 value, void* opaque) -> const char* {
|
||||
return (*static_cast<decltype(to_display_name_function)*>(opaque))(static_cast<DataType>(value));
|
||||
};
|
||||
|
||||
if (EnumChoiceButtonImpl(title, summary, &value, to_display_name_wrapper, &to_display_name_function,
|
||||
static_cast<u32>(count), enabled, height, font, summary_font))
|
||||
{
|
||||
*value_pointer = static_cast<DataType>(value);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void BeginNavBar(float x_padding = LAYOUT_MENU_BUTTON_X_PADDING, float y_padding = LAYOUT_MENU_BUTTON_Y_PADDING);
|
||||
void EndNavBar();
|
||||
void NavTitle(const char* title, float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, ImFont* font = g_large_font);
|
||||
void RightAlignNavButtons(u32 num_items = 0, float item_width = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY,
|
||||
float item_height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
|
||||
bool NavButton(const char* title, bool is_active, bool enabled = true, float width = -1.0f,
|
||||
float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, ImFont* font = g_large_font);
|
||||
|
||||
using FileSelectorCallback = std::function<void(const std::string& path)>;
|
||||
using FileSelectorFilters = std::vector<std::string>;
|
||||
bool IsFileSelectorOpen();
|
||||
void OpenFileSelector(const char* title, bool select_directory, FileSelectorCallback callback,
|
||||
FileSelectorFilters filters = FileSelectorFilters(),
|
||||
std::string initial_directory = std::string());
|
||||
void CloseFileSelector();
|
||||
|
||||
using ChoiceDialogCallback = std::function<void(s32 index, const std::string& title, bool checked)>;
|
||||
using ChoiceDialogOptions = std::vector<std::pair<std::string, bool>>;
|
||||
bool IsChoiceDialogOpen();
|
||||
void OpenChoiceDialog(const char* title, bool checkable, ChoiceDialogOptions options, ChoiceDialogCallback callback);
|
||||
void CloseChoiceDialog();
|
||||
|
||||
float GetNotificationVerticalPosition();
|
||||
float GetNotificationVerticalDirection();
|
||||
void SetNotificationVerticalPosition(float position, float direction);
|
||||
|
||||
void OpenBackgroundProgressDialog(const char* str_id, std::string message, s32 min, s32 max, s32 value);
|
||||
void UpdateBackgroundProgressDialog(const char* str_id, std::string message, s32 min, s32 max, s32 value);
|
||||
void CloseBackgroundProgressDialog(const char* str_id);
|
||||
|
||||
void AddNotification(float duration, std::string title, std::string text, std::string image_path);
|
||||
void ClearNotifications();
|
||||
|
||||
void ShowToast(std::string title, std::string message, float duration = 10.0f);
|
||||
void ClearToast();
|
||||
} // namespace ImGuiFullscreen
|
814
src/frontend-common/imgui_manager.cpp
Normal file
814
src/frontend-common/imgui_manager.cpp
Normal file
@ -0,0 +1,814 @@
|
||||
#include "imgui_manager.h"
|
||||
#include "IconsFontAwesome5.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/file_system.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "common/timer.h"
|
||||
#include "common_host.h"
|
||||
#include "core/gpu.h"
|
||||
#include "core/host.h"
|
||||
#include "core/host_display.h"
|
||||
#include "core/system.h"
|
||||
#include "fmt/format.h"
|
||||
#include "fullscreen_ui.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_fullscreen.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "input_manager.h"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
Log_SetChannel(ImGuiManager);
|
||||
|
||||
namespace ImGuiManager {
|
||||
static void SetStyle();
|
||||
static void SetKeyMap();
|
||||
static bool LoadFontData();
|
||||
static bool AddImGuiFonts(bool fullscreen_fonts);
|
||||
static ImFont* AddTextFont(float size);
|
||||
static ImFont* AddFixedFont(float size);
|
||||
static bool AddIconFonts(float size);
|
||||
static void AcquirePendingOSDMessages();
|
||||
static void DrawOSDMessages();
|
||||
} // namespace ImGuiManager
|
||||
|
||||
static float s_global_scale = 1.0f;
|
||||
|
||||
static std::string s_font_path;
|
||||
static const ImWchar* s_font_range = nullptr;
|
||||
|
||||
static ImFont* s_standard_font;
|
||||
static ImFont* s_fixed_font;
|
||||
static ImFont* s_medium_font;
|
||||
static ImFont* s_large_font;
|
||||
|
||||
static std::vector<u8> s_standard_font_data;
|
||||
static std::vector<u8> s_fixed_font_data;
|
||||
static std::vector<u8> s_icon_font_data;
|
||||
|
||||
static Common::Timer s_last_render_time;
|
||||
|
||||
// cached copies of WantCaptureKeyboard/Mouse, used to know when to dispatch events
|
||||
static std::atomic_bool s_imgui_wants_keyboard{false};
|
||||
static std::atomic_bool s_imgui_wants_mouse{false};
|
||||
|
||||
// mapping of host key -> imgui key
|
||||
static std::unordered_map<u32, ImGuiKey> s_imgui_key_map;
|
||||
|
||||
void ImGuiManager::SetFontPath(std::string path)
|
||||
{
|
||||
s_font_path = std::move(path);
|
||||
s_standard_font_data = {};
|
||||
}
|
||||
|
||||
void ImGuiManager::SetFontRange(const u16* range)
|
||||
{
|
||||
s_font_range = range;
|
||||
s_standard_font_data = {};
|
||||
}
|
||||
|
||||
bool ImGuiManager::Initialize()
|
||||
{
|
||||
if (!LoadFontData())
|
||||
{
|
||||
Panic("Failed to load font data");
|
||||
return false;
|
||||
}
|
||||
|
||||
s_global_scale =
|
||||
std::max(1.0f, g_host_display->GetWindowScale() * static_cast<float>(g_settings.display_osd_scale / 100.0f));
|
||||
|
||||
ImGui::CreateContext();
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.IniFilename = nullptr;
|
||||
io.BackendFlags |= ImGuiBackendFlags_HasGamepad;
|
||||
io.BackendUsingLegacyKeyArrays = 0;
|
||||
io.BackendUsingLegacyNavInputArray = 0;
|
||||
#ifndef __ANDROID__
|
||||
// Android has no keyboard, nor are we using ImGui for any actual user-interactable windows.
|
||||
io.ConfigFlags |=
|
||||
ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_NavEnableGamepad | ImGuiConfigFlags_NoMouseCursorChange;
|
||||
#else
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_NavEnableGamepad;
|
||||
#endif
|
||||
|
||||
io.DisplayFramebufferScale = ImVec2(1, 1); // We already scale things ourselves, this would double-apply scaling
|
||||
io.DisplaySize.x = static_cast<float>(g_host_display->GetWindowWidth());
|
||||
io.DisplaySize.y = static_cast<float>(g_host_display->GetWindowHeight());
|
||||
|
||||
SetKeyMap();
|
||||
SetStyle();
|
||||
|
||||
AssertMsg(!FullscreenUI::IsInitialized(), "Fullscreen UI is not initialized on ImGui init");
|
||||
|
||||
if (!g_host_display->CreateImGuiContext())
|
||||
{
|
||||
Panic("Failed to create ImGui device context");
|
||||
g_host_display->DestroyImGuiContext();
|
||||
ImGui::DestroyContext();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!AddImGuiFonts(false) || !g_host_display->UpdateImGuiFontTexture())
|
||||
{
|
||||
Panic("Failed to create ImGui font text");
|
||||
g_host_display->DestroyImGuiContext();
|
||||
ImGui::DestroyContext();
|
||||
return false;
|
||||
}
|
||||
|
||||
// don't need the font data anymore, save some memory
|
||||
ImGui::GetIO().Fonts->ClearTexData();
|
||||
|
||||
NewFrame();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGuiManager::Shutdown()
|
||||
{
|
||||
FullscreenUI::Shutdown();
|
||||
|
||||
if (g_host_display)
|
||||
g_host_display->DestroyImGuiContext();
|
||||
if (ImGui::GetCurrentContext())
|
||||
ImGui::DestroyContext();
|
||||
|
||||
s_standard_font = nullptr;
|
||||
s_fixed_font = nullptr;
|
||||
s_medium_font = nullptr;
|
||||
s_large_font = nullptr;
|
||||
ImGuiFullscreen::SetFonts(nullptr, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void ImGuiManager::WindowResized()
|
||||
{
|
||||
const u32 new_width = g_host_display ? g_host_display->GetWindowWidth() : 0;
|
||||
const u32 new_height = g_host_display ? g_host_display->GetWindowHeight() : 0;
|
||||
|
||||
ImGui::GetIO().DisplaySize = ImVec2(static_cast<float>(new_width), static_cast<float>(new_height));
|
||||
|
||||
UpdateScale();
|
||||
|
||||
// restart imgui frame on the new window size to pick it up, otherwise we draw to the old size
|
||||
ImGui::EndFrame();
|
||||
NewFrame();
|
||||
}
|
||||
|
||||
void ImGuiManager::UpdateScale()
|
||||
{
|
||||
const float window_scale = g_host_display ? g_host_display->GetWindowScale() : 1.0f;
|
||||
const float scale = std::max(window_scale * static_cast<float>(/*EmuConfig.GS.OsdScale*/ 100.0 / 100.0), 1.0f);
|
||||
|
||||
if (scale == s_global_scale && (!HasFullscreenFonts() || !ImGuiFullscreen::UpdateLayoutScale()))
|
||||
return;
|
||||
|
||||
// This is assumed to be called mid-frame.
|
||||
ImGui::EndFrame();
|
||||
|
||||
s_global_scale = scale;
|
||||
|
||||
ImGui::GetStyle() = ImGuiStyle();
|
||||
ImGui::GetStyle().WindowMinSize = ImVec2(1.0f, 1.0f);
|
||||
SetStyle();
|
||||
ImGui::GetStyle().ScaleAllSizes(scale);
|
||||
|
||||
if (!AddImGuiFonts(HasFullscreenFonts()))
|
||||
Panic("Failed to create ImGui font text");
|
||||
|
||||
if (!g_host_display->UpdateImGuiFontTexture())
|
||||
Panic("Failed to recreate font texture after scale+resize");
|
||||
|
||||
NewFrame();
|
||||
}
|
||||
|
||||
void ImGuiManager::NewFrame()
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.DeltaTime = static_cast<float>(s_last_render_time.GetTimeSecondsAndReset());
|
||||
|
||||
ImGui::NewFrame();
|
||||
|
||||
// Disable nav input on the implicit (Debug##Default) window. Otherwise we end up requesting keyboard
|
||||
// focus when there's nothing there. We use GetCurrentWindowRead() because otherwise it'll make it visible.
|
||||
ImGui::GetCurrentWindowRead()->Flags |= ImGuiWindowFlags_NoNavInputs;
|
||||
s_imgui_wants_keyboard.store(io.WantCaptureKeyboard, std::memory_order_relaxed);
|
||||
s_imgui_wants_mouse.store(io.WantCaptureMouse, std::memory_order_release);
|
||||
}
|
||||
|
||||
void ImGuiManager::SetStyle()
|
||||
{
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
style = ImGuiStyle();
|
||||
style.WindowMinSize = ImVec2(1.0f, 1.0f);
|
||||
|
||||
ImVec4* colors = style.Colors;
|
||||
colors[ImGuiCol_Text] = ImVec4(0.95f, 0.96f, 0.98f, 1.00f);
|
||||
colors[ImGuiCol_TextDisabled] = ImVec4(0.36f, 0.42f, 0.47f, 1.00f);
|
||||
colors[ImGuiCol_WindowBg] = ImVec4(0.11f, 0.15f, 0.17f, 1.00f);
|
||||
colors[ImGuiCol_ChildBg] = ImVec4(0.15f, 0.18f, 0.22f, 1.00f);
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 0.94f);
|
||||
colors[ImGuiCol_Border] = ImVec4(0.08f, 0.10f, 0.12f, 1.00f);
|
||||
colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f);
|
||||
colors[ImGuiCol_FrameBg] = ImVec4(0.20f, 0.25f, 0.29f, 1.00f);
|
||||
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.12f, 0.20f, 0.28f, 1.00f);
|
||||
colors[ImGuiCol_FrameBgActive] = ImVec4(0.09f, 0.12f, 0.14f, 1.00f);
|
||||
colors[ImGuiCol_TitleBg] = ImVec4(0.09f, 0.12f, 0.14f, 0.65f);
|
||||
colors[ImGuiCol_TitleBgActive] = ImVec4(0.08f, 0.10f, 0.12f, 1.00f);
|
||||
colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f);
|
||||
colors[ImGuiCol_MenuBarBg] = ImVec4(0.15f, 0.18f, 0.22f, 1.00f);
|
||||
colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.02f, 0.02f, 0.39f);
|
||||
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.20f, 0.25f, 0.29f, 1.00f);
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.18f, 0.22f, 0.25f, 1.00f);
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.09f, 0.21f, 0.31f, 1.00f);
|
||||
colors[ImGuiCol_CheckMark] = ImVec4(0.28f, 0.56f, 1.00f, 1.00f);
|
||||
colors[ImGuiCol_SliderGrab] = ImVec4(0.28f, 0.56f, 1.00f, 1.00f);
|
||||
colors[ImGuiCol_SliderGrabActive] = ImVec4(0.37f, 0.61f, 1.00f, 1.00f);
|
||||
colors[ImGuiCol_Button] = ImVec4(0.20f, 0.25f, 0.29f, 1.00f);
|
||||
colors[ImGuiCol_ButtonHovered] = ImVec4(0.33f, 0.38f, 0.46f, 1.00f);
|
||||
colors[ImGuiCol_ButtonActive] = ImVec4(0.27f, 0.32f, 0.38f, 1.00f);
|
||||
colors[ImGuiCol_Header] = ImVec4(0.20f, 0.25f, 0.29f, 0.55f);
|
||||
colors[ImGuiCol_HeaderHovered] = ImVec4(0.33f, 0.38f, 0.46f, 1.00f);
|
||||
colors[ImGuiCol_HeaderActive] = ImVec4(0.27f, 0.32f, 0.38f, 1.00f);
|
||||
colors[ImGuiCol_Separator] = ImVec4(0.20f, 0.25f, 0.29f, 1.00f);
|
||||
colors[ImGuiCol_SeparatorHovered] = ImVec4(0.33f, 0.38f, 0.46f, 1.00f);
|
||||
colors[ImGuiCol_SeparatorActive] = ImVec4(0.27f, 0.32f, 0.38f, 1.00f);
|
||||
colors[ImGuiCol_ResizeGrip] = ImVec4(0.26f, 0.59f, 0.98f, 0.25f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.33f, 0.38f, 0.46f, 1.00f);
|
||||
colors[ImGuiCol_ResizeGripActive] = ImVec4(0.27f, 0.32f, 0.38f, 1.00f);
|
||||
colors[ImGuiCol_Tab] = ImVec4(0.11f, 0.15f, 0.17f, 1.00f);
|
||||
colors[ImGuiCol_TabHovered] = ImVec4(0.33f, 0.38f, 0.46f, 1.00f);
|
||||
colors[ImGuiCol_TabActive] = ImVec4(0.27f, 0.32f, 0.38f, 1.00f);
|
||||
colors[ImGuiCol_TabUnfocused] = ImVec4(0.11f, 0.15f, 0.17f, 1.00f);
|
||||
colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.11f, 0.15f, 0.17f, 1.00f);
|
||||
colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f);
|
||||
colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f);
|
||||
colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f);
|
||||
colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f);
|
||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f);
|
||||
colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f);
|
||||
colors[ImGuiCol_NavHighlight] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f);
|
||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f);
|
||||
|
||||
style.ScaleAllSizes(s_global_scale);
|
||||
}
|
||||
|
||||
void ImGuiManager::SetKeyMap()
|
||||
{
|
||||
struct KeyMapping
|
||||
{
|
||||
int index;
|
||||
const char* name;
|
||||
const char* alt_name;
|
||||
};
|
||||
|
||||
static constexpr KeyMapping mapping[] = {{ImGuiKey_LeftArrow, "Left"},
|
||||
{ImGuiKey_RightArrow, "Right"},
|
||||
{ImGuiKey_UpArrow, "Up"},
|
||||
{ImGuiKey_DownArrow, "Down"},
|
||||
{ImGuiKey_PageUp, "PageUp"},
|
||||
{ImGuiKey_PageDown, "PageDown"},
|
||||
{ImGuiKey_Home, "Home"},
|
||||
{ImGuiKey_End, "End"},
|
||||
{ImGuiKey_Insert, "Insert"},
|
||||
{ImGuiKey_Delete, "Delete"},
|
||||
{ImGuiKey_Backspace, "Backspace"},
|
||||
{ImGuiKey_Space, "Space"},
|
||||
{ImGuiKey_Enter, "Return"},
|
||||
{ImGuiKey_Escape, "Escape"},
|
||||
{ImGuiKey_LeftCtrl, "LeftCtrl", "Ctrl"},
|
||||
{ImGuiKey_LeftShift, "LeftShift", "Shift"},
|
||||
{ImGuiKey_LeftAlt, "LeftAlt", "Alt"},
|
||||
{ImGuiKey_LeftSuper, "LeftSuper", "Super"},
|
||||
{ImGuiKey_RightCtrl, "RightCtrl"},
|
||||
{ImGuiKey_RightShift, "RightShift"},
|
||||
{ImGuiKey_RightAlt, "RightAlt"},
|
||||
{ImGuiKey_RightSuper, "RightSuper"},
|
||||
{ImGuiKey_Menu, "Menu"},
|
||||
{ImGuiKey_0, "0"},
|
||||
{ImGuiKey_1, "1"},
|
||||
{ImGuiKey_2, "2"},
|
||||
{ImGuiKey_3, "3"},
|
||||
{ImGuiKey_4, "4"},
|
||||
{ImGuiKey_5, "5"},
|
||||
{ImGuiKey_6, "6"},
|
||||
{ImGuiKey_7, "7"},
|
||||
{ImGuiKey_8, "8"},
|
||||
{ImGuiKey_9, "9"},
|
||||
{ImGuiKey_A, "A"},
|
||||
{ImGuiKey_B, "B"},
|
||||
{ImGuiKey_C, "C"},
|
||||
{ImGuiKey_D, "D"},
|
||||
{ImGuiKey_E, "E"},
|
||||
{ImGuiKey_F, "F"},
|
||||
{ImGuiKey_G, "G"},
|
||||
{ImGuiKey_H, "H"},
|
||||
{ImGuiKey_I, "I"},
|
||||
{ImGuiKey_J, "J"},
|
||||
{ImGuiKey_K, "K"},
|
||||
{ImGuiKey_L, "L"},
|
||||
{ImGuiKey_M, "M"},
|
||||
{ImGuiKey_N, "N"},
|
||||
{ImGuiKey_O, "O"},
|
||||
{ImGuiKey_P, "P"},
|
||||
{ImGuiKey_Q, "Q"},
|
||||
{ImGuiKey_R, "R"},
|
||||
{ImGuiKey_S, "S"},
|
||||
{ImGuiKey_T, "T"},
|
||||
{ImGuiKey_U, "U"},
|
||||
{ImGuiKey_V, "V"},
|
||||
{ImGuiKey_W, "W"},
|
||||
{ImGuiKey_X, "X"},
|
||||
{ImGuiKey_Y, "Y"},
|
||||
{ImGuiKey_Z, "Z"},
|
||||
{ImGuiKey_F1, "F1"},
|
||||
{ImGuiKey_F2, "F2"},
|
||||
{ImGuiKey_F3, "F3"},
|
||||
{ImGuiKey_F4, "F4"},
|
||||
{ImGuiKey_F5, "F5"},
|
||||
{ImGuiKey_F6, "F6"},
|
||||
{ImGuiKey_F7, "F7"},
|
||||
{ImGuiKey_F8, "F8"},
|
||||
{ImGuiKey_F9, "F9"},
|
||||
{ImGuiKey_F10, "F10"},
|
||||
{ImGuiKey_F11, "F11"},
|
||||
{ImGuiKey_F12, "F12"},
|
||||
{ImGuiKey_Apostrophe, "Apostrophe"},
|
||||
{ImGuiKey_Comma, "Comma"},
|
||||
{ImGuiKey_Minus, "Minus"},
|
||||
{ImGuiKey_Period, "Period"},
|
||||
{ImGuiKey_Slash, "Slash"},
|
||||
{ImGuiKey_Semicolon, "Semicolon"},
|
||||
{ImGuiKey_Equal, "Equal"},
|
||||
{ImGuiKey_LeftBracket, "BracketLeft"},
|
||||
{ImGuiKey_Backslash, "Backslash"},
|
||||
{ImGuiKey_RightBracket, "BracketRight"},
|
||||
{ImGuiKey_GraveAccent, "QuoteLeft"},
|
||||
{ImGuiKey_CapsLock, "CapsLock"},
|
||||
{ImGuiKey_ScrollLock, "ScrollLock"},
|
||||
{ImGuiKey_NumLock, "NumLock"},
|
||||
{ImGuiKey_PrintScreen, "PrintScreen"},
|
||||
{ImGuiKey_Pause, "Pause"},
|
||||
{ImGuiKey_Keypad0, "Keypad0"},
|
||||
{ImGuiKey_Keypad1, "Keypad1"},
|
||||
{ImGuiKey_Keypad2, "Keypad2"},
|
||||
{ImGuiKey_Keypad3, "Keypad3"},
|
||||
{ImGuiKey_Keypad4, "Keypad4"},
|
||||
{ImGuiKey_Keypad5, "Keypad5"},
|
||||
{ImGuiKey_Keypad6, "Keypad6"},
|
||||
{ImGuiKey_Keypad7, "Keypad7"},
|
||||
{ImGuiKey_Keypad8, "Keypad8"},
|
||||
{ImGuiKey_Keypad9, "Keypad9"},
|
||||
{ImGuiKey_KeypadDecimal, "KeypadPeriod"},
|
||||
{ImGuiKey_KeypadDivide, "KeypadDivide"},
|
||||
{ImGuiKey_KeypadMultiply, "KeypadMultiply"},
|
||||
{ImGuiKey_KeypadSubtract, "KeypadMinus"},
|
||||
{ImGuiKey_KeypadAdd, "KeypadPlus"},
|
||||
{ImGuiKey_KeypadEnter, "KeypadReturn"},
|
||||
{ImGuiKey_KeypadEqual, "KeypadEqual"}};
|
||||
|
||||
s_imgui_key_map.clear();
|
||||
for (const KeyMapping& km : mapping)
|
||||
{
|
||||
std::optional<u32> map(InputManager::ConvertHostKeyboardStringToCode(km.name));
|
||||
if (!map.has_value() && km.alt_name)
|
||||
map = InputManager::ConvertHostKeyboardStringToCode(km.alt_name);
|
||||
if (map.has_value())
|
||||
s_imgui_key_map[map.value()] = km.index;
|
||||
}
|
||||
}
|
||||
|
||||
bool ImGuiManager::LoadFontData()
|
||||
{
|
||||
if (s_standard_font_data.empty())
|
||||
{
|
||||
std::optional<std::vector<u8>> font_data = s_font_path.empty() ?
|
||||
Host::ReadResourceFile("fonts/Roboto-Regular.ttf") :
|
||||
FileSystem::ReadBinaryFile(s_font_path.c_str());
|
||||
if (!font_data.has_value())
|
||||
return false;
|
||||
|
||||
s_standard_font_data = std::move(font_data.value());
|
||||
}
|
||||
|
||||
if (s_fixed_font_data.empty())
|
||||
{
|
||||
std::optional<std::vector<u8>> font_data = Host::ReadResourceFile("fonts/RobotoMono-Medium.ttf");
|
||||
if (!font_data.has_value())
|
||||
return false;
|
||||
|
||||
s_fixed_font_data = std::move(font_data.value());
|
||||
}
|
||||
|
||||
if (s_icon_font_data.empty())
|
||||
{
|
||||
std::optional<std::vector<u8>> font_data = Host::ReadResourceFile("fonts/fa-solid-900.ttf");
|
||||
if (!font_data.has_value())
|
||||
return false;
|
||||
|
||||
s_icon_font_data = std::move(font_data.value());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ImFont* ImGuiManager::AddTextFont(float size)
|
||||
{
|
||||
static const ImWchar default_ranges[] = {
|
||||
// Basic Latin + Latin Supplement + Central European diacritics
|
||||
0x0020,
|
||||
0x017F,
|
||||
|
||||
// Cyrillic + Cyrillic Supplement
|
||||
0x0400,
|
||||
0x052F,
|
||||
|
||||
// Cyrillic Extended-A
|
||||
0x2DE0,
|
||||
0x2DFF,
|
||||
|
||||
// Cyrillic Extended-B
|
||||
0xA640,
|
||||
0xA69F,
|
||||
|
||||
0,
|
||||
};
|
||||
|
||||
ImFontConfig cfg;
|
||||
cfg.FontDataOwnedByAtlas = false;
|
||||
return ImGui::GetIO().Fonts->AddFontFromMemoryTTF(s_standard_font_data.data(),
|
||||
static_cast<int>(s_standard_font_data.size()), size, &cfg,
|
||||
s_font_range ? s_font_range : default_ranges);
|
||||
}
|
||||
|
||||
ImFont* ImGuiManager::AddFixedFont(float size)
|
||||
{
|
||||
ImFontConfig cfg;
|
||||
cfg.FontDataOwnedByAtlas = false;
|
||||
return ImGui::GetIO().Fonts->AddFontFromMemoryTTF(s_fixed_font_data.data(),
|
||||
static_cast<int>(s_fixed_font_data.size()), size, &cfg, nullptr);
|
||||
}
|
||||
|
||||
bool ImGuiManager::AddIconFonts(float size)
|
||||
{
|
||||
static const ImWchar range_fa[] = {ICON_MIN_FA, ICON_MAX_FA, 0};
|
||||
|
||||
ImFontConfig cfg;
|
||||
cfg.MergeMode = true;
|
||||
cfg.PixelSnapH = true;
|
||||
cfg.GlyphMinAdvanceX = size * 0.75f;
|
||||
cfg.GlyphMaxAdvanceX = size * 0.75f;
|
||||
cfg.FontDataOwnedByAtlas = false;
|
||||
|
||||
return (ImGui::GetIO().Fonts->AddFontFromMemoryTTF(s_icon_font_data.data(), static_cast<int>(s_icon_font_data.size()),
|
||||
size * 0.75f, &cfg, range_fa) != nullptr);
|
||||
}
|
||||
|
||||
bool ImGuiManager::AddImGuiFonts(bool fullscreen_fonts)
|
||||
{
|
||||
const float standard_font_size = std::ceil(15.0f * s_global_scale);
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.Fonts->Clear();
|
||||
|
||||
s_standard_font = AddTextFont(standard_font_size);
|
||||
if (!s_standard_font || !AddIconFonts(standard_font_size))
|
||||
return false;
|
||||
|
||||
s_fixed_font = AddFixedFont(standard_font_size);
|
||||
if (!s_fixed_font)
|
||||
return false;
|
||||
|
||||
if (fullscreen_fonts)
|
||||
{
|
||||
const float medium_font_size = std::ceil(ImGuiFullscreen::LayoutScale(ImGuiFullscreen::LAYOUT_MEDIUM_FONT_SIZE));
|
||||
s_medium_font = AddTextFont(medium_font_size);
|
||||
if (!s_medium_font || !AddIconFonts(medium_font_size))
|
||||
return false;
|
||||
|
||||
const float large_font_size = std::ceil(ImGuiFullscreen::LayoutScale(ImGuiFullscreen::LAYOUT_LARGE_FONT_SIZE));
|
||||
s_large_font = AddTextFont(large_font_size);
|
||||
if (!s_large_font || !AddIconFonts(large_font_size))
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
s_medium_font = nullptr;
|
||||
s_large_font = nullptr;
|
||||
}
|
||||
|
||||
ImGuiFullscreen::SetFonts(s_standard_font, s_medium_font, s_large_font);
|
||||
|
||||
return io.Fonts->Build();
|
||||
}
|
||||
|
||||
bool ImGuiManager::AddFullscreenFontsIfMissing()
|
||||
{
|
||||
if (HasFullscreenFonts())
|
||||
return true;
|
||||
|
||||
// can't do this in the middle of a frame
|
||||
ImGui::EndFrame();
|
||||
|
||||
if (!AddImGuiFonts(true))
|
||||
{
|
||||
Log_ErrorPrint("Failed to lazily allocate fullscreen fonts.");
|
||||
AddImGuiFonts(false);
|
||||
}
|
||||
|
||||
g_host_display->UpdateImGuiFontTexture();
|
||||
NewFrame();
|
||||
|
||||
return HasFullscreenFonts();
|
||||
}
|
||||
|
||||
bool ImGuiManager::HasFullscreenFonts()
|
||||
{
|
||||
return (s_medium_font && s_large_font);
|
||||
}
|
||||
|
||||
struct OSDMessage
|
||||
{
|
||||
std::string key;
|
||||
std::string text;
|
||||
std::chrono::steady_clock::time_point time;
|
||||
float duration;
|
||||
};
|
||||
|
||||
static std::deque<OSDMessage> s_osd_active_messages;
|
||||
static std::deque<OSDMessage> s_osd_posted_messages;
|
||||
static std::mutex s_osd_messages_lock;
|
||||
|
||||
void Host::AddOSDMessage(std::string message, float duration /*= 2.0f*/)
|
||||
{
|
||||
AddKeyedOSDMessage(std::string(), std::move(message), duration);
|
||||
}
|
||||
|
||||
void Host::AddKeyedOSDMessage(std::string key, std::string message, float duration /* = 2.0f */)
|
||||
{
|
||||
OSDMessage msg;
|
||||
msg.key = std::move(key);
|
||||
msg.text = std::move(message);
|
||||
msg.duration = duration;
|
||||
msg.time = std::chrono::steady_clock::now();
|
||||
|
||||
std::unique_lock<std::mutex> lock(s_osd_messages_lock);
|
||||
s_osd_posted_messages.push_back(std::move(msg));
|
||||
}
|
||||
|
||||
void Host::AddFormattedOSDMessage(float duration, const char* format, ...)
|
||||
{
|
||||
std::va_list ap;
|
||||
va_start(ap, format);
|
||||
std::string ret = StringUtil::StdStringFromFormatV(format, ap);
|
||||
va_end(ap);
|
||||
return AddKeyedOSDMessage(std::string(), std::move(ret), duration);
|
||||
}
|
||||
|
||||
void Host::AddKeyedFormattedOSDMessage(std::string key, float duration, const char* format, ...)
|
||||
{
|
||||
std::va_list ap;
|
||||
va_start(ap, format);
|
||||
std::string ret = StringUtil::StdStringFromFormatV(format, ap);
|
||||
va_end(ap);
|
||||
return AddKeyedOSDMessage(std::move(key), std::move(ret), duration);
|
||||
}
|
||||
|
||||
void Host::RemoveKeyedOSDMessage(std::string key)
|
||||
{
|
||||
OSDMessage msg;
|
||||
msg.key = std::move(key);
|
||||
msg.duration = 0.0f;
|
||||
msg.time = std::chrono::steady_clock::now();
|
||||
|
||||
std::unique_lock<std::mutex> lock(s_osd_messages_lock);
|
||||
s_osd_posted_messages.push_back(std::move(msg));
|
||||
}
|
||||
|
||||
void Host::ClearOSDMessages()
|
||||
{
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(s_osd_messages_lock);
|
||||
s_osd_posted_messages.clear();
|
||||
}
|
||||
|
||||
s_osd_active_messages.clear();
|
||||
}
|
||||
|
||||
void ImGuiManager::AcquirePendingOSDMessages()
|
||||
{
|
||||
std::atomic_thread_fence(std::memory_order_consume);
|
||||
if (s_osd_posted_messages.empty())
|
||||
return;
|
||||
|
||||
std::unique_lock lock(s_osd_messages_lock);
|
||||
for (;;)
|
||||
{
|
||||
if (s_osd_posted_messages.empty())
|
||||
break;
|
||||
|
||||
if (g_settings.display_show_osd_messages)
|
||||
{
|
||||
OSDMessage& new_msg = s_osd_posted_messages.front();
|
||||
std::deque<OSDMessage>::iterator iter;
|
||||
if (!new_msg.key.empty() && (iter = std::find_if(s_osd_active_messages.begin(), s_osd_active_messages.end(),
|
||||
[&new_msg](const OSDMessage& other) {
|
||||
return new_msg.key == other.key;
|
||||
})) != s_osd_active_messages.end())
|
||||
{
|
||||
iter->text = std::move(new_msg.text);
|
||||
iter->duration = new_msg.duration;
|
||||
iter->time = new_msg.time;
|
||||
}
|
||||
else
|
||||
{
|
||||
s_osd_active_messages.push_back(std::move(new_msg));
|
||||
}
|
||||
}
|
||||
|
||||
s_osd_posted_messages.pop_front();
|
||||
|
||||
static constexpr size_t MAX_ACTIVE_OSD_MESSAGES = 512;
|
||||
if (s_osd_active_messages.size() > MAX_ACTIVE_OSD_MESSAGES)
|
||||
s_osd_active_messages.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiManager::DrawOSDMessages()
|
||||
{
|
||||
ImFont* const font = ImGui::GetFont();
|
||||
const float scale = s_global_scale;
|
||||
const float spacing = std::ceil(5.0f * scale);
|
||||
const float margin = std::ceil(10.0f * scale);
|
||||
const float padding = std::ceil(8.0f * scale);
|
||||
const float rounding = std::ceil(5.0f * scale);
|
||||
const float max_width = ImGui::GetIO().DisplaySize.x - (margin + padding) * 2.0f;
|
||||
float position_x = margin;
|
||||
float position_y = margin;
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
|
||||
auto iter = s_osd_active_messages.begin();
|
||||
while (iter != s_osd_active_messages.end())
|
||||
{
|
||||
const OSDMessage& msg = *iter;
|
||||
const double time = std::chrono::duration<double>(now - msg.time).count();
|
||||
const float time_remaining = static_cast<float>(msg.duration - time);
|
||||
if (time_remaining <= 0.0f)
|
||||
{
|
||||
iter = s_osd_active_messages.erase(iter);
|
||||
continue;
|
||||
}
|
||||
|
||||
++iter;
|
||||
|
||||
const float opacity = std::min(time_remaining, 1.0f);
|
||||
const u32 alpha = static_cast<u32>(opacity * 255.0f);
|
||||
|
||||
if (position_y >= ImGui::GetIO().DisplaySize.y)
|
||||
break;
|
||||
|
||||
const ImVec2 pos(position_x, position_y);
|
||||
const ImVec2 text_size(font->CalcTextSizeA(font->FontSize, max_width, max_width, msg.text.c_str(),
|
||||
msg.text.c_str() + msg.text.length()));
|
||||
const ImVec2 size(text_size.x + padding * 2.0f, text_size.y + padding * 2.0f);
|
||||
const ImVec4 text_rect(pos.x + padding, pos.y + padding, pos.x + size.x - padding, pos.y + size.y - padding);
|
||||
|
||||
ImDrawList* dl = ImGui::GetBackgroundDrawList();
|
||||
dl->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y), IM_COL32(0x21, 0x21, 0x21, alpha), rounding);
|
||||
dl->AddRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), IM_COL32(0x48, 0x48, 0x48, alpha), rounding);
|
||||
dl->AddText(font, font->FontSize, ImVec2(text_rect.x, text_rect.y), IM_COL32(0xff, 0xff, 0xff, alpha),
|
||||
msg.text.c_str(), msg.text.c_str() + msg.text.length(), max_width, &text_rect);
|
||||
position_y += size.y + spacing;
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiManager::RenderOSD()
|
||||
{
|
||||
AcquirePendingOSDMessages();
|
||||
DrawOSDMessages();
|
||||
}
|
||||
|
||||
float ImGuiManager::GetGlobalScale()
|
||||
{
|
||||
return s_global_scale;
|
||||
}
|
||||
|
||||
float Host::GetOSDScale()
|
||||
{
|
||||
return s_global_scale;
|
||||
}
|
||||
|
||||
ImFont* ImGuiManager::GetStandardFont()
|
||||
{
|
||||
return s_standard_font;
|
||||
}
|
||||
|
||||
ImFont* ImGuiManager::GetFixedFont()
|
||||
{
|
||||
return s_fixed_font;
|
||||
}
|
||||
|
||||
ImFont* ImGuiManager::GetMediumFont()
|
||||
{
|
||||
AddFullscreenFontsIfMissing();
|
||||
return s_medium_font;
|
||||
}
|
||||
|
||||
ImFont* ImGuiManager::GetLargeFont()
|
||||
{
|
||||
AddFullscreenFontsIfMissing();
|
||||
return s_large_font;
|
||||
}
|
||||
|
||||
void ImGuiManager::UpdateMousePosition(float x, float y)
|
||||
{
|
||||
if (!ImGui::GetCurrentContext())
|
||||
return;
|
||||
|
||||
ImGui::GetIO().MousePos = ImVec2(x, y);
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
}
|
||||
|
||||
bool ImGuiManager::ProcessPointerButtonEvent(InputBindingKey key, float value)
|
||||
{
|
||||
if (!ImGui::GetCurrentContext() || (key.data - 1u) >= std::size(ImGui::GetIO().MouseDown))
|
||||
return false;
|
||||
|
||||
// still update state anyway
|
||||
ImGui::GetIO().AddMouseButtonEvent(key.data - 1, value != 0.0f);
|
||||
|
||||
return s_imgui_wants_mouse.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
bool ImGuiManager::ProcessPointerAxisEvent(InputBindingKey key, float value)
|
||||
{
|
||||
if (!ImGui::GetCurrentContext() || value == 0.0f || key.data < static_cast<u32>(InputPointerAxis::WheelX))
|
||||
return false;
|
||||
|
||||
// still update state anyway
|
||||
const bool horizontal = (key.data == static_cast<u32>(InputPointerAxis::WheelX));
|
||||
ImGui::GetIO().AddMouseWheelEvent(horizontal ? value : 0.0f, horizontal ? 0.0f : value);
|
||||
|
||||
return s_imgui_wants_mouse.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
bool ImGuiManager::ProcessHostKeyEvent(InputBindingKey key, float value)
|
||||
{
|
||||
decltype(s_imgui_key_map)::iterator iter;
|
||||
if (!ImGui::GetCurrentContext() || (iter = s_imgui_key_map.find(key.data)) == s_imgui_key_map.end())
|
||||
return false;
|
||||
|
||||
// still update state anyway
|
||||
ImGui::GetIO().AddKeyEvent(iter->second, value != 0.0);
|
||||
|
||||
return s_imgui_wants_keyboard.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
bool ImGuiManager::ProcessGenericInputEvent(GenericInputBinding key, float value)
|
||||
{
|
||||
static constexpr ImGuiKey key_map[] = {
|
||||
ImGuiKey_None, // Unknown,
|
||||
ImGuiKey_GamepadDpadUp, // DPadUp
|
||||
ImGuiKey_GamepadDpadRight, // DPadRight
|
||||
ImGuiKey_GamepadDpadLeft, // DPadLeft
|
||||
ImGuiKey_GamepadDpadDown, // DPadDown
|
||||
ImGuiKey_None, // LeftStickUp
|
||||
ImGuiKey_None, // LeftStickRight
|
||||
ImGuiKey_None, // LeftStickDown
|
||||
ImGuiKey_None, // LeftStickLeft
|
||||
ImGuiKey_GamepadL3, // L3
|
||||
ImGuiKey_None, // RightStickUp
|
||||
ImGuiKey_None, // RightStickRight
|
||||
ImGuiKey_None, // RightStickDown
|
||||
ImGuiKey_None, // RightStickLeft
|
||||
ImGuiKey_GamepadR3, // R3
|
||||
ImGuiKey_GamepadFaceUp, // Triangle
|
||||
ImGuiKey_GamepadFaceRight, // Circle
|
||||
ImGuiKey_GamepadFaceDown, // Cross
|
||||
ImGuiKey_GamepadFaceLeft, // Square
|
||||
ImGuiKey_GamepadBack, // Select
|
||||
ImGuiKey_GamepadStart, // Start
|
||||
ImGuiKey_None, // System
|
||||
ImGuiKey_GamepadL1, // L1
|
||||
ImGuiKey_GamepadL2, // L2
|
||||
ImGuiKey_GamepadR1, // R1
|
||||
ImGuiKey_GamepadL2, // R2
|
||||
};
|
||||
|
||||
if (!ImGui::GetCurrentContext() || !s_imgui_wants_keyboard.load(std::memory_order_acquire))
|
||||
return false;
|
||||
|
||||
if (static_cast<u32>(key) >= std::size(key_map) || key_map[static_cast<u32>(key)] == ImGuiKey_None)
|
||||
return false;
|
||||
|
||||
ImGui::GetIO().AddKeyAnalogEvent(key_map[static_cast<u32>(key)], (value > 0.0f), value);
|
||||
return true;
|
||||
}
|
91
src/frontend-common/imgui_manager.h
Normal file
91
src/frontend-common/imgui_manager.h
Normal file
@ -0,0 +1,91 @@
|
||||
/* PCSX2 - PS2 Emulator for PCs
|
||||
* Copyright (C) 2002-2021 PCSX2 Dev Team
|
||||
*
|
||||
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
|
||||
* of the GNU Lesser General Public License as published by the Free Software Found-
|
||||
* ation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
* PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with PCSX2.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/types.h"
|
||||
#include <string>
|
||||
|
||||
struct ImFont;
|
||||
|
||||
union InputBindingKey;
|
||||
enum class GenericInputBinding : u8;
|
||||
|
||||
namespace ImGuiManager {
|
||||
/// Sets the path to the font to use. Empty string means to use the default.
|
||||
void SetFontPath(std::string path);
|
||||
|
||||
/// Sets the glyph range to use when loading fonts.
|
||||
void SetFontRange(const u16* range);
|
||||
|
||||
/// Initializes ImGui, creates fonts, etc.
|
||||
bool Initialize();
|
||||
|
||||
/// Frees all ImGui resources.
|
||||
void Shutdown();
|
||||
|
||||
/// Updates internal state when the window is size.
|
||||
void WindowResized();
|
||||
|
||||
/// Updates scaling of the on-screen elements.
|
||||
void UpdateScale();
|
||||
|
||||
/// Call at the beginning of the frame to set up ImGui state.
|
||||
void NewFrame();
|
||||
|
||||
/// Renders any on-screen display elements.
|
||||
void RenderOSD();
|
||||
|
||||
/// Returns the scale of all on-screen elements.
|
||||
float GetGlobalScale();
|
||||
|
||||
/// Returns true if fullscreen fonts are present.
|
||||
bool HasFullscreenFonts();
|
||||
|
||||
/// Allocates/adds fullscreen fonts if they're not loaded.
|
||||
bool AddFullscreenFontsIfMissing();
|
||||
|
||||
/// Returns the standard font for external drawing.
|
||||
ImFont* GetStandardFont();
|
||||
|
||||
/// Returns the fixed-width font for external drawing.
|
||||
ImFont* GetFixedFont();
|
||||
|
||||
/// Returns the medium font for external drawing, scaled by ImGuiFullscreen.
|
||||
/// This font is allocated on demand.
|
||||
ImFont* GetMediumFont();
|
||||
|
||||
/// Returns the large font for external drawing, scaled by ImGuiFullscreen.
|
||||
/// This font is allocated on demand.
|
||||
ImFont* GetLargeFont();
|
||||
|
||||
/// Called on the UI or CPU thread in response to mouse movement.
|
||||
void UpdateMousePosition(float x, float y);
|
||||
|
||||
/// Called on the CPU thread in response to a mouse button press.
|
||||
/// Returns true if ImGui intercepted the event, and regular handlers should not execute.
|
||||
bool ProcessPointerButtonEvent(InputBindingKey key, float value);
|
||||
|
||||
/// Called on the CPU thread in response to a mouse wheel movement.
|
||||
/// Returns true if ImGui intercepted the event, and regular handlers should not execute.
|
||||
bool ProcessPointerAxisEvent(InputBindingKey key, float value);
|
||||
|
||||
/// Called on the CPU thread in response to a key press.
|
||||
/// Returns true if ImGui intercepted the event, and regular handlers should not execute.
|
||||
bool ProcessHostKeyEvent(InputBindingKey key, float value);
|
||||
|
||||
/// Called on the CPU thread when any input event fires. Allows imgui to take over controller navigation.
|
||||
bool ProcessGenericInputEvent(GenericInputBinding key, float value);
|
||||
} // namespace ImGuiManager
|
650
src/frontend-common/imgui_overlays.cpp
Normal file
650
src/frontend-common/imgui_overlays.cpp
Normal file
@ -0,0 +1,650 @@
|
||||
#include "imgui_overlays.h"
|
||||
#include "IconsFontAwesome5.h"
|
||||
#include "achievements.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/file_system.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "common/timer.h"
|
||||
#include "common_host.h"
|
||||
#include "core/controller.h"
|
||||
#include "core/gpu.h"
|
||||
#include "core/host.h"
|
||||
#include "core/host_display.h"
|
||||
#include "core/host_settings.h"
|
||||
#include "core/settings.h"
|
||||
#include "core/system.h"
|
||||
#include "fmt/chrono.h"
|
||||
#include "fmt/format.h"
|
||||
#include "fullscreen_ui.h"
|
||||
#include "icon.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_fullscreen.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "imgui_manager.h"
|
||||
#include "input_manager.h"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
Log_SetChannel(ImGuiManager);
|
||||
|
||||
namespace ImGuiManager {
|
||||
static void FormatProcessorStat(String& text, double usage, double time);
|
||||
static void DrawPerformanceOverlay();
|
||||
static void DrawEnhancementsOverlay();
|
||||
static void DrawInputsOverlay();
|
||||
} // namespace ImGuiManager
|
||||
|
||||
namespace SaveStateSelectorUI {
|
||||
static void Draw();
|
||||
}
|
||||
|
||||
static bool s_save_state_selector_ui_open = false;
|
||||
|
||||
void ImGuiManager::RenderOverlays()
|
||||
{
|
||||
if (System::IsValid())
|
||||
{
|
||||
DrawPerformanceOverlay();
|
||||
|
||||
if (g_settings.display_show_enhancements)
|
||||
DrawEnhancementsOverlay();
|
||||
|
||||
if (g_settings.display_show_inputs)
|
||||
DrawInputsOverlay();
|
||||
|
||||
if (s_save_state_selector_ui_open)
|
||||
SaveStateSelectorUI::Draw();
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiManager::FormatProcessorStat(String& text, double usage, double time)
|
||||
{
|
||||
// Some values, such as GPU (and even CPU to some extent) can be out of phase with the wall clock,
|
||||
// which the processor time is divided by to get a utilization percentage. Let's clamp it at 100%,
|
||||
// so that people don't get confused, and remove the decimal places when it's there while we're at it.
|
||||
if (usage >= 99.95)
|
||||
text.AppendFmtString("100% ({:.2f}ms)", time);
|
||||
else
|
||||
text.AppendFmtString("{:.1f}% ({:.2f}ms)", usage, time);
|
||||
}
|
||||
|
||||
void ImGuiManager::DrawPerformanceOverlay()
|
||||
{
|
||||
if (!(g_settings.display_show_fps || g_settings.display_show_speed || g_settings.display_show_resolution ||
|
||||
g_settings.display_show_cpu ||
|
||||
(g_settings.display_show_status_indicators &&
|
||||
(System::IsPaused() || System::IsFastForwardEnabled() || System::IsTurboEnabled()))))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const float scale = ImGuiManager::GetGlobalScale();
|
||||
const float shadow_offset = std::ceil(1.0f * scale);
|
||||
const float margin = std::ceil(10.0f * scale);
|
||||
const float spacing = std::ceil(5.0f * scale);
|
||||
ImFont* fixed_font = ImGuiManager::GetFixedFont();
|
||||
ImFont* standard_font = ImGuiManager::GetStandardFont();
|
||||
float position_y = margin;
|
||||
|
||||
ImDrawList* dl = ImGui::GetBackgroundDrawList();
|
||||
SmallString text;
|
||||
ImVec2 text_size;
|
||||
bool first = true;
|
||||
|
||||
#define DRAW_LINE(font, text, color) \
|
||||
do \
|
||||
{ \
|
||||
text_size = \
|
||||
font->CalcTextSizeA(font->FontSize, std::numeric_limits<float>::max(), -1.0f, (text), nullptr, nullptr); \
|
||||
dl->AddText( \
|
||||
font, font->FontSize, \
|
||||
ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x + shadow_offset, position_y + shadow_offset), \
|
||||
IM_COL32(0, 0, 0, 100), text, text.GetCharArray() + text.GetLength()); \
|
||||
dl->AddText(font, font->FontSize, ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x, position_y), color, \
|
||||
(text)); \
|
||||
position_y += text_size.y + spacing; \
|
||||
} while (0)
|
||||
|
||||
const System::State state = System::GetState();
|
||||
if (state == System::State::Running)
|
||||
{
|
||||
const float speed = System::GetEmulationSpeed();
|
||||
if (g_settings.display_show_fps)
|
||||
{
|
||||
text.AppendFmtString("G: {:.2f} | V: {:.2f}", System::GetFPS(), System::GetVPS());
|
||||
first = false;
|
||||
}
|
||||
if (g_settings.display_show_speed)
|
||||
{
|
||||
text.AppendFmtString("{}{}%", first ? "" : " | ", static_cast<u32>(std::round(speed)));
|
||||
|
||||
const float target_speed = System::GetTargetSpeed();
|
||||
if (target_speed <= 0.0f)
|
||||
text.AppendString(" (Max)");
|
||||
else
|
||||
text.AppendFmtString(" ({:.0f}%)", target_speed * 100.0f);
|
||||
|
||||
first = false;
|
||||
}
|
||||
if (!text.IsEmpty())
|
||||
{
|
||||
ImU32 color;
|
||||
if (speed < 95.0f)
|
||||
color = IM_COL32(255, 100, 100, 255);
|
||||
else if (speed > 105.0f)
|
||||
color = IM_COL32(100, 255, 100, 255);
|
||||
else
|
||||
color = IM_COL32(255, 255, 255, 255);
|
||||
|
||||
DRAW_LINE(fixed_font, text, color);
|
||||
}
|
||||
|
||||
if (g_settings.display_show_resolution)
|
||||
{
|
||||
const auto [effective_width, effective_height] = g_gpu->GetEffectiveDisplayResolution();
|
||||
const bool interlaced = g_gpu->IsInterlacedDisplayEnabled();
|
||||
text.Fmt("{}x{} ({})", effective_width, effective_height, interlaced ? "interlaced" : "progressive");
|
||||
DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
|
||||
}
|
||||
|
||||
if (g_settings.display_show_cpu)
|
||||
{
|
||||
text.Clear();
|
||||
text.AppendFmtString("{:.2f}ms ({:.2f}ms worst)", System::GetAverageFrameTime(), System::GetWorstFrameTime());
|
||||
DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
|
||||
|
||||
text.Clear();
|
||||
if (g_settings.cpu_overclock_active)
|
||||
text.Fmt("CPU[{}]: ", g_settings.GetCPUOverclockPercent());
|
||||
else
|
||||
text.Assign("CPU: ");
|
||||
FormatProcessorStat(text, System::GetCPUThreadUsage(), System::GetCPUThreadAverageTime());
|
||||
DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
|
||||
|
||||
if (!g_gpu->IsHardwareRenderer() && g_settings.gpu_use_thread)
|
||||
{
|
||||
text.Assign("SW: ");
|
||||
FormatProcessorStat(text, System::GetSWThreadUsage(), System::GetSWThreadAverageTime());
|
||||
DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
|
||||
}
|
||||
}
|
||||
|
||||
if (g_settings.display_show_status_indicators)
|
||||
{
|
||||
const bool rewinding = System::IsRewinding();
|
||||
if (rewinding || System::IsFastForwardEnabled() || System::IsTurboEnabled())
|
||||
{
|
||||
text.Assign(rewinding ? ICON_FA_FAST_BACKWARD : ICON_FA_FAST_FORWARD);
|
||||
DRAW_LINE(standard_font, text, IM_COL32(255, 255, 255, 255));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (g_settings.display_show_status_indicators && state == System::State::Paused)
|
||||
{
|
||||
text.Assign(ICON_FA_PAUSE);
|
||||
DRAW_LINE(standard_font, text, IM_COL32(255, 255, 255, 255));
|
||||
}
|
||||
|
||||
#undef DRAW_LINE
|
||||
}
|
||||
|
||||
void ImGuiManager::DrawEnhancementsOverlay()
|
||||
{
|
||||
LargeString text;
|
||||
text.AppendString(Settings::GetConsoleRegionName(System::GetRegion()));
|
||||
|
||||
if (g_settings.rewind_enable)
|
||||
text.AppendFormattedString(" RW=%g/%u", g_settings.rewind_save_frequency, g_settings.rewind_save_slots);
|
||||
if (g_settings.IsRunaheadEnabled())
|
||||
text.AppendFormattedString(" RA=%u", g_settings.runahead_frames);
|
||||
|
||||
if (g_settings.cpu_overclock_active)
|
||||
text.AppendFormattedString(" CPU=%u%%", g_settings.GetCPUOverclockPercent());
|
||||
if (g_settings.enable_8mb_ram)
|
||||
text.AppendString(" 8MB");
|
||||
if (g_settings.cdrom_read_speedup != 1)
|
||||
text.AppendFormattedString(" CDR=%ux", g_settings.cdrom_read_speedup);
|
||||
if (g_settings.cdrom_seek_speedup != 1)
|
||||
text.AppendFormattedString(" CDS=%ux", g_settings.cdrom_seek_speedup);
|
||||
if (g_settings.gpu_resolution_scale != 1)
|
||||
text.AppendFormattedString(" IR=%ux", g_settings.gpu_resolution_scale);
|
||||
if (g_settings.gpu_multisamples != 1)
|
||||
{
|
||||
text.AppendFormattedString(" %ux%s", g_settings.gpu_multisamples,
|
||||
g_settings.gpu_per_sample_shading ? "MSAA" : "SSAA");
|
||||
}
|
||||
if (g_settings.gpu_true_color)
|
||||
text.AppendString(" TrueCol");
|
||||
if (g_settings.gpu_disable_interlacing)
|
||||
text.AppendString(" ForceProg");
|
||||
if (g_settings.gpu_force_ntsc_timings && System::GetRegion() == ConsoleRegion::PAL)
|
||||
text.AppendString(" PAL60");
|
||||
if (g_settings.gpu_texture_filter != GPUTextureFilter::Nearest)
|
||||
text.AppendFormattedString(" %s", Settings::GetTextureFilterName(g_settings.gpu_texture_filter));
|
||||
if (g_settings.gpu_widescreen_hack && g_settings.display_aspect_ratio != DisplayAspectRatio::Auto &&
|
||||
g_settings.display_aspect_ratio != DisplayAspectRatio::R4_3)
|
||||
{
|
||||
text.AppendString(" WSHack");
|
||||
}
|
||||
if (g_settings.gpu_pgxp_enable)
|
||||
{
|
||||
text.AppendString(" PGXP");
|
||||
if (g_settings.gpu_pgxp_culling)
|
||||
text.AppendString("/Cull");
|
||||
if (g_settings.gpu_pgxp_texture_correction)
|
||||
text.AppendString("/Tex");
|
||||
if (g_settings.gpu_pgxp_vertex_cache)
|
||||
text.AppendString("/VC");
|
||||
if (g_settings.gpu_pgxp_cpu)
|
||||
text.AppendString("/CPU");
|
||||
if (g_settings.gpu_pgxp_depth_buffer)
|
||||
text.AppendString("/Depth");
|
||||
}
|
||||
|
||||
const float scale = ImGuiManager::GetGlobalScale();
|
||||
const float shadow_offset = 1.0f * scale;
|
||||
const float margin = 10.0f * scale;
|
||||
ImFont* font = ImGuiManager::GetFixedFont();
|
||||
const float position_y = ImGui::GetIO().DisplaySize.y - margin - font->FontSize;
|
||||
|
||||
ImDrawList* dl = ImGui::GetBackgroundDrawList();
|
||||
ImVec2 text_size = font->CalcTextSizeA(font->FontSize, std::numeric_limits<float>::max(), -1.0f, text,
|
||||
text.GetCharArray() + text.GetLength(), nullptr);
|
||||
dl->AddText(font, font->FontSize,
|
||||
ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x + shadow_offset, position_y + shadow_offset),
|
||||
IM_COL32(0, 0, 0, 100), text, text.GetCharArray() + text.GetLength());
|
||||
dl->AddText(font, font->FontSize, ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x, position_y),
|
||||
IM_COL32(255, 255, 255, 255), text, text.GetCharArray() + text.GetLength());
|
||||
}
|
||||
|
||||
void ImGuiManager::DrawInputsOverlay()
|
||||
{
|
||||
const float scale = ImGuiManager::GetGlobalScale();
|
||||
const float shadow_offset = 1.0f * scale;
|
||||
const float margin = 10.0f * scale;
|
||||
const float spacing = 5.0f * scale;
|
||||
ImFont* font = ImGuiManager::GetFixedFont();
|
||||
|
||||
static constexpr u32 text_color = IM_COL32(0xff, 0xff, 0xff, 255);
|
||||
static constexpr u32 shadow_color = IM_COL32(0x00, 0x00, 0x00, 100);
|
||||
|
||||
const ImVec2& display_size = ImGui::GetIO().DisplaySize;
|
||||
ImDrawList* dl = ImGui::GetBackgroundDrawList();
|
||||
|
||||
u32 num_ports = 0;
|
||||
for (u32 port = 0; port < NUM_CONTROLLER_AND_CARD_PORTS; port++)
|
||||
{
|
||||
if (g_settings.controller_types[port] != ControllerType::None)
|
||||
num_ports++;
|
||||
}
|
||||
|
||||
float current_x = margin;
|
||||
float current_y = display_size.y - margin - ((static_cast<float>(num_ports) * (font->FontSize + spacing)) - spacing);
|
||||
|
||||
const ImVec4 clip_rect(current_x, current_y, display_size.x - margin, display_size.y - margin);
|
||||
|
||||
LargeString text;
|
||||
|
||||
for (u32 port = 0; port < NUM_CONTROLLER_AND_CARD_PORTS; port++)
|
||||
{
|
||||
if (g_settings.controller_types[port] == ControllerType::None)
|
||||
continue;
|
||||
|
||||
const Controller* controller = System::GetController(port);
|
||||
const Controller::ControllerInfo* cinfo =
|
||||
controller ? Controller::GetControllerInfo(controller->GetType()) : nullptr;
|
||||
if (!cinfo)
|
||||
continue;
|
||||
|
||||
text.Fmt("P{} |", port + 1u);
|
||||
|
||||
for (u32 bind = 0; bind < cinfo->num_bindings; bind++)
|
||||
{
|
||||
const Controller::ControllerBindingInfo& bi = cinfo->bindings[bind];
|
||||
switch (bi.type)
|
||||
{
|
||||
case Controller::ControllerBindingType::Axis:
|
||||
case Controller::ControllerBindingType::HalfAxis:
|
||||
{
|
||||
// axes are always shown
|
||||
const float value = controller->GetBindState(bi.bind_index);
|
||||
if (value >= (254.0f / 255.0f))
|
||||
text.AppendFmtString(" {}", bi.name);
|
||||
else if (value > (1.0f / 255.0f))
|
||||
text.AppendFmtString(" {}: {:.2f}", bi.name, value);
|
||||
}
|
||||
break;
|
||||
|
||||
case Controller::ControllerBindingType::Button:
|
||||
{
|
||||
// buttons only shown when active
|
||||
const float value = controller->GetBindState(bi.bind_index);
|
||||
if (value >= 0.5f)
|
||||
text.AppendFmtString(" {}", bi.name);
|
||||
}
|
||||
break;
|
||||
|
||||
case Controller::ControllerBindingType::Motor:
|
||||
case Controller::ControllerBindingType::Macro:
|
||||
case Controller::ControllerBindingType::Unknown:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dl->AddText(font, font->FontSize, ImVec2(current_x + shadow_offset, current_y + shadow_offset), shadow_color,
|
||||
text.GetCharArray(), text.GetCharArray() + text.GetLength(), 0.0f, &clip_rect);
|
||||
dl->AddText(font, font->FontSize, ImVec2(current_x, current_y), text_color, text.GetCharArray(),
|
||||
text.GetCharArray() + text.GetLength(), 0.0f, &clip_rect);
|
||||
|
||||
current_y += font->FontSize + spacing;
|
||||
}
|
||||
}
|
||||
|
||||
namespace SaveStateSelectorUI {
|
||||
struct ListEntry
|
||||
{
|
||||
std::string path;
|
||||
std::string game_code;
|
||||
std::string title;
|
||||
std::string formatted_timestamp;
|
||||
std::unique_ptr<HostDisplayTexture> preview_texture;
|
||||
s32 slot;
|
||||
bool global;
|
||||
};
|
||||
|
||||
static void InitializePlaceholderListEntry(ListEntry* li, std::string path, s32 slot, bool global);
|
||||
static void InitializeListEntry(ListEntry* li, ExtendedSaveStateInfo* ssi, std::string path, s32 slot, bool global);
|
||||
|
||||
static void RefreshList();
|
||||
static void RefreshHotkeyLegend();
|
||||
|
||||
std::string m_load_legend;
|
||||
std::string m_save_legend;
|
||||
std::string m_prev_legend;
|
||||
std::string m_next_legend;
|
||||
|
||||
std::vector<ListEntry> m_slots;
|
||||
u32 m_current_selection = 0;
|
||||
|
||||
Common::Timer m_open_timer;
|
||||
float m_open_time = 0.0f;
|
||||
} // namespace SaveStateSelectorUI
|
||||
|
||||
void SaveStateSelectorUI::Open(float open_time /* = DEFAULT_OPEN_TIME */)
|
||||
{
|
||||
m_open_timer.Reset();
|
||||
m_open_time = open_time;
|
||||
|
||||
if (s_save_state_selector_ui_open)
|
||||
return;
|
||||
|
||||
s_save_state_selector_ui_open = true;
|
||||
RefreshList();
|
||||
RefreshHotkeyLegend();
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::Close()
|
||||
{
|
||||
if (!s_save_state_selector_ui_open)
|
||||
return;
|
||||
|
||||
s_save_state_selector_ui_open = false;
|
||||
m_load_legend = {};
|
||||
m_save_legend = {};
|
||||
m_prev_legend = {};
|
||||
m_next_legend = {};
|
||||
m_slots.clear();
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::RefreshList()
|
||||
{
|
||||
if (!System::GetRunningCode().empty())
|
||||
{
|
||||
for (s32 i = 1; i <= System::PER_GAME_SAVE_STATE_SLOTS; i++)
|
||||
{
|
||||
std::string path(System::GetGameSaveStateFileName(System::GetRunningCode(), i));
|
||||
std::optional<ExtendedSaveStateInfo> ssi = System::GetExtendedSaveStateInfo(path.c_str());
|
||||
|
||||
ListEntry li;
|
||||
if (ssi)
|
||||
InitializeListEntry(&li, &ssi.value(), std::move(path), i, true);
|
||||
else
|
||||
InitializePlaceholderListEntry(&li, std::move(path), i, false);
|
||||
|
||||
m_slots.push_back(std::move(li));
|
||||
}
|
||||
}
|
||||
|
||||
for (s32 i = 1; i <= System::GLOBAL_SAVE_STATE_SLOTS; i++)
|
||||
{
|
||||
std::string path(System::GetGlobalSaveStateFileName(i));
|
||||
std::optional<ExtendedSaveStateInfo> ssi = System::GetExtendedSaveStateInfo(path.c_str());
|
||||
|
||||
ListEntry li;
|
||||
if (ssi)
|
||||
InitializeListEntry(&li, &ssi.value(), std::move(path), i, true);
|
||||
else
|
||||
InitializePlaceholderListEntry(&li, std::move(path), i, true);
|
||||
|
||||
m_slots.push_back(std::move(li));
|
||||
}
|
||||
|
||||
if (m_slots.empty() || m_current_selection >= m_slots.size())
|
||||
m_current_selection = 0;
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::RefreshHotkeyLegend()
|
||||
{
|
||||
auto format_legend_entry = [](std::string_view setting, std::string_view caption) {
|
||||
auto slash_pos = setting.find_first_of('/');
|
||||
if (slash_pos != setting.npos)
|
||||
{
|
||||
setting = setting.substr(slash_pos + 1);
|
||||
}
|
||||
|
||||
return StringUtil::StdStringFromFormat("%.*s - %.*s", static_cast<int>(setting.size()), setting.data(),
|
||||
static_cast<int>(caption.size()), caption.data());
|
||||
};
|
||||
|
||||
m_load_legend = format_legend_entry(Host::GetStringSettingValue("Hotkeys", "LoadSelectedSaveState"),
|
||||
Host::TranslateStdString("SaveStateSelectorUI", "Load"));
|
||||
m_save_legend = format_legend_entry(Host::GetStringSettingValue("Hotkeys", "SaveSelectedSaveState"),
|
||||
Host::TranslateStdString("SaveStateSelectorUI", "Save"));
|
||||
m_prev_legend = format_legend_entry(Host::GetStringSettingValue("Hotkeys", "SelectPreviousSaveStateSlot"),
|
||||
Host::TranslateStdString("SaveStateSelectorUI", "Select Previous"));
|
||||
m_next_legend = format_legend_entry(Host::GetStringSettingValue("Hotkeys", "SelectNextSaveStateSlot"),
|
||||
Host::TranslateStdString("SaveStateSelectorUI", "Select Next"));
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::SelectNextSlot()
|
||||
{
|
||||
if (!s_save_state_selector_ui_open)
|
||||
Open();
|
||||
|
||||
m_open_timer.Reset();
|
||||
m_current_selection = (m_current_selection == static_cast<u32>(m_slots.size() - 1)) ? 0 : (m_current_selection + 1);
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::SelectPreviousSlot()
|
||||
{
|
||||
if (!s_save_state_selector_ui_open)
|
||||
Open();
|
||||
|
||||
m_open_timer.Reset();
|
||||
m_current_selection =
|
||||
(m_current_selection == 0) ? (static_cast<u32>(m_slots.size()) - 1u) : (m_current_selection - 1);
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::InitializeListEntry(ListEntry* li, ExtendedSaveStateInfo* ssi, std::string path, s32 slot,
|
||||
bool global)
|
||||
{
|
||||
li->title = std::move(ssi->title);
|
||||
li->game_code = std::move(ssi->game_code);
|
||||
li->path = std::move(path);
|
||||
li->formatted_timestamp = fmt::format("{:%c}", fmt::localtime(ssi->timestamp));
|
||||
li->slot = slot;
|
||||
li->global = global;
|
||||
|
||||
li->preview_texture.reset();
|
||||
if (ssi && !ssi->screenshot_data.empty())
|
||||
{
|
||||
li->preview_texture = g_host_display->CreateTexture(ssi->screenshot_width, ssi->screenshot_height, 1, 1, 1,
|
||||
HostDisplayPixelFormat::RGBA8, ssi->screenshot_data.data(),
|
||||
sizeof(u32) * ssi->screenshot_width, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
li->preview_texture = g_host_display->CreateTexture(PLACEHOLDER_ICON_WIDTH, PLACEHOLDER_ICON_HEIGHT, 1, 1, 1,
|
||||
HostDisplayPixelFormat::RGBA8, PLACEHOLDER_ICON_DATA,
|
||||
sizeof(u32) * PLACEHOLDER_ICON_WIDTH, false);
|
||||
}
|
||||
|
||||
if (!li->preview_texture)
|
||||
Log_ErrorPrintf("Failed to upload save state image to GPU");
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::InitializePlaceholderListEntry(ListEntry* li, std::string path, s32 slot, bool global)
|
||||
{
|
||||
li->title = Host::TranslateStdString("SaveStateSelectorUI", "No Save State");
|
||||
std::string().swap(li->game_code);
|
||||
li->path = std::move(path);
|
||||
std::string().swap(li->formatted_timestamp);
|
||||
li->slot = slot;
|
||||
li->global = global;
|
||||
|
||||
li->preview_texture = g_host_display->CreateTexture(PLACEHOLDER_ICON_WIDTH, PLACEHOLDER_ICON_HEIGHT, 1, 1, 1,
|
||||
HostDisplayPixelFormat::RGBA8, PLACEHOLDER_ICON_DATA,
|
||||
sizeof(u32) * PLACEHOLDER_ICON_WIDTH, false);
|
||||
|
||||
if (!li->preview_texture)
|
||||
Log_ErrorPrintf("Failed to upload save state image to GPU");
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::Draw()
|
||||
{
|
||||
const float framebuffer_scale = ImGui::GetIO().DisplayFramebufferScale.x;
|
||||
const float window_width = ImGui::GetIO().DisplaySize.x * (2.0f / 3.0f);
|
||||
const float window_height = ImGui::GetIO().DisplaySize.y * 0.5f;
|
||||
const float rounding = 4.0f * framebuffer_scale;
|
||||
ImGui::SetNextWindowSize(ImVec2(window_width, window_height), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f, ImGui::GetIO().DisplaySize.y * 0.5f),
|
||||
ImGuiCond_Always, ImVec2(0.5f, 0.5f));
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.11f, 0.15f, 0.17f, 0.8f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, rounding);
|
||||
|
||||
if (ImGui::Begin("##save_state_selector", nullptr,
|
||||
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_NoScrollbar))
|
||||
{
|
||||
// Leave 2 lines for the legend
|
||||
const float legend_margin = ImGui::GetFontSize() * 2.0f + ImGui::GetStyle().ItemSpacing.y * 3.0f;
|
||||
const float padding = 10.0f * framebuffer_scale;
|
||||
|
||||
ImGui::BeginChild("##item_list", ImVec2(0, -legend_margin), false,
|
||||
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar);
|
||||
{
|
||||
const ImVec2 image_size = ImVec2(128.0f * framebuffer_scale, (128.0f / (4.0f / 3.0f)) * framebuffer_scale);
|
||||
const float item_height = image_size.y + padding * 2.0f;
|
||||
const float text_indent = image_size.x + padding + padding;
|
||||
|
||||
for (size_t i = 0; i < m_slots.size(); i++)
|
||||
{
|
||||
const ListEntry& entry = m_slots[i];
|
||||
const float y_start = item_height * static_cast<float>(i);
|
||||
|
||||
if (i == m_current_selection)
|
||||
{
|
||||
ImGui::SetCursorPosY(y_start);
|
||||
ImGui::SetScrollHereY();
|
||||
|
||||
const ImVec2 p_start(ImGui::GetCursorScreenPos());
|
||||
const ImVec2 p_end(p_start.x + window_width, p_start.y + item_height);
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(p_start, p_end, ImColor(0.22f, 0.30f, 0.34f, 0.9f), rounding);
|
||||
}
|
||||
|
||||
if (entry.preview_texture)
|
||||
{
|
||||
ImGui::SetCursorPosY(y_start + padding);
|
||||
ImGui::SetCursorPosX(padding);
|
||||
ImGui::Image(reinterpret_cast<ImTextureID>(entry.preview_texture->GetHandle()), image_size);
|
||||
}
|
||||
|
||||
ImGui::SetCursorPosY(y_start + padding);
|
||||
|
||||
ImGui::Indent(text_indent);
|
||||
|
||||
if (entry.global)
|
||||
{
|
||||
ImGui::Text(Host::TranslateString("SaveStateSelectorUI", "Global Slot %d"), entry.slot);
|
||||
}
|
||||
else if (entry.game_code.empty())
|
||||
{
|
||||
ImGui::Text(Host::TranslateString("SaveStateSelectorUI", "Game Slot %d"), entry.slot);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::Text(Host::TranslateString("SaveStateSelectorUI", "%s Slot %d"), entry.game_code.c_str(), entry.slot);
|
||||
}
|
||||
ImGui::TextUnformatted(entry.title.c_str());
|
||||
ImGui::TextUnformatted(entry.formatted_timestamp.c_str());
|
||||
ImGui::TextUnformatted(entry.path.c_str());
|
||||
|
||||
ImGui::Unindent(text_indent);
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::BeginChild("##legend", ImVec2(0, 0), false,
|
||||
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_NoScrollbar);
|
||||
{
|
||||
ImGui::SetCursorPosX(padding);
|
||||
ImGui::BeginTable("table", 2);
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(m_load_legend.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(m_prev_legend.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(m_save_legend.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(m_next_legend.c_str());
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// auto-close
|
||||
if (m_open_timer.GetTimeSeconds() >= m_open_time)
|
||||
Close();
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::LoadCurrentSlot()
|
||||
{
|
||||
if (m_slots.empty() || m_current_selection >= m_slots.size() || m_slots[m_current_selection].path.empty())
|
||||
return;
|
||||
|
||||
System::LoadState(m_slots[m_current_selection].path.c_str());
|
||||
Close();
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::SaveCurrentSlot()
|
||||
{
|
||||
if (m_slots.empty() || m_current_selection >= m_slots.size() || m_slots[m_current_selection].path.empty())
|
||||
return;
|
||||
|
||||
System::SaveState(m_slots[m_current_selection].path.c_str(), g_settings.create_save_state_backups);
|
||||
Close();
|
||||
}
|
22
src/frontend-common/imgui_overlays.h
Normal file
22
src/frontend-common/imgui_overlays.h
Normal file
@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "imgui_manager.h"
|
||||
|
||||
namespace ImGuiManager {
|
||||
void RenderOverlays();
|
||||
}
|
||||
|
||||
namespace SaveStateSelectorUI {
|
||||
|
||||
static constexpr float DEFAULT_OPEN_TIME = 5.0f;
|
||||
|
||||
void Open(float open_time = DEFAULT_OPEN_TIME);
|
||||
void Close();
|
||||
|
||||
void SelectNextSlot();
|
||||
void SelectPreviousSlot();
|
||||
|
||||
void LoadCurrentSlot();
|
||||
void SaveCurrentSlot();
|
||||
|
||||
} // namespace SaveStateSelectorUI
|
@ -1,249 +0,0 @@
|
||||
/* PCSX2 - PS2 Emulator for PCs
|
||||
* Copyright (C) 2002-2021 PCSX2 Dev Team
|
||||
*
|
||||
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
|
||||
* of the GNU Lesser General Public License as published by the Free Software Found-
|
||||
* ation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
* PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with PCSX2.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ini_settings_interface.h"
|
||||
#include "common/file_system.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
|
||||
Log_SetChannel(INISettingsInterface);
|
||||
|
||||
INISettingsInterface::INISettingsInterface(std::string filename) : m_filename(std::move(filename)), m_ini(true, true) {}
|
||||
|
||||
INISettingsInterface::~INISettingsInterface()
|
||||
{
|
||||
if (m_dirty)
|
||||
Save();
|
||||
}
|
||||
|
||||
bool INISettingsInterface::Load()
|
||||
{
|
||||
if (m_filename.empty())
|
||||
return false;
|
||||
|
||||
SI_Error err = SI_FAIL;
|
||||
auto fp = FileSystem::OpenManagedCFile(m_filename.c_str(), "rb");
|
||||
if (fp)
|
||||
err = m_ini.LoadFile(fp.get());
|
||||
|
||||
return (err == SI_OK);
|
||||
}
|
||||
|
||||
bool INISettingsInterface::Save()
|
||||
{
|
||||
if (m_filename.empty())
|
||||
return false;
|
||||
|
||||
SI_Error err = SI_FAIL;
|
||||
std::FILE* fp = FileSystem::OpenCFile(m_filename.c_str(), "wb");
|
||||
if (fp)
|
||||
{
|
||||
err = m_ini.SaveFile(fp, false);
|
||||
std::fclose(fp);
|
||||
}
|
||||
|
||||
if (err != SI_OK)
|
||||
{
|
||||
Log_WarningPrintf("Failed to save settings to '%s'.", m_filename.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
m_dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void INISettingsInterface::Clear()
|
||||
{
|
||||
m_ini.Reset();
|
||||
}
|
||||
|
||||
bool INISettingsInterface::GetIntValue(const char* section, const char* key, s32* value) const
|
||||
{
|
||||
const char* str_value = m_ini.GetValue(section, key);
|
||||
if (!str_value)
|
||||
return false;
|
||||
|
||||
std::optional<s32> parsed_value = StringUtil::FromChars<s32>(str_value, 10);
|
||||
if (!parsed_value.has_value())
|
||||
return false;
|
||||
|
||||
*value = parsed_value.value();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool INISettingsInterface::GetUIntValue(const char* section, const char* key, u32* value) const
|
||||
{
|
||||
const char* str_value = m_ini.GetValue(section, key);
|
||||
if (!str_value)
|
||||
return false;
|
||||
|
||||
std::optional<u32> parsed_value = StringUtil::FromChars<u32>(str_value, 10);
|
||||
if (!parsed_value.has_value())
|
||||
return false;
|
||||
|
||||
*value = parsed_value.value();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool INISettingsInterface::GetFloatValue(const char* section, const char* key, float* value) const
|
||||
{
|
||||
const char* str_value = m_ini.GetValue(section, key);
|
||||
if (!str_value)
|
||||
return false;
|
||||
|
||||
std::optional<float> parsed_value = StringUtil::FromChars<float>(str_value);
|
||||
if (!parsed_value.has_value())
|
||||
return false;
|
||||
|
||||
*value = parsed_value.value();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool INISettingsInterface::GetDoubleValue(const char* section, const char* key, double* value) const
|
||||
{
|
||||
const char* str_value = m_ini.GetValue(section, key);
|
||||
if (!str_value)
|
||||
return false;
|
||||
|
||||
std::optional<double> parsed_value = StringUtil::FromChars<double>(str_value);
|
||||
if (!parsed_value.has_value())
|
||||
return false;
|
||||
|
||||
*value = parsed_value.value();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool INISettingsInterface::GetBoolValue(const char* section, const char* key, bool* value) const
|
||||
{
|
||||
const char* str_value = m_ini.GetValue(section, key);
|
||||
if (!str_value)
|
||||
return false;
|
||||
|
||||
std::optional<bool> parsed_value = StringUtil::FromChars<bool>(str_value);
|
||||
if (!parsed_value.has_value())
|
||||
return false;
|
||||
|
||||
*value = parsed_value.value();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool INISettingsInterface::GetStringValue(const char* section, const char* key, std::string* value) const
|
||||
{
|
||||
const char* str_value = m_ini.GetValue(section, key);
|
||||
if (!str_value)
|
||||
return false;
|
||||
|
||||
value->assign(str_value);
|
||||
return true;
|
||||
}
|
||||
|
||||
void INISettingsInterface::SetIntValue(const char* section, const char* key, int value)
|
||||
{
|
||||
m_dirty = true;
|
||||
m_ini.SetLongValue(section, key, static_cast<long>(value), nullptr, false, true);
|
||||
}
|
||||
|
||||
void INISettingsInterface::SetUIntValue(const char* section, const char* key, u32 value)
|
||||
{
|
||||
m_dirty = true;
|
||||
m_ini.SetLongValue(section, key, static_cast<long>(value), nullptr, false, true);
|
||||
}
|
||||
|
||||
void INISettingsInterface::SetFloatValue(const char* section, const char* key, float value)
|
||||
{
|
||||
m_dirty = true;
|
||||
m_ini.SetDoubleValue(section, key, static_cast<double>(value), nullptr, true);
|
||||
}
|
||||
|
||||
void INISettingsInterface::SetDoubleValue(const char* section, const char* key, double value)
|
||||
{
|
||||
m_dirty = true;
|
||||
m_ini.SetDoubleValue(section, key, value, nullptr, true);
|
||||
}
|
||||
|
||||
void INISettingsInterface::SetBoolValue(const char* section, const char* key, bool value)
|
||||
{
|
||||
m_dirty = true;
|
||||
m_ini.SetBoolValue(section, key, value, nullptr, true);
|
||||
}
|
||||
|
||||
void INISettingsInterface::SetStringValue(const char* section, const char* key, const char* value)
|
||||
{
|
||||
m_dirty = true;
|
||||
m_ini.SetValue(section, key, value, nullptr, true);
|
||||
}
|
||||
|
||||
bool INISettingsInterface::ContainsValue(const char* section, const char* key) const
|
||||
{
|
||||
return (m_ini.GetValue(section, key, nullptr) != nullptr);
|
||||
}
|
||||
|
||||
void INISettingsInterface::DeleteValue(const char* section, const char* key)
|
||||
{
|
||||
m_dirty = true;
|
||||
m_ini.Delete(section, key);
|
||||
}
|
||||
|
||||
void INISettingsInterface::ClearSection(const char* section)
|
||||
{
|
||||
m_dirty = true;
|
||||
m_ini.Delete(section, nullptr);
|
||||
m_ini.SetValue(section, nullptr, nullptr);
|
||||
}
|
||||
|
||||
std::vector<std::string> INISettingsInterface::GetStringList(const char* section, const char* key) const
|
||||
{
|
||||
std::list<CSimpleIniA::Entry> entries;
|
||||
if (!m_ini.GetAllValues(section, key, entries))
|
||||
return {};
|
||||
|
||||
std::vector<std::string> ret;
|
||||
ret.reserve(entries.size());
|
||||
for (const CSimpleIniA::Entry& entry : entries)
|
||||
ret.emplace_back(entry.pItem);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void INISettingsInterface::SetStringList(const char* section, const char* key, const std::vector<std::string>& items)
|
||||
{
|
||||
m_dirty = true;
|
||||
m_ini.Delete(section, key);
|
||||
|
||||
for (const std::string& sv : items)
|
||||
m_ini.SetValue(section, key, sv.c_str(), nullptr, false);
|
||||
}
|
||||
|
||||
bool INISettingsInterface::RemoveFromStringList(const char* section, const char* key, const char* item)
|
||||
{
|
||||
m_dirty = true;
|
||||
return m_ini.DeleteValue(section, key, item, true);
|
||||
}
|
||||
|
||||
bool INISettingsInterface::AddToStringList(const char* section, const char* key, const char* item)
|
||||
{
|
||||
std::list<CSimpleIniA::Entry> entries;
|
||||
if (m_ini.GetAllValues(section, key, entries) &&
|
||||
std::find_if(entries.begin(), entries.end(),
|
||||
[item](const CSimpleIniA::Entry& e) { return (std::strcmp(e.pItem, item) == 0); }) != entries.end())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_dirty = true;
|
||||
m_ini.SetValue(section, key, item, nullptr, false);
|
||||
return true;
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
/* PCSX2 - PS2 Emulator for PCs
|
||||
* Copyright (C) 2002-2021 PCSX2 Dev Team
|
||||
*
|
||||
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
|
||||
* of the GNU Lesser General Public License as published by the Free Software Found-
|
||||
* ation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
* PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with PCSX2.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "common/settings_interface.h"
|
||||
|
||||
// being a pain here...
|
||||
#ifdef _WIN32
|
||||
#include "common/windows_headers.h"
|
||||
#endif
|
||||
#include "SimpleIni.h"
|
||||
|
||||
class INISettingsInterface final : public SettingsInterface
|
||||
{
|
||||
public:
|
||||
INISettingsInterface(std::string filename);
|
||||
~INISettingsInterface() override;
|
||||
|
||||
const std::string& GetFileName() const { return m_filename; }
|
||||
|
||||
bool Load();
|
||||
bool Save() override;
|
||||
|
||||
void Clear() override;
|
||||
|
||||
bool GetIntValue(const char* section, const char* key, s32* value) const override;
|
||||
bool GetUIntValue(const char* section, const char* key, u32* value) const override;
|
||||
bool GetFloatValue(const char* section, const char* key, float* value) const override;
|
||||
bool GetDoubleValue(const char* section, const char* key, double* value) const override;
|
||||
bool GetBoolValue(const char* section, const char* key, bool* value) const override;
|
||||
bool GetStringValue(const char* section, const char* key, std::string* value) const override;
|
||||
|
||||
void SetIntValue(const char* section, const char* key, s32 value) override;
|
||||
void SetUIntValue(const char* section, const char* key, u32 value) override;
|
||||
void SetFloatValue(const char* section, const char* key, float value) override;
|
||||
void SetDoubleValue(const char* section, const char* key, double value) override;
|
||||
void SetBoolValue(const char* section, const char* key, bool value) override;
|
||||
void SetStringValue(const char* section, const char* key, const char* value) override;
|
||||
bool ContainsValue(const char* section, const char* key) const override;
|
||||
void DeleteValue(const char* section, const char* key) override;
|
||||
void ClearSection(const char* section) override;
|
||||
|
||||
std::vector<std::string> GetStringList(const char* section, const char* key) const override;
|
||||
void SetStringList(const char* section, const char* key, const std::vector<std::string>& items) override;
|
||||
bool RemoveFromStringList(const char* section, const char* key, const char* item) override;
|
||||
bool AddToStringList(const char* section, const char* key, const char* item) override;
|
||||
|
||||
// default parameter overloads
|
||||
using SettingsInterface::GetBoolValue;
|
||||
using SettingsInterface::GetDoubleValue;
|
||||
using SettingsInterface::GetFloatValue;
|
||||
using SettingsInterface::GetIntValue;
|
||||
using SettingsInterface::GetStringValue;
|
||||
using SettingsInterface::GetUIntValue;
|
||||
|
||||
private:
|
||||
std::string m_filename;
|
||||
CSimpleIniA m_ini;
|
||||
bool m_dirty = false;
|
||||
};
|
1473
src/frontend-common/input_manager.cpp
Normal file
1473
src/frontend-common/input_manager.cpp
Normal file
File diff suppressed because it is too large
Load Diff
276
src/frontend-common/input_manager.h
Normal file
276
src/frontend-common/input_manager.h
Normal file
@ -0,0 +1,276 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
#include "common/settings_interface.h"
|
||||
#include "common/types.h"
|
||||
|
||||
/// Class, or source of an input event.
|
||||
enum class InputSourceType : u32
|
||||
{
|
||||
Keyboard,
|
||||
Pointer,
|
||||
#ifdef _WIN32
|
||||
DInput,
|
||||
XInput,
|
||||
RawInput,
|
||||
#endif
|
||||
#ifdef WITH_SDL2
|
||||
SDL,
|
||||
#endif
|
||||
Count,
|
||||
};
|
||||
|
||||
/// Subtype of a key for an input source.
|
||||
enum class InputSubclass : u32
|
||||
{
|
||||
None = 0,
|
||||
|
||||
PointerButton = 0,
|
||||
PointerAxis = 1,
|
||||
|
||||
ControllerButton = 0,
|
||||
ControllerAxis = 1,
|
||||
ControllerMotor = 2,
|
||||
ControllerHaptic = 3,
|
||||
};
|
||||
|
||||
/// A composite type representing a full input key which is part of an event.
|
||||
union InputBindingKey
|
||||
{
|
||||
struct
|
||||
{
|
||||
InputSourceType source_type : 4;
|
||||
u32 source_index : 8; ///< controller number
|
||||
InputSubclass source_subtype : 2; ///< if 1, binding is for an axis and not a button (used for controllers)
|
||||
u32 negative : 1; ///< if 1, binding is for the negative side of the axis
|
||||
u32 unused : 17;
|
||||
u32 data;
|
||||
};
|
||||
|
||||
u64 bits;
|
||||
|
||||
bool operator==(const InputBindingKey& k) const { return bits == k.bits; }
|
||||
bool operator!=(const InputBindingKey& k) const { return bits != k.bits; }
|
||||
|
||||
/// Removes the direction bit from the key, which is used to look up the bindings for it.
|
||||
/// This is because negative bindings should still fire when they reach zero again.
|
||||
InputBindingKey MaskDirection() const
|
||||
{
|
||||
InputBindingKey r;
|
||||
r.bits = bits;
|
||||
r.negative = false;
|
||||
return r;
|
||||
}
|
||||
};
|
||||
static_assert(sizeof(InputBindingKey) == sizeof(u64), "Input binding key is 64 bits");
|
||||
|
||||
/// Hashability for InputBindingKey
|
||||
struct InputBindingKeyHash
|
||||
{
|
||||
std::size_t operator()(const InputBindingKey& k) const { return std::hash<u64>{}(k.bits); }
|
||||
};
|
||||
|
||||
/// Callback type for a binary event. Usually used for hotkeys.
|
||||
using InputButtonEventHandler = std::function<void(s32 value)>;
|
||||
|
||||
/// Callback types for a normalized event. Usually used for pads.
|
||||
using InputAxisEventHandler = std::function<void(float value)>;
|
||||
|
||||
/// Input monitoring for external access.
|
||||
struct InputInterceptHook
|
||||
{
|
||||
enum class CallbackResult
|
||||
{
|
||||
StopProcessingEvent,
|
||||
ContinueProcessingEvent,
|
||||
RemoveHookAndStopProcessingEvent,
|
||||
RemoveHookAndContinueProcessingEvent,
|
||||
};
|
||||
|
||||
using Callback = std::function<CallbackResult(InputBindingKey key, float value)>;
|
||||
};
|
||||
|
||||
/// Hotkeys are actions (e.g. toggle frame limit) which can be bound to keys or chords.
|
||||
/// The handler is called with an integer representing the key state, where 0 means that
|
||||
/// one or more keys were released, 1 means all the keys were pressed, and -1 means that
|
||||
/// the hotkey was cancelled due to a chord with more keys being activated.
|
||||
struct HotkeyInfo
|
||||
{
|
||||
const char* name;
|
||||
const char* category;
|
||||
const char* display_name;
|
||||
void (*handler)(s32 pressed);
|
||||
};
|
||||
#define DECLARE_HOTKEY_LIST(name) extern const HotkeyInfo name[]
|
||||
#define BEGIN_HOTKEY_LIST(name) const HotkeyInfo name[] = {
|
||||
#define DEFINE_HOTKEY(name, category, display_name, handler) {(name), (category), (display_name), (handler)},
|
||||
#define END_HOTKEY_LIST() \
|
||||
{ \
|
||||
nullptr, nullptr, nullptr, nullptr \
|
||||
} \
|
||||
} \
|
||||
;
|
||||
|
||||
DECLARE_HOTKEY_LIST(g_common_hotkeys);
|
||||
DECLARE_HOTKEY_LIST(g_host_hotkeys);
|
||||
|
||||
/// Generic input bindings. These roughly match a DualShock 4 or XBox One controller.
|
||||
/// They are used for automatic binding to PS2 controller types, and for big picture mode navigation.
|
||||
enum class GenericInputBinding : u8;
|
||||
using GenericInputBindingMapping = std::vector<std::pair<GenericInputBinding, std::string>>;
|
||||
|
||||
/// Host mouse relative axes are X, Y, wheel horizontal, wheel vertical.
|
||||
enum class InputPointerAxis : u8
|
||||
{
|
||||
X,
|
||||
Y,
|
||||
WheelX,
|
||||
WheelY,
|
||||
Count
|
||||
};
|
||||
|
||||
/// External input source class.
|
||||
class InputSource;
|
||||
|
||||
namespace InputManager {
|
||||
/// Minimum interval between vibration updates when the effect is continuous.
|
||||
static constexpr double VIBRATION_UPDATE_INTERVAL_SECONDS = 0.5; // 500ms
|
||||
|
||||
/// Maximum number of host mouse devices.
|
||||
static constexpr u32 MAX_POINTER_DEVICES = 1;
|
||||
|
||||
/// Number of macro buttons per controller.
|
||||
static constexpr u32 NUM_MACRO_BUTTONS_PER_CONTROLLER = 4;
|
||||
|
||||
/// Returns a pointer to the external input source class, if present.
|
||||
InputSource* GetInputSourceInterface(InputSourceType type);
|
||||
|
||||
/// Converts an input class to a string.
|
||||
const char* InputSourceToString(InputSourceType clazz);
|
||||
|
||||
/// Parses an input class string.
|
||||
std::optional<InputSourceType> ParseInputSourceString(const std::string_view& str);
|
||||
|
||||
/// Converts a key code from a human-readable string to an identifier.
|
||||
std::optional<u32> ConvertHostKeyboardStringToCode(const std::string_view& str);
|
||||
|
||||
/// Converts a key code from an identifier to a human-readable string.
|
||||
std::optional<std::string> ConvertHostKeyboardCodeToString(u32 code);
|
||||
|
||||
/// Creates a key for a host-specific key code.
|
||||
InputBindingKey MakeHostKeyboardKey(u32 key_code);
|
||||
|
||||
/// Creates a key for a host-specific button.
|
||||
InputBindingKey MakePointerButtonKey(u32 index, u32 button_index);
|
||||
|
||||
/// Creates a key for a host-specific mouse relative event
|
||||
/// (axis 0 = horizontal, 1 = vertical, 2 = wheel horizontal, 3 = wheel vertical).
|
||||
InputBindingKey MakePointerAxisKey(u32 index, InputPointerAxis axis);
|
||||
|
||||
/// Parses an input binding key string.
|
||||
std::optional<InputBindingKey> ParseInputBindingKey(const std::string_view& binding);
|
||||
|
||||
/// Converts a input key to a string.
|
||||
std::string ConvertInputBindingKeyToString(InputBindingKey key);
|
||||
|
||||
/// Converts a chord of binding keys to a string.
|
||||
std::string ConvertInputBindingKeysToString(const InputBindingKey* keys, size_t num_keys);
|
||||
|
||||
/// Returns a list of all hotkeys.
|
||||
std::vector<const HotkeyInfo*> GetHotkeyList();
|
||||
|
||||
/// Enumerates available devices. Returns a pair of the prefix (e.g. SDL-0) and the device name.
|
||||
std::vector<std::pair<std::string, std::string>> EnumerateDevices();
|
||||
|
||||
/// Enumerates available vibration motors at the time of call.
|
||||
std::vector<InputBindingKey> EnumerateMotors();
|
||||
|
||||
/// Retrieves bindings that match the generic bindings for the specified device.
|
||||
GenericInputBindingMapping GetGenericBindingMapping(const std::string_view& device);
|
||||
|
||||
/// Re-parses the config and registers all hotkey and pad bindings.
|
||||
void ReloadBindings(SettingsInterface& si, SettingsInterface& binding_si);
|
||||
|
||||
/// Re-parses the sources part of the config and initializes any backends.
|
||||
void ReloadSources(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock);
|
||||
|
||||
/// Shuts down any enabled input sources.
|
||||
void CloseSources();
|
||||
|
||||
/// Polls input sources for events (e.g. external controllers).
|
||||
void PollSources();
|
||||
|
||||
/// Returns true if any bindings exist for the specified key.
|
||||
/// Can be safely called on another thread.
|
||||
bool HasAnyBindingsForKey(InputBindingKey key);
|
||||
|
||||
/// Returns true if any bindings exist for the specified source + index.
|
||||
/// Can be safely called on another thread.
|
||||
bool HasAnyBindingsForSource(InputBindingKey key);
|
||||
|
||||
/// Updates internal state for any binds for this key, and fires callbacks as needed.
|
||||
/// Returns true if anything was bound to this key, otherwise false.
|
||||
bool InvokeEvents(InputBindingKey key, float value, GenericInputBinding generic_key);
|
||||
|
||||
/// Sets a hook which can be used to intercept events before they're processed by the normal bindings.
|
||||
/// This is typically used when binding new controls to detect what gets pressed.
|
||||
void SetHook(InputInterceptHook::Callback callback);
|
||||
|
||||
/// Removes any currently-active interception hook.
|
||||
void RemoveHook();
|
||||
|
||||
/// Returns true if there is an interception hook present.
|
||||
bool HasHook();
|
||||
|
||||
/// Internal method used by pads to dispatch vibration updates to input sources.
|
||||
/// Intensity is normalized from 0 to 1.
|
||||
void SetPadVibrationIntensity(u32 pad_index, float large_or_single_motor_intensity, float small_motor_intensity);
|
||||
|
||||
/// Zeros all vibration intensities. Call when pausing.
|
||||
/// The pad vibration state will internally remain, so that when emulation is unpaused, the effect resumes.
|
||||
void PauseVibration();
|
||||
|
||||
/// Updates absolute pointer position. Can call from UI thread, use when the host only reports absolute coordinates.
|
||||
void UpdatePointerAbsolutePosition(u32 index, float x, float y);
|
||||
|
||||
/// Updates relative pointer position. Can call from the UI thread, use when host supports relative coordinate
|
||||
/// reporting.
|
||||
void UpdatePointerRelativeDelta(u32 index, InputPointerAxis axis, float d, bool raw_input = false);
|
||||
|
||||
/// Returns true if the raw input source is being used.
|
||||
bool IsUsingRawInput();
|
||||
|
||||
/// Returns true if any bindings are present which require relative mouse movement.
|
||||
bool HasPointerAxisBinds();
|
||||
|
||||
/// Restores default configuration.
|
||||
void SetDefaultConfig(SettingsInterface& si);
|
||||
|
||||
/// Clears all bindings for a given port.
|
||||
void ClearPortBindings(SettingsInterface& si, u32 port);
|
||||
|
||||
/// Copies pad configuration from one interface (ini) to another.
|
||||
void CopyConfiguration(SettingsInterface* dest_si, const SettingsInterface& src_si, bool copy_pad_config = true,
|
||||
bool copy_pad_bindings = true, bool copy_hotkey_bindings = true);
|
||||
|
||||
/// Performs automatic controller mapping with the provided list of generic mappings.
|
||||
bool MapController(SettingsInterface& si, u32 controller,
|
||||
const std::vector<std::pair<GenericInputBinding, std::string>>& mapping);
|
||||
|
||||
/// Returns a list of input profiles available.
|
||||
std::vector<std::string> GetInputProfileNames();
|
||||
} // namespace InputManager
|
||||
|
||||
namespace Host {
|
||||
/// Called when a new input device is connected.
|
||||
void OnInputDeviceConnected(const std::string_view& identifier, const std::string_view& device_name);
|
||||
|
||||
/// Called when an input device is disconnected.
|
||||
void OnInputDeviceDisconnected(const std::string_view& identifier);
|
||||
} // namespace Host
|
@ -1,118 +0,0 @@
|
||||
#include "input_overlay_ui.h"
|
||||
#include "common_host_interface.h"
|
||||
#include "core/imgui_fullscreen.h"
|
||||
#include "core/pad.h"
|
||||
#include "core/settings.h"
|
||||
#include "core/system.h"
|
||||
#include "fullscreen_ui.h"
|
||||
|
||||
static CommonHostInterface* GetHostInterface()
|
||||
{
|
||||
return static_cast<CommonHostInterface*>(g_host_interface);
|
||||
}
|
||||
|
||||
namespace FrontendCommon {
|
||||
|
||||
InputOverlayUI::InputOverlayUI() = default;
|
||||
|
||||
InputOverlayUI::~InputOverlayUI() = default;
|
||||
|
||||
void InputOverlayUI::Draw()
|
||||
{
|
||||
UpdateNames();
|
||||
|
||||
if (m_active_ports == 0)
|
||||
return;
|
||||
|
||||
ImFont* font;
|
||||
float margin, spacing, shadow_offset;
|
||||
|
||||
if (GetHostInterface()->IsFullscreenUIEnabled())
|
||||
{
|
||||
font = ImGuiFullscreen::g_large_font;
|
||||
margin = ImGuiFullscreen::LayoutScale(10.0f);
|
||||
spacing = ImGuiFullscreen::LayoutScale(5.0f);
|
||||
shadow_offset = ImGuiFullscreen::LayoutScale(1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
font = ImGui::GetFont();
|
||||
margin = ImGuiFullscreen::DPIScale(10.0f);
|
||||
spacing = ImGuiFullscreen::DPIScale(5.0f);
|
||||
shadow_offset = ImGuiFullscreen::DPIScale(1.0f);
|
||||
}
|
||||
|
||||
static constexpr u32 text_color = IM_COL32(0xff, 0xff, 0xff, 255);
|
||||
static constexpr u32 shadow_color = IM_COL32(0x00, 0x00, 0x00, 100);
|
||||
|
||||
const ImVec2& display_size = ImGui::GetIO().DisplaySize;
|
||||
ImDrawList* dl = ImGui::GetBackgroundDrawList();
|
||||
|
||||
float current_x = margin;
|
||||
float current_y =
|
||||
display_size.y - margin - ((static_cast<float>(m_active_ports) * (font->FontSize + spacing)) - spacing);
|
||||
|
||||
const ImVec4 clip_rect(current_x, current_y, display_size.x - margin, display_size.y - margin);
|
||||
|
||||
LargeString text;
|
||||
|
||||
for (u32 port = 0; port < NUM_CONTROLLER_AND_CARD_PORTS; port++)
|
||||
{
|
||||
if (m_types[port] == ControllerType::None)
|
||||
continue;
|
||||
|
||||
const Controller* controller = g_pad.GetController(port);
|
||||
DebugAssert(controller);
|
||||
|
||||
text.Format("P%u |", port + 1u);
|
||||
|
||||
if (!m_axis_names[port].empty())
|
||||
{
|
||||
for (const auto& [axis_name, axis_code, axis_type] : m_axis_names[port])
|
||||
{
|
||||
const float value = controller->GetAxisState(axis_code);
|
||||
text.AppendFormattedString(" %s: %.2f", axis_name.c_str(), value);
|
||||
}
|
||||
|
||||
text.AppendString(" |");
|
||||
}
|
||||
|
||||
for (const auto& [button_name, button_code] : m_button_names[port])
|
||||
{
|
||||
const bool pressed = controller->GetButtonState(button_code);
|
||||
if (pressed)
|
||||
text.AppendFormattedString(" %s", button_name.c_str());
|
||||
}
|
||||
|
||||
dl->AddText(font, font->FontSize, ImVec2(current_x + shadow_offset, current_y + shadow_offset), shadow_color,
|
||||
text.GetCharArray(), text.GetCharArray() + text.GetLength(), 0.0f, &clip_rect);
|
||||
dl->AddText(font, font->FontSize, ImVec2(current_x, current_y), text_color, text.GetCharArray(),
|
||||
text.GetCharArray() + text.GetLength(), 0.0f, &clip_rect);
|
||||
|
||||
current_y += font->FontSize + spacing;
|
||||
}
|
||||
}
|
||||
|
||||
void InputOverlayUI::UpdateNames()
|
||||
{
|
||||
m_active_ports = 0;
|
||||
if (!System::IsValid())
|
||||
return;
|
||||
|
||||
for (u32 port = 0; port < NUM_CONTROLLER_AND_CARD_PORTS; port++)
|
||||
{
|
||||
const Controller* controller = g_pad.GetController(port);
|
||||
const ControllerType type = (controller) ? controller->GetType() : ControllerType::None;
|
||||
if (type != ControllerType::None)
|
||||
m_active_ports++;
|
||||
|
||||
if (type == m_types[port])
|
||||
continue;
|
||||
|
||||
m_axis_names[port] = Controller::GetAxisNames(type);
|
||||
m_button_names[port] = Controller::GetButtonNames(type);
|
||||
m_types[port] = type;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace FrontendCommon
|
@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
#include "core/controller.h"
|
||||
#include <array>
|
||||
|
||||
namespace FrontendCommon {
|
||||
|
||||
class InputOverlayUI
|
||||
{
|
||||
public:
|
||||
InputOverlayUI();
|
||||
~InputOverlayUI();
|
||||
|
||||
void Draw();
|
||||
|
||||
private:
|
||||
void UpdateNames();
|
||||
|
||||
std::array<Controller::AxisList, NUM_CONTROLLER_AND_CARD_PORTS> m_axis_names;
|
||||
std::array<Controller::ButtonList, NUM_CONTROLLER_AND_CARD_PORTS> m_button_names;
|
||||
std::array<ControllerType, NUM_CONTROLLER_AND_CARD_PORTS> m_types{};
|
||||
u32 m_active_ports = 0;
|
||||
};
|
||||
|
||||
} // namespace FrontendCommon
|
120
src/frontend-common/input_source.cpp
Normal file
120
src/frontend-common/input_source.cpp
Normal file
@ -0,0 +1,120 @@
|
||||
#include "input_source.h"
|
||||
#include "common/string_util.h"
|
||||
|
||||
InputSource::InputSource() = default;
|
||||
|
||||
InputSource::~InputSource() = default;
|
||||
|
||||
void InputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
|
||||
float small_intensity)
|
||||
{
|
||||
if (large_key.bits != 0)
|
||||
UpdateMotorState(large_key, large_intensity);
|
||||
if (small_key.bits != 0)
|
||||
UpdateMotorState(small_key, small_intensity);
|
||||
}
|
||||
|
||||
InputBindingKey InputSource::MakeGenericControllerAxisKey(InputSourceType clazz, u32 controller_index, s32 axis_index)
|
||||
{
|
||||
InputBindingKey key = {};
|
||||
key.source_type = clazz;
|
||||
key.source_index = controller_index;
|
||||
key.source_subtype = InputSubclass::ControllerAxis;
|
||||
key.data = static_cast<u32>(axis_index);
|
||||
return key;
|
||||
}
|
||||
|
||||
InputBindingKey InputSource::MakeGenericControllerButtonKey(InputSourceType clazz, u32 controller_index,
|
||||
s32 button_index)
|
||||
{
|
||||
InputBindingKey key = {};
|
||||
key.source_type = clazz;
|
||||
key.source_index = controller_index;
|
||||
key.source_subtype = InputSubclass::ControllerButton;
|
||||
key.data = static_cast<u32>(button_index);
|
||||
return key;
|
||||
}
|
||||
|
||||
InputBindingKey InputSource::MakeGenericControllerMotorKey(InputSourceType clazz, u32 controller_index, s32 motor_index)
|
||||
{
|
||||
InputBindingKey key = {};
|
||||
key.source_type = clazz;
|
||||
key.source_index = controller_index;
|
||||
key.source_subtype = InputSubclass::ControllerMotor;
|
||||
key.data = static_cast<u32>(motor_index);
|
||||
return key;
|
||||
}
|
||||
|
||||
std::optional<InputBindingKey> InputSource::ParseGenericControllerKey(InputSourceType clazz,
|
||||
const std::string_view& source,
|
||||
const std::string_view& sub_binding)
|
||||
{
|
||||
// try to find the number, this function doesn't care about whether it's xinput or sdl or whatever
|
||||
std::string_view::size_type pos = 0;
|
||||
while (pos < source.size())
|
||||
{
|
||||
if (source[pos] >= '0' && source[pos] <= '9')
|
||||
break;
|
||||
pos++;
|
||||
}
|
||||
if (pos == source.size())
|
||||
return std::nullopt;
|
||||
|
||||
const std::optional<s32> source_index = StringUtil::FromChars<s32>(source.substr(pos));
|
||||
if (source_index.has_value() || source_index.value() < 0)
|
||||
return std::nullopt;
|
||||
|
||||
InputBindingKey key = {};
|
||||
key.source_type = clazz;
|
||||
key.source_index = source_index.value();
|
||||
|
||||
if (StringUtil::StartsWith(sub_binding, "+Axis") || StringUtil::StartsWith(sub_binding, "-Axis"))
|
||||
{
|
||||
const std::optional<s32> axis_number = StringUtil::FromChars<s32>(sub_binding.substr(5));
|
||||
if (!axis_number.has_value() || axis_number.value() < 0)
|
||||
return std::nullopt;
|
||||
|
||||
key.source_subtype = InputSubclass::ControllerAxis;
|
||||
key.data = static_cast<u32>(axis_number.value());
|
||||
|
||||
if (sub_binding[0] == '+')
|
||||
key.negative = false;
|
||||
else if (sub_binding[0] == '-')
|
||||
key.negative = true;
|
||||
else
|
||||
return std::nullopt;
|
||||
}
|
||||
else if (StringUtil::StartsWith(sub_binding, "Button"))
|
||||
{
|
||||
const std::optional<s32> button_number = StringUtil::FromChars<s32>(sub_binding.substr(6));
|
||||
if (!button_number.has_value() || button_number.value() < 0)
|
||||
return std::nullopt;
|
||||
|
||||
key.source_subtype = InputSubclass::ControllerButton;
|
||||
key.data = static_cast<u32>(button_number.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
std::string InputSource::ConvertGenericControllerKeyToString(InputBindingKey key)
|
||||
{
|
||||
if (key.source_subtype == InputSubclass::ControllerAxis)
|
||||
{
|
||||
return StringUtil::StdStringFromFormat("%s-%u/%cAxis%u", InputManager::InputSourceToString(key.source_type),
|
||||
key.source_index, key.negative ? '+' : '-', key.data);
|
||||
}
|
||||
else if (key.source_subtype == InputSubclass::ControllerButton)
|
||||
{
|
||||
return StringUtil::StdStringFromFormat("%s%u/Button%u", InputManager::InputSourceToString(key.source_type),
|
||||
key.source_index, key.data);
|
||||
}
|
||||
else
|
||||
{
|
||||
return {};
|
||||
}
|
||||
}
|
72
src/frontend-common/input_source.h
Normal file
72
src/frontend-common/input_source.h
Normal file
@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "common/types.h"
|
||||
#include "input_manager.h"
|
||||
|
||||
class SettingsInterface;
|
||||
|
||||
class InputSource
|
||||
{
|
||||
public:
|
||||
InputSource();
|
||||
virtual ~InputSource();
|
||||
|
||||
virtual bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) = 0;
|
||||
virtual void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) = 0;
|
||||
virtual void Shutdown() = 0;
|
||||
|
||||
virtual void PollEvents() = 0;
|
||||
|
||||
virtual std::optional<InputBindingKey> ParseKeyString(const std::string_view& device,
|
||||
const std::string_view& binding) = 0;
|
||||
virtual std::string ConvertKeyToString(InputBindingKey key) = 0;
|
||||
|
||||
/// Enumerates available devices. Returns a pair of the prefix (e.g. SDL-0) and the device name.
|
||||
virtual std::vector<std::pair<std::string, std::string>> EnumerateDevices() = 0;
|
||||
|
||||
/// Enumerates available vibration motors at the time of call.
|
||||
virtual std::vector<InputBindingKey> EnumerateMotors() = 0;
|
||||
|
||||
/// Retrieves bindings that match the generic bindings for the specified device.
|
||||
/// Returns false if it's not one of our devices.
|
||||
virtual bool GetGenericBindingMapping(const std::string_view& device, GenericInputBindingMapping* mapping) = 0;
|
||||
|
||||
/// Informs the source of a new vibration motor state. Changes may not take effect until the next PollEvents() call.
|
||||
virtual void UpdateMotorState(InputBindingKey key, float intensity) = 0;
|
||||
|
||||
/// Concurrently update both motors where possible, to avoid redundant packets.
|
||||
virtual void UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
|
||||
float small_intensity);
|
||||
|
||||
/// Creates a key for a generic controller axis event.
|
||||
static InputBindingKey MakeGenericControllerAxisKey(InputSourceType clazz, u32 controller_index, s32 axis_index);
|
||||
|
||||
/// Creates a key for a generic controller button event.
|
||||
static InputBindingKey MakeGenericControllerButtonKey(InputSourceType clazz, u32 controller_index, s32 button_index);
|
||||
|
||||
/// Creates a key for a generic controller motor event.
|
||||
static InputBindingKey MakeGenericControllerMotorKey(InputSourceType clazz, u32 controller_index, s32 motor_index);
|
||||
|
||||
/// Parses a generic controller key string.
|
||||
static std::optional<InputBindingKey> ParseGenericControllerKey(InputSourceType clazz, const std::string_view& source,
|
||||
const std::string_view& sub_binding);
|
||||
|
||||
/// Converts a generic controller key to a string.
|
||||
static std::string ConvertGenericControllerKeyToString(InputBindingKey key);
|
||||
|
||||
#ifdef _WIN32
|
||||
static std::unique_ptr<InputSource> CreateDInputSource();
|
||||
static std::unique_ptr<InputSource> CreateXInputSource();
|
||||
static std::unique_ptr<InputSource> CreateWin32RawInputSource();
|
||||
#endif
|
||||
#ifdef WITH_SDL2
|
||||
static std::unique_ptr<InputSource> CreateSDLSource();
|
||||
#endif
|
||||
};
|
@ -3,7 +3,7 @@
|
||||
#include "common/assert.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "common_host_interface.h"
|
||||
#include "common_host.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_impl_opengl3.h"
|
||||
#include "postprocessing_shadergen.h"
|
||||
@ -521,7 +521,7 @@ HostDisplay::AdapterAndModeList OpenGLHostDisplay::GetAdapterAndModeList()
|
||||
for (const GL::Context::FullscreenModeInfo& fmi : m_gl_context->EnumerateFullscreenModes())
|
||||
{
|
||||
aml.fullscreen_modes.push_back(
|
||||
CommonHostInterface::GetFullscreenModeString(fmi.width, fmi.height, fmi.refresh_rate));
|
||||
GetFullscreenModeString(fmi.width, fmi.height, fmi.refresh_rate));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,10 @@
|
||||
#include "common/file_system.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string.h"
|
||||
#include "core/host_interface.h"
|
||||
#include "common/path.h"
|
||||
#include "core/host.h"
|
||||
#include "core/settings.h"
|
||||
#include "fmt/format.h"
|
||||
#include <sstream>
|
||||
Log_SetChannel(PostProcessingChain);
|
||||
|
||||
@ -11,38 +14,19 @@ namespace FrontendCommon {
|
||||
|
||||
static bool TryLoadingShader(PostProcessingShader* shader, const std::string_view& shader_name)
|
||||
{
|
||||
std::string shader_name_str(shader_name);
|
||||
|
||||
std::string filename = g_host_interface->GetUserDirectoryRelativePath("shaders" FS_OSPATH_SEPARATOR_STR "%s.glsl",
|
||||
shader_name_str.c_str());
|
||||
std::string filename(Path::Combine(EmuFolders::Shaders, fmt::format("{}.glsl", shader_name)));
|
||||
if (FileSystem::FileExists(filename.c_str()))
|
||||
{
|
||||
if (!shader->LoadFromFile(std::move(shader_name_str), filename.c_str()))
|
||||
{
|
||||
Log_ErrorPrintf("Failed to load shader from '%s'", filename.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
filename = g_host_interface->GetProgramDirectoryRelativePath("shaders" FS_OSPATH_SEPARATOR_STR "%s.glsl",
|
||||
shader_name_str.c_str());
|
||||
if (FileSystem::FileExists(filename.c_str()))
|
||||
{
|
||||
if (!shader->LoadFromFile(std::move(shader_name_str), filename.c_str()))
|
||||
{
|
||||
Log_ErrorPrintf("Failed to load shader from '%s'", filename.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log_ErrorPrintf("Could not find shader '%s'", std::string(shader_name).c_str());
|
||||
return false;
|
||||
}
|
||||
if (shader->LoadFromFile(std::string(shader_name), filename.c_str()))
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
std::optional<std::string> resource_str(Host::ReadResourceFileToString(fmt::format("shaders" FS_OSPATH_SEPARATOR_STR "{}.glsl", shader_name).c_str()));
|
||||
if (resource_str.has_value() && shader->LoadFromString(std::string(shader_name), std::move(resource_str.value())))
|
||||
return true;
|
||||
|
||||
Log_ErrorPrintf("Failed to load shader from '%s'", filename.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
PostProcessingChain::PostProcessingChain() = default;
|
||||
@ -132,18 +116,13 @@ std::vector<std::string> PostProcessingChain::GetAvailableShaderNames()
|
||||
{
|
||||
std::vector<std::string> names;
|
||||
|
||||
std::string program_dir = g_host_interface->GetProgramDirectoryRelativePath("shaders");
|
||||
std::string user_dir = g_host_interface->GetUserDirectoryRelativePath("shaders");
|
||||
FileSystem::FindResultsArray results;
|
||||
FileSystem::FindFiles(user_dir.c_str(), "*.glsl",
|
||||
FileSystem::FindFiles(Path::Combine(EmuFolders::Resources, "shaders").c_str(), "*.glsl",
|
||||
FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_RECURSIVE | FILESYSTEM_FIND_RELATIVE_PATHS, &results);
|
||||
if (program_dir != user_dir)
|
||||
{
|
||||
FileSystem::FindFiles(program_dir.c_str(), "*.glsl",
|
||||
FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_RECURSIVE | FILESYSTEM_FIND_RELATIVE_PATHS |
|
||||
FILESYSTEM_FIND_KEEP_ARRAY,
|
||||
&results);
|
||||
}
|
||||
FileSystem::FindFiles(EmuFolders::Shaders.c_str(), "*.glsl",
|
||||
FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_RECURSIVE | FILESYSTEM_FIND_RELATIVE_PATHS |
|
||||
FILESYSTEM_FIND_KEEP_ARRAY,
|
||||
&results);
|
||||
|
||||
for (FILESYSTEM_FIND_DATA& fd : results)
|
||||
{
|
||||
|
@ -110,8 +110,13 @@ bool PostProcessingShader::LoadFromFile(std::string name, const char* filename)
|
||||
if (!code.has_value() || code->empty())
|
||||
return false;
|
||||
|
||||
return LoadFromString(std::move(name), code.value());
|
||||
}
|
||||
|
||||
bool PostProcessingShader::LoadFromString(std::string name, std::string code)
|
||||
{
|
||||
m_name = std::move(name);
|
||||
m_code = std::move(code.value());
|
||||
m_code = std::move(code);
|
||||
m_options.clear();
|
||||
LoadOptions();
|
||||
return true;
|
||||
|
@ -77,6 +77,7 @@ public:
|
||||
void SetConfigString(const std::string_view& str);
|
||||
|
||||
bool LoadFromFile(std::string name, const char* filename);
|
||||
bool LoadFromString(std::string name, std::string code);
|
||||
|
||||
bool UsePushConstants() const;
|
||||
u32 GetUniformsSize() const;
|
||||
|
@ -1,323 +0,0 @@
|
||||
#include "save_state_selector_ui.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/host_display.h"
|
||||
#include "core/system.h"
|
||||
#include "fmt/chrono.h"
|
||||
#include "fmt/format.h"
|
||||
#include "icon.h"
|
||||
#include "imgui.h"
|
||||
Log_SetChannel(SaveStateSelectorUI);
|
||||
|
||||
namespace FrontendCommon {
|
||||
|
||||
SaveStateSelectorUI::SaveStateSelectorUI(CommonHostInterface* host_interface) : m_host_interface(host_interface) {}
|
||||
|
||||
SaveStateSelectorUI::~SaveStateSelectorUI() = default;
|
||||
|
||||
void SaveStateSelectorUI::Open(float open_time /* = DEFAULT_OPEN_TIME */)
|
||||
{
|
||||
m_open_timer.Reset();
|
||||
m_open_time = open_time;
|
||||
|
||||
if (m_open)
|
||||
return;
|
||||
|
||||
m_open = true;
|
||||
RefreshList();
|
||||
RefreshHotkeyLegend();
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::Close()
|
||||
{
|
||||
if (!m_open)
|
||||
return;
|
||||
|
||||
m_open = false;
|
||||
ClearList();
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::ClearList()
|
||||
{
|
||||
m_slots.clear();
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::RefreshList()
|
||||
{
|
||||
ClearList();
|
||||
|
||||
if (!System::GetRunningCode().empty())
|
||||
{
|
||||
for (s32 i = 1; i <= CommonHostInterface::PER_GAME_SAVE_STATE_SLOTS; i++)
|
||||
{
|
||||
std::optional<CommonHostInterface::ExtendedSaveStateInfo> ssi =
|
||||
m_host_interface->GetExtendedSaveStateInfo(System::GetRunningCode().c_str(), i);
|
||||
|
||||
ListEntry li;
|
||||
if (ssi)
|
||||
InitializeListEntry(&li, &ssi.value());
|
||||
else
|
||||
InitializePlaceholderListEntry(&li, i, false);
|
||||
|
||||
m_slots.push_back(std::move(li));
|
||||
}
|
||||
}
|
||||
|
||||
for (s32 i = 1; i <= CommonHostInterface::GLOBAL_SAVE_STATE_SLOTS; i++)
|
||||
{
|
||||
std::optional<CommonHostInterface::ExtendedSaveStateInfo> ssi =
|
||||
m_host_interface->GetExtendedSaveStateInfo(nullptr, i);
|
||||
|
||||
ListEntry li;
|
||||
if (ssi)
|
||||
InitializeListEntry(&li, &ssi.value());
|
||||
else
|
||||
InitializePlaceholderListEntry(&li, i, true);
|
||||
|
||||
m_slots.push_back(std::move(li));
|
||||
}
|
||||
|
||||
if (m_slots.empty() || m_current_selection >= m_slots.size())
|
||||
m_current_selection = 0;
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::RefreshHotkeyLegend()
|
||||
{
|
||||
if (!m_open)
|
||||
return;
|
||||
|
||||
auto format_legend_entry = [](std::string_view setting, std::string_view caption) {
|
||||
auto slash_pos = setting.find_first_of('/');
|
||||
if (slash_pos != setting.npos)
|
||||
{
|
||||
setting = setting.substr(slash_pos + 1);
|
||||
}
|
||||
|
||||
return StringUtil::StdStringFromFormat("%.*s - %.*s", static_cast<int>(setting.size()), setting.data(),
|
||||
static_cast<int>(caption.size()), caption.data());
|
||||
};
|
||||
|
||||
m_load_legend = format_legend_entry(m_host_interface->GetStringSettingValue("Hotkeys", "LoadSelectedSaveState"),
|
||||
m_host_interface->TranslateStdString("SaveStateSelectorUI", "Load"));
|
||||
m_save_legend = format_legend_entry(m_host_interface->GetStringSettingValue("Hotkeys", "SaveSelectedSaveState"),
|
||||
m_host_interface->TranslateStdString("SaveStateSelectorUI", "Save"));
|
||||
m_prev_legend = format_legend_entry(m_host_interface->GetStringSettingValue("Hotkeys", "SelectPreviousSaveStateSlot"),
|
||||
m_host_interface->TranslateStdString("SaveStateSelectorUI", "Select Previous"));
|
||||
m_next_legend = format_legend_entry(m_host_interface->GetStringSettingValue("Hotkeys", "SelectNextSaveStateSlot"),
|
||||
m_host_interface->TranslateStdString("SaveStateSelectorUI", "Select Next"));
|
||||
}
|
||||
|
||||
const char* SaveStateSelectorUI::GetSelectedStatePath() const
|
||||
{
|
||||
if (m_slots.empty() || m_slots[m_current_selection].path.empty())
|
||||
return nullptr;
|
||||
|
||||
return m_slots[m_current_selection].path.c_str();
|
||||
}
|
||||
|
||||
s32 SaveStateSelectorUI::GetSelectedStateSlot() const
|
||||
{
|
||||
if (m_slots.empty())
|
||||
return 0;
|
||||
|
||||
return m_slots[m_current_selection].slot;
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::SelectNextSlot()
|
||||
{
|
||||
if (!m_open)
|
||||
Open();
|
||||
|
||||
ResetOpenTimer();
|
||||
m_current_selection = (m_current_selection == static_cast<u32>(m_slots.size() - 1)) ? 0 : (m_current_selection + 1);
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::SelectPreviousSlot()
|
||||
{
|
||||
if (!m_open)
|
||||
Open();
|
||||
|
||||
ResetOpenTimer();
|
||||
m_current_selection =
|
||||
(m_current_selection == 0) ? (static_cast<u32>(m_slots.size()) - 1u) : (m_current_selection - 1);
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::InitializeListEntry(ListEntry* li, CommonHostInterface::ExtendedSaveStateInfo* ssi)
|
||||
{
|
||||
li->title = std::move(ssi->title);
|
||||
li->game_code = std::move(ssi->game_code);
|
||||
li->path = std::move(ssi->path);
|
||||
li->formatted_timestamp = fmt::format("{:%c}", fmt::localtime(ssi->timestamp));
|
||||
li->slot = ssi->slot;
|
||||
li->global = ssi->global;
|
||||
|
||||
li->preview_texture.reset();
|
||||
if (ssi && !ssi->screenshot_data.empty())
|
||||
{
|
||||
li->preview_texture = m_host_interface->GetDisplay()->CreateTexture(
|
||||
ssi->screenshot_width, ssi->screenshot_height, 1, 1, 1, HostDisplayPixelFormat::RGBA8,
|
||||
ssi->screenshot_data.data(), sizeof(u32) * ssi->screenshot_width, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
li->preview_texture = m_host_interface->GetDisplay()->CreateTexture(
|
||||
PLACEHOLDER_ICON_WIDTH, PLACEHOLDER_ICON_HEIGHT, 1, 1, 1, HostDisplayPixelFormat::RGBA8, PLACEHOLDER_ICON_DATA,
|
||||
sizeof(u32) * PLACEHOLDER_ICON_WIDTH, false);
|
||||
}
|
||||
|
||||
if (!li->preview_texture)
|
||||
Log_ErrorPrintf("Failed to upload save state image to GPU");
|
||||
}
|
||||
|
||||
std::pair<s32, bool> SaveStateSelectorUI::GetSlotTypeFromSelection(u32 selection) const
|
||||
{
|
||||
if (selection < CommonHostInterface::PER_GAME_SAVE_STATE_SLOTS)
|
||||
{
|
||||
return {selection + 1, false};
|
||||
}
|
||||
|
||||
return {selection - CommonHostInterface::PER_GAME_SAVE_STATE_SLOTS + 1, true};
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::InitializePlaceholderListEntry(ListEntry* li, s32 slot, bool global)
|
||||
{
|
||||
li->title = m_host_interface->TranslateStdString("SaveStateSelectorUI", "No Save State");
|
||||
std::string().swap(li->game_code);
|
||||
std::string().swap(li->path);
|
||||
std::string().swap(li->formatted_timestamp);
|
||||
li->slot = slot;
|
||||
li->global = global;
|
||||
|
||||
li->preview_texture = m_host_interface->GetDisplay()->CreateTexture(
|
||||
PLACEHOLDER_ICON_WIDTH, PLACEHOLDER_ICON_HEIGHT, 1, 1, 1, HostDisplayPixelFormat::RGBA8, PLACEHOLDER_ICON_DATA,
|
||||
sizeof(u32) * PLACEHOLDER_ICON_WIDTH, false);
|
||||
|
||||
if (!li->preview_texture)
|
||||
Log_ErrorPrintf("Failed to upload save state image to GPU");
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::Draw()
|
||||
{
|
||||
const float framebuffer_scale = ImGui::GetIO().DisplayFramebufferScale.x;
|
||||
const float window_width = ImGui::GetIO().DisplaySize.x * (2.0f / 3.0f);
|
||||
const float window_height = ImGui::GetIO().DisplaySize.y * 0.5f;
|
||||
const float rounding = 4.0f * framebuffer_scale;
|
||||
ImGui::SetNextWindowSize(ImVec2(window_width, window_height), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f, ImGui::GetIO().DisplaySize.y * 0.5f),
|
||||
ImGuiCond_Always, ImVec2(0.5f, 0.5f));
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.11f, 0.15f, 0.17f, 0.8f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, rounding);
|
||||
|
||||
if (ImGui::Begin("##save_state_selector", nullptr,
|
||||
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_NoScrollbar))
|
||||
{
|
||||
// Leave 2 lines for the legend
|
||||
const float legend_margin = ImGui::GetFontSize() * 2.0f + ImGui::GetStyle().ItemSpacing.y * 3.0f;
|
||||
const float padding = 10.0f * framebuffer_scale;
|
||||
|
||||
ImGui::BeginChild("##item_list", ImVec2(0, -legend_margin), false,
|
||||
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar);
|
||||
{
|
||||
const ImVec2 image_size = ImVec2(128.0f * framebuffer_scale, (128.0f / (4.0f / 3.0f)) * framebuffer_scale);
|
||||
const float item_height = image_size.y + padding * 2.0f;
|
||||
const float text_indent = image_size.x + padding + padding;
|
||||
|
||||
for (size_t i = 0; i < m_slots.size(); i++)
|
||||
{
|
||||
const ListEntry& entry = m_slots[i];
|
||||
const float y_start = item_height * static_cast<float>(i);
|
||||
|
||||
if (i == m_current_selection)
|
||||
{
|
||||
ImGui::SetCursorPosY(y_start);
|
||||
ImGui::SetScrollHereY();
|
||||
|
||||
const ImVec2 p_start(ImGui::GetCursorScreenPos());
|
||||
const ImVec2 p_end(p_start.x + window_width, p_start.y + item_height);
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(p_start, p_end, ImColor(0.22f, 0.30f, 0.34f, 0.9f), rounding);
|
||||
}
|
||||
|
||||
if (entry.preview_texture)
|
||||
{
|
||||
ImGui::SetCursorPosY(y_start + padding);
|
||||
ImGui::SetCursorPosX(padding);
|
||||
ImGui::Image(reinterpret_cast<ImTextureID>(entry.preview_texture->GetHandle()), image_size);
|
||||
}
|
||||
|
||||
ImGui::SetCursorPosY(y_start + padding);
|
||||
|
||||
ImGui::Indent(text_indent);
|
||||
|
||||
if (entry.global)
|
||||
{
|
||||
ImGui::Text(m_host_interface->TranslateString("SaveStateSelectorUI", "Global Slot %d"), entry.slot);
|
||||
}
|
||||
else if (entry.game_code.empty())
|
||||
{
|
||||
ImGui::Text(m_host_interface->TranslateString("SaveStateSelectorUI", "Game Slot %d"), entry.slot);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::Text(m_host_interface->TranslateString("SaveStateSelectorUI", "%s Slot %d"), entry.game_code.c_str(),
|
||||
entry.slot);
|
||||
}
|
||||
ImGui::TextUnformatted(entry.title.c_str());
|
||||
ImGui::TextUnformatted(entry.formatted_timestamp.c_str());
|
||||
ImGui::TextUnformatted(entry.path.c_str());
|
||||
|
||||
ImGui::Unindent(text_indent);
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::BeginChild("##legend", ImVec2(0, 0), false,
|
||||
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_NoScrollbar);
|
||||
{
|
||||
ImGui::SetCursorPosX(padding);
|
||||
ImGui::BeginTable("table", 2);
|
||||
|
||||
const bool hide_load_button = m_host_interface->IsCheevosChallengeModeActive();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(!hide_load_button ? m_load_legend.c_str() : m_save_legend.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(m_prev_legend.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
if (!hide_load_button)
|
||||
{
|
||||
ImGui::TextUnformatted(m_save_legend.c_str());
|
||||
}
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(m_next_legend.c_str());
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// auto-close
|
||||
if (m_open_timer.GetTimeSeconds() >= m_open_time)
|
||||
Close();
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::LoadCurrentSlot()
|
||||
{
|
||||
const auto slot_info = GetSlotTypeFromSelection(m_current_selection);
|
||||
m_host_interface->LoadState(slot_info.second, slot_info.first);
|
||||
Close();
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::SaveCurrentSlot()
|
||||
{
|
||||
const auto slot_info = GetSlotTypeFromSelection(m_current_selection);
|
||||
m_host_interface->SaveState(slot_info.second, slot_info.first);
|
||||
Close();
|
||||
}
|
||||
|
||||
} // namespace FrontendCommon
|
@ -1,71 +0,0 @@
|
||||
#pragma once
|
||||
#include "common/timer.h"
|
||||
#include "common_host_interface.h"
|
||||
#include <memory>
|
||||
|
||||
class HostDisplayTexture;
|
||||
|
||||
namespace FrontendCommon {
|
||||
|
||||
class SaveStateSelectorUI
|
||||
{
|
||||
public:
|
||||
static constexpr float DEFAULT_OPEN_TIME = 5.0f;
|
||||
|
||||
SaveStateSelectorUI(CommonHostInterface* host_interface);
|
||||
~SaveStateSelectorUI();
|
||||
|
||||
ALWAYS_INLINE bool IsOpen() const { return m_open; }
|
||||
ALWAYS_INLINE void ResetOpenTimer() { m_open_timer.Reset(); }
|
||||
|
||||
void Open(float open_time = DEFAULT_OPEN_TIME);
|
||||
void Close();
|
||||
|
||||
void ClearList();
|
||||
void RefreshList();
|
||||
|
||||
void RefreshHotkeyLegend();
|
||||
|
||||
const char* GetSelectedStatePath() const;
|
||||
s32 GetSelectedStateSlot() const;
|
||||
|
||||
void SelectNextSlot();
|
||||
void SelectPreviousSlot();
|
||||
|
||||
void Draw();
|
||||
|
||||
void LoadCurrentSlot();
|
||||
void SaveCurrentSlot();
|
||||
|
||||
private:
|
||||
struct ListEntry
|
||||
{
|
||||
std::string path;
|
||||
std::string game_code;
|
||||
std::string title;
|
||||
std::string formatted_timestamp;
|
||||
std::unique_ptr<HostDisplayTexture> preview_texture;
|
||||
s32 slot;
|
||||
bool global;
|
||||
};
|
||||
|
||||
void InitializePlaceholderListEntry(ListEntry* li, s32 slot, bool global);
|
||||
void InitializeListEntry(ListEntry* li, CommonHostInterface::ExtendedSaveStateInfo* ssi);
|
||||
std::pair<s32, bool> GetSlotTypeFromSelection(u32 selection) const;
|
||||
|
||||
std::string m_load_legend;
|
||||
std::string m_save_legend;
|
||||
std::string m_prev_legend;
|
||||
std::string m_next_legend;
|
||||
|
||||
CommonHostInterface* m_host_interface;
|
||||
std::vector<ListEntry> m_slots;
|
||||
u32 m_current_selection = 0;
|
||||
|
||||
Common::Timer m_open_timer;
|
||||
float m_open_time = 0.0f;
|
||||
|
||||
bool m_open = false;
|
||||
};
|
||||
|
||||
} // namespace FrontendCommon
|
@ -1,823 +0,0 @@
|
||||
#include "sdl_controller_interface.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/file_system.h"
|
||||
#include "common/log.h"
|
||||
#include "core/controller.h"
|
||||
#include "core/host_interface.h"
|
||||
#include "core/system.h"
|
||||
#include "sdl_initializer.h"
|
||||
#include <SDL.h>
|
||||
#include <cmath>
|
||||
Log_SetChannel(SDLControllerInterface);
|
||||
|
||||
SDLControllerInterface::SDLControllerInterface() = default;
|
||||
|
||||
SDLControllerInterface::~SDLControllerInterface()
|
||||
{
|
||||
Assert(m_controllers.empty());
|
||||
}
|
||||
|
||||
ControllerInterface::Backend SDLControllerInterface::GetBackend() const
|
||||
{
|
||||
return ControllerInterface::Backend::SDL;
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::Initialize(CommonHostInterface* host_interface)
|
||||
{
|
||||
if (!ControllerInterface::Initialize(host_interface))
|
||||
return false;
|
||||
|
||||
FrontendCommon::EnsureSDLInitialized();
|
||||
|
||||
const std::string gcdb_file_name = GetGameControllerDBFileName();
|
||||
if (!gcdb_file_name.empty())
|
||||
{
|
||||
Log_InfoPrintf("Loading game controller mappings from '%s'", gcdb_file_name.c_str());
|
||||
if (SDL_GameControllerAddMappingsFromFile(gcdb_file_name.c_str()) < 0)
|
||||
{
|
||||
Log_ErrorPrintf("SDL_GameControllerAddMappingsFromFile(%s) failed: %s", gcdb_file_name.c_str(), SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
const bool ds4_rumble_enabled = host_interface->GetBoolSettingValue("Main", "ControllerEnhancedMode", false);
|
||||
if (ds4_rumble_enabled)
|
||||
{
|
||||
Log_InfoPrintf("Enabling PS4/PS5 enhanced mode.");
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4, "true");
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "true");
|
||||
#endif
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 16)
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5, "true");
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "true");
|
||||
#endif
|
||||
}
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) < 0)
|
||||
{
|
||||
Log_ErrorPrintf("SDL_InitSubSystem(SDL_INIT_JOYSTICK |SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// we should open the controllers as the connected events come in, so no need to do any more here
|
||||
m_sdl_subsystem_initialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SDLControllerInterface::Shutdown()
|
||||
{
|
||||
while (!m_controllers.empty())
|
||||
CloseGameController(m_controllers.begin()->joystick_id, false);
|
||||
|
||||
if (m_sdl_subsystem_initialized)
|
||||
{
|
||||
SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC);
|
||||
m_sdl_subsystem_initialized = false;
|
||||
}
|
||||
|
||||
ControllerInterface::Shutdown();
|
||||
}
|
||||
|
||||
std::string SDLControllerInterface::GetGameControllerDBFileName() const
|
||||
{
|
||||
// prefer the userdir copy
|
||||
std::string filename(m_host_interface->GetUserDirectoryRelativePath("gamecontrollerdb.txt"));
|
||||
if (FileSystem::FileExists(filename.c_str()))
|
||||
return filename;
|
||||
|
||||
filename =
|
||||
m_host_interface->GetProgramDirectoryRelativePath("database" FS_OSPATH_SEPARATOR_STR "gamecontrollerdb.txt");
|
||||
if (FileSystem::FileExists(filename.c_str()))
|
||||
return filename;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void SDLControllerInterface::PollEvents()
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
SDL_Event ev;
|
||||
if (SDL_PollEvent(&ev))
|
||||
ProcessSDLEvent(&ev);
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::ProcessSDLEvent(const SDL_Event* event)
|
||||
{
|
||||
switch (event->type)
|
||||
{
|
||||
case SDL_CONTROLLERDEVICEADDED:
|
||||
{
|
||||
Log_InfoPrintf("Controller %d inserted", event->cdevice.which);
|
||||
OpenGameController(event->cdevice.which);
|
||||
return true;
|
||||
}
|
||||
|
||||
case SDL_CONTROLLERDEVICEREMOVED:
|
||||
{
|
||||
Log_InfoPrintf("Controller %d removed", event->cdevice.which);
|
||||
CloseGameController(event->cdevice.which, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
case SDL_CONTROLLERAXISMOTION:
|
||||
return HandleControllerAxisEvent(&event->caxis);
|
||||
|
||||
case SDL_CONTROLLERBUTTONDOWN:
|
||||
case SDL_CONTROLLERBUTTONUP:
|
||||
return HandleControllerButtonEvent(&event->cbutton);
|
||||
|
||||
case SDL_JOYDEVICEADDED:
|
||||
if (SDL_IsGameController(event->jdevice.which))
|
||||
return true;
|
||||
|
||||
Log_InfoPrintf("Joystick %d inserted", event->jdevice.which);
|
||||
OpenJoystick(event->jdevice.which);
|
||||
return true;
|
||||
|
||||
case SDL_JOYAXISMOTION:
|
||||
return HandleJoystickAxisEvent(&event->jaxis);
|
||||
|
||||
case SDL_JOYHATMOTION:
|
||||
return HandleJoystickHatEvent(&event->jhat);
|
||||
|
||||
case SDL_JOYBUTTONDOWN:
|
||||
case SDL_JOYBUTTONUP:
|
||||
return HandleJoystickButtonEvent(&event->jbutton);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
SDLControllerInterface::ControllerDataVector::iterator SDLControllerInterface::GetControllerDataForJoystickId(int id)
|
||||
{
|
||||
return std::find_if(m_controllers.begin(), m_controllers.end(),
|
||||
[id](const ControllerData& cd) { return cd.joystick_id == id; });
|
||||
}
|
||||
|
||||
SDLControllerInterface::ControllerDataVector::iterator SDLControllerInterface::GetControllerDataForPlayerId(int id)
|
||||
{
|
||||
return std::find_if(m_controllers.begin(), m_controllers.end(),
|
||||
[id](const ControllerData& cd) { return cd.player_id == id; });
|
||||
}
|
||||
|
||||
int SDLControllerInterface::GetFreePlayerId() const
|
||||
{
|
||||
for (int player_id = 0;; player_id++)
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < m_controllers.size(); i++)
|
||||
{
|
||||
if (m_controllers[i].player_id == player_id)
|
||||
break;
|
||||
}
|
||||
if (i == m_controllers.size())
|
||||
return player_id;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::OpenGameController(int index)
|
||||
{
|
||||
SDL_GameController* gcontroller = SDL_GameControllerOpen(index);
|
||||
SDL_Joystick* joystick = gcontroller ? SDL_GameControllerGetJoystick(gcontroller) : nullptr;
|
||||
if (!gcontroller || !joystick)
|
||||
{
|
||||
Log_WarningPrintf("Failed to open controller %d", index);
|
||||
if (gcontroller)
|
||||
SDL_GameControllerClose(gcontroller);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int joystick_id = SDL_JoystickInstanceID(joystick);
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||
int player_id = SDL_GameControllerGetPlayerIndex(gcontroller);
|
||||
#else
|
||||
int player_id = -1;
|
||||
#endif
|
||||
if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end())
|
||||
{
|
||||
const int free_player_id = GetFreePlayerId();
|
||||
Log_WarningPrintf(
|
||||
"Controller %d (joystick %d) returned player ID %d, which is invalid or in use. Using ID %d instead.", index,
|
||||
joystick_id, player_id, free_player_id);
|
||||
player_id = free_player_id;
|
||||
}
|
||||
|
||||
Log_InfoPrintf("Opened controller %d (instance id %d, player id %d): %s", index, joystick_id, player_id,
|
||||
SDL_GameControllerName(gcontroller));
|
||||
|
||||
ControllerData cd = {};
|
||||
cd.player_id = player_id;
|
||||
cd.joystick_id = joystick_id;
|
||||
cd.haptic_left_right_effect = -1;
|
||||
cd.game_controller = gcontroller;
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||
cd.use_game_controller_rumble = (SDL_GameControllerRumble(gcontroller, 0, 0, 0) == 0);
|
||||
#else
|
||||
cd.use_game_controller_rumble = false;
|
||||
#endif
|
||||
|
||||
if (cd.use_game_controller_rumble)
|
||||
{
|
||||
Log_InfoPrintf("Rumble is supported on '%s' via gamecontroller", SDL_GameControllerName(gcontroller));
|
||||
}
|
||||
else
|
||||
{
|
||||
SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick);
|
||||
if (haptic)
|
||||
{
|
||||
SDL_HapticEffect ef = {};
|
||||
ef.leftright.type = SDL_HAPTIC_LEFTRIGHT;
|
||||
ef.leftright.length = 1000;
|
||||
|
||||
int ef_id = SDL_HapticNewEffect(haptic, &ef);
|
||||
if (ef_id >= 0)
|
||||
{
|
||||
cd.haptic = haptic;
|
||||
cd.haptic_left_right_effect = ef_id;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log_ErrorPrintf("Failed to create haptic left/right effect: %s", SDL_GetError());
|
||||
if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0)
|
||||
{
|
||||
cd.haptic = haptic;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log_ErrorPrintf("No haptic rumble supported: %s", SDL_GetError());
|
||||
SDL_HapticClose(haptic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cd.haptic)
|
||||
Log_InfoPrintf("Rumble is supported on '%s' via haptic", SDL_GameControllerName(gcontroller));
|
||||
}
|
||||
|
||||
if (!cd.haptic && !cd.use_game_controller_rumble)
|
||||
Log_WarningPrintf("Rumble is not supported on '%s'", SDL_GameControllerName(gcontroller));
|
||||
|
||||
m_controllers.push_back(std::move(cd));
|
||||
OnControllerConnected(player_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::CloseGameController(int joystick_index, bool notify)
|
||||
{
|
||||
auto it = GetControllerDataForJoystickId(joystick_index);
|
||||
if (it == m_controllers.end())
|
||||
return false;
|
||||
|
||||
const int player_id = it->player_id;
|
||||
|
||||
if (it->haptic)
|
||||
SDL_HapticClose(static_cast<SDL_Haptic*>(it->haptic));
|
||||
|
||||
SDL_GameControllerClose(static_cast<SDL_GameController*>(it->game_controller));
|
||||
m_controllers.erase(it);
|
||||
|
||||
if (notify)
|
||||
OnControllerDisconnected(player_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::OpenJoystick(int index)
|
||||
{
|
||||
SDL_Joystick* joystick = SDL_JoystickOpen(index);
|
||||
if (!joystick)
|
||||
{
|
||||
Log_WarningPrintf("Failed to open joystick %d", index);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int joystick_id = SDL_JoystickInstanceID(joystick);
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||
int player_id = SDL_JoystickGetDevicePlayerIndex(index);
|
||||
#else
|
||||
int player_id = -1;
|
||||
#endif
|
||||
if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end())
|
||||
{
|
||||
const int free_player_id = GetFreePlayerId();
|
||||
Log_WarningPrintf(
|
||||
"Controller %d (joystick %d) returned player ID %d, which is invalid or in use. Using ID %d instead.", index,
|
||||
joystick_id, player_id, free_player_id);
|
||||
player_id = free_player_id;
|
||||
}
|
||||
|
||||
const char* name = SDL_JoystickName(joystick);
|
||||
|
||||
Log_InfoPrintf("Opened controller %d (instance id %d, player id %d): %s", index, joystick_id, player_id, name);
|
||||
|
||||
ControllerData cd = {};
|
||||
cd.player_id = player_id;
|
||||
cd.joystick_id = joystick_id;
|
||||
cd.haptic_left_right_effect = -1;
|
||||
cd.game_controller = nullptr;
|
||||
cd.use_game_controller_rumble = false;
|
||||
|
||||
SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick);
|
||||
if (haptic)
|
||||
{
|
||||
SDL_HapticEffect ef = {};
|
||||
ef.leftright.type = SDL_HAPTIC_LEFTRIGHT;
|
||||
ef.leftright.length = 1000;
|
||||
|
||||
int ef_id = SDL_HapticNewEffect(haptic, &ef);
|
||||
if (ef_id >= 0)
|
||||
{
|
||||
cd.haptic = haptic;
|
||||
cd.haptic_left_right_effect = ef_id;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log_ErrorPrintf("Failed to create haptic left/right effect: %s", SDL_GetError());
|
||||
if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0)
|
||||
{
|
||||
cd.haptic = haptic;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log_ErrorPrintf("No haptic rumble supported: %s", SDL_GetError());
|
||||
SDL_HapticClose(haptic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cd.haptic)
|
||||
Log_InfoPrintf("Rumble is supported on '%s'", name);
|
||||
else
|
||||
Log_WarningPrintf("Rumble is not supported on '%s'", name);
|
||||
|
||||
m_controllers.push_back(std::move(cd));
|
||||
OnControllerConnected(player_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::HandleJoystickAxisEvent(const SDL_JoyAxisEvent* event)
|
||||
{
|
||||
const float value = static_cast<float>(event->value) / (event->value < 0 ? 32768.0f : 32767.0f);
|
||||
Log_DebugPrintf("controller %d axis %d %d %f", event->which, event->axis, event->value, value);
|
||||
|
||||
auto it = GetControllerDataForJoystickId(event->which);
|
||||
if (it == m_controllers.end() || it->IsGameController())
|
||||
return false;
|
||||
|
||||
if (DoEventHook(Hook::Type::Axis, it->player_id, event->axis, value, true))
|
||||
return true;
|
||||
|
||||
bool processed = false;
|
||||
|
||||
const AxisCallback& cb = it->axis_mapping[event->axis][AxisSide::Full];
|
||||
if (cb)
|
||||
{
|
||||
cb(value);
|
||||
processed = true;
|
||||
}
|
||||
|
||||
if (value > 0.0f)
|
||||
{
|
||||
const AxisCallback& hcb = it->axis_mapping[event->axis][AxisSide::Positive];
|
||||
if (hcb)
|
||||
{
|
||||
hcb(value);
|
||||
processed = true;
|
||||
}
|
||||
}
|
||||
else if (value < 0.0f)
|
||||
{
|
||||
const AxisCallback& hcb = it->axis_mapping[event->axis][AxisSide::Negative];
|
||||
if (hcb)
|
||||
{
|
||||
hcb(value);
|
||||
processed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (processed)
|
||||
return true;
|
||||
|
||||
// set the other direction to false so large movements don't leave the opposite on
|
||||
const bool outside_deadzone = (std::abs(value) >= it->deadzone);
|
||||
const bool positive = (value >= 0.0f);
|
||||
const ButtonCallback& other_button_cb = it->axis_button_mapping[event->axis][BoolToUInt8(!positive)];
|
||||
const ButtonCallback& button_cb = it->axis_button_mapping[event->axis][BoolToUInt8(positive)];
|
||||
if (button_cb)
|
||||
{
|
||||
button_cb(outside_deadzone);
|
||||
if (other_button_cb)
|
||||
other_button_cb(false);
|
||||
return true;
|
||||
}
|
||||
else if (other_button_cb)
|
||||
{
|
||||
other_button_cb(false);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::HandleJoystickButtonEvent(const SDL_JoyButtonEvent* event)
|
||||
{
|
||||
Log_DebugPrintf("controller %d button %d %s", event->which, event->button,
|
||||
event->state == SDL_PRESSED ? "pressed" : "released");
|
||||
|
||||
auto it = GetControllerDataForJoystickId(event->which);
|
||||
if (it == m_controllers.end() || it->IsGameController())
|
||||
return false;
|
||||
|
||||
const bool pressed = (event->state == SDL_PRESSED);
|
||||
if (DoEventHook(Hook::Type::Button, it->player_id, event->button, pressed ? 1.0f : 0.0f))
|
||||
return true;
|
||||
|
||||
const ButtonCallback& cb = it->button_mapping[event->button];
|
||||
if (cb)
|
||||
{
|
||||
cb(pressed);
|
||||
return true;
|
||||
}
|
||||
|
||||
const AxisCallback& axis_cb = it->button_axis_mapping[event->button];
|
||||
if (axis_cb)
|
||||
{
|
||||
axis_cb(pressed ? 1.0f : -1.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::HandleJoystickHatEvent(const SDL_JoyHatEvent* event)
|
||||
{
|
||||
Log_DebugPrintf("controller %d hat %d %d", event->which, event->hat, event->value);
|
||||
|
||||
auto it = GetControllerDataForJoystickId(event->which);
|
||||
if (it == m_controllers.end() || it->IsGameController())
|
||||
return false;
|
||||
|
||||
auto HatEventHook = [hat = event->hat, value = event->value, player_id = it->player_id, this](int hat_position) {
|
||||
if ((value & hat_position) == 0)
|
||||
return false;
|
||||
|
||||
std::string_view position_str;
|
||||
switch (value)
|
||||
{
|
||||
case SDL_HAT_UP:
|
||||
position_str = "Up";
|
||||
break;
|
||||
case SDL_HAT_RIGHT:
|
||||
position_str = "Right";
|
||||
break;
|
||||
case SDL_HAT_DOWN:
|
||||
position_str = "Down";
|
||||
break;
|
||||
case SDL_HAT_LEFT:
|
||||
position_str = "Left";
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return DoEventHook(Hook::Type::Hat, player_id, hat, position_str);
|
||||
};
|
||||
|
||||
if (event->value == SDL_HAT_CENTERED)
|
||||
{
|
||||
if (HatEventHook(SDL_HAT_CENTERED))
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// event->value can be a bitmask of multiple direction, so probe them all
|
||||
if (HatEventHook(SDL_HAT_UP) || HatEventHook(SDL_HAT_RIGHT) || HatEventHook(SDL_HAT_DOWN) ||
|
||||
HatEventHook(SDL_HAT_LEFT))
|
||||
return true;
|
||||
}
|
||||
|
||||
bool processed = false;
|
||||
|
||||
if (event->hat < it->hat_button_mapping.size())
|
||||
{
|
||||
if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][0]; cb)
|
||||
{
|
||||
cb(event->value & SDL_HAT_UP);
|
||||
processed = true;
|
||||
}
|
||||
if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][1]; cb)
|
||||
{
|
||||
cb(event->value & SDL_HAT_RIGHT);
|
||||
processed = true;
|
||||
}
|
||||
if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][2]; cb)
|
||||
{
|
||||
cb(event->value & SDL_HAT_DOWN);
|
||||
processed = true;
|
||||
}
|
||||
if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][3]; cb)
|
||||
{
|
||||
cb(event->value & SDL_HAT_LEFT);
|
||||
processed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
void SDLControllerInterface::ClearBindings()
|
||||
{
|
||||
for (auto& it : m_controllers)
|
||||
{
|
||||
it.axis_mapping.fill({});
|
||||
it.button_mapping.fill({});
|
||||
it.axis_button_mapping.fill({});
|
||||
it.button_axis_mapping.fill({});
|
||||
it.hat_button_mapping.clear();
|
||||
}
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side,
|
||||
AxisCallback callback)
|
||||
{
|
||||
auto it = GetControllerDataForPlayerId(controller_index);
|
||||
if (it == m_controllers.end())
|
||||
return false;
|
||||
|
||||
if (axis_number < 0 || axis_number >= MAX_NUM_AXES)
|
||||
return false;
|
||||
|
||||
it->axis_mapping[axis_number][axis_side] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::BindControllerButton(int controller_index, int button_number, ButtonCallback callback)
|
||||
{
|
||||
auto it = GetControllerDataForPlayerId(controller_index);
|
||||
if (it == m_controllers.end())
|
||||
return false;
|
||||
|
||||
if (button_number < 0 || button_number >= MAX_NUM_BUTTONS)
|
||||
return false;
|
||||
|
||||
it->button_mapping[button_number] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||
ButtonCallback callback)
|
||||
{
|
||||
auto it = GetControllerDataForPlayerId(controller_index);
|
||||
if (it == m_controllers.end())
|
||||
return false;
|
||||
|
||||
if (axis_number < 0 || axis_number >= MAX_NUM_AXES)
|
||||
return false;
|
||||
|
||||
it->axis_button_mapping[axis_number][BoolToUInt8(direction)] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::BindControllerHatToButton(int controller_index, int hat_number,
|
||||
std::string_view hat_position, ButtonCallback callback)
|
||||
{
|
||||
auto it = GetControllerDataForPlayerId(controller_index);
|
||||
if (it == m_controllers.end())
|
||||
return false;
|
||||
|
||||
size_t index;
|
||||
if (hat_position == "Up")
|
||||
index = 0;
|
||||
else if (hat_position == "Right")
|
||||
index = 1;
|
||||
else if (hat_position == "Down")
|
||||
index = 2;
|
||||
else if (hat_position == "Left")
|
||||
index = 3;
|
||||
else
|
||||
return false;
|
||||
|
||||
// We need 4 entries per hat_number
|
||||
if (static_cast<int>(it->hat_button_mapping.size()) < hat_number + 1)
|
||||
it->hat_button_mapping.resize(hat_number + 1);
|
||||
|
||||
it->hat_button_mapping[hat_number][index] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback)
|
||||
{
|
||||
auto it = GetControllerDataForPlayerId(controller_index);
|
||||
if (it == m_controllers.end())
|
||||
return false;
|
||||
|
||||
if (button_number < 0 || button_number >= MAX_NUM_BUTTONS)
|
||||
return false;
|
||||
|
||||
it->button_axis_mapping[button_number] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev)
|
||||
{
|
||||
const float value = static_cast<float>(ev->value) / (ev->value < 0 ? 32768.0f : 32767.0f);
|
||||
Log_DebugPrintf("controller %d axis %d %d %f", ev->which, ev->axis, ev->value, value);
|
||||
|
||||
auto it = GetControllerDataForJoystickId(ev->which);
|
||||
if (it == m_controllers.end())
|
||||
return false;
|
||||
|
||||
if (DoEventHook(Hook::Type::Axis, it->player_id, ev->axis, value))
|
||||
return true;
|
||||
|
||||
if (ev->axis >= MAX_NUM_AXES)
|
||||
return false;
|
||||
|
||||
const AxisCallback& cb = it->axis_mapping[ev->axis][AxisSide::Full];
|
||||
if (cb)
|
||||
{
|
||||
cb(value);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
const AxisCallback& positive_cb = it->axis_mapping[ev->axis][AxisSide::Positive];
|
||||
const AxisCallback& negative_cb = it->axis_mapping[ev->axis][AxisSide::Negative];
|
||||
if (positive_cb || negative_cb)
|
||||
{
|
||||
if (positive_cb)
|
||||
positive_cb((value < 0.0f) ? 0.0f : value);
|
||||
if (negative_cb)
|
||||
negative_cb((value >= 0.0f) ? 0.0f : -value);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// set the other direction to false so large movements don't leave the opposite on
|
||||
const bool outside_deadzone = (std::abs(value) >= it->deadzone);
|
||||
const bool positive = (value >= 0.0f);
|
||||
const ButtonCallback& other_button_cb = it->axis_button_mapping[ev->axis][BoolToUInt8(!positive)];
|
||||
const ButtonCallback& button_cb = it->axis_button_mapping[ev->axis][BoolToUInt8(positive)];
|
||||
if (button_cb)
|
||||
{
|
||||
button_cb(outside_deadzone);
|
||||
if (other_button_cb)
|
||||
other_button_cb(false);
|
||||
return true;
|
||||
}
|
||||
else if (other_button_cb)
|
||||
{
|
||||
other_button_cb(false);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::HandleControllerButtonEvent(const SDL_ControllerButtonEvent* ev)
|
||||
{
|
||||
Log_DebugPrintf("controller %d button %d %s", ev->which, ev->button,
|
||||
ev->state == SDL_PRESSED ? "pressed" : "released");
|
||||
|
||||
auto it = GetControllerDataForJoystickId(ev->which);
|
||||
if (it == m_controllers.end())
|
||||
return false;
|
||||
|
||||
static constexpr std::array<FrontendCommon::ControllerNavigationButton, SDL_CONTROLLER_BUTTON_MAX>
|
||||
nav_button_mapping = {{
|
||||
FrontendCommon::ControllerNavigationButton::Activate, // SDL_CONTROLLER_BUTTON_A
|
||||
FrontendCommon::ControllerNavigationButton::Cancel, // SDL_CONTROLLER_BUTTON_B
|
||||
FrontendCommon::ControllerNavigationButton::Count, // SDL_CONTROLLER_BUTTON_X
|
||||
FrontendCommon::ControllerNavigationButton::Count, // SDL_CONTROLLER_BUTTON_Y
|
||||
FrontendCommon::ControllerNavigationButton::Count, // SDL_CONTROLLER_BUTTON_BACK
|
||||
FrontendCommon::ControllerNavigationButton::Count, // SDL_CONTROLLER_BUTTON_GUIDE
|
||||
FrontendCommon::ControllerNavigationButton::Count, // SDL_CONTROLLER_BUTTON_START
|
||||
FrontendCommon::ControllerNavigationButton::Count, // SDL_CONTROLLER_BUTTON_LEFTSTICK
|
||||
FrontendCommon::ControllerNavigationButton::Count, // SDL_CONTROLLER_BUTTON_RIGHTSTICK
|
||||
FrontendCommon::ControllerNavigationButton::LeftShoulder, // SDL_CONTROLLER_BUTTON_LEFTSHOULDER
|
||||
FrontendCommon::ControllerNavigationButton::RightShoulder, // SDL_CONTROLLER_BUTTON_RIGHTSHOULDER
|
||||
FrontendCommon::ControllerNavigationButton::DPadUp, // SDL_CONTROLLER_BUTTON_DPAD_UP
|
||||
FrontendCommon::ControllerNavigationButton::DPadDown, // SDL_CONTROLLER_BUTTON_DPAD_DOWN
|
||||
FrontendCommon::ControllerNavigationButton::DPadLeft, // SDL_CONTROLLER_BUTTON_DPAD_LEFT
|
||||
FrontendCommon::ControllerNavigationButton::DPadRight, // SDL_CONTROLLER_BUTTON_DPAD_RIGHT
|
||||
}};
|
||||
|
||||
const bool pressed = (ev->state == SDL_PRESSED);
|
||||
if (DoEventHook(Hook::Type::Button, it->player_id, ev->button, pressed ? 1.0f : 0.0f))
|
||||
return true;
|
||||
|
||||
if (ev->button < nav_button_mapping.size() &&
|
||||
nav_button_mapping[ev->button] != FrontendCommon::ControllerNavigationButton::Count)
|
||||
{
|
||||
m_host_interface->SetControllerNavigationButtonState(nav_button_mapping[ev->button], pressed);
|
||||
}
|
||||
|
||||
if (m_host_interface->IsControllerNavigationActive())
|
||||
{
|
||||
// UI consumed the event
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ev->button >= MAX_NUM_BUTTONS)
|
||||
return false;
|
||||
|
||||
const ButtonCallback& cb = it->button_mapping[ev->button];
|
||||
if (cb)
|
||||
{
|
||||
cb(pressed);
|
||||
return true;
|
||||
}
|
||||
|
||||
const AxisCallback& axis_cb = it->button_axis_mapping[ev->button];
|
||||
if (axis_cb)
|
||||
{
|
||||
axis_cb(pressed ? 1.0f : -1.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
u32 SDLControllerInterface::GetControllerRumbleMotorCount(int controller_index)
|
||||
{
|
||||
auto it = GetControllerDataForPlayerId(controller_index);
|
||||
if (it == m_controllers.end())
|
||||
return 0;
|
||||
|
||||
return (it->use_game_controller_rumble ? 2 : ((it->haptic_left_right_effect >= 0) ? 2 : (it->haptic ? 1 : 0)));
|
||||
}
|
||||
|
||||
void SDLControllerInterface::SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors)
|
||||
{
|
||||
auto it = GetControllerDataForPlayerId(controller_index);
|
||||
if (it == m_controllers.end())
|
||||
return;
|
||||
|
||||
// we'll update before this duration is elapsed
|
||||
static constexpr u32 DURATION = 65535; // SDL_MAX_RUMBLE_DURATION_MS
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||
if (it->use_game_controller_rumble)
|
||||
{
|
||||
const u16 large = static_cast<u16>(strengths[0] * 65535.0f);
|
||||
const u16 small = static_cast<u16>(strengths[1] * 65535.0f);
|
||||
SDL_GameControllerRumble(static_cast<SDL_GameController*>(it->game_controller), large, small, DURATION);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
SDL_Haptic* haptic = static_cast<SDL_Haptic*>(it->haptic);
|
||||
if (it->haptic_left_right_effect >= 0 && num_motors > 1)
|
||||
{
|
||||
if (strengths[0] > 0.0f || strengths[1] > 0.0f)
|
||||
{
|
||||
SDL_HapticEffect ef;
|
||||
ef.type = SDL_HAPTIC_LEFTRIGHT;
|
||||
ef.leftright.large_magnitude = static_cast<u16>(strengths[0] * 65535.0f);
|
||||
ef.leftright.small_magnitude = static_cast<u16>(strengths[1] * 65535.0f);
|
||||
ef.leftright.length = DURATION;
|
||||
SDL_HapticUpdateEffect(haptic, it->haptic_left_right_effect, &ef);
|
||||
SDL_HapticRunEffect(haptic, it->haptic_left_right_effect, SDL_HAPTIC_INFINITY);
|
||||
}
|
||||
else
|
||||
{
|
||||
SDL_HapticStopEffect(haptic, it->haptic_left_right_effect);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float max_strength = 0.0f;
|
||||
for (u32 i = 0; i < num_motors; i++)
|
||||
max_strength = std::max(max_strength, strengths[i]);
|
||||
|
||||
if (max_strength > 0.0f)
|
||||
SDL_HapticRumblePlay(haptic, max_strength, DURATION);
|
||||
else
|
||||
SDL_HapticRumbleStop(haptic);
|
||||
}
|
||||
}
|
||||
|
||||
bool SDLControllerInterface::SetControllerDeadzone(int controller_index, float size /* = 0.25f */)
|
||||
{
|
||||
auto it = GetControllerDataForPlayerId(controller_index);
|
||||
if (it == m_controllers.end())
|
||||
return false;
|
||||
|
||||
it->deadzone = std::clamp(std::abs(size), 0.01f, 0.99f);
|
||||
Log_InfoPrintf("Controller %d deadzone size set to %f", controller_index, it->deadzone);
|
||||
return true;
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
#pragma once
|
||||
#include "controller_interface.h"
|
||||
#include "core/types.h"
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
class SDLControllerInterface final : public ControllerInterface
|
||||
{
|
||||
public:
|
||||
SDLControllerInterface();
|
||||
~SDLControllerInterface();
|
||||
|
||||
Backend GetBackend() const override;
|
||||
bool Initialize(CommonHostInterface* host_interface) override;
|
||||
void Shutdown() override;
|
||||
|
||||
/// Returns the path of the optional game controller database file.
|
||||
std::string GetGameControllerDBFileName() const;
|
||||
|
||||
// Removes all bindings. Call before setting new bindings.
|
||||
void ClearBindings() override;
|
||||
|
||||
// Binding to events. If a binding for this axis/button already exists, returns false.
|
||||
bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) override;
|
||||
bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override;
|
||||
bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||
ButtonCallback callback) override;
|
||||
bool BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position,
|
||||
ButtonCallback callback) override;
|
||||
bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override;
|
||||
|
||||
// Changing rumble strength.
|
||||
u32 GetControllerRumbleMotorCount(int controller_index) override;
|
||||
void SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors) override;
|
||||
|
||||
// Set deadzone that will be applied on axis-to-button mappings
|
||||
bool SetControllerDeadzone(int controller_index, float size = 0.25f) override;
|
||||
|
||||
void PollEvents() override;
|
||||
|
||||
bool ProcessSDLEvent(const union SDL_Event* event);
|
||||
|
||||
private:
|
||||
enum : int
|
||||
{
|
||||
MAX_NUM_AXES = 7,
|
||||
MAX_NUM_BUTTONS = 16,
|
||||
};
|
||||
|
||||
struct ControllerData
|
||||
{
|
||||
void* haptic;
|
||||
void* game_controller;
|
||||
int haptic_left_right_effect;
|
||||
int joystick_id;
|
||||
int player_id;
|
||||
bool use_game_controller_rumble;
|
||||
|
||||
float deadzone = 0.25f;
|
||||
|
||||
// TODO: Turn to vectors to support arbitrary amounts of buttons and axes (for Joysticks)
|
||||
// Preferably implement a simple "flat map", an ordered view over a vector
|
||||
std::array<std::array<AxisCallback, 3>, MAX_NUM_AXES> axis_mapping;
|
||||
std::array<ButtonCallback, MAX_NUM_BUTTONS> button_mapping;
|
||||
std::array<std::array<ButtonCallback, 2>, MAX_NUM_AXES> axis_button_mapping;
|
||||
std::array<AxisCallback, MAX_NUM_BUTTONS> button_axis_mapping;
|
||||
std::vector<std::array<ButtonCallback, 4>> hat_button_mapping;
|
||||
|
||||
ALWAYS_INLINE bool IsGameController() const { return (game_controller != nullptr); }
|
||||
};
|
||||
|
||||
using ControllerDataVector = std::vector<ControllerData>;
|
||||
|
||||
ControllerDataVector::iterator GetControllerDataForJoystickId(int id);
|
||||
ControllerDataVector::iterator GetControllerDataForPlayerId(int id);
|
||||
int GetFreePlayerId() const;
|
||||
|
||||
bool OpenGameController(int index);
|
||||
bool CloseGameController(int joystick_index, bool notify);
|
||||
bool HandleControllerAxisEvent(const struct SDL_ControllerAxisEvent* event);
|
||||
bool HandleControllerButtonEvent(const struct SDL_ControllerButtonEvent* event);
|
||||
|
||||
bool OpenJoystick(int index);
|
||||
bool HandleJoystickAxisEvent(const struct SDL_JoyAxisEvent* event);
|
||||
bool HandleJoystickButtonEvent(const struct SDL_JoyButtonEvent* event);
|
||||
bool HandleJoystickHatEvent(const struct SDL_JoyHatEvent* event);
|
||||
|
||||
ControllerDataVector m_controllers;
|
||||
|
||||
std::mutex m_event_intercept_mutex;
|
||||
Hook::Callback m_event_intercept_callback;
|
||||
|
||||
bool m_sdl_subsystem_initialized = false;
|
||||
};
|
655
src/frontend-common/sdl_input_source.cpp
Normal file
655
src/frontend-common/sdl_input_source.cpp
Normal file
@ -0,0 +1,655 @@
|
||||
#include "sdl_input_source.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/host.h"
|
||||
#include "core/host_settings.h"
|
||||
#include "input_manager.h"
|
||||
#include <cmath>
|
||||
#ifdef __APPLE__
|
||||
#include <dispatch/dispatch.h>
|
||||
#endif
|
||||
Log_SetChannel(SDLInputSource);
|
||||
|
||||
static const char* s_sdl_axis_names[] = {
|
||||
"LeftX", // SDL_CONTROLLER_AXIS_LEFTX
|
||||
"LeftY", // SDL_CONTROLLER_AXIS_LEFTY
|
||||
"RightX", // SDL_CONTROLLER_AXIS_RIGHTX
|
||||
"RightY", // SDL_CONTROLLER_AXIS_RIGHTY
|
||||
"LeftTrigger", // SDL_CONTROLLER_AXIS_TRIGGERLEFT
|
||||
"RightTrigger", // SDL_CONTROLLER_AXIS_TRIGGERRIGHT
|
||||
};
|
||||
static const GenericInputBinding s_sdl_generic_binding_axis_mapping[][2] = {
|
||||
{GenericInputBinding::LeftStickLeft, GenericInputBinding::LeftStickRight}, // SDL_CONTROLLER_AXIS_LEFTX
|
||||
{GenericInputBinding::LeftStickUp, GenericInputBinding::LeftStickDown}, // SDL_CONTROLLER_AXIS_LEFTY
|
||||
{GenericInputBinding::RightStickLeft, GenericInputBinding::RightStickRight}, // SDL_CONTROLLER_AXIS_RIGHTX
|
||||
{GenericInputBinding::RightStickUp, GenericInputBinding::RightStickDown}, // SDL_CONTROLLER_AXIS_RIGHTY
|
||||
{GenericInputBinding::Unknown, GenericInputBinding::L2}, // SDL_CONTROLLER_AXIS_TRIGGERLEFT
|
||||
{GenericInputBinding::Unknown, GenericInputBinding::R2}, // SDL_CONTROLLER_AXIS_TRIGGERRIGHT
|
||||
};
|
||||
|
||||
static const char* s_sdl_button_names[] = {
|
||||
"A", // SDL_CONTROLLER_BUTTON_A
|
||||
"B", // SDL_CONTROLLER_BUTTON_B
|
||||
"X", // SDL_CONTROLLER_BUTTON_X
|
||||
"Y", // SDL_CONTROLLER_BUTTON_Y
|
||||
"Back", // SDL_CONTROLLER_BUTTON_BACK
|
||||
"Guide", // SDL_CONTROLLER_BUTTON_GUIDE
|
||||
"Start", // SDL_CONTROLLER_BUTTON_START
|
||||
"LeftStick", // SDL_CONTROLLER_BUTTON_LEFTSTICK
|
||||
"RightStick", // SDL_CONTROLLER_BUTTON_RIGHTSTICK
|
||||
"LeftShoulder", // SDL_CONTROLLER_BUTTON_LEFTSHOULDER
|
||||
"RightShoulder", // SDL_CONTROLLER_BUTTON_RIGHTSHOULDER
|
||||
"DPadUp", // SDL_CONTROLLER_BUTTON_DPAD_UP
|
||||
"DPadDown", // SDL_CONTROLLER_BUTTON_DPAD_DOWN
|
||||
"DPadLeft", // SDL_CONTROLLER_BUTTON_DPAD_LEFT
|
||||
"DPadRight", // SDL_CONTROLLER_BUTTON_DPAD_RIGHT
|
||||
"Misc1", // SDL_CONTROLLER_BUTTON_MISC1
|
||||
"Paddle1", // SDL_CONTROLLER_BUTTON_PADDLE1
|
||||
"Paddle2", // SDL_CONTROLLER_BUTTON_PADDLE2
|
||||
"Paddle3", // SDL_CONTROLLER_BUTTON_PADDLE3
|
||||
"Paddle4", // SDL_CONTROLLER_BUTTON_PADDLE4
|
||||
"Touchpad", // SDL_CONTROLLER_BUTTON_TOUCHPAD
|
||||
};
|
||||
static const GenericInputBinding s_sdl_generic_binding_button_mapping[] = {
|
||||
GenericInputBinding::Cross, // SDL_CONTROLLER_BUTTON_A
|
||||
GenericInputBinding::Circle, // SDL_CONTROLLER_BUTTON_B
|
||||
GenericInputBinding::Square, // SDL_CONTROLLER_BUTTON_X
|
||||
GenericInputBinding::Triangle, // SDL_CONTROLLER_BUTTON_Y
|
||||
GenericInputBinding::Select, // SDL_CONTROLLER_BUTTON_BACK
|
||||
GenericInputBinding::System, // SDL_CONTROLLER_BUTTON_GUIDE
|
||||
GenericInputBinding::Start, // SDL_CONTROLLER_BUTTON_START
|
||||
GenericInputBinding::L3, // SDL_CONTROLLER_BUTTON_LEFTSTICK
|
||||
GenericInputBinding::R3, // SDL_CONTROLLER_BUTTON_RIGHTSTICK
|
||||
GenericInputBinding::L1, // SDL_CONTROLLER_BUTTON_LEFTSHOULDER
|
||||
GenericInputBinding::R1, // SDL_CONTROLLER_BUTTON_RIGHTSHOULDER
|
||||
GenericInputBinding::DPadUp, // SDL_CONTROLLER_BUTTON_DPAD_UP
|
||||
GenericInputBinding::DPadDown, // SDL_CONTROLLER_BUTTON_DPAD_DOWN
|
||||
GenericInputBinding::DPadLeft, // SDL_CONTROLLER_BUTTON_DPAD_LEFT
|
||||
GenericInputBinding::DPadRight, // SDL_CONTROLLER_BUTTON_DPAD_RIGHT
|
||||
GenericInputBinding::Unknown, // SDL_CONTROLLER_BUTTON_MISC1
|
||||
GenericInputBinding::Unknown, // SDL_CONTROLLER_BUTTON_PADDLE1
|
||||
GenericInputBinding::Unknown, // SDL_CONTROLLER_BUTTON_PADDLE2
|
||||
GenericInputBinding::Unknown, // SDL_CONTROLLER_BUTTON_PADDLE3
|
||||
GenericInputBinding::Unknown, // SDL_CONTROLLER_BUTTON_PADDLE4
|
||||
GenericInputBinding::Unknown, // SDL_CONTROLLER_BUTTON_TOUCHPAD
|
||||
};
|
||||
|
||||
SDLInputSource::SDLInputSource() = default;
|
||||
|
||||
SDLInputSource::~SDLInputSource()
|
||||
{
|
||||
DebugAssert(m_controllers.empty());
|
||||
}
|
||||
|
||||
bool SDLInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
|
||||
{
|
||||
std::optional<std::vector<u8>> controller_db_data = Host::ReadResourceFile("gamecontrollerdb.txt");
|
||||
if (controller_db_data.has_value())
|
||||
{
|
||||
SDL_RWops* ops = SDL_RWFromConstMem(controller_db_data->data(), static_cast<int>(controller_db_data->size()));
|
||||
if (SDL_GameControllerAddMappingsFromRW(ops, true) < 0)
|
||||
Log_ErrorPrintf("SDL_GameControllerAddMappingsFromRW() failed: %s", SDL_GetError());
|
||||
}
|
||||
else
|
||||
{
|
||||
Log_ErrorPrintf("Controller database resource is missing.");
|
||||
}
|
||||
|
||||
LoadSettings(si);
|
||||
settings_lock.unlock();
|
||||
SetHints();
|
||||
bool result = InitializeSubsystem();
|
||||
settings_lock.lock();
|
||||
return result;
|
||||
}
|
||||
|
||||
void SDLInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
|
||||
{
|
||||
const bool old_controller_enhanced_mode = m_controller_enhanced_mode;
|
||||
|
||||
LoadSettings(si);
|
||||
|
||||
if (m_controller_enhanced_mode != old_controller_enhanced_mode)
|
||||
{
|
||||
settings_lock.unlock();
|
||||
ShutdownSubsystem();
|
||||
SetHints();
|
||||
InitializeSubsystem();
|
||||
settings_lock.lock();
|
||||
}
|
||||
}
|
||||
|
||||
void SDLInputSource::Shutdown()
|
||||
{
|
||||
ShutdownSubsystem();
|
||||
}
|
||||
|
||||
void SDLInputSource::LoadSettings(SettingsInterface& si)
|
||||
{
|
||||
m_controller_enhanced_mode = si.GetBoolValue("InputSources", "SDLControllerEnhancedMode", false);
|
||||
}
|
||||
|
||||
void SDLInputSource::SetHints()
|
||||
{
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, m_controller_enhanced_mode ? "1" : "0");
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, m_controller_enhanced_mode ? "1" : "0");
|
||||
}
|
||||
|
||||
bool SDLInputSource::InitializeSubsystem()
|
||||
{
|
||||
int result;
|
||||
#ifdef __APPLE__
|
||||
// On macOS, SDL_InitSubSystem runs a main-thread-only call to some GameController framework method
|
||||
// So send this to be run on the main thread
|
||||
dispatch_sync_f(dispatch_get_main_queue(), &result, [](void* ctx) {
|
||||
*static_cast<int*>(ctx) = SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC);
|
||||
});
|
||||
#else
|
||||
result = SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC);
|
||||
#endif
|
||||
if (result < 0)
|
||||
{
|
||||
Log_ErrorPrintf("SDL_InitSubSystem(SDL_INIT_JOYSTICK |SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// we should open the controllers as the connected events come in, so no need to do any more here
|
||||
m_sdl_subsystem_initialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SDLInputSource::ShutdownSubsystem()
|
||||
{
|
||||
while (!m_controllers.empty())
|
||||
CloseGameController(m_controllers.begin()->joystick_id);
|
||||
|
||||
if (m_sdl_subsystem_initialized)
|
||||
{
|
||||
#ifdef __APPLE__
|
||||
dispatch_sync_f(dispatch_get_main_queue(), nullptr,
|
||||
[](void*) { SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC); });
|
||||
#else
|
||||
SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC);
|
||||
#endif
|
||||
m_sdl_subsystem_initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
void SDLInputSource::PollEvents()
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
SDL_Event ev;
|
||||
if (SDL_PollEvent(&ev))
|
||||
ProcessSDLEvent(&ev);
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> SDLInputSource::EnumerateDevices()
|
||||
{
|
||||
std::vector<std::pair<std::string, std::string>> ret;
|
||||
|
||||
for (const ControllerData& cd : m_controllers)
|
||||
{
|
||||
std::string id(StringUtil::StdStringFromFormat("SDL-%d", cd.player_id));
|
||||
|
||||
const char* name = SDL_GameControllerName(cd.game_controller);
|
||||
if (name)
|
||||
ret.emplace_back(std::move(id), name);
|
||||
else
|
||||
ret.emplace_back(std::move(id), "Unknown Device");
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::optional<InputBindingKey> SDLInputSource::ParseKeyString(const std::string_view& device,
|
||||
const std::string_view& binding)
|
||||
{
|
||||
if (!StringUtil::StartsWith(device, "SDL-") || binding.empty())
|
||||
return std::nullopt;
|
||||
|
||||
const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(4));
|
||||
if (!player_id.has_value() || player_id.value() < 0)
|
||||
return std::nullopt;
|
||||
|
||||
InputBindingKey key = {};
|
||||
key.source_type = InputSourceType::SDL;
|
||||
key.source_index = static_cast<u32>(player_id.value());
|
||||
|
||||
if (StringUtil::EndsWith(binding, "Motor"))
|
||||
{
|
||||
key.source_subtype = InputSubclass::ControllerMotor;
|
||||
if (binding == "LargeMotor")
|
||||
{
|
||||
key.data = 0;
|
||||
return key;
|
||||
}
|
||||
else if (binding == "SmallMotor")
|
||||
{
|
||||
key.data = 1;
|
||||
return key;
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
else if (StringUtil::EndsWith(binding, "Haptic"))
|
||||
{
|
||||
key.source_subtype = InputSubclass::ControllerHaptic;
|
||||
key.data = 0;
|
||||
return key;
|
||||
}
|
||||
else if (binding[0] == '+' || binding[0] == '-')
|
||||
{
|
||||
// likely an axis
|
||||
const std::string_view axis_name(binding.substr(1));
|
||||
for (u32 i = 0; i < std::size(s_sdl_axis_names); i++)
|
||||
{
|
||||
if (axis_name == s_sdl_axis_names[i])
|
||||
{
|
||||
// found an axis!
|
||||
key.source_subtype = InputSubclass::ControllerAxis;
|
||||
key.data = i;
|
||||
key.negative = (binding[0] == '-');
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// must be a button
|
||||
for (u32 i = 0; i < std::size(s_sdl_button_names); i++)
|
||||
{
|
||||
if (binding == s_sdl_button_names[i])
|
||||
{
|
||||
key.source_subtype = InputSubclass::ControllerButton;
|
||||
key.data = i;
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unknown axis/button
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string SDLInputSource::ConvertKeyToString(InputBindingKey key)
|
||||
{
|
||||
std::string ret;
|
||||
|
||||
if (key.source_type == InputSourceType::SDL)
|
||||
{
|
||||
if (key.source_subtype == InputSubclass::ControllerAxis && key.data < std::size(s_sdl_axis_names))
|
||||
{
|
||||
ret = StringUtil::StdStringFromFormat("SDL-%u/%c%s", key.source_index, key.negative ? '-' : '+',
|
||||
s_sdl_axis_names[key.data]);
|
||||
}
|
||||
else if (key.source_subtype == InputSubclass::ControllerButton && key.data < std::size(s_sdl_button_names))
|
||||
{
|
||||
ret = StringUtil::StdStringFromFormat("SDL-%u/%s", key.source_index, s_sdl_button_names[key.data]);
|
||||
}
|
||||
else if (key.source_subtype == InputSubclass::ControllerMotor)
|
||||
{
|
||||
ret = StringUtil::StdStringFromFormat("SDL-%u/%sMotor", key.source_index, key.data ? "Large" : "Small");
|
||||
}
|
||||
else if (key.source_subtype == InputSubclass::ControllerHaptic)
|
||||
{
|
||||
ret = StringUtil::StdStringFromFormat("SDL-%u/Haptic", key.source_index);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool SDLInputSource::ProcessSDLEvent(const SDL_Event* event)
|
||||
{
|
||||
switch (event->type)
|
||||
{
|
||||
case SDL_CONTROLLERDEVICEADDED:
|
||||
{
|
||||
Log_InfoPrintf("(SDLInputSource) Controller %d inserted", event->cdevice.which);
|
||||
OpenGameController(event->cdevice.which);
|
||||
return true;
|
||||
}
|
||||
|
||||
case SDL_CONTROLLERDEVICEREMOVED:
|
||||
{
|
||||
Log_InfoPrintf("(SDLInputSource) Controller %d removed", event->cdevice.which);
|
||||
CloseGameController(event->cdevice.which);
|
||||
return true;
|
||||
}
|
||||
|
||||
case SDL_CONTROLLERAXISMOTION:
|
||||
return HandleControllerAxisEvent(&event->caxis);
|
||||
|
||||
case SDL_CONTROLLERBUTTONDOWN:
|
||||
case SDL_CONTROLLERBUTTONUP:
|
||||
return HandleControllerButtonEvent(&event->cbutton);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
SDLInputSource::ControllerDataVector::iterator SDLInputSource::GetControllerDataForJoystickId(int id)
|
||||
{
|
||||
return std::find_if(m_controllers.begin(), m_controllers.end(),
|
||||
[id](const ControllerData& cd) { return cd.joystick_id == id; });
|
||||
}
|
||||
|
||||
SDLInputSource::ControllerDataVector::iterator SDLInputSource::GetControllerDataForPlayerId(int id)
|
||||
{
|
||||
return std::find_if(m_controllers.begin(), m_controllers.end(),
|
||||
[id](const ControllerData& cd) { return cd.player_id == id; });
|
||||
}
|
||||
|
||||
int SDLInputSource::GetFreePlayerId() const
|
||||
{
|
||||
for (int player_id = 0;; player_id++)
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < m_controllers.size(); i++)
|
||||
{
|
||||
if (m_controllers[i].player_id == player_id)
|
||||
break;
|
||||
}
|
||||
if (i == m_controllers.size())
|
||||
return player_id;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool SDLInputSource::OpenGameController(int index)
|
||||
{
|
||||
SDL_GameController* gcontroller = SDL_GameControllerOpen(index);
|
||||
SDL_Joystick* joystick = gcontroller ? SDL_GameControllerGetJoystick(gcontroller) : nullptr;
|
||||
if (!gcontroller || !joystick)
|
||||
{
|
||||
Log_ErrorPrintf("(SDLInputSource) Failed to open controller %d", index);
|
||||
if (gcontroller)
|
||||
SDL_GameControllerClose(gcontroller);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const int joystick_id = SDL_JoystickInstanceID(joystick);
|
||||
int player_id = SDL_GameControllerGetPlayerIndex(gcontroller);
|
||||
if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end())
|
||||
{
|
||||
const int free_player_id = GetFreePlayerId();
|
||||
Log_WarningPrintf("(SDLInputSource) Controller %d (joystick %d) returned player ID %d, which is invalid or in "
|
||||
"use. Using ID %d instead.",
|
||||
index, joystick_id, player_id, free_player_id);
|
||||
player_id = free_player_id;
|
||||
}
|
||||
|
||||
Log_InfoPrintf("(SDLInputSource) Opened controller %d (instance id %d, player id %d): %s", index, joystick_id,
|
||||
player_id, SDL_GameControllerName(gcontroller));
|
||||
|
||||
ControllerData cd = {};
|
||||
cd.player_id = player_id;
|
||||
cd.joystick_id = joystick_id;
|
||||
cd.haptic_left_right_effect = -1;
|
||||
cd.game_controller = gcontroller;
|
||||
|
||||
cd.use_game_controller_rumble = (SDL_GameControllerRumble(gcontroller, 0, 0, 0) == 0);
|
||||
if (cd.use_game_controller_rumble)
|
||||
{
|
||||
Log_DevPrintf("(SDLInputSource) Rumble is supported on '%s' via gamecontroller",
|
||||
SDL_GameControllerName(gcontroller));
|
||||
}
|
||||
else
|
||||
{
|
||||
SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick);
|
||||
if (haptic)
|
||||
{
|
||||
SDL_HapticEffect ef = {};
|
||||
ef.leftright.type = SDL_HAPTIC_LEFTRIGHT;
|
||||
ef.leftright.length = 1000;
|
||||
|
||||
int ef_id = SDL_HapticNewEffect(haptic, &ef);
|
||||
if (ef_id >= 0)
|
||||
{
|
||||
cd.haptic = haptic;
|
||||
cd.haptic_left_right_effect = ef_id;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log_WarningPrintf("(SDLInputSource) Failed to create haptic left/right effect: %s", SDL_GetError());
|
||||
if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0)
|
||||
{
|
||||
cd.haptic = haptic;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log_ErrorPrintf("(SDLInputSource) No haptic rumble supported: %s", SDL_GetError());
|
||||
SDL_HapticClose(haptic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cd.haptic)
|
||||
Log_DevPrintf("(SDLInputSource) Rumble is supported on '%s' via haptic", SDL_GameControllerName(gcontroller));
|
||||
}
|
||||
|
||||
if (!cd.haptic && !cd.use_game_controller_rumble)
|
||||
Log_WarningPrintf("(SDLInputSource) Rumble is not supported on '%s'", SDL_GameControllerName(gcontroller));
|
||||
|
||||
m_controllers.push_back(std::move(cd));
|
||||
|
||||
const char* name = SDL_GameControllerName(cd.game_controller);
|
||||
Host::OnInputDeviceConnected(StringUtil::StdStringFromFormat("SDL-%d", player_id), name ? name : "Unknown Device");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDLInputSource::CloseGameController(int joystick_index)
|
||||
{
|
||||
auto it = GetControllerDataForJoystickId(joystick_index);
|
||||
if (it == m_controllers.end())
|
||||
return false;
|
||||
|
||||
if (it->haptic)
|
||||
SDL_HapticClose(static_cast<SDL_Haptic*>(it->haptic));
|
||||
|
||||
SDL_GameControllerClose(static_cast<SDL_GameController*>(it->game_controller));
|
||||
|
||||
const int player_id = it->player_id;
|
||||
m_controllers.erase(it);
|
||||
|
||||
Host::OnInputDeviceDisconnected(StringUtil::StdStringFromFormat("SDL-%d", player_id));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDLInputSource::HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev)
|
||||
{
|
||||
auto it = GetControllerDataForJoystickId(ev->which);
|
||||
if (it == m_controllers.end())
|
||||
return false;
|
||||
|
||||
const InputBindingKey key(MakeGenericControllerAxisKey(InputSourceType::SDL, it->player_id, ev->axis));
|
||||
const float value = static_cast<float>(ev->value) / (ev->value < 0 ? 32768.0f : 32767.0f);
|
||||
return InputManager::InvokeEvents(key, value, GenericInputBinding::Unknown);
|
||||
}
|
||||
|
||||
bool SDLInputSource::HandleControllerButtonEvent(const SDL_ControllerButtonEvent* ev)
|
||||
{
|
||||
auto it = GetControllerDataForJoystickId(ev->which);
|
||||
if (it == m_controllers.end())
|
||||
return false;
|
||||
|
||||
const InputBindingKey key(MakeGenericControllerButtonKey(InputSourceType::SDL, it->player_id, ev->button));
|
||||
const GenericInputBinding generic_key = (ev->button < std::size(s_sdl_generic_binding_button_mapping)) ?
|
||||
s_sdl_generic_binding_button_mapping[ev->button] :
|
||||
GenericInputBinding::Unknown;
|
||||
return InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f, generic_key);
|
||||
}
|
||||
|
||||
std::vector<InputBindingKey> SDLInputSource::EnumerateMotors()
|
||||
{
|
||||
std::vector<InputBindingKey> ret;
|
||||
|
||||
InputBindingKey key = {};
|
||||
key.source_type = InputSourceType::SDL;
|
||||
|
||||
for (ControllerData& cd : m_controllers)
|
||||
{
|
||||
key.source_index = cd.player_id;
|
||||
|
||||
if (cd.use_game_controller_rumble || cd.haptic_left_right_effect)
|
||||
{
|
||||
// two motors
|
||||
key.source_subtype = InputSubclass::ControllerMotor;
|
||||
key.data = 0;
|
||||
ret.push_back(key);
|
||||
key.data = 1;
|
||||
ret.push_back(key);
|
||||
}
|
||||
else if (cd.haptic)
|
||||
{
|
||||
// haptic effect
|
||||
key.source_subtype = InputSubclass::ControllerHaptic;
|
||||
key.data = 0;
|
||||
ret.push_back(key);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool SDLInputSource::GetGenericBindingMapping(const std::string_view& device, GenericInputBindingMapping* mapping)
|
||||
{
|
||||
if (!StringUtil::StartsWith(device, "SDL-"))
|
||||
return false;
|
||||
|
||||
const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(4));
|
||||
if (!player_id.has_value() || player_id.value() < 0)
|
||||
return false;
|
||||
|
||||
ControllerDataVector::iterator it = GetControllerDataForPlayerId(player_id.value());
|
||||
if (it == m_controllers.end())
|
||||
return false;
|
||||
|
||||
if (it->game_controller)
|
||||
{
|
||||
// assume all buttons are present.
|
||||
const s32 pid = player_id.value();
|
||||
for (u32 i = 0; i < std::size(s_sdl_generic_binding_axis_mapping); i++)
|
||||
{
|
||||
const GenericInputBinding negative = s_sdl_generic_binding_axis_mapping[i][0];
|
||||
const GenericInputBinding positive = s_sdl_generic_binding_axis_mapping[i][1];
|
||||
if (negative != GenericInputBinding::Unknown)
|
||||
mapping->emplace_back(negative, StringUtil::StdStringFromFormat("SDL-%d/-%s", pid, s_sdl_axis_names[i]));
|
||||
|
||||
if (positive != GenericInputBinding::Unknown)
|
||||
mapping->emplace_back(positive, StringUtil::StdStringFromFormat("SDL-%d/+%s", pid, s_sdl_axis_names[i]));
|
||||
}
|
||||
for (u32 i = 0; i < std::size(s_sdl_generic_binding_button_mapping); i++)
|
||||
{
|
||||
const GenericInputBinding binding = s_sdl_generic_binding_button_mapping[i];
|
||||
if (binding != GenericInputBinding::Unknown)
|
||||
mapping->emplace_back(binding, StringUtil::StdStringFromFormat("SDL-%d/%s", pid, s_sdl_button_names[i]));
|
||||
}
|
||||
|
||||
if (it->use_game_controller_rumble || it->haptic_left_right_effect)
|
||||
{
|
||||
mapping->emplace_back(GenericInputBinding::SmallMotor, StringUtil::StdStringFromFormat("SDL-%d/SmallMotor", pid));
|
||||
mapping->emplace_back(GenericInputBinding::LargeMotor, StringUtil::StdStringFromFormat("SDL-%d/LargeMotor", pid));
|
||||
}
|
||||
else
|
||||
{
|
||||
mapping->emplace_back(GenericInputBinding::SmallMotor, StringUtil::StdStringFromFormat("SDL-%d/Haptic", pid));
|
||||
mapping->emplace_back(GenericInputBinding::LargeMotor, StringUtil::StdStringFromFormat("SDL-%d/Haptic", pid));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// joysticks, which we haven't implemented yet anyway.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void SDLInputSource::UpdateMotorState(InputBindingKey key, float intensity)
|
||||
{
|
||||
if (key.source_subtype != InputSubclass::ControllerMotor && key.source_subtype != InputSubclass::ControllerHaptic)
|
||||
return;
|
||||
|
||||
auto it = GetControllerDataForPlayerId(key.source_index);
|
||||
if (it == m_controllers.end())
|
||||
return;
|
||||
|
||||
it->rumble_intensity[key.data] = static_cast<u16>(intensity * 65535.0f);
|
||||
SendRumbleUpdate(&(*it));
|
||||
}
|
||||
|
||||
void SDLInputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
|
||||
float small_intensity)
|
||||
{
|
||||
if (large_key.source_index != small_key.source_index || large_key.source_subtype != InputSubclass::ControllerMotor ||
|
||||
small_key.source_subtype != InputSubclass::ControllerMotor)
|
||||
{
|
||||
// bonkers config where they're mapped to different controllers... who would do such a thing?
|
||||
UpdateMotorState(large_key, large_intensity);
|
||||
UpdateMotorState(small_key, small_intensity);
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = GetControllerDataForPlayerId(large_key.source_index);
|
||||
if (it == m_controllers.end())
|
||||
return;
|
||||
|
||||
it->rumble_intensity[large_key.data] = static_cast<u16>(large_intensity * 65535.0f);
|
||||
it->rumble_intensity[small_key.data] = static_cast<u16>(small_intensity * 65535.0f);
|
||||
SendRumbleUpdate(&(*it));
|
||||
}
|
||||
|
||||
void SDLInputSource::SendRumbleUpdate(ControllerData* cd)
|
||||
{
|
||||
// we'll update before this duration is elapsed
|
||||
static constexpr u32 DURATION = 65535; // SDL_MAX_RUMBLE_DURATION_MS
|
||||
|
||||
if (cd->use_game_controller_rumble)
|
||||
{
|
||||
SDL_GameControllerRumble(cd->game_controller, cd->rumble_intensity[0], cd->rumble_intensity[1], DURATION);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cd->haptic_left_right_effect >= 0)
|
||||
{
|
||||
if ((static_cast<u32>(cd->rumble_intensity[0]) + static_cast<u32>(cd->rumble_intensity[1])) > 0)
|
||||
{
|
||||
SDL_HapticEffect ef;
|
||||
ef.type = SDL_HAPTIC_LEFTRIGHT;
|
||||
ef.leftright.large_magnitude = cd->rumble_intensity[0];
|
||||
ef.leftright.small_magnitude = cd->rumble_intensity[1];
|
||||
ef.leftright.length = DURATION;
|
||||
SDL_HapticUpdateEffect(cd->haptic, cd->haptic_left_right_effect, &ef);
|
||||
SDL_HapticRunEffect(cd->haptic, cd->haptic_left_right_effect, SDL_HAPTIC_INFINITY);
|
||||
}
|
||||
else
|
||||
{
|
||||
SDL_HapticStopEffect(cd->haptic, cd->haptic_left_right_effect);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const float strength =
|
||||
static_cast<float>(std::max(cd->rumble_intensity[0], cd->rumble_intensity[1])) * (1.0f / 65535.0f);
|
||||
if (strength > 0.0f)
|
||||
SDL_HapticRumblePlay(cd->haptic, strength, DURATION);
|
||||
else
|
||||
SDL_HapticRumbleStop(cd->haptic);
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<InputSource> InputSource::CreateSDLSource()
|
||||
{
|
||||
return std::make_unique<SDLInputSource>();
|
||||
}
|
74
src/frontend-common/sdl_input_source.h
Normal file
74
src/frontend-common/sdl_input_source.h
Normal file
@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
#include "SDL.h"
|
||||
#include "input_source.h"
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
class SettingsInterface;
|
||||
|
||||
class SDLInputSource final : public InputSource
|
||||
{
|
||||
public:
|
||||
SDLInputSource();
|
||||
~SDLInputSource();
|
||||
|
||||
bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
|
||||
void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
|
||||
void Shutdown() override;
|
||||
|
||||
void PollEvents() override;
|
||||
std::vector<std::pair<std::string, std::string>> EnumerateDevices() override;
|
||||
std::vector<InputBindingKey> EnumerateMotors() override;
|
||||
bool GetGenericBindingMapping(const std::string_view& device, GenericInputBindingMapping* mapping) override;
|
||||
void UpdateMotorState(InputBindingKey key, float intensity) override;
|
||||
void UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
|
||||
float small_intensity) override;
|
||||
|
||||
std::optional<InputBindingKey> ParseKeyString(const std::string_view& device,
|
||||
const std::string_view& binding) override;
|
||||
std::string ConvertKeyToString(InputBindingKey key) override;
|
||||
|
||||
bool ProcessSDLEvent(const SDL_Event* event);
|
||||
|
||||
private:
|
||||
enum : int
|
||||
{
|
||||
MAX_NUM_AXES = 7,
|
||||
MAX_NUM_BUTTONS = 16,
|
||||
};
|
||||
|
||||
struct ControllerData
|
||||
{
|
||||
SDL_Haptic* haptic;
|
||||
SDL_GameController* game_controller;
|
||||
u16 rumble_intensity[2];
|
||||
int haptic_left_right_effect;
|
||||
int joystick_id;
|
||||
int player_id;
|
||||
bool use_game_controller_rumble;
|
||||
};
|
||||
|
||||
using ControllerDataVector = std::vector<ControllerData>;
|
||||
|
||||
bool InitializeSubsystem();
|
||||
void ShutdownSubsystem();
|
||||
void LoadSettings(SettingsInterface& si);
|
||||
void SetHints();
|
||||
|
||||
ControllerDataVector::iterator GetControllerDataForJoystickId(int id);
|
||||
ControllerDataVector::iterator GetControllerDataForPlayerId(int id);
|
||||
int GetFreePlayerId() const;
|
||||
|
||||
bool OpenGameController(int index);
|
||||
bool CloseGameController(int joystick_index);
|
||||
bool HandleControllerAxisEvent(const SDL_ControllerAxisEvent* event);
|
||||
bool HandleControllerButtonEvent(const SDL_ControllerButtonEvent* event);
|
||||
void SendRumbleUpdate(ControllerData* cd);
|
||||
|
||||
ControllerDataVector m_controllers;
|
||||
|
||||
bool m_sdl_subsystem_initialized = false;
|
||||
bool m_controller_enhanced_mode = false;
|
||||
};
|
@ -9,7 +9,7 @@
|
||||
#include "common/vulkan/stream_buffer.h"
|
||||
#include "common/vulkan/swap_chain.h"
|
||||
#include "common/vulkan/util.h"
|
||||
#include "common_host_interface.h"
|
||||
#include "common_host.h"
|
||||
#include "core/shader_cache_version.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_impl_vulkan.h"
|
||||
@ -908,7 +908,7 @@ HostDisplay::AdapterAndModeList VulkanHostDisplay::StaticGetAdapterAndModeList(c
|
||||
for (const Vulkan::SwapChain::FullscreenModeInfo& fmi : fsmodes)
|
||||
{
|
||||
ret.fullscreen_modes.push_back(
|
||||
CommonHostInterface::GetFullscreenModeString(fmi.width, fmi.height, fmi.refresh_rate));
|
||||
GetFullscreenModeString(fmi.width, fmi.height, fmi.refresh_rate));
|
||||
}
|
||||
}
|
||||
|
||||
|
264
src/frontend-common/win32_raw_input_source.cpp
Normal file
264
src/frontend-common/win32_raw_input_source.cpp
Normal file
@ -0,0 +1,264 @@
|
||||
#include "win32_raw_input_source.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/host.h"
|
||||
#include "core/system.h"
|
||||
#include "input_manager.h"
|
||||
#include <cmath>
|
||||
#include <hidusage.h>
|
||||
#include <malloc.h>
|
||||
|
||||
Log_SetChannel(Win32RawInputSource);
|
||||
|
||||
static const wchar_t* WINDOW_CLASS_NAME = L"Win32RawInputSource";
|
||||
static bool s_window_class_registered = false;
|
||||
|
||||
static constexpr const u32 ALL_BUTTON_MASKS = RI_MOUSE_BUTTON_1_DOWN | RI_MOUSE_BUTTON_1_UP | RI_MOUSE_BUTTON_2_DOWN |
|
||||
RI_MOUSE_BUTTON_2_UP | RI_MOUSE_BUTTON_3_DOWN | RI_MOUSE_BUTTON_3_UP |
|
||||
RI_MOUSE_BUTTON_4_DOWN | RI_MOUSE_BUTTON_4_UP | RI_MOUSE_BUTTON_5_DOWN |
|
||||
RI_MOUSE_BUTTON_5_UP;
|
||||
|
||||
Win32RawInputSource::Win32RawInputSource() = default;
|
||||
|
||||
Win32RawInputSource::~Win32RawInputSource() = default;
|
||||
|
||||
bool Win32RawInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
|
||||
{
|
||||
if (!RegisterDummyClass())
|
||||
{
|
||||
Log_ErrorPrintf("(Win32RawInputSource) Failed to register dummy window class");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CreateDummyWindow())
|
||||
{
|
||||
Log_ErrorPrintf("(Win32RawInputSource) Failed to create dummy window");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!OpenDevices())
|
||||
{
|
||||
Log_ErrorPrintf("(Win32RawInputSource) Failed to open devices");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Win32RawInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) {}
|
||||
|
||||
void Win32RawInputSource::Shutdown()
|
||||
{
|
||||
CloseDevices();
|
||||
DestroyDummyWindow();
|
||||
}
|
||||
|
||||
void Win32RawInputSource::PollEvents()
|
||||
{
|
||||
// noop, handled by message pump
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> Win32RawInputSource::EnumerateDevices()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void Win32RawInputSource::UpdateMotorState(InputBindingKey key, float intensity) {}
|
||||
|
||||
void Win32RawInputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
|
||||
float small_intensity)
|
||||
{
|
||||
}
|
||||
|
||||
std::optional<InputBindingKey> Win32RawInputSource::ParseKeyString(const std::string_view& device,
|
||||
const std::string_view& binding)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string Win32RawInputSource::ConvertKeyToString(InputBindingKey key)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<InputBindingKey> Win32RawInputSource::EnumerateMotors()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
bool Win32RawInputSource::GetGenericBindingMapping(const std::string_view& device, GenericInputBindingMapping* mapping)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
bool Win32RawInputSource::RegisterDummyClass()
|
||||
{
|
||||
if (s_window_class_registered)
|
||||
return true;
|
||||
|
||||
WNDCLASSW wc = {};
|
||||
wc.hInstance = GetModuleHandleW(nullptr);
|
||||
wc.lpfnWndProc = DummyWindowProc;
|
||||
wc.lpszClassName = WINDOW_CLASS_NAME;
|
||||
return (RegisterClassW(&wc) != 0);
|
||||
}
|
||||
|
||||
bool Win32RawInputSource::CreateDummyWindow()
|
||||
{
|
||||
m_dummy_window = CreateWindowExW(0, WINDOW_CLASS_NAME, WINDOW_CLASS_NAME, WS_OVERLAPPED, CW_USEDEFAULT, CW_USEDEFAULT,
|
||||
CW_USEDEFAULT, CW_USEDEFAULT, HWND_MESSAGE, NULL, GetModuleHandleW(nullptr), NULL);
|
||||
if (!m_dummy_window)
|
||||
return false;
|
||||
|
||||
SetWindowLongPtrW(m_dummy_window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
|
||||
return true;
|
||||
}
|
||||
|
||||
void Win32RawInputSource::DestroyDummyWindow()
|
||||
{
|
||||
if (!m_dummy_window)
|
||||
return;
|
||||
|
||||
DestroyWindow(m_dummy_window);
|
||||
m_dummy_window = {};
|
||||
}
|
||||
|
||||
LRESULT CALLBACK Win32RawInputSource::DummyWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
if (msg != WM_INPUT)
|
||||
return DefWindowProcW(hwnd, msg, wParam, lParam);
|
||||
|
||||
UINT size = 0;
|
||||
GetRawInputData((HRAWINPUT)lParam, RID_INPUT, nullptr, &size, sizeof(RAWINPUTHEADER));
|
||||
|
||||
PRAWINPUT data = static_cast<PRAWINPUT>(_alloca(size));
|
||||
GetRawInputData((HRAWINPUT)lParam, RID_INPUT, data, &size, sizeof(RAWINPUTHEADER));
|
||||
|
||||
// we shouldn't get any WM_INPUT messages prior to SetWindowLongPtr(), so this'll be fine
|
||||
Win32RawInputSource* ris = reinterpret_cast<Win32RawInputSource*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||
if (ris->ProcessRawInputEvent(data))
|
||||
return 0;
|
||||
|
||||
// forward through to normal message processing
|
||||
return DefWindowProcW(hwnd, msg, wParam, lParam);
|
||||
}
|
||||
|
||||
bool Win32RawInputSource::OpenDevices()
|
||||
{
|
||||
UINT num_devices = 0;
|
||||
if (GetRawInputDeviceList(nullptr, &num_devices, sizeof(RAWINPUTDEVICELIST)) == static_cast<UINT>(-1) ||
|
||||
num_devices == 0)
|
||||
return false;
|
||||
|
||||
std::vector<RAWINPUTDEVICELIST> devices(num_devices);
|
||||
if (GetRawInputDeviceList(devices.data(), &num_devices, sizeof(RAWINPUTDEVICELIST)) == static_cast<UINT>(-1))
|
||||
return false;
|
||||
devices.resize(num_devices);
|
||||
|
||||
for (const RAWINPUTDEVICELIST& rid : devices)
|
||||
{
|
||||
#if 0
|
||||
if (rid.dwType == RIM_TYPEKEYBOARD)
|
||||
m_num_keyboards++;
|
||||
#endif
|
||||
if (rid.dwType == RIM_TYPEMOUSE)
|
||||
m_mice.push_back({rid.hDevice});
|
||||
}
|
||||
|
||||
Log_DevPrintf("(Win32RawInputSource) Found %u keyboards and %zu mice", m_num_keyboards, m_mice.size());
|
||||
|
||||
// Grab all keyboard/mouse input.
|
||||
if (m_num_keyboards > 0)
|
||||
{
|
||||
const RAWINPUTDEVICE rrid = {HID_USAGE_PAGE_GENERIC, HID_USAGE_GENERIC_KEYBOARD, 0, m_dummy_window};
|
||||
if (!RegisterRawInputDevices(&rrid, 1, sizeof(rrid)))
|
||||
return false;
|
||||
}
|
||||
if (!m_mice.empty())
|
||||
{
|
||||
const RAWINPUTDEVICE rrid = {HID_USAGE_PAGE_GENERIC, HID_USAGE_GENERIC_MOUSE, 0, m_dummy_window};
|
||||
if (!RegisterRawInputDevices(&rrid, 1, sizeof(rrid)))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Win32RawInputSource::CloseDevices()
|
||||
{
|
||||
if (m_num_keyboards > 0)
|
||||
{
|
||||
const RAWINPUTDEVICE rrid = {HID_USAGE_PAGE_GENERIC, HID_USAGE_GENERIC_MOUSE, RIDEV_REMOVE, m_dummy_window};
|
||||
RegisterRawInputDevices(&rrid, 1, sizeof(rrid));
|
||||
m_num_keyboards = 0;
|
||||
}
|
||||
|
||||
if (!m_mice.empty())
|
||||
{
|
||||
const RAWINPUTDEVICE rrid = {HID_USAGE_PAGE_GENERIC, HID_USAGE_GENERIC_KEYBOARD, RIDEV_REMOVE, m_dummy_window};
|
||||
RegisterRawInputDevices(&rrid, 1, sizeof(rrid));
|
||||
m_mice.clear();
|
||||
}
|
||||
}
|
||||
|
||||
bool Win32RawInputSource::ProcessRawInputEvent(const RAWINPUT* event)
|
||||
{
|
||||
if (event->header.dwType == RIM_TYPEMOUSE)
|
||||
{
|
||||
const u32 mouse_index = 0;
|
||||
for (MouseState& state : m_mice)
|
||||
{
|
||||
if (state.device != event->header.hDevice)
|
||||
continue;
|
||||
|
||||
const RAWMOUSE& rm = event->data.mouse;
|
||||
|
||||
s32 dx = rm.lLastX;
|
||||
s32 dy = rm.lLastY;
|
||||
|
||||
// handle absolute positioned devices
|
||||
if ((rm.usFlags & MOUSE_MOVE_ABSOLUTE) == MOUSE_MOVE_ABSOLUTE)
|
||||
{
|
||||
dx -= std::exchange(dx, state.last_x);
|
||||
dy -= std::exchange(dy, state.last_y);
|
||||
}
|
||||
|
||||
unsigned long button_mask =
|
||||
(rm.usButtonFlags & (rm.usButtonFlags ^ std::exchange(state.button_state, rm.usButtonFlags))) &
|
||||
ALL_BUTTON_MASKS;
|
||||
|
||||
// when the VM isn't running, allow events to run as normal (so we don't break the UI)
|
||||
if (System::GetState() != System::State::Running)
|
||||
return false;
|
||||
|
||||
while (button_mask != 0)
|
||||
{
|
||||
unsigned long bit_index;
|
||||
_BitScanForward(&bit_index, button_mask);
|
||||
|
||||
// these are ordered down..up for each button
|
||||
const u32 button_number = bit_index >> 1;
|
||||
const bool button_pressed = (bit_index & 1u) != 0;
|
||||
InputManager::InvokeEvents(InputManager::MakePointerButtonKey(mouse_index, button_number),
|
||||
static_cast<float>(button_pressed), GenericInputBinding::Unknown);
|
||||
|
||||
button_mask &= ~(1u << bit_index);
|
||||
}
|
||||
|
||||
if (dx != 0)
|
||||
InputManager::UpdatePointerRelativeDelta(mouse_index, InputPointerAxis::X, static_cast<float>(dx), true);
|
||||
if (dy != 0)
|
||||
InputManager::UpdatePointerRelativeDelta(mouse_index, InputPointerAxis::Y, static_cast<float>(dy), true);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<InputSource> InputSource::CreateWin32RawInputSource()
|
||||
{
|
||||
return std::make_unique<Win32RawInputSource>();
|
||||
}
|
57
src/frontend-common/win32_raw_input_source.h
Normal file
57
src/frontend-common/win32_raw_input_source.h
Normal file
@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
#include "common/windows_headers.h"
|
||||
#include "input_source.h"
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
class SettingsInterface;
|
||||
|
||||
class Win32RawInputSource final : public InputSource
|
||||
{
|
||||
public:
|
||||
Win32RawInputSource();
|
||||
~Win32RawInputSource();
|
||||
|
||||
bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
|
||||
void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
|
||||
void Shutdown() override;
|
||||
|
||||
void PollEvents() override;
|
||||
std::vector<std::pair<std::string, std::string>> EnumerateDevices() override;
|
||||
std::vector<InputBindingKey> EnumerateMotors() override;
|
||||
bool GetGenericBindingMapping(const std::string_view& device, GenericInputBindingMapping* mapping) override;
|
||||
void UpdateMotorState(InputBindingKey key, float intensity) override;
|
||||
void UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
|
||||
float small_intensity) override;
|
||||
|
||||
std::optional<InputBindingKey> ParseKeyString(const std::string_view& device,
|
||||
const std::string_view& binding) override;
|
||||
std::string ConvertKeyToString(InputBindingKey key) override;
|
||||
|
||||
private:
|
||||
struct MouseState
|
||||
{
|
||||
HANDLE device;
|
||||
u32 button_state;
|
||||
s32 last_x;
|
||||
s32 last_y;
|
||||
};
|
||||
|
||||
static bool RegisterDummyClass();
|
||||
static LRESULT CALLBACK DummyWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
|
||||
|
||||
bool CreateDummyWindow();
|
||||
void DestroyDummyWindow();
|
||||
bool OpenDevices();
|
||||
void CloseDevices();
|
||||
|
||||
bool ProcessRawInputEvent(const RAWINPUT* event);
|
||||
|
||||
HWND m_dummy_window = {};
|
||||
u32 m_num_keyboards = 0;
|
||||
u32 m_num_mice = 0;
|
||||
|
||||
std::vector<MouseState> m_mice;
|
||||
};
|
@ -1,369 +0,0 @@
|
||||
#include "xinput_controller_interface.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/file_system.h"
|
||||
#include "common/log.h"
|
||||
#include "core/controller.h"
|
||||
#include "core/host_interface.h"
|
||||
#include "core/system.h"
|
||||
#include <cmath>
|
||||
Log_SetChannel(XInputControllerInterface);
|
||||
|
||||
XInputControllerInterface::XInputControllerInterface() = default;
|
||||
|
||||
XInputControllerInterface::~XInputControllerInterface()
|
||||
{
|
||||
if (m_xinput_module)
|
||||
FreeLibrary(m_xinput_module);
|
||||
}
|
||||
|
||||
ControllerInterface::Backend XInputControllerInterface::GetBackend() const
|
||||
{
|
||||
return ControllerInterface::Backend::XInput;
|
||||
}
|
||||
|
||||
bool XInputControllerInterface::Initialize(CommonHostInterface* host_interface)
|
||||
{
|
||||
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
|
||||
m_xinput_module = LoadLibraryW(L"xinput1_4");
|
||||
if (!m_xinput_module)
|
||||
{
|
||||
m_xinput_module = LoadLibraryW(L"xinput1_3");
|
||||
}
|
||||
if (!m_xinput_module)
|
||||
{
|
||||
m_xinput_module = LoadLibraryW(L"xinput9_1_0");
|
||||
}
|
||||
if (!m_xinput_module)
|
||||
{
|
||||
Log_ErrorPrintf("Failed to load XInput module.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try the hidden version of XInputGetState(), which lets us query the guide button.
|
||||
m_xinput_get_state =
|
||||
reinterpret_cast<decltype(m_xinput_get_state)>(GetProcAddress(m_xinput_module, reinterpret_cast<LPCSTR>(100)));
|
||||
if (!m_xinput_get_state)
|
||||
reinterpret_cast<decltype(m_xinput_get_state)>(GetProcAddress(m_xinput_module, "XInputGetState"));
|
||||
m_xinput_set_state =
|
||||
reinterpret_cast<decltype(m_xinput_set_state)>(GetProcAddress(m_xinput_module, "XInputSetState"));
|
||||
#else
|
||||
m_xinput_get_state = XInputGetState;
|
||||
m_xinput_set_state = XInputSetState;
|
||||
#endif
|
||||
if (!m_xinput_get_state || !m_xinput_set_state)
|
||||
{
|
||||
Log_ErrorPrintf("Failed to get XInput function pointers.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ControllerInterface::Initialize(host_interface))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void XInputControllerInterface::Shutdown()
|
||||
{
|
||||
ControllerInterface::Shutdown();
|
||||
}
|
||||
|
||||
void XInputControllerInterface::PollEvents()
|
||||
{
|
||||
for (u32 i = 0; i < XUSER_MAX_COUNT; i++)
|
||||
{
|
||||
XINPUT_STATE new_state;
|
||||
const DWORD result = m_xinput_get_state(i, &new_state);
|
||||
ControllerData& cd = m_controllers[i];
|
||||
if (result == ERROR_SUCCESS)
|
||||
{
|
||||
if (!cd.connected)
|
||||
{
|
||||
cd.connected = true;
|
||||
OnControllerConnected(static_cast<int>(i));
|
||||
}
|
||||
|
||||
CheckForStateChanges(i, new_state);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (result != ERROR_DEVICE_NOT_CONNECTED)
|
||||
Log_WarningPrintf("XInputGetState(%u) failed: 0x%08X / 0x%08X", i, result, GetLastError());
|
||||
|
||||
if (cd.connected)
|
||||
{
|
||||
cd.connected = false;
|
||||
cd.last_state = {};
|
||||
OnControllerDisconnected(static_cast<int>(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void XInputControllerInterface::CheckForStateChanges(u32 index, const XINPUT_STATE& new_state)
|
||||
{
|
||||
ControllerData& cd = m_controllers[index];
|
||||
if (new_state.dwPacketNumber == cd.last_state.dwPacketNumber)
|
||||
return;
|
||||
|
||||
cd.last_state.dwPacketNumber = new_state.dwPacketNumber;
|
||||
|
||||
XINPUT_GAMEPAD& ogp = cd.last_state.Gamepad;
|
||||
const XINPUT_GAMEPAD& ngp = new_state.Gamepad;
|
||||
if (ogp.sThumbLX != ngp.sThumbLX)
|
||||
{
|
||||
HandleAxisEvent(index, Axis::LeftX, ngp.sThumbLX);
|
||||
ogp.sThumbLX = ngp.sThumbLX;
|
||||
}
|
||||
if (ogp.sThumbLY != ngp.sThumbLY)
|
||||
{
|
||||
HandleAxisEvent(index, Axis::LeftY, -ngp.sThumbLY);
|
||||
ogp.sThumbLY = ngp.sThumbLY;
|
||||
}
|
||||
if (ogp.sThumbRX != ngp.sThumbRX)
|
||||
{
|
||||
HandleAxisEvent(index, Axis::RightX, ngp.sThumbRX);
|
||||
ogp.sThumbRX = ngp.sThumbRX;
|
||||
}
|
||||
if (ogp.sThumbRY != ngp.sThumbRY)
|
||||
{
|
||||
HandleAxisEvent(index, Axis::RightY, -ngp.sThumbRY);
|
||||
ogp.sThumbRY = ngp.sThumbRY;
|
||||
}
|
||||
if (ogp.bLeftTrigger != ngp.bLeftTrigger)
|
||||
{
|
||||
HandleAxisEvent(index, Axis::LeftTrigger, static_cast<s32>(ZeroExtend32(ngp.bLeftTrigger) << 7));
|
||||
ogp.bLeftTrigger = ngp.bLeftTrigger;
|
||||
}
|
||||
if (ogp.bRightTrigger != ngp.bRightTrigger)
|
||||
{
|
||||
HandleAxisEvent(index, Axis::RightTrigger, static_cast<s32>(ZeroExtend32(ngp.bRightTrigger) << 7));
|
||||
ogp.bRightTrigger = ngp.bRightTrigger;
|
||||
}
|
||||
|
||||
static constexpr std::array<u16, NUM_BUTTONS> button_masks = {
|
||||
{XINPUT_GAMEPAD_A, XINPUT_GAMEPAD_B, XINPUT_GAMEPAD_X, XINPUT_GAMEPAD_Y, XINPUT_GAMEPAD_BACK,
|
||||
0x400 /* XINPUT_GAMEPAD_GUIDE */, XINPUT_GAMEPAD_START, XINPUT_GAMEPAD_LEFT_THUMB, XINPUT_GAMEPAD_RIGHT_THUMB,
|
||||
XINPUT_GAMEPAD_LEFT_SHOULDER, XINPUT_GAMEPAD_RIGHT_SHOULDER, XINPUT_GAMEPAD_DPAD_UP, XINPUT_GAMEPAD_DPAD_DOWN,
|
||||
XINPUT_GAMEPAD_DPAD_LEFT, XINPUT_GAMEPAD_DPAD_RIGHT}};
|
||||
|
||||
const u16 old_button_bits = ogp.wButtons;
|
||||
const u16 new_button_bits = ngp.wButtons;
|
||||
if (old_button_bits != new_button_bits)
|
||||
{
|
||||
for (u32 button = 0; button < static_cast<u32>(button_masks.size()); button++)
|
||||
{
|
||||
const u16 button_mask = button_masks[button];
|
||||
if ((old_button_bits & button_mask) != (new_button_bits & button_mask))
|
||||
HandleButtonEvent(index, button, (new_button_bits & button_mask) != 0);
|
||||
}
|
||||
|
||||
ogp.wButtons = ngp.wButtons;
|
||||
}
|
||||
}
|
||||
|
||||
void XInputControllerInterface::ClearBindings()
|
||||
{
|
||||
for (ControllerData& cd : m_controllers)
|
||||
{
|
||||
cd.axis_mapping.fill({});
|
||||
cd.button_mapping.fill({});
|
||||
cd.axis_button_mapping.fill({});
|
||||
cd.button_axis_mapping.fill({});
|
||||
}
|
||||
}
|
||||
|
||||
bool XInputControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side,
|
||||
AxisCallback callback)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size() || !m_controllers[controller_index].connected)
|
||||
return false;
|
||||
|
||||
if (axis_number < 0 || axis_number >= NUM_AXES)
|
||||
return false;
|
||||
|
||||
m_controllers[controller_index].axis_mapping[axis_number][axis_side] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XInputControllerInterface::BindControllerButton(int controller_index, int button_number, ButtonCallback callback)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size() || !m_controllers[controller_index].connected)
|
||||
return false;
|
||||
|
||||
if (button_number < 0 || button_number >= NUM_BUTTONS)
|
||||
return false;
|
||||
|
||||
m_controllers[controller_index].button_mapping[button_number] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XInputControllerInterface::BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||
ButtonCallback callback)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size() || !m_controllers[controller_index].connected)
|
||||
return false;
|
||||
|
||||
if (axis_number < 0 || axis_number >= NUM_AXES)
|
||||
return false;
|
||||
|
||||
m_controllers[controller_index].axis_button_mapping[axis_number][BoolToUInt8(direction)] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XInputControllerInterface::BindControllerHatToButton(int controller_index, int hat_number,
|
||||
std::string_view hat_position, ButtonCallback callback)
|
||||
{
|
||||
// Hats don't exist in XInput
|
||||
return false;
|
||||
}
|
||||
|
||||
bool XInputControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number,
|
||||
AxisCallback callback)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size() || !m_controllers[controller_index].connected)
|
||||
return false;
|
||||
|
||||
if (button_number < 0 || button_number >= NUM_BUTTONS)
|
||||
return false;
|
||||
|
||||
m_controllers[controller_index].button_axis_mapping[button_number] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XInputControllerInterface::HandleAxisEvent(u32 index, Axis axis, s32 value)
|
||||
{
|
||||
const float f_value = static_cast<float>(value) / (value < 0 ? 32768.0f : 32767.0f);
|
||||
Log_DevPrintf("controller %u axis %u %d %f", index, static_cast<u32>(axis), value, f_value);
|
||||
DebugAssert(index < XUSER_MAX_COUNT);
|
||||
|
||||
if (DoEventHook(Hook::Type::Axis, index, static_cast<u32>(axis), f_value))
|
||||
return true;
|
||||
|
||||
const AxisCallback& cb = m_controllers[index].axis_mapping[static_cast<u32>(axis)][AxisSide::Full];
|
||||
if (cb)
|
||||
{
|
||||
cb(f_value);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
const AxisCallback& positive_cb = m_controllers[index].axis_mapping[static_cast<u32>(axis)][AxisSide::Positive];
|
||||
const AxisCallback& negative_cb = m_controllers[index].axis_mapping[static_cast<u32>(axis)][AxisSide::Negative];
|
||||
if (positive_cb || negative_cb)
|
||||
{
|
||||
if (positive_cb)
|
||||
positive_cb((f_value < 0.0f) ? 0.0f : f_value);
|
||||
if (negative_cb)
|
||||
negative_cb((f_value >= 0.0f) ? 0.0f : -f_value);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// set the other direction to false so large movements don't leave the opposite on
|
||||
const bool outside_deadzone = (std::abs(f_value) >= m_controllers[index].deadzone);
|
||||
const bool positive = (f_value >= 0.0f);
|
||||
const ButtonCallback& other_button_cb =
|
||||
m_controllers[index].axis_button_mapping[static_cast<u32>(axis)][BoolToUInt8(!positive)];
|
||||
const ButtonCallback& button_cb =
|
||||
m_controllers[index].axis_button_mapping[static_cast<u32>(axis)][BoolToUInt8(positive)];
|
||||
if (button_cb)
|
||||
{
|
||||
button_cb(outside_deadzone);
|
||||
if (other_button_cb)
|
||||
other_button_cb(false);
|
||||
return true;
|
||||
}
|
||||
else if (other_button_cb)
|
||||
{
|
||||
other_button_cb(false);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool XInputControllerInterface::HandleButtonEvent(u32 index, u32 button, bool pressed)
|
||||
{
|
||||
Log_DevPrintf("controller %u button %u %s", index, button, pressed ? "pressed" : "released");
|
||||
DebugAssert(index < XUSER_MAX_COUNT);
|
||||
|
||||
static constexpr std::array<FrontendCommon::ControllerNavigationButton, NUM_BUTTONS> nav_button_mapping = {{
|
||||
FrontendCommon::ControllerNavigationButton::Activate, // XINPUT_GAMEPAD_A
|
||||
FrontendCommon::ControllerNavigationButton::Cancel, // XINPUT_GAMEPAD_B
|
||||
FrontendCommon::ControllerNavigationButton::Count, // XINPUT_GAMEPAD_X
|
||||
FrontendCommon::ControllerNavigationButton::Count, // XINPUT_GAMEPAD_Y
|
||||
FrontendCommon::ControllerNavigationButton::Count, // XINPUT_GAMEPAD_BACK
|
||||
FrontendCommon::ControllerNavigationButton::Count, // XINPUT_GAMEPAD_GUIDE
|
||||
FrontendCommon::ControllerNavigationButton::Count, // XINPUT_GAMEPAD_START
|
||||
FrontendCommon::ControllerNavigationButton::Count, // XINPUT_GAMEPAD_LEFT_THUMB
|
||||
FrontendCommon::ControllerNavigationButton::Count, // XINPUT_GAMEPAD_RIGHT_THUMB
|
||||
FrontendCommon::ControllerNavigationButton::LeftShoulder, // XINPUT_GAMEPAD_LEFT_SHOULDER
|
||||
FrontendCommon::ControllerNavigationButton::RightShoulder, // XINPUT_GAMEPAD_RIGHT_SHOULDER
|
||||
FrontendCommon::ControllerNavigationButton::DPadUp, // XINPUT_GAMEPAD_DPAD_UP
|
||||
FrontendCommon::ControllerNavigationButton::DPadDown, // XINPUT_GAMEPAD_DPAD_DOWN
|
||||
FrontendCommon::ControllerNavigationButton::DPadLeft, // XINPUT_GAMEPAD_DPAD_LEFT
|
||||
FrontendCommon::ControllerNavigationButton::DPadRight, // XINPUT_GAMEPAD_DPAD_RIGHT
|
||||
}};
|
||||
|
||||
if (DoEventHook(Hook::Type::Button, index, button, pressed ? 1.0f : 0.0f))
|
||||
return true;
|
||||
|
||||
if (button < nav_button_mapping.size() &&
|
||||
nav_button_mapping[button] != FrontendCommon::ControllerNavigationButton::Count)
|
||||
{
|
||||
m_host_interface->SetControllerNavigationButtonState(nav_button_mapping[button], pressed);
|
||||
}
|
||||
|
||||
if (m_host_interface->IsControllerNavigationActive())
|
||||
{
|
||||
// UI consumed the event
|
||||
return true;
|
||||
}
|
||||
|
||||
const ButtonCallback& cb = m_controllers[index].button_mapping[button];
|
||||
if (cb)
|
||||
{
|
||||
cb(pressed);
|
||||
return true;
|
||||
}
|
||||
|
||||
const AxisCallback& axis_cb = m_controllers[index].button_axis_mapping[button];
|
||||
if (axis_cb)
|
||||
{
|
||||
axis_cb(pressed ? 1.0f : -1.0f);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
u32 XInputControllerInterface::GetControllerRumbleMotorCount(int controller_index)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= XUSER_MAX_COUNT || !m_controllers[controller_index].connected)
|
||||
return 0;
|
||||
|
||||
return NUM_RUMBLE_MOTORS;
|
||||
}
|
||||
|
||||
void XInputControllerInterface::SetControllerRumbleStrength(int controller_index, const float* strengths,
|
||||
u32 num_motors)
|
||||
{
|
||||
DebugAssert(static_cast<u32>(controller_index) < XUSER_MAX_COUNT);
|
||||
|
||||
XINPUT_VIBRATION vib;
|
||||
vib.wLeftMotorSpeed = static_cast<u16>(strengths[0] * 65535.0f);
|
||||
vib.wRightMotorSpeed = static_cast<u16>(strengths[1] * 65535.0f);
|
||||
m_xinput_set_state(static_cast<u32>(controller_index), &vib);
|
||||
}
|
||||
|
||||
bool XInputControllerInterface::SetControllerDeadzone(int controller_index, float size /* = 0.25f */)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= XUSER_MAX_COUNT || !m_controllers[controller_index].connected)
|
||||
return false;
|
||||
|
||||
m_controllers[static_cast<u32>(controller_index)].deadzone = std::clamp(std::abs(size), 0.01f, 0.99f);
|
||||
Log_InfoPrintf("Controller %d deadzone size set to %f", controller_index,
|
||||
m_controllers[static_cast<u32>(controller_index)].deadzone);
|
||||
return true;
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
#pragma once
|
||||
#include "common/windows_headers.h"
|
||||
#include "controller_interface.h"
|
||||
#include "core/types.h"
|
||||
#include <Xinput.h>
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
class XInputControllerInterface final : public ControllerInterface
|
||||
{
|
||||
public:
|
||||
XInputControllerInterface();
|
||||
~XInputControllerInterface() override;
|
||||
|
||||
Backend GetBackend() const override;
|
||||
bool Initialize(CommonHostInterface* host_interface) override;
|
||||
void Shutdown() override;
|
||||
|
||||
// Removes all bindings. Call before setting new bindings.
|
||||
void ClearBindings() override;
|
||||
|
||||
// Binding to events. If a binding for this axis/button already exists, returns false.
|
||||
bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) override;
|
||||
bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override;
|
||||
bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||
ButtonCallback callback) override;
|
||||
bool BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position,
|
||||
ButtonCallback callback) override;
|
||||
bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override;
|
||||
|
||||
// Changing rumble strength.
|
||||
u32 GetControllerRumbleMotorCount(int controller_index) override;
|
||||
void SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors) override;
|
||||
|
||||
// Set deadzone that will be applied on axis-to-button mappings
|
||||
bool SetControllerDeadzone(int controller_index, float size = 0.25f) override;
|
||||
|
||||
void PollEvents() override;
|
||||
|
||||
private:
|
||||
enum : u32
|
||||
{
|
||||
NUM_AXES = 6,
|
||||
NUM_BUTTONS = 15,
|
||||
NUM_RUMBLE_MOTORS = 2
|
||||
};
|
||||
enum class Axis : u32
|
||||
{
|
||||
LeftX,
|
||||
LeftY,
|
||||
RightX,
|
||||
RightY,
|
||||
LeftTrigger,
|
||||
RightTrigger
|
||||
};
|
||||
|
||||
struct ControllerData
|
||||
{
|
||||
XINPUT_STATE last_state = {};
|
||||
bool connected = false;
|
||||
|
||||
float deadzone = 0.25f;
|
||||
|
||||
std::array<std::array<AxisCallback, 3>, NUM_AXES> axis_mapping;
|
||||
std::array<ButtonCallback, NUM_BUTTONS> button_mapping;
|
||||
std::array<std::array<ButtonCallback, 2>, NUM_AXES> axis_button_mapping;
|
||||
std::array<AxisCallback, NUM_BUTTONS> button_axis_mapping;
|
||||
};
|
||||
|
||||
using ControllerDataArray = std::array<ControllerData, XUSER_MAX_COUNT>;
|
||||
|
||||
void CheckForStateChanges(u32 index, const XINPUT_STATE& new_state);
|
||||
bool HandleAxisEvent(u32 index, Axis axis, s32 value);
|
||||
bool HandleButtonEvent(u32 index, u32 button, bool pressed);
|
||||
|
||||
ControllerDataArray m_controllers;
|
||||
|
||||
HMODULE m_xinput_module{};
|
||||
DWORD(WINAPI* m_xinput_get_state)(DWORD, XINPUT_STATE*);
|
||||
DWORD(WINAPI* m_xinput_set_state)(DWORD, XINPUT_VIBRATION*);
|
||||
std::mutex m_event_intercept_mutex;
|
||||
Hook::Callback m_event_intercept_callback;
|
||||
};
|
461
src/frontend-common/xinput_source.cpp
Normal file
461
src/frontend-common/xinput_source.cpp
Normal file
@ -0,0 +1,461 @@
|
||||
#include "xinput_source.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/host.h"
|
||||
#include "input_manager.h"
|
||||
#include <cmath>
|
||||
Log_SetChannel(XInputSource);
|
||||
|
||||
const char* XInputSource::s_axis_names[XInputSource::NUM_AXES] = {
|
||||
"LeftX", // AXIS_LEFTX
|
||||
"LeftY", // AXIS_LEFTY
|
||||
"RightX", // AXIS_RIGHTX
|
||||
"RightY", // AXIS_RIGHTY
|
||||
"LeftTrigger", // AXIS_TRIGGERLEFT
|
||||
"RightTrigger", // AXIS_TRIGGERRIGHT
|
||||
};
|
||||
static const GenericInputBinding s_xinput_generic_binding_axis_mapping[][2] = {
|
||||
{GenericInputBinding::LeftStickLeft, GenericInputBinding::LeftStickRight}, // AXIS_LEFTX
|
||||
{GenericInputBinding::LeftStickUp, GenericInputBinding::LeftStickDown}, // AXIS_LEFTY
|
||||
{GenericInputBinding::RightStickLeft, GenericInputBinding::RightStickRight}, // AXIS_RIGHTX
|
||||
{GenericInputBinding::RightStickUp, GenericInputBinding::RightStickDown}, // AXIS_RIGHTY
|
||||
{GenericInputBinding::Unknown, GenericInputBinding::L2}, // AXIS_TRIGGERLEFT
|
||||
{GenericInputBinding::Unknown, GenericInputBinding::R2}, // AXIS_TRIGGERRIGHT
|
||||
};
|
||||
|
||||
const char* XInputSource::s_button_names[XInputSource::NUM_BUTTONS] = {
|
||||
"DPadUp", // XINPUT_GAMEPAD_DPAD_UP
|
||||
"DPadDown", // XINPUT_GAMEPAD_DPAD_DOWN
|
||||
"DPadLeft", // XINPUT_GAMEPAD_DPAD_LEFT
|
||||
"DPadRight", // XINPUT_GAMEPAD_DPAD_RIGHT
|
||||
"Start", // XINPUT_GAMEPAD_START
|
||||
"Back", // XINPUT_GAMEPAD_BACK
|
||||
"LeftStick", // XINPUT_GAMEPAD_LEFT_THUMB
|
||||
"RightStick", // XINPUT_GAMEPAD_RIGHT_THUMB
|
||||
"LeftShoulder", // XINPUT_GAMEPAD_LEFT_SHOULDER
|
||||
"RightShoulder", // XINPUT_GAMEPAD_RIGHT_SHOULDER
|
||||
"A", // XINPUT_GAMEPAD_A
|
||||
"B", // XINPUT_GAMEPAD_B
|
||||
"X", // XINPUT_GAMEPAD_X
|
||||
"Y", // XINPUT_GAMEPAD_Y
|
||||
"Guide", // XINPUT_GAMEPAD_GUIDE
|
||||
};
|
||||
const u16 XInputSource::s_button_masks[XInputSource::NUM_BUTTONS] = {
|
||||
XINPUT_GAMEPAD_DPAD_UP,
|
||||
XINPUT_GAMEPAD_DPAD_DOWN,
|
||||
XINPUT_GAMEPAD_DPAD_LEFT,
|
||||
XINPUT_GAMEPAD_DPAD_RIGHT,
|
||||
XINPUT_GAMEPAD_START,
|
||||
XINPUT_GAMEPAD_BACK,
|
||||
XINPUT_GAMEPAD_LEFT_THUMB,
|
||||
XINPUT_GAMEPAD_RIGHT_THUMB,
|
||||
XINPUT_GAMEPAD_LEFT_SHOULDER,
|
||||
XINPUT_GAMEPAD_RIGHT_SHOULDER,
|
||||
XINPUT_GAMEPAD_A,
|
||||
XINPUT_GAMEPAD_B,
|
||||
XINPUT_GAMEPAD_X,
|
||||
XINPUT_GAMEPAD_Y,
|
||||
0x400, // XINPUT_GAMEPAD_GUIDE
|
||||
};
|
||||
static const GenericInputBinding s_xinput_generic_binding_button_mapping[] = {
|
||||
GenericInputBinding::DPadUp, // XINPUT_GAMEPAD_DPAD_UP
|
||||
GenericInputBinding::DPadDown, // XINPUT_GAMEPAD_DPAD_DOWN
|
||||
GenericInputBinding::DPadLeft, // XINPUT_GAMEPAD_DPAD_LEFT
|
||||
GenericInputBinding::DPadRight, // XINPUT_GAMEPAD_DPAD_RIGHT
|
||||
GenericInputBinding::Start, // XINPUT_GAMEPAD_START
|
||||
GenericInputBinding::Select, // XINPUT_GAMEPAD_BACK
|
||||
GenericInputBinding::L3, // XINPUT_GAMEPAD_LEFT_THUMB
|
||||
GenericInputBinding::R3, // XINPUT_GAMEPAD_RIGHT_THUMB
|
||||
GenericInputBinding::L1, // XINPUT_GAMEPAD_LEFT_SHOULDER
|
||||
GenericInputBinding::R1, // XINPUT_GAMEPAD_RIGHT_SHOULDER
|
||||
GenericInputBinding::Cross, // XINPUT_GAMEPAD_A
|
||||
GenericInputBinding::Circle, // XINPUT_GAMEPAD_B
|
||||
GenericInputBinding::Square, // XINPUT_GAMEPAD_X
|
||||
GenericInputBinding::Triangle, // XINPUT_GAMEPAD_Y
|
||||
GenericInputBinding::System, // XINPUT_GAMEPAD_GUIDE
|
||||
};
|
||||
|
||||
XInputSource::XInputSource() = default;
|
||||
|
||||
XInputSource::~XInputSource() = default;
|
||||
|
||||
bool XInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
|
||||
{
|
||||
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
|
||||
// xinput1_3.dll is flawed and obsolete, but it's also commonly used by wrappers.
|
||||
// For this reason, try to load it *only* from the application directory, and not system32.
|
||||
m_xinput_module = LoadLibraryExW(L"xinput1_3", nullptr, LOAD_LIBRARY_SEARCH_APPLICATION_DIR);
|
||||
if (!m_xinput_module)
|
||||
{
|
||||
m_xinput_module = LoadLibraryW(L"xinput1_4");
|
||||
}
|
||||
if (!m_xinput_module)
|
||||
{
|
||||
m_xinput_module = LoadLibraryW(L"xinput9_1_0");
|
||||
}
|
||||
if (!m_xinput_module)
|
||||
{
|
||||
Log_ErrorPrintf("Failed to load XInput module.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try the hidden version of XInputGetState(), which lets us query the guide button.
|
||||
m_xinput_get_state =
|
||||
reinterpret_cast<decltype(m_xinput_get_state)>(GetProcAddress(m_xinput_module, reinterpret_cast<LPCSTR>(100)));
|
||||
if (!m_xinput_get_state)
|
||||
reinterpret_cast<decltype(m_xinput_get_state)>(GetProcAddress(m_xinput_module, "XInputGetState"));
|
||||
m_xinput_set_state =
|
||||
reinterpret_cast<decltype(m_xinput_set_state)>(GetProcAddress(m_xinput_module, "XInputSetState"));
|
||||
m_xinput_get_capabilities =
|
||||
reinterpret_cast<decltype(m_xinput_get_capabilities)>(GetProcAddress(m_xinput_module, "XInputGetCapabilities"));
|
||||
#else
|
||||
m_xinput_get_state = XInputGetState;
|
||||
m_xinput_set_state = XInputSetState;
|
||||
m_xinput_get_capabilities = XInputGetCapabilities;
|
||||
m_xinput_get_extended = nullptr;
|
||||
#endif
|
||||
if (!m_xinput_get_state || !m_xinput_set_state || !m_xinput_get_capabilities)
|
||||
{
|
||||
Log_ErrorPrintf("Failed to get XInput function pointers.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void XInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) {}
|
||||
|
||||
void XInputSource::Shutdown()
|
||||
{
|
||||
for (u32 i = 0; i < NUM_CONTROLLERS; i++)
|
||||
{
|
||||
if (m_controllers[i].connected)
|
||||
HandleControllerDisconnection(i);
|
||||
}
|
||||
|
||||
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
|
||||
if (m_xinput_module)
|
||||
{
|
||||
FreeLibrary(m_xinput_module);
|
||||
m_xinput_module = nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
m_xinput_get_state = nullptr;
|
||||
m_xinput_set_state = nullptr;
|
||||
m_xinput_get_capabilities = nullptr;
|
||||
}
|
||||
|
||||
void XInputSource::PollEvents()
|
||||
{
|
||||
for (u32 i = 0; i < NUM_CONTROLLERS; i++)
|
||||
{
|
||||
const bool was_connected = m_controllers[i].connected;
|
||||
|
||||
XINPUT_STATE new_state;
|
||||
DWORD result = m_xinput_get_state(i, &new_state);
|
||||
|
||||
if (result == ERROR_SUCCESS)
|
||||
{
|
||||
if (!was_connected)
|
||||
HandleControllerConnection(i);
|
||||
|
||||
CheckForStateChanges(i, new_state);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (result != ERROR_DEVICE_NOT_CONNECTED)
|
||||
Log_WarningPrintf("XInputGetState(%u) failed: 0x%08X / 0x%08X", i, result, GetLastError());
|
||||
|
||||
if (was_connected)
|
||||
HandleControllerDisconnection(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> XInputSource::EnumerateDevices()
|
||||
{
|
||||
std::vector<std::pair<std::string, std::string>> ret;
|
||||
|
||||
for (u32 i = 0; i < NUM_CONTROLLERS; i++)
|
||||
{
|
||||
if (!m_controllers[i].connected)
|
||||
continue;
|
||||
|
||||
ret.emplace_back(StringUtil::StdStringFromFormat("XInput-%u", i),
|
||||
StringUtil::StdStringFromFormat("XInput Controller %u", i));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::optional<InputBindingKey> XInputSource::ParseKeyString(const std::string_view& device,
|
||||
const std::string_view& binding)
|
||||
{
|
||||
if (!StringUtil::StartsWith(device, "XInput-") || binding.empty())
|
||||
return std::nullopt;
|
||||
|
||||
const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(7));
|
||||
if (!player_id.has_value() || player_id.value() < 0)
|
||||
return std::nullopt;
|
||||
|
||||
InputBindingKey key = {};
|
||||
key.source_type = InputSourceType::XInput;
|
||||
key.source_index = static_cast<u32>(player_id.value());
|
||||
|
||||
if (StringUtil::EndsWith(binding, "Motor"))
|
||||
{
|
||||
key.source_subtype = InputSubclass::ControllerMotor;
|
||||
if (binding == "LargeMotor")
|
||||
{
|
||||
key.data = 0;
|
||||
return key;
|
||||
}
|
||||
else if (binding == "SmallMotor")
|
||||
{
|
||||
key.data = 1;
|
||||
return key;
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
else if (binding[0] == '+' || binding[0] == '-')
|
||||
{
|
||||
// likely an axis
|
||||
const std::string_view axis_name(binding.substr(1));
|
||||
for (u32 i = 0; i < std::size(s_axis_names); i++)
|
||||
{
|
||||
if (axis_name == s_axis_names[i])
|
||||
{
|
||||
// found an axis!
|
||||
key.source_subtype = InputSubclass::ControllerAxis;
|
||||
key.data = i;
|
||||
key.negative = (binding[0] == '-');
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// must be a button
|
||||
for (u32 i = 0; i < std::size(s_button_names); i++)
|
||||
{
|
||||
if (binding == s_button_names[i])
|
||||
{
|
||||
key.source_subtype = InputSubclass::ControllerButton;
|
||||
key.data = i;
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unknown axis/button
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string XInputSource::ConvertKeyToString(InputBindingKey key)
|
||||
{
|
||||
std::string ret;
|
||||
|
||||
if (key.source_type == InputSourceType::XInput)
|
||||
{
|
||||
if (key.source_subtype == InputSubclass::ControllerAxis && key.data < std::size(s_axis_names))
|
||||
{
|
||||
ret = StringUtil::StdStringFromFormat("XInput-%u/%c%s", key.source_index, key.negative ? '-' : '+',
|
||||
s_axis_names[key.data]);
|
||||
}
|
||||
else if (key.source_subtype == InputSubclass::ControllerButton && key.data < std::size(s_button_names))
|
||||
{
|
||||
ret = StringUtil::StdStringFromFormat("XInput-%u/%s", key.source_index, s_button_names[key.data]);
|
||||
}
|
||||
else if (key.source_subtype == InputSubclass::ControllerMotor)
|
||||
{
|
||||
ret = StringUtil::StdStringFromFormat("XInput-%u/%sMotor", key.source_index, key.data ? "Large" : "Small");
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<InputBindingKey> XInputSource::EnumerateMotors()
|
||||
{
|
||||
std::vector<InputBindingKey> ret;
|
||||
|
||||
for (u32 i = 0; i < NUM_CONTROLLERS; i++)
|
||||
{
|
||||
const ControllerData& cd = m_controllers[i];
|
||||
if (!cd.connected)
|
||||
continue;
|
||||
|
||||
if (cd.has_large_motor)
|
||||
ret.push_back(MakeGenericControllerMotorKey(InputSourceType::XInput, i, 0));
|
||||
|
||||
if (cd.has_small_motor)
|
||||
ret.push_back(MakeGenericControllerMotorKey(InputSourceType::XInput, i, 1));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool XInputSource::GetGenericBindingMapping(const std::string_view& device, GenericInputBindingMapping* mapping)
|
||||
{
|
||||
if (!StringUtil::StartsWith(device, "XInput-"))
|
||||
return false;
|
||||
|
||||
const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(7));
|
||||
if (!player_id.has_value() || player_id.value() < 0)
|
||||
return false;
|
||||
|
||||
if (player_id.value() < 0 || player_id.value() >= static_cast<s32>(XUSER_MAX_COUNT))
|
||||
return false;
|
||||
|
||||
// assume all buttons are present.
|
||||
const s32 pid = player_id.value();
|
||||
for (u32 i = 0; i < std::size(s_xinput_generic_binding_axis_mapping); i++)
|
||||
{
|
||||
const GenericInputBinding negative = s_xinput_generic_binding_axis_mapping[i][0];
|
||||
const GenericInputBinding positive = s_xinput_generic_binding_axis_mapping[i][1];
|
||||
if (negative != GenericInputBinding::Unknown)
|
||||
mapping->emplace_back(negative, StringUtil::StdStringFromFormat("XInput-%d/-%s", pid, s_axis_names[i]));
|
||||
|
||||
if (positive != GenericInputBinding::Unknown)
|
||||
mapping->emplace_back(positive, StringUtil::StdStringFromFormat("XInput-%d/+%s", pid, s_axis_names[i]));
|
||||
}
|
||||
for (u32 i = 0; i < std::size(s_xinput_generic_binding_button_mapping); i++)
|
||||
{
|
||||
const GenericInputBinding binding = s_xinput_generic_binding_button_mapping[i];
|
||||
if (binding != GenericInputBinding::Unknown)
|
||||
mapping->emplace_back(binding, StringUtil::StdStringFromFormat("XInput-%d/%s", pid, s_button_names[i]));
|
||||
}
|
||||
|
||||
if (m_controllers[pid].has_small_motor)
|
||||
mapping->emplace_back(GenericInputBinding::SmallMotor,
|
||||
StringUtil::StdStringFromFormat("XInput-%d/SmallMotor", pid));
|
||||
if (m_controllers[pid].has_large_motor)
|
||||
mapping->emplace_back(GenericInputBinding::LargeMotor,
|
||||
StringUtil::StdStringFromFormat("XInput-%d/LargeMotor", pid));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void XInputSource::HandleControllerConnection(u32 index)
|
||||
{
|
||||
Log_InfoPrintf("XInput controller %u connected.", index);
|
||||
|
||||
XINPUT_CAPABILITIES caps = {};
|
||||
if (m_xinput_get_capabilities(index, 0, &caps) != ERROR_SUCCESS)
|
||||
Log_WarningPrintf("Failed to get XInput capabilities for controller %u", index);
|
||||
|
||||
ControllerData& cd = m_controllers[index];
|
||||
cd.connected = true;
|
||||
cd.has_large_motor = caps.Vibration.wLeftMotorSpeed != 0;
|
||||
cd.has_small_motor = caps.Vibration.wRightMotorSpeed != 0;
|
||||
cd.last_state = {};
|
||||
|
||||
Host::OnInputDeviceConnected(StringUtil::StdStringFromFormat("XInput-%u", index),
|
||||
StringUtil::StdStringFromFormat("XInput Controller %u", index));
|
||||
}
|
||||
|
||||
void XInputSource::HandleControllerDisconnection(u32 index)
|
||||
{
|
||||
Log_InfoPrintf("XInput controller %u disconnected.", index);
|
||||
m_controllers[index] = {};
|
||||
|
||||
Host::OnInputDeviceDisconnected(StringUtil::StdStringFromFormat("XInput-%u", index));
|
||||
}
|
||||
|
||||
void XInputSource::CheckForStateChanges(u32 index, const XINPUT_STATE& new_state)
|
||||
{
|
||||
ControllerData& cd = m_controllers[index];
|
||||
if (new_state.dwPacketNumber == cd.last_state.dwPacketNumber)
|
||||
return;
|
||||
|
||||
XINPUT_GAMEPAD& ogp = cd.last_state.Gamepad;
|
||||
const XINPUT_GAMEPAD& ngp = new_state.Gamepad;
|
||||
|
||||
#define CHECK_AXIS(field, axis, min_value, max_value) \
|
||||
if (ogp.field != ngp.field) \
|
||||
{ \
|
||||
InputManager::InvokeEvents(MakeGenericControllerAxisKey(InputSourceType::XInput, index, axis), \
|
||||
static_cast<float>(ngp.field) / ((ngp.field < 0) ? min_value : max_value), \
|
||||
GenericInputBinding::Unknown); \
|
||||
}
|
||||
|
||||
// Y axes is inverted in XInput when compared to SDL.
|
||||
CHECK_AXIS(sThumbLX, AXIS_LEFTX, 32768, 32767);
|
||||
CHECK_AXIS(sThumbLY, AXIS_LEFTY, -32768, -32767);
|
||||
CHECK_AXIS(sThumbRX, AXIS_RIGHTX, 32768, 32767);
|
||||
CHECK_AXIS(sThumbRY, AXIS_RIGHTY, -32768, -32767);
|
||||
CHECK_AXIS(bLeftTrigger, AXIS_LEFTTRIGGER, 0, 255);
|
||||
CHECK_AXIS(bRightTrigger, AXIS_RIGHTTRIGGER, 0, 255);
|
||||
|
||||
#undef CHECK_AXIS
|
||||
|
||||
const u16 old_button_bits = ogp.wButtons;
|
||||
const u16 new_button_bits = ngp.wButtons;
|
||||
if (old_button_bits != new_button_bits)
|
||||
{
|
||||
for (u32 button = 0; button < NUM_BUTTONS; button++)
|
||||
{
|
||||
const u16 button_mask = s_button_masks[button];
|
||||
if ((old_button_bits & button_mask) != (new_button_bits & button_mask))
|
||||
{
|
||||
const GenericInputBinding generic_key = (button < std::size(s_xinput_generic_binding_button_mapping)) ?
|
||||
s_xinput_generic_binding_button_mapping[button] :
|
||||
GenericInputBinding::Unknown;
|
||||
const float value = ((new_button_bits & button_mask) != 0) ? 1.0f : 0.0f;
|
||||
InputManager::InvokeEvents(MakeGenericControllerButtonKey(InputSourceType::XInput, index, button), value,
|
||||
generic_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cd.last_state = new_state;
|
||||
}
|
||||
|
||||
void XInputSource::UpdateMotorState(InputBindingKey key, float intensity)
|
||||
{
|
||||
if (key.source_subtype != InputSubclass::ControllerMotor || key.source_index >= NUM_CONTROLLERS)
|
||||
return;
|
||||
|
||||
ControllerData& cd = m_controllers[key.source_index];
|
||||
if (!cd.connected)
|
||||
return;
|
||||
|
||||
const u16 i_intensity = static_cast<u16>(intensity * 65535.0f);
|
||||
if (key.data != 0)
|
||||
cd.last_vibration.wRightMotorSpeed = i_intensity;
|
||||
else
|
||||
cd.last_vibration.wLeftMotorSpeed = i_intensity;
|
||||
|
||||
m_xinput_set_state(key.source_index, &cd.last_vibration);
|
||||
}
|
||||
|
||||
void XInputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
|
||||
float small_intensity)
|
||||
{
|
||||
if (large_key.source_index != small_key.source_index || large_key.source_subtype != InputSubclass::ControllerMotor ||
|
||||
small_key.source_subtype != InputSubclass::ControllerMotor)
|
||||
{
|
||||
// bonkers config where they're mapped to different controllers... who would do such a thing?
|
||||
UpdateMotorState(large_key, large_intensity);
|
||||
UpdateMotorState(small_key, small_intensity);
|
||||
return;
|
||||
}
|
||||
|
||||
ControllerData& cd = m_controllers[large_key.source_index];
|
||||
if (!cd.connected)
|
||||
return;
|
||||
|
||||
cd.last_vibration.wLeftMotorSpeed = static_cast<u16>(large_intensity * 65535.0f);
|
||||
cd.last_vibration.wRightMotorSpeed = static_cast<u16>(small_intensity * 65535.0f);
|
||||
m_xinput_set_state(large_key.source_index, &cd.last_vibration);
|
||||
}
|
||||
|
||||
std::unique_ptr<InputSource> InputSource::CreateXInputSource()
|
||||
{
|
||||
return std::make_unique<XInputSource>();
|
||||
}
|
77
src/frontend-common/xinput_source.h
Normal file
77
src/frontend-common/xinput_source.h
Normal file
@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
#include "common/windows_headers.h"
|
||||
#include "input_source.h"
|
||||
#include <Xinput.h>
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
class SettingsInterface;
|
||||
|
||||
class XInputSource final : public InputSource
|
||||
{
|
||||
public:
|
||||
XInputSource();
|
||||
~XInputSource();
|
||||
|
||||
bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
|
||||
void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
|
||||
void Shutdown() override;
|
||||
|
||||
void PollEvents() override;
|
||||
std::vector<std::pair<std::string, std::string>> EnumerateDevices() override;
|
||||
std::vector<InputBindingKey> EnumerateMotors() override;
|
||||
bool GetGenericBindingMapping(const std::string_view& device, GenericInputBindingMapping* mapping) override;
|
||||
void UpdateMotorState(InputBindingKey key, float intensity) override;
|
||||
void UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
|
||||
float small_intensity) override;
|
||||
|
||||
std::optional<InputBindingKey> ParseKeyString(const std::string_view& device,
|
||||
const std::string_view& binding) override;
|
||||
std::string ConvertKeyToString(InputBindingKey key) override;
|
||||
|
||||
private:
|
||||
enum : u32
|
||||
{
|
||||
NUM_CONTROLLERS = XUSER_MAX_COUNT, // 4
|
||||
NUM_BUTTONS = 15,
|
||||
};
|
||||
|
||||
enum : u32
|
||||
{
|
||||
AXIS_LEFTX,
|
||||
AXIS_LEFTY,
|
||||
AXIS_RIGHTX,
|
||||
AXIS_RIGHTY,
|
||||
AXIS_LEFTTRIGGER,
|
||||
AXIS_RIGHTTRIGGER,
|
||||
NUM_AXES,
|
||||
};
|
||||
|
||||
struct ControllerData
|
||||
{
|
||||
XINPUT_STATE last_state;
|
||||
XINPUT_VIBRATION last_vibration = {};
|
||||
bool connected = false;
|
||||
bool has_large_motor = false;
|
||||
bool has_small_motor = false;
|
||||
};
|
||||
|
||||
using ControllerDataArray = std::array<ControllerData, NUM_CONTROLLERS>;
|
||||
|
||||
void CheckForStateChanges(u32 index, const XINPUT_STATE& new_state);
|
||||
void HandleControllerConnection(u32 index);
|
||||
void HandleControllerDisconnection(u32 index);
|
||||
|
||||
ControllerDataArray m_controllers;
|
||||
|
||||
HMODULE m_xinput_module{};
|
||||
DWORD(WINAPI* m_xinput_get_state)(DWORD, XINPUT_STATE*);
|
||||
DWORD(WINAPI* m_xinput_set_state)(DWORD, XINPUT_VIBRATION*);
|
||||
DWORD(WINAPI* m_xinput_get_capabilities)(DWORD, DWORD, XINPUT_CAPABILITIES*);
|
||||
|
||||
static const char* s_axis_names[NUM_AXES];
|
||||
static const char* s_button_names[NUM_BUTTONS];
|
||||
static const u16 s_button_masks[NUM_BUTTONS];
|
||||
};
|
Reference in New Issue
Block a user