duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

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(&current_tick, &current_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 }