// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "justifier.h" #include "gpu.h" #include "host.h" #include "interrupt_controller.h" #include "resources.h" #include "system.h" #include "util/imgui_manager.h" #include "util/input_manager.h" #include "util/state_wrapper.h" #include "common/assert.h" #include "common/log.h" #include "common/path.h" #include "common/string_util.h" #include "IconsPromptFont.h" #include Log_SetChannel(Justifier); // #define CHECK_TIMING 1 #ifdef CHECK_TIMING static u32 s_irq_current_line; #endif static constexpr std::array(Justifier::Binding::ButtonCount)> s_button_indices = {{15, 3, 14}}; Justifier::Justifier(u32 index) : Controller(index) { m_irq_event = TimingEvents::CreateTimingEvent( "Justifier IRQ", 1, 1, [](void* param, TickCount, TickCount) { static_cast(param)->IRQEvent(); }, this, false); } Justifier::~Justifier() { if (!m_cursor_path.empty()) { const u32 cursor_index = GetSoftwarePointerIndex(); if (cursor_index < InputManager::MAX_SOFTWARE_CURSORS) ImGuiManager::ClearSoftwareCursor(cursor_index); } } ControllerType Justifier::GetType() const { return ControllerType::Justifier; } void Justifier::Reset() { m_transfer_state = TransferState::Idle; } bool Justifier::DoState(StateWrapper& sw, bool apply_input_state) { if (!Controller::DoState(sw, apply_input_state)) return false; u16 irq_first_line = m_irq_first_line; u16 irq_last_line = m_irq_last_line; u16 irq_tick = m_irq_tick; u16 button_state = m_button_state; bool shoot_offscreen = m_shoot_offscreen; bool position_valid = m_position_valid; sw.Do(&irq_first_line); sw.Do(&irq_last_line); sw.Do(&irq_tick); sw.Do(&button_state); sw.Do(&shoot_offscreen); sw.Do(&position_valid); if (apply_input_state) { m_irq_first_line = irq_first_line; m_irq_last_line = irq_last_line; m_irq_tick = irq_tick; m_button_state = button_state; m_shoot_offscreen = shoot_offscreen; m_position_valid = position_valid; } sw.Do(&m_transfer_state); if (sw.IsReading()) UpdateIRQEvent(); return true; } float Justifier::GetBindState(u32 index) const { if (index >= s_button_indices.size()) return 0.0f; const u32 bit = s_button_indices[index]; return static_cast(((m_button_state >> bit) & 1u) ^ 1u); } void Justifier::SetBindState(u32 index, float value) { const bool pressed = (value >= 0.5f); if (index == static_cast(Binding::ShootOffscreen)) { if (pressed) m_shoot_offscreen = m_shoot_offscreen ? m_shoot_offscreen : m_offscreen_oob_frames; return; } else if (index >= static_cast(Binding::ButtonCount)) { if (index >= static_cast(Binding::BindingCount) || !m_has_relative_binds) return; if (m_relative_pos[index - static_cast(Binding::RelativeLeft)] != value) { m_relative_pos[index - static_cast(Binding::RelativeLeft)] = value; UpdateSoftwarePointerPosition(); } return; } if (pressed) m_button_state &= ~(u16(1) << s_button_indices[static_cast(index)]); else m_button_state |= u16(1) << s_button_indices[static_cast(index)]; } bool Justifier::IsTriggerPressed() const { return ((m_button_state & (1u << 15)) != 0); } void Justifier::ResetTransferState() { m_transfer_state = TransferState::Idle; } bool Justifier::Transfer(const u8 data_in, u8* data_out) { static constexpr u16 ID = 0x5A31; switch (m_transfer_state) { case TransferState::Idle: { // ack when sent 0x01, send ID for 0x42 if (data_in == 0x42) { *data_out = Truncate8(ID); m_transfer_state = TransferState::IDMSB; UpdatePosition(); return true; } else { *data_out = 0xFF; return (data_in == 0x01); } } case TransferState::IDMSB: { *data_out = Truncate8(ID >> 8); m_transfer_state = TransferState::ButtonsLSB; return true; } case TransferState::ButtonsLSB: { *data_out = Truncate8(m_button_state); m_transfer_state = TransferState::ButtonsMSB; return true; } case TransferState::ButtonsMSB: { *data_out = Truncate8(m_button_state >> 8); m_transfer_state = TransferState::Idle; return true; } default: { UnreachableCode(); } } } void Justifier::UpdatePosition() { if (m_shoot_offscreen > 0) { if (m_shoot_offscreen == m_offscreen_trigger_frames) SetBindState(static_cast(Binding::Trigger), 1.0f); else if (m_shoot_offscreen == m_offscreen_release_frames) SetBindState(static_cast(Binding::Trigger), 0.0f); m_shoot_offscreen--; m_position_valid = false; UpdateIRQEvent(); return; } float display_x, display_y; const auto [window_x, window_y] = (m_has_relative_binds) ? GetAbsolutePositionFromRelativeAxes() : InputManager::GetPointerAbsolutePosition(0); g_gpu->ConvertScreenCoordinatesToDisplayCoordinates(window_x, window_y, &display_x, &display_y); // are we within the active display area? u32 tick, line; if (display_x < 0 || display_y < 0 || !g_gpu->ConvertDisplayCoordinatesToBeamTicksAndLines(display_x, display_y, m_x_scale, &tick, &line) || m_shoot_offscreen) { Log_DevFmt("Lightgun out of range for window coordinates {:.0f},{:.0f}", window_x, window_y); m_position_valid = false; UpdateIRQEvent(); return; } m_position_valid = true; m_irq_tick = static_cast(static_cast(tick) + System::ScaleTicksToOverclock(static_cast(m_tick_offset))); m_irq_first_line = static_cast(std::clamp(static_cast(line) + m_first_line_offset, static_cast(g_gpu->GetCRTCActiveStartLine()), static_cast(g_gpu->GetCRTCActiveEndLine()))); m_irq_last_line = static_cast(std::clamp(static_cast(line) + m_last_line_offset, static_cast(g_gpu->GetCRTCActiveStartLine()), static_cast(g_gpu->GetCRTCActiveEndLine()))); Log_DevFmt("Lightgun window coordinates {},{} -> dpy {},{} -> tick {} line {} [{}-{}]", window_x, window_y, display_x, display_y, tick, line, m_irq_first_line, m_irq_last_line); UpdateIRQEvent(); } void Justifier::UpdateIRQEvent() { // TODO: Avoid deactivate and event sort. m_irq_event->Deactivate(); if (!m_position_valid) return; u32 current_tick, current_line; g_gpu->GetBeamPosition(¤t_tick, ¤t_line); u32 target_line; if (current_line < m_irq_first_line || current_line >= m_irq_last_line) target_line = m_irq_first_line; else target_line = current_line + 1; const TickCount ticks_until_pos = g_gpu->GetSystemTicksUntilTicksAndLine(m_irq_tick, target_line); Log_DebugFmt("Triggering IRQ in {} ticks @ tick {} line {}", ticks_until_pos, m_irq_tick, target_line); m_irq_event->Schedule(ticks_until_pos); } void Justifier::IRQEvent() { #ifdef CHECK_TIMING u32 ticks, line; g_gpu->GetBeamPosition(&ticks, &line); const u32 expected_line = (s_irq_current_line == m_irq_last_line) ? m_irq_first_line : (s_irq_current_line + 1); if (line < expected_line) Log_WarningFmt("IRQ event fired {} lines too early", expected_line - line); else if (line > expected_line) Log_WarningFmt("IRQ event fired {} lines too late", line - expected_line); if (ticks < m_irq_tick) Log_WarningFmt("IRQ event fired {} ticks too early", m_irq_tick - ticks); else if (ticks > m_irq_tick) Log_WarningFmt("IRQ event fired {} ticks too late", ticks - m_irq_tick); s_irq_current_line = line; #endif InterruptController::SetLineState(InterruptController::IRQ::IRQ10, true); InterruptController::SetLineState(InterruptController::IRQ::IRQ10, false); UpdateIRQEvent(); } // TODO: Merge all this crap with guncon std::pair Justifier::GetAbsolutePositionFromRelativeAxes() const { const float screen_rel_x = (((m_relative_pos[1] > 0.0f) ? m_relative_pos[1] : -m_relative_pos[0]) + 1.0f) * 0.5f; const float screen_rel_y = (((m_relative_pos[3] > 0.0f) ? m_relative_pos[3] : -m_relative_pos[2]) + 1.0f) * 0.5f; return std::make_pair(screen_rel_x * ImGuiManager::GetWindowWidth(), screen_rel_y * ImGuiManager::GetWindowHeight()); } bool Justifier::CanUseSoftwareCursor() const { return (InputManager::MAX_POINTER_DEVICES + m_index) < InputManager::MAX_SOFTWARE_CURSORS; } u32 Justifier::GetSoftwarePointerIndex() const { return m_has_relative_binds ? (InputManager::MAX_POINTER_DEVICES + m_index) : 0; } void Justifier::UpdateSoftwarePointerPosition() { if (m_cursor_path.empty() || !CanUseSoftwareCursor()) return; const auto& [window_x, window_y] = GetAbsolutePositionFromRelativeAxes(); ImGuiManager::SetSoftwareCursorPosition(GetSoftwarePointerIndex(), window_x, window_y); } std::unique_ptr Justifier::Create(u32 index) { return std::make_unique(index); } static const Controller::ControllerBindingInfo s_binding_info[] = { #define BUTTON(name, display_name, icon_name, binding, genb) \ { \ name, display_name, icon_name, static_cast(binding), InputBindingInfo::Type::Button, genb \ } #define HALFAXIS(name, display_name, icon_name, binding, genb) \ { \ name, display_name, icon_name, static_cast(binding), InputBindingInfo::Type::HalfAxis, genb \ } // clang-format off BUTTON("Trigger", TRANSLATE_NOOP("Justifier", "Trigger"), ICON_PF_CROSS, Justifier::Binding::Trigger, GenericInputBinding::R2), BUTTON("ShootOffscreen", TRANSLATE_NOOP("Justifier", "Shoot Offscreen"), nullptr, Justifier::Binding::ShootOffscreen, GenericInputBinding::L2), BUTTON("Start", TRANSLATE_NOOP("Justifier", "Start"), ICON_PF_START, Justifier::Binding::Start, GenericInputBinding::Cross), BUTTON("Back", TRANSLATE_NOOP("Justifier", "Back"), ICON_PF_BACK, Justifier::Binding::Back, GenericInputBinding::Circle), HALFAXIS("RelativeLeft", TRANSLATE_NOOP("Justifier", "Relative Left"), ICON_PF_ANALOG_LEFT, Justifier::Binding::RelativeLeft, GenericInputBinding::Unknown), HALFAXIS("RelativeRight", TRANSLATE_NOOP("Justifier", "Relative Right"), ICON_PF_ANALOG_RIGHT, Justifier::Binding::RelativeRight, GenericInputBinding::Unknown), HALFAXIS("RelativeUp", TRANSLATE_NOOP("Justifier", "Relative Up"), ICON_PF_ANALOG_UP, Justifier::Binding::RelativeUp, GenericInputBinding::Unknown), HALFAXIS("RelativeDown", TRANSLATE_NOOP("Justifier", "Relative Down"), ICON_PF_ANALOG_DOWN, Justifier::Binding::RelativeDown, GenericInputBinding::Unknown), // clang-format on #undef BUTTON }; static const SettingInfo s_settings[] = { {SettingInfo::Type::Path, "CrosshairImagePath", TRANSLATE_NOOP("Justifier", "Crosshair Image Path"), TRANSLATE_NOOP("Justifier", "Path to an image to use as a crosshair/cursor."), nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, 0.0f}, {SettingInfo::Type::Float, "CrosshairScale", TRANSLATE_NOOP("Justifier", "Crosshair Image Scale"), TRANSLATE_NOOP("Justifier", "Scale of crosshair image on screen."), "1.0", "0.0001", "100.0", "0.10", "%.0f%%", nullptr, 100.0f}, {SettingInfo::Type::String, "CrosshairColor", TRANSLATE_NOOP("Justifier", "Cursor Color"), TRANSLATE_NOOP("Justifier", "Applies a color to the chosen crosshair images, can be used for multiple players. Specify " "in HTML/CSS format (e.g. #aabbcc)"), "#ffffff", nullptr, nullptr, nullptr, nullptr, nullptr, 0.0f}, {SettingInfo::Type::Float, "XScale", TRANSLATE_NOOP("Justifier", "X Scale"), TRANSLATE_NOOP("Justifier", "Scales X coordinates relative to the center of the screen."), "1.0", "0.01", "2.0", "0.01", "%.0f%%", nullptr, 100.0f}, {SettingInfo::Type::Integer, "FirstLineOffset", TRANSLATE_NOOP("Justifier", "Line Start Offset"), TRANSLATE_NOOP("Justifier", "Offset applied to lightgun vertical position that the Justifier will first trigger on."), "-14", "-128", "127", "1", "%u", nullptr, 0.0f}, {SettingInfo::Type::Integer, "LastLineOffset", TRANSLATE_NOOP("Justifier", "Line End Offset"), TRANSLATE_NOOP("Justifier", "Offset applied to lightgun vertical position that the Justifier will last trigger on."), "-8", "-128", "127", "1", "%u", nullptr, 0.0f}, {SettingInfo::Type::Integer, "TickOffset", TRANSLATE_NOOP("Justifier", "Tick Offset"), TRANSLATE_NOOP("Justifier", "Offset applied to lightgun horizontal position that the Justifier will trigger on."), "50", "-1000", "1000", "1", "%u", nullptr, 0.0f}, {SettingInfo::Type::Integer, "OffscreenOOBFrames", TRANSLATE_NOOP("Justifier", "Off-Screen Out-Of-Bounds Frames"), TRANSLATE_NOOP("Justifier", "Number of frames that the Justifier is pointed out-of-bounds for an off-screen shot."), "5", "0", "80", "1", "%u", nullptr, 0.0f}, {SettingInfo::Type::Integer, "OffscreenTriggerFrames", TRANSLATE_NOOP("Justifier", "Off-Screen Trigger Frames"), TRANSLATE_NOOP("Justifier", "Number of frames that the trigger is held for an off-screen shot."), "5", "0", "80", "1", "%u", nullptr, 0.0f}, {SettingInfo::Type::Integer, "OffscreenReleaseFrames", TRANSLATE_NOOP("Justifier", "Off-Screen Trigger Frames"), TRANSLATE_NOOP("Justifier", "Number of frames that the Justifier is pointed out-of-bounds after the trigger is " "released, for an off-screen shot."), "5", "0", "80", "1", "%u", nullptr, 0.0f}, }; const Controller::ControllerInfo Justifier::INFO = {ControllerType::Justifier, "Justifier", TRANSLATE_NOOP("ControllerType", "Justifier"), nullptr, s_binding_info, s_settings, Controller::VibrationCapabilities::NoVibration}; void Justifier::LoadSettings(SettingsInterface& si, const char* section) { Controller::LoadSettings(si, section); m_x_scale = si.GetFloatValue(section, "XScale", 1.0f); std::string cursor_path = si.GetStringValue(section, "CrosshairImagePath"); const float cursor_scale = si.GetFloatValue(section, "CrosshairScale", 1.0f); u32 cursor_color = 0xFFFFFF; if (std::string cursor_color_str = si.GetStringValue(section, "CrosshairColor", ""); !cursor_color_str.empty()) { // Strip the leading hash, if it's a CSS style colour. const std::optional cursor_color_opt(StringUtil::FromChars( cursor_color_str[0] == '#' ? std::string_view(cursor_color_str).substr(1) : std::string_view(cursor_color_str), 16)); if (cursor_color_opt.has_value()) cursor_color = cursor_color_opt.value(); } #ifndef __ANDROID__ if (cursor_path.empty()) cursor_path = Path::Combine(EmuFolders::Resources, "images/crosshair.png"); #endif const s32 prev_pointer_index = GetSoftwarePointerIndex(); m_has_relative_binds = (si.ContainsValue(section, "RelativeLeft") || si.ContainsValue(section, "RelativeRight") || si.ContainsValue(section, "RelativeUp") || si.ContainsValue(section, "RelativeDown")); const s32 new_pointer_index = GetSoftwarePointerIndex(); if (prev_pointer_index != new_pointer_index || m_cursor_path != cursor_path || m_cursor_scale != cursor_scale || m_cursor_color != cursor_color) { if (prev_pointer_index != new_pointer_index && static_cast(prev_pointer_index) < InputManager::MAX_SOFTWARE_CURSORS) { ImGuiManager::ClearSoftwareCursor(prev_pointer_index); } // Pointer changed, so need to update software cursor. const bool had_software_cursor = m_cursor_path.empty(); m_cursor_path = std::move(cursor_path); m_cursor_scale = cursor_scale; m_cursor_color = cursor_color; if (static_cast(new_pointer_index) < InputManager::MAX_SOFTWARE_CURSORS) { if (!m_cursor_path.empty()) { ImGuiManager::SetSoftwareCursor(new_pointer_index, m_cursor_path, m_cursor_scale, m_cursor_color); if (m_has_relative_binds) UpdateSoftwarePointerPosition(); } else if (had_software_cursor) { ImGuiManager::ClearSoftwareCursor(new_pointer_index); } } } m_first_line_offset = static_cast(std::clamp(si.GetIntValue(section, "FirstLineOffset", DEFAULT_FIRST_LINE_OFFSET), std::numeric_limits::min(), std::numeric_limits::max())); m_last_line_offset = static_cast(std::clamp(si.GetIntValue(section, "LastLineOffset", DEFAULT_LAST_LINE_OFFSET), std::numeric_limits::min(), std::numeric_limits::max())); m_tick_offset = static_cast(std::clamp(si.GetIntValue(section, "TickOffset", DEFAULT_TICK_OFFSET), std::numeric_limits::min(), std::numeric_limits::max())); const s8 offscreen_oob_frames = static_cast(std::clamp(si.GetIntValue(section, "OffscreenOOBFrames", DEFAULT_OFFSCREEN_OOB_FRAMES), std::numeric_limits::min(), std::numeric_limits::max())); const s8 offscreen_trigger_frames = static_cast(std::clamp(si.GetIntValue(section, "OffscreenTriggerFrames", DEFAULT_OFFSCREEN_TRIGGER_FRAMES), std::numeric_limits::min(), std::numeric_limits::max())); const s8 offscreen_release_frames = static_cast(std::clamp(si.GetIntValue(section, "OffscreenReleaseFrames", DEFAULT_OFFSCREEN_RELEASE_FRAMES), std::numeric_limits::min(), std::numeric_limits::max())); m_offscreen_oob_frames = offscreen_oob_frames + offscreen_trigger_frames + offscreen_release_frames; m_offscreen_trigger_frames = m_offscreen_oob_frames - offscreen_trigger_frames; m_offscreen_release_frames = m_offscreen_trigger_frames - offscreen_release_frames; }