justifier.cpp (19513B)
1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> 2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) 3 4 #include "justifier.h" 5 #include "gpu.h" 6 #include "host.h" 7 #include "interrupt_controller.h" 8 #include "system.h" 9 10 #include "util/imgui_manager.h" 11 #include "util/input_manager.h" 12 #include "util/state_wrapper.h" 13 14 #include "common/assert.h" 15 #include "common/log.h" 16 #include "common/path.h" 17 #include "common/string_util.h" 18 19 #include "IconsPromptFont.h" 20 #include <array> 21 22 Log_SetChannel(Justifier); 23 24 // #define CHECK_TIMING 1 25 #ifdef CHECK_TIMING 26 static u32 s_irq_current_line; 27 #endif 28 29 static constexpr std::array<u8, static_cast<size_t>(Justifier::Binding::ButtonCount)> s_button_indices = {{15, 3, 14}}; 30 static constexpr std::array<const char*, NUM_CONTROLLER_AND_CARD_PORTS> s_event_names = { 31 {"Justifier IRQ P0", "Justifier IRQ P1", "Justifier IRQ P2", "Justifier IRQ P3", "Justifier IRQ P4", 32 "Justifier IRQ P5", "Justifier IRQ P6", "Justifier IRQ P7"}}; 33 34 Justifier::Justifier(u32 index) 35 : Controller(index), m_irq_event( 36 s_event_names[index], 1, 1, 37 [](void* param, TickCount, TickCount) { static_cast<Justifier*>(param)->IRQEvent(); }, this) 38 { 39 } 40 41 Justifier::~Justifier() 42 { 43 m_irq_event.Deactivate(); 44 45 if (!m_cursor_path.empty()) 46 { 47 const u32 cursor_index = GetSoftwarePointerIndex(); 48 if (cursor_index < InputManager::MAX_SOFTWARE_CURSORS) 49 ImGuiManager::ClearSoftwareCursor(cursor_index); 50 } 51 } 52 53 ControllerType Justifier::GetType() const 54 { 55 return ControllerType::Justifier; 56 } 57 58 void Justifier::Reset() 59 { 60 m_transfer_state = TransferState::Idle; 61 } 62 63 bool Justifier::DoState(StateWrapper& sw, bool apply_input_state) 64 { 65 if (!Controller::DoState(sw, apply_input_state)) 66 return false; 67 68 u16 irq_first_line = m_irq_first_line; 69 u16 irq_last_line = m_irq_last_line; 70 u16 irq_tick = m_irq_tick; 71 u16 button_state = m_button_state; 72 bool shoot_offscreen = m_shoot_offscreen; 73 bool position_valid = m_position_valid; 74 75 sw.Do(&irq_first_line); 76 sw.Do(&irq_last_line); 77 sw.Do(&irq_tick); 78 sw.Do(&button_state); 79 sw.Do(&shoot_offscreen); 80 sw.Do(&position_valid); 81 82 if (apply_input_state) 83 { 84 m_irq_first_line = irq_first_line; 85 m_irq_last_line = irq_last_line; 86 m_irq_tick = irq_tick; 87 m_button_state = button_state; 88 m_shoot_offscreen = shoot_offscreen; 89 m_position_valid = position_valid; 90 } 91 92 sw.Do(&m_transfer_state); 93 94 if (sw.IsReading()) 95 UpdateIRQEvent(); 96 97 return true; 98 } 99 100 float Justifier::GetBindState(u32 index) const 101 { 102 if (index >= s_button_indices.size()) 103 return 0.0f; 104 105 const u32 bit = s_button_indices[index]; 106 return static_cast<float>(((m_button_state >> bit) & 1u) ^ 1u); 107 } 108 109 void Justifier::SetBindState(u32 index, float value) 110 { 111 const bool pressed = (value >= 0.5f); 112 if (index == static_cast<u32>(Binding::ShootOffscreen)) 113 { 114 if (pressed) 115 m_shoot_offscreen = m_shoot_offscreen ? m_shoot_offscreen : m_offscreen_oob_frames; 116 117 return; 118 } 119 else if (index >= static_cast<u32>(Binding::ButtonCount)) 120 { 121 if (index >= static_cast<u32>(Binding::BindingCount) || !m_has_relative_binds) 122 return; 123 124 if (m_relative_pos[index - static_cast<u32>(Binding::RelativeLeft)] != value) 125 { 126 m_relative_pos[index - static_cast<u32>(Binding::RelativeLeft)] = value; 127 UpdateSoftwarePointerPosition(); 128 } 129 130 return; 131 } 132 133 if (pressed) 134 m_button_state &= ~(u16(1) << s_button_indices[static_cast<u8>(index)]); 135 else 136 m_button_state |= u16(1) << s_button_indices[static_cast<u8>(index)]; 137 } 138 139 bool Justifier::IsTriggerPressed() const 140 { 141 return ((m_button_state & (1u << 15)) != 0); 142 } 143 144 void Justifier::ResetTransferState() 145 { 146 m_transfer_state = TransferState::Idle; 147 } 148 149 bool Justifier::Transfer(const u8 data_in, u8* data_out) 150 { 151 static constexpr u16 ID = 0x5A31; 152 153 switch (m_transfer_state) 154 { 155 case TransferState::Idle: 156 { 157 // ack when sent 0x01, send ID for 0x42 158 if (data_in == 0x42) 159 { 160 *data_out = Truncate8(ID); 161 m_transfer_state = TransferState::IDMSB; 162 UpdatePosition(); 163 return true; 164 } 165 else 166 { 167 *data_out = 0xFF; 168 return (data_in == 0x01); 169 } 170 } 171 172 case TransferState::IDMSB: 173 { 174 *data_out = Truncate8(ID >> 8); 175 m_transfer_state = TransferState::ButtonsLSB; 176 return true; 177 } 178 179 case TransferState::ButtonsLSB: 180 { 181 *data_out = Truncate8(m_button_state); 182 m_transfer_state = TransferState::ButtonsMSB; 183 return true; 184 } 185 186 case TransferState::ButtonsMSB: 187 { 188 *data_out = Truncate8(m_button_state >> 8); 189 m_transfer_state = TransferState::Idle; 190 return true; 191 } 192 193 default: 194 { 195 UnreachableCode(); 196 } 197 } 198 } 199 200 void Justifier::UpdatePosition() 201 { 202 if (m_shoot_offscreen > 0) 203 { 204 if (m_shoot_offscreen == m_offscreen_trigger_frames) 205 SetBindState(static_cast<u32>(Binding::Trigger), 1.0f); 206 else if (m_shoot_offscreen == m_offscreen_release_frames) 207 SetBindState(static_cast<u32>(Binding::Trigger), 0.0f); 208 209 m_shoot_offscreen--; 210 m_position_valid = false; 211 UpdateIRQEvent(); 212 return; 213 } 214 215 float display_x, display_y; 216 const auto [window_x, window_y] = (m_has_relative_binds) ? GetAbsolutePositionFromRelativeAxes() : 217 InputManager::GetPointerAbsolutePosition(m_cursor_index); 218 g_gpu->ConvertScreenCoordinatesToDisplayCoordinates(window_x, window_y, &display_x, &display_y); 219 220 // are we within the active display area? 221 u32 tick, line; 222 if (display_x < 0 || display_y < 0 || 223 !g_gpu->ConvertDisplayCoordinatesToBeamTicksAndLines(display_x, display_y, m_x_scale, &tick, &line) || 224 m_shoot_offscreen) 225 { 226 DEV_LOG("Lightgun out of range for window coordinates {:.0f},{:.0f}", window_x, window_y); 227 m_position_valid = false; 228 UpdateIRQEvent(); 229 return; 230 } 231 232 m_position_valid = true; 233 234 m_irq_tick = static_cast<u16>(static_cast<TickCount>(tick) + 235 System::ScaleTicksToOverclock(static_cast<TickCount>(m_tick_offset))); 236 m_irq_first_line = static_cast<u16>(std::clamp<s32>(static_cast<s32>(line) + m_first_line_offset, 237 static_cast<s32>(g_gpu->GetCRTCActiveStartLine()), 238 static_cast<s32>(g_gpu->GetCRTCActiveEndLine()))); 239 m_irq_last_line = static_cast<u16>(std::clamp<s32>(static_cast<s32>(line) + m_last_line_offset, 240 static_cast<s32>(g_gpu->GetCRTCActiveStartLine()), 241 static_cast<s32>(g_gpu->GetCRTCActiveEndLine()))); 242 243 DEV_LOG("Lightgun window coordinates {},{} -> dpy {},{} -> tick {} line {} [{}-{}]", window_x, window_y, display_x, 244 display_y, tick, line, m_irq_first_line, m_irq_last_line); 245 246 UpdateIRQEvent(); 247 } 248 249 void Justifier::UpdateIRQEvent() 250 { 251 // TODO: Avoid deactivate and event sort. 252 m_irq_event.Deactivate(); 253 254 if (!m_position_valid) 255 return; 256 257 u32 current_tick, current_line; 258 g_gpu->GetBeamPosition(¤t_tick, ¤t_line); 259 260 u32 target_line; 261 if (current_line < m_irq_first_line || current_line >= m_irq_last_line) 262 target_line = m_irq_first_line; 263 else 264 target_line = current_line + 1; 265 266 const TickCount ticks_until_pos = g_gpu->GetSystemTicksUntilTicksAndLine(m_irq_tick, target_line); 267 DEBUG_LOG("Triggering IRQ in {} ticks @ tick {} line {}", ticks_until_pos, m_irq_tick, target_line); 268 m_irq_event.Schedule(ticks_until_pos); 269 } 270 271 void Justifier::IRQEvent() 272 { 273 #ifdef CHECK_TIMING 274 u32 ticks, line; 275 g_gpu->GetBeamPosition(&ticks, &line); 276 277 const u32 expected_line = (s_irq_current_line == m_irq_last_line) ? m_irq_first_line : (s_irq_current_line + 1); 278 if (line < expected_line) 279 WARNING_LOG("IRQ event fired {} lines too early", expected_line - line); 280 else if (line > expected_line) 281 WARNING_LOG("IRQ event fired {} lines too late", line - expected_line); 282 if (ticks < m_irq_tick) 283 WARNING_LOG("IRQ event fired {} ticks too early", m_irq_tick - ticks); 284 else if (ticks > m_irq_tick) 285 WARNING_LOG("IRQ event fired {} ticks too late", ticks - m_irq_tick); 286 s_irq_current_line = line; 287 #endif 288 289 InterruptController::SetLineState(InterruptController::IRQ::IRQ10, true); 290 InterruptController::SetLineState(InterruptController::IRQ::IRQ10, false); 291 292 UpdateIRQEvent(); 293 } 294 295 // TODO: Merge all this crap with guncon 296 297 std::pair<float, float> Justifier::GetAbsolutePositionFromRelativeAxes() const 298 { 299 const float screen_rel_x = (((m_relative_pos[1] > 0.0f) ? m_relative_pos[1] : -m_relative_pos[0]) + 1.0f) * 0.5f; 300 const float screen_rel_y = (((m_relative_pos[3] > 0.0f) ? m_relative_pos[3] : -m_relative_pos[2]) + 1.0f) * 0.5f; 301 return std::make_pair(screen_rel_x * ImGuiManager::GetWindowWidth(), screen_rel_y * ImGuiManager::GetWindowHeight()); 302 } 303 304 bool Justifier::CanUseSoftwareCursor() const 305 { 306 return (InputManager::MAX_POINTER_DEVICES + m_index) < InputManager::MAX_SOFTWARE_CURSORS; 307 } 308 309 u32 Justifier::GetSoftwarePointerIndex() const 310 { 311 return m_has_relative_binds ? (InputManager::MAX_POINTER_DEVICES + m_index) : m_cursor_index; 312 } 313 314 void Justifier::UpdateSoftwarePointerPosition() 315 { 316 if (m_cursor_path.empty() || !CanUseSoftwareCursor()) 317 return; 318 319 const auto& [window_x, window_y] = GetAbsolutePositionFromRelativeAxes(); 320 ImGuiManager::SetSoftwareCursorPosition(GetSoftwarePointerIndex(), window_x, window_y); 321 } 322 323 std::unique_ptr<Justifier> Justifier::Create(u32 index) 324 { 325 return std::make_unique<Justifier>(index); 326 } 327 328 static const Controller::ControllerBindingInfo s_binding_info[] = { 329 #define BUTTON(name, display_name, icon_name, binding, genb) \ 330 { \ 331 name, display_name, icon_name, static_cast<u32>(binding), InputBindingInfo::Type::Button, genb \ 332 } 333 #define HALFAXIS(name, display_name, icon_name, binding, genb) \ 334 { \ 335 name, display_name, icon_name, static_cast<u32>(binding), InputBindingInfo::Type::HalfAxis, genb \ 336 } 337 338 // clang-format off 339 {"Pointer", TRANSLATE_NOOP("Justifier", "Pointer/Aiming"), ICON_PF_MOUSE, static_cast<u32>(Justifier::Binding::ButtonCount), InputBindingInfo::Type::Pointer, GenericInputBinding::Unknown}, 340 BUTTON("Trigger", TRANSLATE_NOOP("Justifier", "Trigger"), ICON_PF_CROSS, Justifier::Binding::Trigger, GenericInputBinding::R2), 341 BUTTON("ShootOffscreen", TRANSLATE_NOOP("Justifier", "Shoot Offscreen"), nullptr, Justifier::Binding::ShootOffscreen, GenericInputBinding::L2), 342 BUTTON("Start", TRANSLATE_NOOP("Justifier", "Start"), ICON_PF_START, Justifier::Binding::Start, GenericInputBinding::Cross), 343 BUTTON("Back", TRANSLATE_NOOP("Justifier", "Back"), ICON_PF_BACK, Justifier::Binding::Back, GenericInputBinding::Circle), 344 345 HALFAXIS("RelativeLeft", TRANSLATE_NOOP("Justifier", "Relative Left"), ICON_PF_ANALOG_LEFT, Justifier::Binding::RelativeLeft, GenericInputBinding::Unknown), 346 HALFAXIS("RelativeRight", TRANSLATE_NOOP("Justifier", "Relative Right"), ICON_PF_ANALOG_RIGHT, Justifier::Binding::RelativeRight, GenericInputBinding::Unknown), 347 HALFAXIS("RelativeUp", TRANSLATE_NOOP("Justifier", "Relative Up"), ICON_PF_ANALOG_UP, Justifier::Binding::RelativeUp, GenericInputBinding::Unknown), 348 HALFAXIS("RelativeDown", TRANSLATE_NOOP("Justifier", "Relative Down"), ICON_PF_ANALOG_DOWN, Justifier::Binding::RelativeDown, GenericInputBinding::Unknown), 349 // clang-format on 350 351 #undef BUTTON 352 }; 353 354 static const SettingInfo s_settings[] = { 355 {SettingInfo::Type::Path, "CrosshairImagePath", TRANSLATE_NOOP("Justifier", "Crosshair Image Path"), 356 TRANSLATE_NOOP("Justifier", "Path to an image to use as a crosshair/cursor."), nullptr, nullptr, nullptr, nullptr, 357 nullptr, nullptr, 0.0f}, 358 {SettingInfo::Type::Float, "CrosshairScale", TRANSLATE_NOOP("Justifier", "Crosshair Image Scale"), 359 TRANSLATE_NOOP("Justifier", "Scale of crosshair image on screen."), "1.0", "0.0001", "100.0", "0.10", "%.0f%%", 360 nullptr, 100.0f}, 361 {SettingInfo::Type::String, "CrosshairColor", TRANSLATE_NOOP("Justifier", "Cursor Color"), 362 TRANSLATE_NOOP("Justifier", 363 "Applies a color to the chosen crosshair images, can be used for multiple players. Specify " 364 "in HTML/CSS format (e.g. #aabbcc)"), 365 "#ffffff", nullptr, nullptr, nullptr, nullptr, nullptr, 0.0f}, 366 {SettingInfo::Type::Float, "XScale", TRANSLATE_NOOP("Justifier", "X Scale"), 367 TRANSLATE_NOOP("Justifier", "Scales X coordinates relative to the center of the screen."), "1.0", "0.01", "2.0", 368 "0.01", "%.0f%%", nullptr, 100.0f}, 369 {SettingInfo::Type::Integer, "FirstLineOffset", TRANSLATE_NOOP("Justifier", "Line Start Offset"), 370 TRANSLATE_NOOP("Justifier", 371 "Offset applied to lightgun vertical position that the Justifier will first trigger on."), 372 "-14", "-128", "127", "1", "%u", nullptr, 0.0f}, 373 {SettingInfo::Type::Integer, "LastLineOffset", TRANSLATE_NOOP("Justifier", "Line End Offset"), 374 TRANSLATE_NOOP("Justifier", "Offset applied to lightgun vertical position that the Justifier will last trigger on."), 375 "-8", "-128", "127", "1", "%u", nullptr, 0.0f}, 376 {SettingInfo::Type::Integer, "TickOffset", TRANSLATE_NOOP("Justifier", "Tick Offset"), 377 TRANSLATE_NOOP("Justifier", "Offset applied to lightgun horizontal position that the Justifier will trigger on."), 378 "50", "-1000", "1000", "1", "%u", nullptr, 0.0f}, 379 {SettingInfo::Type::Integer, "OffscreenOOBFrames", TRANSLATE_NOOP("Justifier", "Off-Screen Out-Of-Bounds Frames"), 380 TRANSLATE_NOOP("Justifier", "Number of frames that the Justifier is pointed out-of-bounds for an off-screen shot."), 381 "5", "0", "80", "1", "%u", nullptr, 0.0f}, 382 {SettingInfo::Type::Integer, "OffscreenTriggerFrames", TRANSLATE_NOOP("Justifier", "Off-Screen Trigger Frames"), 383 TRANSLATE_NOOP("Justifier", "Number of frames that the trigger is held for an off-screen shot."), "5", "0", "80", 384 "1", "%u", nullptr, 0.0f}, 385 {SettingInfo::Type::Integer, "OffscreenReleaseFrames", TRANSLATE_NOOP("Justifier", "Off-Screen Trigger Frames"), 386 TRANSLATE_NOOP("Justifier", "Number of frames that the Justifier is pointed out-of-bounds after the trigger is " 387 "released, for an off-screen shot."), 388 "5", "0", "80", "1", "%u", nullptr, 0.0f}, 389 }; 390 391 const Controller::ControllerInfo Justifier::INFO = {ControllerType::Justifier, 392 "Justifier", 393 TRANSLATE_NOOP("ControllerType", "Justifier"), 394 ICON_PF_LIGHT_GUN, 395 s_binding_info, 396 s_settings, 397 Controller::VibrationCapabilities::NoVibration}; 398 399 void Justifier::LoadSettings(SettingsInterface& si, const char* section, bool initial) 400 { 401 Controller::LoadSettings(si, section, initial); 402 403 m_x_scale = si.GetFloatValue(section, "XScale", 1.0f); 404 405 std::string cursor_path = si.GetStringValue(section, "CrosshairImagePath"); 406 const float cursor_scale = si.GetFloatValue(section, "CrosshairScale", 1.0f); 407 u32 cursor_color = 0xFFFFFF; 408 if (std::string cursor_color_str = si.GetStringValue(section, "CrosshairColor", ""); !cursor_color_str.empty()) 409 { 410 // Strip the leading hash, if it's a CSS style colour. 411 const std::optional<u32> cursor_color_opt(StringUtil::FromChars<u32>( 412 cursor_color_str[0] == '#' ? std::string_view(cursor_color_str).substr(1) : std::string_view(cursor_color_str), 413 16)); 414 if (cursor_color_opt.has_value()) 415 cursor_color = cursor_color_opt.value(); 416 } 417 418 #ifndef __ANDROID__ 419 if (cursor_path.empty()) 420 cursor_path = Path::Combine(EmuFolders::Resources, "images/crosshair.png"); 421 #endif 422 423 const s32 prev_pointer_index = GetSoftwarePointerIndex(); 424 425 m_has_relative_binds = (si.ContainsValue(section, "RelativeLeft") || si.ContainsValue(section, "RelativeRight") || 426 si.ContainsValue(section, "RelativeUp") || si.ContainsValue(section, "RelativeDown")); 427 m_cursor_index = 428 static_cast<u8>(InputManager::GetIndexFromPointerBinding(si.GetStringValue(section, "Pointer")).value_or(0)); 429 430 const s32 new_pointer_index = GetSoftwarePointerIndex(); 431 432 if (prev_pointer_index != new_pointer_index || m_cursor_path != cursor_path || m_cursor_scale != cursor_scale || 433 m_cursor_color != cursor_color) 434 { 435 if (!initial && prev_pointer_index != new_pointer_index && 436 static_cast<u32>(prev_pointer_index) < InputManager::MAX_SOFTWARE_CURSORS) 437 { 438 ImGuiManager::ClearSoftwareCursor(prev_pointer_index); 439 } 440 441 // Pointer changed, so need to update software cursor. 442 const bool had_software_cursor = m_cursor_path.empty(); 443 m_cursor_path = std::move(cursor_path); 444 m_cursor_scale = cursor_scale; 445 m_cursor_color = cursor_color; 446 if (static_cast<u32>(new_pointer_index) < InputManager::MAX_SOFTWARE_CURSORS) 447 { 448 if (!m_cursor_path.empty()) 449 { 450 ImGuiManager::SetSoftwareCursor(new_pointer_index, m_cursor_path, m_cursor_scale, m_cursor_color); 451 if (m_has_relative_binds) 452 UpdateSoftwarePointerPosition(); 453 } 454 else if (had_software_cursor) 455 { 456 ImGuiManager::ClearSoftwareCursor(new_pointer_index); 457 } 458 } 459 } 460 461 m_first_line_offset = 462 static_cast<s8>(std::clamp<int>(si.GetIntValue(section, "FirstLineOffset", DEFAULT_FIRST_LINE_OFFSET), 463 std::numeric_limits<s8>::min(), std::numeric_limits<s8>::max())); 464 m_last_line_offset = 465 static_cast<s8>(std::clamp<int>(si.GetIntValue(section, "LastLineOffset", DEFAULT_LAST_LINE_OFFSET), 466 std::numeric_limits<s8>::min(), std::numeric_limits<s8>::max())); 467 m_tick_offset = static_cast<s16>(std::clamp<int>(si.GetIntValue(section, "TickOffset", DEFAULT_TICK_OFFSET), 468 std::numeric_limits<s16>::min(), std::numeric_limits<s16>::max())); 469 470 const s8 offscreen_oob_frames = 471 static_cast<s8>(std::clamp<int>(si.GetIntValue(section, "OffscreenOOBFrames", DEFAULT_OFFSCREEN_OOB_FRAMES), 472 std::numeric_limits<s8>::min(), std::numeric_limits<s8>::max())); 473 const s8 offscreen_trigger_frames = 474 static_cast<s8>(std::clamp<int>(si.GetIntValue(section, "OffscreenTriggerFrames", DEFAULT_OFFSCREEN_TRIGGER_FRAMES), 475 std::numeric_limits<s8>::min(), std::numeric_limits<s8>::max())); 476 const s8 offscreen_release_frames = 477 static_cast<s8>(std::clamp<int>(si.GetIntValue(section, "OffscreenReleaseFrames", DEFAULT_OFFSCREEN_RELEASE_FRAMES), 478 std::numeric_limits<s8>::min(), std::numeric_limits<s8>::max())); 479 m_offscreen_oob_frames = offscreen_oob_frames + offscreen_trigger_frames + offscreen_release_frames; 480 m_offscreen_trigger_frames = m_offscreen_oob_frames - offscreen_trigger_frames; 481 m_offscreen_release_frames = m_offscreen_trigger_frames - offscreen_release_frames; 482 }