/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2020 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ #include #include #include #include #include #include "third_party/fmt/include/fmt/format.h" #include "third_party/imgui/imgui.h" #include "xenia/base/clock.h" #include "xenia/base/cvar.h" #include "xenia/base/logging.h" #include "xenia/base/main.h" #include "xenia/base/threading.h" #include "xenia/hid/hid_flags.h" #include "xenia/hid/input_system.h" #include "xenia/ui/imgui_drawer.h" #include "xenia/ui/vulkan/vulkan_provider.h" #include "xenia/ui/window.h" // Available input drivers: #include "xenia/hid/nop/nop_hid.h" #include "xenia/hid/sdl/sdl_hid.h" #if XE_PLATFORM_WIN32 #include "xenia/hid/winkey/winkey_hid.h" #include "xenia/hid/xinput/xinput_hid.h" #endif // XE_PLATFORM_WIN32 DEFINE_string(hid, "any", "Input system. Use: [any, nop, sdl, winkey, xinput]", "General"); #define MAX_USERS 4 #define COL_WIDTH_STATE 320 #define COL_WIDTH_STROKE 416 namespace xe { namespace hid { std::unique_ptr input_system_; std::vector> CreateInputDrivers( ui::Window* window) { std::vector> drivers; if (cvars::hid.compare("nop") == 0) { drivers.emplace_back(xe::hid::nop::Create(window)); } else if (cvars::hid.compare("sdl") == 0) { auto driver = xe::hid::sdl::Create(window); if (XSUCCEEDED(driver->Setup())) { drivers.emplace_back(std::move(driver)); } #if XE_PLATFORM_WIN32 } else if (cvars::hid.compare("winkey") == 0) { auto driver = xe::hid::winkey::Create(window); if (XSUCCEEDED(driver->Setup())) { drivers.emplace_back(std::move(driver)); } } else if (cvars::hid.compare("xinput") == 0) { auto driver = xe::hid::xinput::Create(window); if (XSUCCEEDED(driver->Setup())) { drivers.emplace_back(std::move(driver)); } #endif // XE_PLATFORM_WIN32 } else { auto sdl_driver = xe::hid::sdl::Create(window); if (sdl_driver && XSUCCEEDED(sdl_driver->Setup())) { drivers.emplace_back(std::move(sdl_driver)); } #if XE_PLATFORM_WIN32 auto xinput_driver = xe::hid::xinput::Create(window); if (xinput_driver && XSUCCEEDED(xinput_driver->Setup())) { drivers.emplace_back(std::move(xinput_driver)); } auto winkey_driver = xe::hid::winkey::Create(window); if (winkey_driver && XSUCCEEDED(winkey_driver->Setup())) { drivers.emplace_back(std::move(winkey_driver)); } #endif // XE_PLATFORM_WIN32 if (drivers.empty()) { // Fallback to nop if none created. drivers.emplace_back(xe::hid::nop::Create(window)); } } return drivers; } std::unique_ptr CreateDemoGraphicsProvider( xe::ui::Window* window) { return xe::ui::vulkan::VulkanProvider::Create(window); } void DrawInputGetState(); void DrawInputGetKeystroke(bool poll, bool hide_repeats, bool clear_log); int hid_demo_main(const std::vector& args) { // Create run loop and the window. auto loop = ui::Loop::Create(); auto window = xe::ui::Window::Create(loop.get(), GetEntryInfo().name); loop->PostSynchronous([&window]() { xe::threading::set_name("Win32 Loop"); if (!window->Initialize()) { FatalError("Failed to initialize main window"); return; } }); window->on_closed.AddListener([&loop](xe::ui::UIEvent* e) { loop->Quit(); XELOGI("User-initiated death!"); exit(1); }); loop->on_quit.AddListener([&window](xe::ui::UIEvent* e) { window.reset(); }); // Initial size setting, done here so that it knows the menu exists. window->Resize(COL_WIDTH_STATE + COL_WIDTH_STROKE, 500); // Create the graphics context used for drawing and setup the window. std::unique_ptr graphics_provider; loop->PostSynchronous([&window, &graphics_provider]() { // Create context and give it to the window. // The window will finish initialization wtih the context (loading // resources, etc). graphics_provider = CreateDemoGraphicsProvider(window.get()); window->set_context(graphics_provider->CreateHostContext(window.get())); // Initialize input system and all drivers. input_system_ = std::make_unique(window.get()); auto drivers = CreateInputDrivers(window.get()); for (size_t i = 0; i < drivers.size(); ++i) { input_system_->AddDriver(std::move(drivers[i])); } window->Invalidate(); }); window->set_imgui_input_enabled(true); window->on_painting.AddListener([&](xe::ui::UIEvent* e) { auto& io = window->imgui_drawer()->GetIO(); const ImGuiWindowFlags wflags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar; ImGui::Begin("GetState()", nullptr, wflags); { ImGui::SetWindowPos(ImVec2(0, 0)); ImGui::SetWindowSize(ImVec2(COL_WIDTH_STATE, io.DisplaySize.y)); static bool enable_GetState = false; ImGui::Checkbox("Active", &enable_GetState); ImGui::SameLine(); ImGui::Checkbox("Guide Button", &cvars::guide_button); if (enable_GetState) { ImGui::Spacing(); DrawInputGetState(); } } ImGui::End(); ImGui::Begin("GetKeystroke()", nullptr, wflags); { ImGui::SetWindowPos(ImVec2(COL_WIDTH_STATE, 0)); ImGui::SetWindowSize(ImVec2(COL_WIDTH_STROKE, io.DisplaySize.y)); static bool enable_GetKeystroke = false; static bool hide_repeats = false; ImGui::Checkbox("Active", &enable_GetKeystroke); ImGui::SameLine(); ImGui::Checkbox("Hide repeats", &hide_repeats); ImGui::SameLine(); const bool clear_log = ImGui::Button("Clear"); ImGui::Spacing(); DrawInputGetKeystroke(enable_GetKeystroke, hide_repeats, clear_log); } ImGui::End(); // Continuous paint. window->Invalidate(); }); // Wait until we are exited. loop->AwaitQuit(); input_system_.reset(); loop->PostSynchronous([&graphics_provider]() { graphics_provider.reset(); }); window.reset(); loop.reset(); return 0; } void DrawUserInputGetState(uint32_t user_index) { ImGui::Text("User %u:", user_index); X_INPUT_STATE state; if (input_system_->GetState(user_index, &state) != X_ERROR_SUCCESS) { ImGui::Text(" No controller detected"); return; } ImGui::Text(" Packet Number: %u", static_cast(state.packet_number)); auto& gamepad = state.gamepad; ImGui::Text(" Right Buttons: [%c][%c][%c][%c]", gamepad.buttons & X_INPUT_GAMEPAD_A ? 'A' : ' ', gamepad.buttons & X_INPUT_GAMEPAD_B ? 'B' : ' ', gamepad.buttons & X_INPUT_GAMEPAD_X ? 'X' : ' ', gamepad.buttons & X_INPUT_GAMEPAD_Y ? 'Y' : ' '); ImGui::Text("Special Buttons: [%s][%s][%s]", gamepad.buttons & X_INPUT_GAMEPAD_BACK ? "back" : " ", gamepad.buttons & X_INPUT_GAMEPAD_GUIDE ? "guide" : " ", gamepad.buttons & X_INPUT_GAMEPAD_START ? "start" : " "); ImGui::Text(" D-pad: [%c][%c][%c][%c]", gamepad.buttons & X_INPUT_GAMEPAD_DPAD_UP ? 'U' : ' ', gamepad.buttons & X_INPUT_GAMEPAD_DPAD_DOWN ? 'D' : ' ', gamepad.buttons & X_INPUT_GAMEPAD_DPAD_LEFT ? 'L' : ' ', gamepad.buttons & X_INPUT_GAMEPAD_DPAD_RIGHT ? 'R' : ' '); ImGui::Text(" Thumbs: [%c][%c]", gamepad.buttons & X_INPUT_GAMEPAD_LEFT_THUMB ? 'L' : ' ', gamepad.buttons & X_INPUT_GAMEPAD_RIGHT_THUMB ? 'R' : ' '); ImGui::Text(" Shoulders: [%c][%c]", gamepad.buttons & X_INPUT_GAMEPAD_LEFT_SHOULDER ? 'L' : ' ', gamepad.buttons & X_INPUT_GAMEPAD_RIGHT_SHOULDER ? 'R' : ' '); ImGui::Text(" Left Trigger: %3u", static_cast(gamepad.left_trigger)); ImGui::Text(" Right Trigger: %3u", static_cast(gamepad.right_trigger)); ImGui::Text(" Left Thumb: %6d, %6d", static_cast(gamepad.thumb_lx), static_cast(gamepad.thumb_ly)); ImGui::Text(" Right Thumb: %6d, %6d", static_cast(gamepad.thumb_rx), static_cast(gamepad.thumb_ry)); X_INPUT_VIBRATION vibration; vibration.left_motor_speed = static_cast( float(static_cast(gamepad.left_trigger)) / 255.0f * UINT16_MAX); vibration.right_motor_speed = static_cast( float(static_cast(gamepad.right_trigger)) / 255.0f * UINT16_MAX); input_system_->SetState(user_index, &vibration); ImGui::Text(" Motor Speeds: L %5u, R %5u", static_cast(vibration.left_motor_speed), static_cast(vibration.right_motor_speed)); ImGui::Text(" "); } void DrawInputGetState() { ImGui::BeginChild("##input_get_state_scroll"); for (uint32_t user_index = 0; user_index < MAX_USERS; ++user_index) { DrawUserInputGetState(user_index); } ImGui::EndChild(); } static const std::map::type, const std::string> vk_pretty = { {X_INPUT_GAMEPAD_VK_A, "A"}, {X_INPUT_GAMEPAD_VK_B, "B"}, {X_INPUT_GAMEPAD_VK_X, "X"}, {X_INPUT_GAMEPAD_VK_Y, "Y"}, {X_INPUT_GAMEPAD_VK_RSHOULDER, "R Shoulder"}, {X_INPUT_GAMEPAD_VK_LSHOULDER, "L Shoulder"}, {X_INPUT_GAMEPAD_VK_LTRIGGER, "L Trigger"}, {X_INPUT_GAMEPAD_VK_RTRIGGER, "R Trigger"}, {X_INPUT_GAMEPAD_VK_DPAD_UP, "DPad up"}, {X_INPUT_GAMEPAD_VK_DPAD_DOWN, "DPad down"}, {X_INPUT_GAMEPAD_VK_DPAD_LEFT, "DPad left"}, {X_INPUT_GAMEPAD_VK_DPAD_RIGHT, "DPad right"}, {X_INPUT_GAMEPAD_VK_START, "Start"}, {X_INPUT_GAMEPAD_VK_BACK, "Back"}, {X_INPUT_GAMEPAD_VK_LTHUMB_PRESS, "L Thumb press"}, {X_INPUT_GAMEPAD_VK_RTHUMB_PRESS, "R Thumb press"}, {X_INPUT_GAMEPAD_VK_LTHUMB_UP, "L Thumb up"}, {X_INPUT_GAMEPAD_VK_LTHUMB_DOWN, "L Thumb down"}, {X_INPUT_GAMEPAD_VK_LTHUMB_RIGHT, "L Thumb right"}, {X_INPUT_GAMEPAD_VK_LTHUMB_LEFT, "L Thumb left"}, {X_INPUT_GAMEPAD_VK_LTHUMB_UPLEFT, "L Thumb up & left"}, {X_INPUT_GAMEPAD_VK_LTHUMB_UPRIGHT, "L Thumb up & right"}, {X_INPUT_GAMEPAD_VK_LTHUMB_DOWNRIGHT, "L Thumb down & right"}, {X_INPUT_GAMEPAD_VK_LTHUMB_DOWNLEFT, "L Thumb down & left"}, {X_INPUT_GAMEPAD_VK_RTHUMB_UP, "R Thumb up"}, {X_INPUT_GAMEPAD_VK_RTHUMB_DOWN, "R Thumb down"}, {X_INPUT_GAMEPAD_VK_RTHUMB_RIGHT, "R Thumb right"}, {X_INPUT_GAMEPAD_VK_RTHUMB_LEFT, "R Thumb left"}, {X_INPUT_GAMEPAD_VK_RTHUMB_UPLEFT, "R Thumb up & left"}, {X_INPUT_GAMEPAD_VK_RTHUMB_UPRIGHT, "R Thumb up & right"}, {X_INPUT_GAMEPAD_VK_RTHUMB_DOWNRIGHT, "R Thumb down & right"}, {X_INPUT_GAMEPAD_VK_RTHUMB_DOWNLEFT, "R Thumb down & left"}, }; void DrawUserInputGetKeystroke(uint32_t user_index, bool poll, bool hide_repeats, bool clear_log) { const size_t maxLog = 128; static std::array, MAX_USERS> event_logs; static std::array last_event_times = {}; auto& event_log = event_logs.at(user_index); if (clear_log) { event_log.clear(); } if (poll) { X_INPUT_KEYSTROKE stroke; bool continue_poll; do { continue_poll = false; auto status = input_system_->GetKeystroke(user_index, 0, &stroke); switch (status) { case X_ERROR_SUCCESS: { if (!stroke.virtual_key) { event_log.emplace_front("Error: Empty KEYSTROKE on SUCCESS."); break; } continue_poll = true; const auto now = Clock::QueryHostUptimeMillis(); const auto dur = now - last_event_times[user_index]; last_event_times[user_index] = now; if (hide_repeats && (stroke.flags & X_INPUT_KEYSTROKE_REPEAT)) { break; } const auto key_search = vk_pretty.find(stroke.virtual_key); const auto key = key_search != vk_pretty.end() ? key_search->second : fmt::format("0x{:04x}", stroke.virtual_key); event_log.emplace_front(fmt::format( "{:>6} {:>9}ms {:<20} {} {} {}", ImGui::GetFrameCount(), dur, key, ((stroke.flags & X_INPUT_KEYSTROKE_KEYDOWN) ? "down" : " "), ((stroke.flags & X_INPUT_KEYSTROKE_KEYUP) ? "up" : " "), ((stroke.flags & X_INPUT_KEYSTROKE_REPEAT) ? "repeat" : ""))); break; } case X_ERROR_DEVICE_NOT_CONNECTED: case X_ERROR_EMPTY: break; default: event_log.emplace_front( fmt::format("Error: Unknown result code: 0x{:08X}", status)); break; } } while (continue_poll); } if (ImGui::BeginTabItem(fmt::format("User {}", user_index).c_str())) { ImGui::BeginChild( fmt::format("##input_get_keystroke_scroll_{}", user_index).c_str()); ImGui::Text(" frame since last button flags"); for (auto [it, count] = std::tuple{event_log.begin(), 1}; it != event_log.end(); ++it, ++count) { ImGui::Text("%s", it->c_str()); if (count >= maxLog) { auto last = it; if (++it != event_log.end()) { event_log.erase_after(last); } break; } } ImGui::EndChild(); ImGui::EndTabItem(); } } void DrawInputGetKeystroke(bool poll, bool hide_repeats, bool clear_log) { bool tab_bar = ImGui::BeginTabBar("DrawInputGetKeystroke"); for (uint32_t user_index = 0; user_index < MAX_USERS; ++user_index) { DrawUserInputGetKeystroke(user_index, poll, hide_repeats, clear_log); } if (tab_bar) ImGui::EndTabBar(); } } // namespace hid } // namespace xe DEFINE_ENTRY_POINT("xenia-hid-demo", xe::hid::hid_demo_main, "");