From 3325d2c42c78b28f926f271f9b30ed514831c1d9 Mon Sep 17 00:00:00 2001
From: Connor McLaughlin <stenzek@gmail.com>
Date: Fri, 10 Apr 2020 13:34:12 +1000
Subject: [PATCH] GPU: Add Force NTSC timings option

This option forces NTSC timings for PAL games, causing them to either
run faster (more likely) or smoother (less likely).
---
 src/core/gpu.cpp                           | 133 +++++++++++++--------
 src/core/gpu.h                             |   5 +
 src/core/host_interface.cpp                |   3 +
 src/core/settings.cpp                      |   2 +
 src/core/settings.h                        |   1 +
 src/duckstation-qt/gpusettingswidget.cpp   |   1 +
 src/duckstation-qt/gpusettingswidget.ui    |  23 ++--
 src/duckstation-sdl/sdl_host_interface.cpp |   1 +
 8 files changed, 110 insertions(+), 59 deletions(-)

diff --git a/src/core/gpu.cpp b/src/core/gpu.cpp
index c6e22cf78..21ea01d71 100644
--- a/src/core/gpu.cpp
+++ b/src/core/gpu.cpp
@@ -28,6 +28,7 @@ bool GPU::Initialize(HostDisplay* host_display, System* system, DMA* dma, Interr
   m_interrupt_controller = interrupt_controller;
   m_timers = timers;
   m_force_progressive_scan = m_system->GetSettings().display_force_progressive_scan;
+  m_force_ntsc_timings = m_system->GetSettings().gpu_force_ntsc_timings;
   m_tick_event =
     m_system->CreateTimingEvent("GPU Tick", 1, 1, std::bind(&GPU::Execute, this, std::placeholders::_1), true);
   return true;
@@ -37,6 +38,12 @@ void GPU::UpdateSettings()
 {
   m_force_progressive_scan = m_system->GetSettings().display_force_progressive_scan;
 
+  if (m_force_ntsc_timings != m_system->GetSettings().gpu_force_ntsc_timings)
+  {
+    m_force_ntsc_timings = m_system->GetSettings().gpu_force_ntsc_timings;
+    UpdateCRTCConfig();
+  }
+
   // Crop mode calls this, so recalculate the display area
   UpdateCRTCDisplayParameters();
 }
@@ -342,11 +349,6 @@ void GPU::UpdateCRTCConfig()
     cs.current_tick_in_scanline %= NTSC_TICKS_PER_LINE;
   }
 
-  const TickCount ticks_per_frame = cs.horizontal_total * cs.vertical_total;
-  const float vertical_frequency =
-    static_cast<float>(static_cast<double>((u64(MASTER_CLOCK) * 11) / 7) / static_cast<double>(ticks_per_frame));
-  m_system->SetThrottleFrequency(vertical_frequency);
-
   const u8 horizontal_resolution_index = m_GPUSTAT.horizontal_resolution_1 | (m_GPUSTAT.horizontal_resolution_2 << 2);
   cs.dot_clock_divider = dot_clock_dividers[horizontal_resolution_index];
   cs.horizontal_display_start = std::min<u16>(cs.regs.X1, cs.horizontal_total);
@@ -354,6 +356,30 @@ void GPU::UpdateCRTCConfig()
   cs.vertical_display_start = std::min<u16>(cs.regs.Y1, cs.vertical_total);
   cs.vertical_display_end = std::min<u16>(cs.regs.Y2, cs.vertical_total);
 
+  if (m_GPUSTAT.pal_mode && m_force_ntsc_timings)
+  {
+    // scale to NTSC parameters
+    cs.horizontal_display_start =
+      static_cast<u16>((static_cast<u32>(cs.horizontal_display_start) * NTSC_TICKS_PER_LINE) / PAL_TICKS_PER_LINE);
+    cs.horizontal_display_end = static_cast<u16>(
+      ((static_cast<u32>(cs.horizontal_display_end) * NTSC_TICKS_PER_LINE) + (PAL_TICKS_PER_LINE - 1)) /
+      PAL_TICKS_PER_LINE);
+    cs.vertical_display_start =
+      static_cast<u16>((static_cast<u32>(cs.vertical_display_start) * NTSC_TOTAL_LINES) / PAL_TOTAL_LINES);
+    cs.vertical_display_end = static_cast<u16>(
+      ((static_cast<u32>(cs.vertical_display_end) * NTSC_TOTAL_LINES) + (PAL_TOTAL_LINES - 1)) / PAL_TOTAL_LINES);
+
+    cs.vertical_total = NTSC_TOTAL_LINES;
+    cs.current_scanline %= NTSC_TOTAL_LINES;
+    cs.horizontal_total = NTSC_TICKS_PER_LINE;
+    cs.current_tick_in_scanline %= NTSC_TICKS_PER_LINE;
+  }
+
+  const TickCount ticks_per_frame = cs.horizontal_total * cs.vertical_total;
+  const float vertical_frequency =
+    static_cast<float>(static_cast<double>((u64(MASTER_CLOCK) * 11) / 7) / static_cast<double>(ticks_per_frame));
+  m_system->SetThrottleFrequency(vertical_frequency);
+
   UpdateCRTCDisplayParameters();
   UpdateSliceTicks();
 }
@@ -363,33 +389,40 @@ void GPU::UpdateCRTCDisplayParameters()
   CRTCState& cs = m_crtc_state;
   const DisplayCropMode crop_mode = m_system->GetSettings().display_crop_mode;
 
-  u16 horizontal_display_start_tick, horizontal_display_end_tick;
-  u16 vertical_display_start_line, vertical_display_end_line;
+  const u16 horizontal_total = m_GPUSTAT.pal_mode ? PAL_TICKS_PER_LINE : NTSC_TICKS_PER_LINE;
+  const u16 vertical_total = m_GPUSTAT.pal_mode ? PAL_TOTAL_LINES : NTSC_TOTAL_LINES;
+  const u16 horizontal_display_start = std::min<u16>(cs.regs.X1, horizontal_total);
+  const u16 horizontal_display_end = std::min<u16>(cs.regs.X2, horizontal_total);
+  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_display_start_tick = 487;
-        horizontal_display_end_tick = 3282;
-        vertical_display_start_line = 20;
-        vertical_display_end_line = 308;
+        horizontal_visible_start_tick = 487;
+        horizontal_visible_end_tick = 3282;
+        vertical_visible_start_line = 20;
+        vertical_visible_end_line = 308;
         break;
 
       case DisplayCropMode::Overscan:
-        horizontal_display_start_tick = 628;
-        horizontal_display_end_tick = 3188;
-        vertical_display_start_line = 30;
-        vertical_display_end_line = 298;
+        horizontal_visible_start_tick = 628;
+        horizontal_visible_end_tick = 3188;
+        vertical_visible_start_line = 30;
+        vertical_visible_end_line = 298;
         break;
 
       case DisplayCropMode::Borders:
       default:
-        horizontal_display_start_tick = m_crtc_state.horizontal_display_start;
-        horizontal_display_end_tick = m_crtc_state.horizontal_display_end;
-        vertical_display_start_line = m_crtc_state.vertical_display_start;
-        vertical_display_end_line = m_crtc_state.vertical_display_end;
+        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;
         break;
     }
   }
@@ -398,25 +431,25 @@ void GPU::UpdateCRTCDisplayParameters()
     switch (crop_mode)
     {
       case DisplayCropMode::None:
-        horizontal_display_start_tick = 488;
-        horizontal_display_end_tick = 3288;
-        vertical_display_start_line = 16;
-        vertical_display_end_line = 256;
+        horizontal_visible_start_tick = 488;
+        horizontal_visible_end_tick = 3288;
+        vertical_visible_start_line = 16;
+        vertical_visible_end_line = 256;
         break;
 
       case DisplayCropMode::Overscan:
-        horizontal_display_start_tick = 608;
-        horizontal_display_end_tick = 3168;
-        vertical_display_start_line = 24;
-        vertical_display_end_line = 248;
+        horizontal_visible_start_tick = 608;
+        horizontal_visible_end_tick = 3168;
+        vertical_visible_start_line = 24;
+        vertical_visible_end_line = 248;
         break;
 
       case DisplayCropMode::Borders:
       default:
-        horizontal_display_start_tick = m_crtc_state.horizontal_display_start;
-        horizontal_display_end_tick = m_crtc_state.horizontal_display_end;
-        vertical_display_start_line = m_crtc_state.vertical_display_start;
-        vertical_display_end_line = m_crtc_state.vertical_display_end;
+        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;
         break;
     }
   }
@@ -425,69 +458,67 @@ void GPU::UpdateCRTCDisplayParameters()
 
   // Determine screen size.
   cs.display_width = std::max<u16>(
-    ((horizontal_display_end_tick - horizontal_display_start_tick) + (cs.dot_clock_divider - 1)) / cs.dot_clock_divider,
+    ((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_display_end_line - vertical_display_start_line) << height_shift, 1u);
+  cs.display_height = std::max<u16>((vertical_visible_end_line - vertical_visible_start_line) << 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 (cs.horizontal_display_start >= horizontal_display_start_tick)
+  if (horizontal_display_start >= horizontal_visible_start_tick)
   {
-    cs.display_origin_left = (cs.horizontal_display_start - horizontal_display_start_tick) / cs.dot_clock_divider;
+    cs.display_origin_left = (horizontal_display_start - horizontal_visible_start_tick) / 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_display_start_tick - cs.horizontal_display_start) / cs.dot_clock_divider),
+      m_crtc_state.regs.X + ((horizontal_visible_start_tick - horizontal_display_start) / cs.dot_clock_divider),
       VRAM_WIDTH - 1);
   }
 
-  if (cs.horizontal_display_end <= horizontal_display_end_tick)
+  if (horizontal_display_end <= horizontal_visible_end_tick)
   {
     cs.display_vram_width = std::min<u16>(
-      std::max<u16>(
-        (((cs.horizontal_display_end - std::max(cs.horizontal_display_start, horizontal_display_start_tick)) +
-          (cs.dot_clock_divider - 1)) /
-         cs.dot_clock_divider),
-        1u),
+      std::max<u16>((((horizontal_display_end - std::max(horizontal_display_start, horizontal_visible_start_tick)) +
+                      (cs.dot_clock_divider - 1)) /
+                     cs.dot_clock_divider),
+                    1u),
       VRAM_WIDTH - cs.display_vram_left);
   }
   else
   {
     cs.display_vram_width = std::min<u16>(
       std::max<u16>(
-        (((horizontal_display_end_tick - std::max(cs.horizontal_display_start, horizontal_display_start_tick)) +
+        (((horizontal_visible_end_tick - std::max(horizontal_display_start, horizontal_visible_start_tick)) +
           (cs.dot_clock_divider - 1)) /
          cs.dot_clock_divider),
         1u),
       VRAM_WIDTH - cs.display_vram_left);
   }
 
-  if (cs.vertical_display_start >= vertical_display_start_line)
+  if (vertical_display_start >= vertical_visible_start_line)
   {
-    cs.display_origin_top = (cs.vertical_display_start - vertical_display_start_line) << height_shift;
+    cs.display_origin_top = (vertical_display_start - vertical_visible_start_line) << height_shift;
     cs.display_vram_top = m_crtc_state.regs.Y;
   }
   else
   {
     cs.display_origin_top = 0;
-    cs.display_vram_top =
-      std::min<u16>(m_crtc_state.regs.Y + ((vertical_display_start_line - cs.vertical_display_start) << height_shift),
-                    VRAM_HEIGHT - 1);
+    cs.display_vram_top = std::min<u16>(
+      m_crtc_state.regs.Y + ((vertical_visible_start_line - vertical_display_start) << height_shift), VRAM_HEIGHT - 1);
   }
 
-  if (cs.vertical_display_end <= vertical_display_end_line)
+  if (vertical_display_end <= vertical_visible_end_line)
   {
     cs.display_vram_height = std::min<u16>(
-      (cs.vertical_display_end - std::max(cs.vertical_display_start, vertical_display_start_line)) << height_shift,
+      (vertical_display_end - std::max(vertical_display_start, vertical_visible_start_line)) << height_shift,
       VRAM_HEIGHT - cs.display_vram_top);
   }
   else
   {
     cs.display_vram_height = std::min<u16>(
-      (vertical_display_end_line - std::max(cs.vertical_display_start, vertical_display_start_line)) << height_shift,
+      (vertical_visible_end_line - std::max(vertical_display_start, vertical_visible_start_line)) << height_shift,
       VRAM_HEIGHT - cs.display_vram_top);
   }
 
diff --git a/src/core/gpu.h b/src/core/gpu.h
index 21d39c734..9a81eb244 100644
--- a/src/core/gpu.h
+++ b/src/core/gpu.h
@@ -95,6 +95,10 @@ public:
     DOT_TIMER_INDEX = 0,
     HBLANK_TIMER_INDEX = 1,
     MAX_RESOLUTION_SCALE = 16,
+  };
+
+  enum : u16
+  {
     NTSC_TICKS_PER_LINE = 3413,
     NTSC_TOTAL_LINES = 263,
     PAL_TICKS_PER_LINE = 3406,
@@ -531,6 +535,7 @@ protected:
   bool m_set_texture_disable_mask = false;
   bool m_drawing_area_changed = false;
   bool m_force_progressive_scan = false;
+  bool m_force_ntsc_timings = false;
 
   struct CRTCState
   {
diff --git a/src/core/host_interface.cpp b/src/core/host_interface.cpp
index 8eea861d3..ec6c06a3c 100644
--- a/src/core/host_interface.cpp
+++ b/src/core/host_interface.cpp
@@ -877,6 +877,7 @@ void HostInterface::SetDefaultSettings(SettingsInterface& si)
   si.SetBoolValue("GPU", "ScaledDithering", false);
   si.SetBoolValue("GPU", "TextureFiltering", false);
   si.SetBoolValue("GPU", "UseDebugDevice", false);
+  si.SetBoolValue("GPU", "ForceNTSCTimings", false);
 
   si.SetStringValue("Display", "CropMode", "Overscan");
   si.SetBoolValue("Display", "ForceProgressiveScan", true);
@@ -926,6 +927,7 @@ void HostInterface::UpdateSettings(const std::function<void()>& apply_callback)
   const bool old_gpu_true_color = m_settings.gpu_true_color;
   const bool old_gpu_scaled_dithering = m_settings.gpu_scaled_dithering;
   const bool old_gpu_texture_filtering = m_settings.gpu_texture_filtering;
+  const bool old_gpu_force_ntsc_timings = m_settings.gpu_force_ntsc_timings;
   const bool old_display_force_progressive_scan = m_settings.display_force_progressive_scan;
   const bool old_gpu_debug_device = m_settings.gpu_use_debug_device;
   const bool old_vsync_enabled = m_settings.video_sync_enabled;
@@ -979,6 +981,7 @@ void HostInterface::UpdateSettings(const std::function<void()>& apply_callback)
         m_settings.gpu_true_color != old_gpu_true_color ||
         m_settings.gpu_scaled_dithering != old_gpu_scaled_dithering ||
         m_settings.gpu_texture_filtering != old_gpu_texture_filtering ||
+        m_settings.gpu_force_ntsc_timings != old_gpu_force_ntsc_timings ||
         m_settings.display_force_progressive_scan != old_display_force_progressive_scan ||
         m_settings.display_crop_mode != old_display_crop_mode)
     {
diff --git a/src/core/settings.cpp b/src/core/settings.cpp
index 635e4ed29..f48841e08 100644
--- a/src/core/settings.cpp
+++ b/src/core/settings.cpp
@@ -27,6 +27,7 @@ void Settings::Load(SettingsInterface& si)
   gpu_scaled_dithering = si.GetBoolValue("GPU", "ScaledDithering", false);
   gpu_texture_filtering = si.GetBoolValue("GPU", "TextureFiltering", false);
   gpu_use_debug_device = si.GetBoolValue("GPU", "UseDebugDevice", false);
+  gpu_force_ntsc_timings = si.GetBoolValue("GPU", "ForceNTSCTimings", false);
 
   display_crop_mode = ParseDisplayCropMode(
                         si.GetStringValue("Display", "CropMode", GetDisplayCropModeName(DisplayCropMode::None)).c_str())
@@ -89,6 +90,7 @@ void Settings::Save(SettingsInterface& si) const
   si.SetBoolValue("GPU", "ScaledDithering", gpu_scaled_dithering);
   si.SetBoolValue("GPU", "TextureFiltering", gpu_texture_filtering);
   si.SetBoolValue("GPU", "UseDebugDevice", gpu_use_debug_device);
+  si.SetBoolValue("GPU", "ForceNTSCTimings", gpu_force_ntsc_timings);
 
   si.SetStringValue("Display", "CropMode", GetDisplayCropModeName(display_crop_mode));
   si.SetBoolValue("Display", "ForceProgressiveScan", display_force_progressive_scan);
diff --git a/src/core/settings.h b/src/core/settings.h
index 93fdb9c6e..3f1dd5897 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -51,6 +51,7 @@ struct Settings
   bool gpu_scaled_dithering = false;
   bool gpu_texture_filtering = false;
   bool gpu_use_debug_device = false;
+  bool gpu_force_ntsc_timings = false;
   DisplayCropMode display_crop_mode = DisplayCropMode::None;
   bool display_force_progressive_scan = false;
   bool display_linear_filtering = true;
diff --git a/src/duckstation-qt/gpusettingswidget.cpp b/src/duckstation-qt/gpusettingswidget.cpp
index 3ac7026b4..661364c0b 100644
--- a/src/duckstation-qt/gpusettingswidget.cpp
+++ b/src/duckstation-qt/gpusettingswidget.cpp
@@ -22,6 +22,7 @@ GPUSettingsWidget::GPUSettingsWidget(QtHostInterface* host_interface, QWidget* p
   SettingWidgetBinder::BindWidgetToIntSetting(m_host_interface, m_ui.resolutionScale, "GPU/ResolutionScale");
   SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.trueColor, "GPU/TrueColor");
   SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.scaledDithering, "GPU/ScaledDithering");
+  SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.forceNTSCTimings, "GPU/ForceNTSCTimings");
   SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.linearTextureFiltering, "GPU/TextureFiltering");
 
   connect(m_ui.resolutionScale, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
diff --git a/src/duckstation-qt/gpusettingswidget.ui b/src/duckstation-qt/gpusettingswidget.ui
index 2f0bd235f..ea1507bfa 100644
--- a/src/duckstation-qt/gpusettingswidget.ui
+++ b/src/duckstation-qt/gpusettingswidget.ui
@@ -69,20 +69,13 @@
        <widget class="QComboBox" name="cropMode"/>
       </item>
       <item row="1" column="0" colspan="2">
-       <widget class="QCheckBox" name="forceProgressiveScan">
-        <property name="text">
-         <string>Force Progressive Scan</string>
-        </property>
-       </widget>
-      </item>
-      <item row="2" column="0" colspan="2">
        <widget class="QCheckBox" name="displayLinearFiltering">
         <property name="text">
          <string>Linear Upscaling</string>
         </property>
        </widget>
       </item>
-      <item row="3" column="0" colspan="2">
+      <item row="2" column="0" colspan="2">
        <widget class="QCheckBox" name="vsync">
         <property name="text">
          <string>VSync</string>
@@ -123,6 +116,20 @@
        </widget>
       </item>
       <item row="3" column="0" colspan="2">
+       <widget class="QCheckBox" name="forceProgressiveScan">
+        <property name="text">
+         <string>Force Progressive Scan</string>
+        </property>
+       </widget>
+      </item>
+      <item row="4" column="0" colspan="2">
+       <widget class="QCheckBox" name="forceNTSCTimings">
+        <property name="text">
+         <string>Force NTSC Timings (60hz-on-PAL)</string>
+        </property>
+       </widget>
+      </item>
+      <item row="5" column="0" colspan="2">
        <widget class="QCheckBox" name="linearTextureFiltering">
         <property name="text">
          <string>Bilinear Texture Filtering</string>
diff --git a/src/duckstation-sdl/sdl_host_interface.cpp b/src/duckstation-sdl/sdl_host_interface.cpp
index be1524de5..f4d0e64e4 100644
--- a/src/duckstation-sdl/sdl_host_interface.cpp
+++ b/src/duckstation-sdl/sdl_host_interface.cpp
@@ -1302,6 +1302,7 @@ void SDLHostInterface::DrawSettingsWindow()
 
         settings_changed |= ImGui::Checkbox("True 24-bit Color (disables dithering)", &m_settings_copy.gpu_true_color);
         settings_changed |= ImGui::Checkbox("Texture Filtering", &m_settings_copy.gpu_texture_filtering);
+        settings_changed |= ImGui::Checkbox("Force NTSC Timings", &m_settings_copy.gpu_force_ntsc_timings);
         settings_changed |= ImGui::Checkbox("Force Progressive Scan", &m_settings_copy.display_force_progressive_scan);
       }