UI: Massive revamp, new features and improvements

This commit is contained in:
Connor McLaughlin
2022-07-11 23:03:29 +10:00
parent 3fb61865e5
commit b42b5501f6
425 changed files with 39701 additions and 29487 deletions

View File

@ -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}"

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View 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

View File

@ -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
};

View File

@ -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 {};
}

View File

@ -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;
};

View File

@ -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);

View File

@ -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"

View File

@ -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;
}

View File

@ -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;
};

View 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>();
}

View 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;
};

View File

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

View File

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

View File

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

View File

@ -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" />

View File

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

View File

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

View File

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

View File

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

View File

@ -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 &current;
}
}
}
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;
}

View File

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

View File

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

View File

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

View File

@ -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);

File diff suppressed because it is too large Load Diff

View 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

View 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;
}

View 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

View 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();
}

View 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

View File

@ -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;
}

View File

@ -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;
};

File diff suppressed because it is too large Load Diff

View 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

View File

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

View File

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

View 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 {};
}
}

View 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
};

View File

@ -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));
}
}

View File

@ -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)
{

View File

@ -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;

View File

@ -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;

View File

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

View File

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

View File

@ -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;
}

View File

@ -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;
};

View 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>();
}

View 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;
};

View File

@ -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));
}
}

View 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>();
}

View 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;
};

View File

@ -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;
}

View File

@ -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;
};

View 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>();
}

View 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];
};