diff --git a/src/core/gpu.cpp b/src/core/gpu.cpp
index 2ca9234a1..d76b904ff 100644
--- a/src/core/gpu.cpp
+++ b/src/core/gpu.cpp
@@ -142,9 +142,13 @@ bool GPU::DoState(StateWrapper& sw)
   sw.Do(&m_crtc_state.display_vram_width);
   sw.Do(&m_crtc_state.display_vram_height);
   sw.Do(&m_crtc_state.horizontal_total);
+  sw.Do(&m_crtc_state.horizontal_active_start);
+  sw.Do(&m_crtc_state.horizontal_active_end);
   sw.Do(&m_crtc_state.horizontal_display_start);
   sw.Do(&m_crtc_state.horizontal_display_end);
   sw.Do(&m_crtc_state.vertical_total);
+  sw.Do(&m_crtc_state.vertical_active_start);
+  sw.Do(&m_crtc_state.vertical_active_end);
   sw.Do(&m_crtc_state.vertical_display_start);
   sw.Do(&m_crtc_state.vertical_display_end);
   sw.Do(&m_crtc_state.fractional_ticks);
@@ -433,33 +437,31 @@ void GPU::UpdateCRTCDisplayParameters()
   const u16 vertical_display_start = std::min<u16>(cs.regs.Y1, vertical_total);
   const u16 vertical_display_end = std::min<u16>(cs.regs.Y2, vertical_total);
 
-  u16 horizontal_visible_start_tick, horizontal_visible_end_tick;
-  u16 vertical_visible_start_line, vertical_visible_end_line;
   if (m_GPUSTAT.pal_mode)
   {
     // TODO: Verify PAL numbers.
     switch (crop_mode)
     {
       case DisplayCropMode::None:
-        horizontal_visible_start_tick = 487;
-        horizontal_visible_end_tick = 3282;
-        vertical_visible_start_line = 20;
-        vertical_visible_end_line = 308;
+        cs.horizontal_active_start = 487;
+        cs.horizontal_active_end = 3282;
+        cs.vertical_active_start = 20;
+        cs.vertical_active_end = 308;
         break;
 
       case DisplayCropMode::Overscan:
-        horizontal_visible_start_tick = 628;
-        horizontal_visible_end_tick = 3188;
-        vertical_visible_start_line = 30;
-        vertical_visible_end_line = 298;
+        cs.horizontal_active_start = 628;
+        cs.horizontal_active_end = 3188;
+        cs.vertical_active_start = 30;
+        cs.vertical_active_end = 298;
         break;
 
       case DisplayCropMode::Borders:
       default:
-        horizontal_visible_start_tick = horizontal_display_start;
-        horizontal_visible_end_tick = horizontal_display_end;
-        vertical_visible_start_line = vertical_display_start;
-        vertical_visible_end_line = vertical_display_end;
+        cs.horizontal_active_start = horizontal_display_start;
+        cs.horizontal_active_end = horizontal_display_end;
+        cs.vertical_active_start = vertical_display_start;
+        cs.vertical_active_end = vertical_display_end;
         break;
     }
   }
@@ -468,25 +470,25 @@ void GPU::UpdateCRTCDisplayParameters()
     switch (crop_mode)
     {
       case DisplayCropMode::None:
-        horizontal_visible_start_tick = 488;
-        horizontal_visible_end_tick = 3288;
-        vertical_visible_start_line = 16;
-        vertical_visible_end_line = 256;
+        cs.horizontal_active_start = 488;
+        cs.horizontal_active_end = 3288;
+        cs.vertical_active_start = 16;
+        cs.vertical_active_end = 256;
         break;
 
       case DisplayCropMode::Overscan:
-        horizontal_visible_start_tick = 608;
-        horizontal_visible_end_tick = 3168;
-        vertical_visible_start_line = 24;
-        vertical_visible_end_line = 248;
+        cs.horizontal_active_start = 608;
+        cs.horizontal_active_end = 3168;
+        cs.vertical_active_start = 24;
+        cs.vertical_active_end = 248;
         break;
 
       case DisplayCropMode::Borders:
       default:
-        horizontal_visible_start_tick = horizontal_display_start;
-        horizontal_visible_end_tick = horizontal_display_end;
-        vertical_visible_start_line = vertical_display_start;
-        vertical_visible_end_line = vertical_display_end;
+        cs.horizontal_active_start = horizontal_display_start;
+        cs.horizontal_active_end = horizontal_display_end;
+        cs.vertical_active_start = vertical_display_start;
+        cs.vertical_active_end = vertical_display_end;
         break;
     }
   }
@@ -495,62 +497,60 @@ void GPU::UpdateCRTCDisplayParameters()
 
   // Determine screen size.
   cs.display_width = std::max<u16>(
-    ((horizontal_visible_end_tick - horizontal_visible_start_tick) + (cs.dot_clock_divider - 1)) / cs.dot_clock_divider,
-    1u);
-  cs.display_height = std::max<u16>((vertical_visible_end_line - vertical_visible_start_line) << height_shift, 1u);
+    ((cs.horizontal_active_end - cs.horizontal_active_start) + (cs.dot_clock_divider - 1)) / cs.dot_clock_divider, 1u);
+  cs.display_height = std::max<u16>((cs.vertical_active_end - cs.vertical_active_start) << height_shift, 1u);
 
   // Determine if we need to adjust the VRAM rectangle (because the display is starting outside the visible area) or add
   // padding.
-  if (horizontal_display_start >= horizontal_visible_start_tick)
+  if (horizontal_display_start >= cs.horizontal_active_start)
   {
-    cs.display_origin_left = (horizontal_display_start - horizontal_visible_start_tick) / cs.dot_clock_divider;
+    cs.display_origin_left = (horizontal_display_start - cs.horizontal_active_start) / cs.dot_clock_divider;
     cs.display_vram_left = m_crtc_state.regs.X;
   }
   else
   {
     cs.display_origin_left = 0;
     cs.display_vram_left = std::min<u16>(
-      m_crtc_state.regs.X + ((horizontal_visible_start_tick - horizontal_display_start) / cs.dot_clock_divider),
+      m_crtc_state.regs.X + ((cs.horizontal_active_start - horizontal_display_start) / cs.dot_clock_divider),
       VRAM_WIDTH - 1);
   }
 
-  if (horizontal_display_end <= horizontal_visible_end_tick)
+  if (horizontal_display_end <= cs.horizontal_active_end)
   {
     cs.display_vram_width =
-      std::max<u16>((((horizontal_display_end - std::max(horizontal_display_start, horizontal_visible_start_tick)) +
+      std::max<u16>((((horizontal_display_end - std::max(horizontal_display_start, cs.horizontal_active_start)) +
                       (cs.dot_clock_divider - 1)) /
                      cs.dot_clock_divider),
                     1u);
   }
   else
   {
-    cs.display_vram_width = std::max<u16>(
-      (((horizontal_visible_end_tick - std::max(horizontal_display_start, horizontal_visible_start_tick)) +
-        (cs.dot_clock_divider - 1)) /
-       cs.dot_clock_divider),
-      1u);
+    cs.display_vram_width =
+      std::max<u16>((((cs.horizontal_active_end - std::max(horizontal_display_start, cs.horizontal_active_start)) +
+                      (cs.dot_clock_divider - 1)) /
+                     cs.dot_clock_divider),
+                    1u);
   }
 
-  if (vertical_display_start >= vertical_visible_start_line)
+  if (vertical_display_start >= cs.vertical_active_start)
   {
-    cs.display_origin_top = (vertical_display_start - vertical_visible_start_line) << height_shift;
+    cs.display_origin_top = (vertical_display_start - cs.vertical_active_start) << height_shift;
     cs.display_vram_top = m_crtc_state.regs.Y;
   }
   else
   {
     cs.display_origin_top = 0;
-    cs.display_vram_top =
-      m_crtc_state.regs.Y + ((vertical_visible_start_line - vertical_display_start) << height_shift);
+    cs.display_vram_top = m_crtc_state.regs.Y + ((cs.vertical_active_start - vertical_display_start) << height_shift);
   }
 
-  if (vertical_display_end <= vertical_visible_end_line)
+  if (vertical_display_end <= cs.vertical_active_end)
   {
-    cs.display_vram_height = (vertical_display_end - std::max(vertical_display_start, vertical_visible_start_line))
+    cs.display_vram_height = (vertical_display_end - std::max(vertical_display_start, cs.vertical_active_start))
                              << height_shift;
   }
   else
   {
-    cs.display_vram_height = (vertical_visible_end_line - std::max(vertical_display_start, vertical_visible_start_line))
+    cs.display_vram_height = (cs.vertical_active_end - std::max(vertical_display_start, cs.vertical_active_start))
                              << height_shift;
   }
 }
@@ -705,6 +705,27 @@ void GPU::Execute(TickCount ticks)
   UpdateSliceTicks();
 }
 
+bool GPU::ConvertScreenCoordinatesToBeamTicksAndLines(s32 window_x, s32 window_y, u32* out_tick, u32* out_line) const
+{
+  const auto [display_x, display_y] = m_host_display->ConvertWindowCoordinatesToDisplayCoordinates(
+    window_x, window_y, m_host_display->GetWindowWidth(), m_host_display->GetWindowHeight(),
+    m_host_display->GetDisplayTopMargin());
+  Log_DebugPrintf("win %d,%d -> disp %d,%d (size %u,%u frac %f,%f)", window_x, window_y, display_x, display_y,
+                  m_crtc_state.display_width, m_crtc_state.display_height,
+                  static_cast<float>(display_x) / static_cast<float>(m_crtc_state.display_width),
+                  static_cast<float>(display_y) / static_cast<float>(m_crtc_state.display_height));
+
+  if (display_x < 0 || static_cast<u32>(display_x) >= m_crtc_state.display_width || display_y < 0 ||
+      static_cast<u32>(display_y) >= m_crtc_state.display_height)
+  {
+    return false;
+  }
+
+  *out_line = (static_cast<u32>(display_y) >> BoolToUInt8(m_GPUSTAT.In480iMode())) + m_crtc_state.vertical_active_start;
+  *out_tick = (static_cast<u32>(display_x) * m_crtc_state.dot_clock_divider) + m_crtc_state.horizontal_active_start;
+  return true;
+}
+
 u32 GPU::ReadGPUREAD()
 {
   if (m_blitter_state != BlitterState::ReadingVRAM)
diff --git a/src/core/gpu.h b/src/core/gpu.h
index eec502891..8d819b9ec 100644
--- a/src/core/gpu.h
+++ b/src/core/gpu.h
@@ -162,6 +162,9 @@ public:
   // gpu_sw.cpp
   static std::unique_ptr<GPU> CreateSoftwareRenderer();
 
+  // Converts window coordinates into horizontal ticks and scanlines. Returns false if out of range. Used for lightguns.
+  bool ConvertScreenCoordinatesToBeamTicksAndLines(s32 window_x, s32 window_y, u32* out_tick, u32* out_line) const;
+
 protected:
   static TickCount GPUTicksToSystemTicks(TickCount gpu_ticks)
   {
@@ -607,9 +610,13 @@ protected:
     u16 display_vram_height;
 
     u16 horizontal_total;
+    u16 horizontal_active_start;
+    u16 horizontal_active_end;
     u16 horizontal_display_start;
     u16 horizontal_display_end;
     u16 vertical_total;
+    u16 vertical_active_start;
+    u16 vertical_active_end;
     u16 vertical_display_start;
     u16 vertical_display_end;
 
diff --git a/src/core/host_display.cpp b/src/core/host_display.cpp
index ef7c3a208..981df7640 100644
--- a/src/core/host_display.cpp
+++ b/src/core/host_display.cpp
@@ -18,7 +18,9 @@ void HostDisplay::WindowResized(s32 new_window_width, s32 new_window_height)
   m_window_height = new_window_height;
 }
 
-std::tuple<s32, s32, s32, s32> HostDisplay::CalculateDrawRect(s32 window_width, s32 window_height, s32 top_margin) const
+void HostDisplay::CalculateDrawRect(s32 window_width, s32 window_height, s32* out_left, s32* out_top, s32* out_width,
+                                    s32* out_height, s32* out_left_padding, s32* out_top_padding, float* out_scale,
+                                    float* out_y_scale) const
 {
   const float y_scale =
     (static_cast<float>(m_display_width) / static_cast<float>(m_display_height)) / m_display_pixel_aspect_ratio;
@@ -28,38 +30,66 @@ std::tuple<s32, s32, s32, s32> HostDisplay::CalculateDrawRect(s32 window_width,
   const float active_top = static_cast<float>(m_display_active_top) * y_scale;
   const float active_width = static_cast<float>(m_display_active_width);
   const float active_height = static_cast<float>(m_display_active_height) * y_scale;
+  if (out_y_scale)
+    *out_y_scale = y_scale;
 
   // now fit it within the window
   const float window_ratio = static_cast<float>(window_width) / static_cast<float>(window_height);
 
   float scale;
-  int top_padding = 0, left_padding = 0;
-
   if ((display_width / display_height) >= window_ratio)
   {
     // align in middle vertically
     scale = static_cast<float>(window_width) / display_width;
-    top_padding = (window_height - top_margin - static_cast<s32>(display_height * scale)) / 2;
+    if (out_left_padding)
+      *out_left_padding = 0;
+    if (out_top_padding)
+      *out_top_padding = std::max<s32>((window_height - static_cast<s32>(display_height * scale)) / 2, 0);
   }
   else
   {
     // align in middle horizontally
-    scale = static_cast<float>(window_height - top_margin) / display_height;
-    left_padding = (window_width - static_cast<s32>(display_width * scale)) / 2;
+    scale = static_cast<float>(window_height) / display_height;
+    if (out_left_padding)
+      *out_left_padding = std::max<s32>((window_width - static_cast<s32>(display_width * scale)) / 2, 0);
+    if (out_top_padding)
+      *out_top_padding = 0;
   }
 
-  int left, top, width, height;
-  width = static_cast<s32>(active_width * scale);
-  height = static_cast<s32>(active_height * scale);
-  left = static_cast<s32>(active_left * scale);
-  top = static_cast<s32>(active_top * scale);
+  *out_width = static_cast<s32>(active_width * scale);
+  *out_height = static_cast<s32>(active_height * scale);
+  *out_left = static_cast<s32>(active_left * scale);
+  *out_top = static_cast<s32>(active_top * scale);
+  if (out_scale)
+    *out_scale = scale;
+}
 
-  left += std::max(left_padding, 0);
-  top += std::max(top_padding, 0);
+std::tuple<s32, s32, s32, s32> HostDisplay::CalculateDrawRect(s32 window_width, s32 window_height, s32 top_margin) const
+{
+  s32 left, top, width, height, left_padding, top_padding;
+  CalculateDrawRect(window_width, window_height - top_margin, &left, &top, &width, &height, &left_padding, &top_padding,
+                    nullptr, nullptr);
+  return std::make_tuple(left + left_padding, top + top_padding + top_margin, width, height);
+}
 
-  // add in margin
-  top += top_margin;
-  return std::tie(left, top, width, height);
+std::tuple<s32, s32> HostDisplay::ConvertWindowCoordinatesToDisplayCoordinates(s32 window_x, s32 window_y,
+                                                                               s32 window_width, s32 window_height,
+                                                                               s32 top_margin) const
+{
+  s32 left, top, width, height, left_padding, top_padding;
+  float scale, y_scale;
+  CalculateDrawRect(window_width, window_height - top_margin, &left, &top, &width, &height, &left_padding, &top_padding,
+                    &scale, &y_scale);
+
+  // convert coordinates to active display region, then to full display region
+  const float scaled_display_x = static_cast<float>(window_x - (left_padding));
+  const float scaled_display_y = static_cast<float>(window_y - (top_padding + top_margin));
+
+  // scale back to internal resolution
+  const float display_x = scaled_display_x / scale;
+  const float display_y = scaled_display_y / scale / y_scale;
+
+  return std::make_tuple(static_cast<s32>(display_x), static_cast<s32>(display_y));
 }
 
 bool HostDisplay::WriteTextureToFile(const void* texture_handle, u32 x, u32 y, u32 width, u32 height,
diff --git a/src/core/host_display.h b/src/core/host_display.h
index a2314a0d2..013ad3988 100644
--- a/src/core/host_display.h
+++ b/src/core/host_display.h
@@ -30,6 +30,9 @@ public:
 
   virtual ~HostDisplay();
 
+  ALWAYS_INLINE s32 GetWindowWidth() const { return m_window_width; }
+  ALWAYS_INLINE s32 GetWindowHeight() const { return m_window_height; }
+
   virtual RenderAPI GetRenderAPI() const = 0;
   virtual void* GetRenderDevice() const = 0;
   virtual void* GetRenderContext() const = 0;
@@ -96,6 +99,10 @@ public:
   /// Helper function for computing the draw rectangle in a larger window.
   std::tuple<s32, s32, s32, s32> CalculateDrawRect(s32 window_width, s32 window_height, s32 top_margin) const;
 
+  /// Helper function for converting window coordinates to display coordinates.
+  std::tuple<s32, s32> ConvertWindowCoordinatesToDisplayCoordinates(s32 window_x, s32 window_y, s32 window_width,
+                                                                    s32 window_height, s32 top_margin) const;
+
   /// Helper function to save texture data to a PNG. If flip_y is set, the image will be flipped aka OpenGL.
   bool WriteTextureToFile(const void* texture_handle, u32 x, u32 y, u32 width, u32 height, const char* filename,
                           bool clear_alpha = true, bool flip_y = false, u32 resize_width = 0, u32 resize_height = 0);
@@ -108,6 +115,10 @@ public:
                                    bool clear_alpha = true);
 
 protected:
+  void CalculateDrawRect(s32 window_width, s32 window_height, s32* out_left, s32* out_top, s32* out_width,
+                         s32* out_height, s32* out_left_padding, s32* out_top_padding, float* out_scale,
+                         float* out_y_scale) const;
+
   s32 m_window_width = 0;
   s32 m_window_height = 0;