Scanline Sync Issues on Multi-Monitor Systems

Discussion in 'Rivatuner Statistics Server (RTSS) Forum' started by TheRealKaldaien, Oct 26, 2021.

  1. TheRealKaldaien

    TheRealKaldaien Member

    Messages:
    21
    Likes Received:
    10
    GPU:
    RTX 2080 Ti K|NGP|N
    I am currently developing a system similar to Scanline Sync for Special K and having a hard time comparing the two because my system has 4 monitors and RTSS is getting the timing of the wrong monitor.

    At first, I believed RTSS was getting the timing from the Primary monitor, this proved false when I encountered it using timings from monitors that are not even turned on. I do not know what RTSS is currently using for timings, but it could use some work and hopefully my own experience building this functionality on multi-monitor systems can help you out.


    I know nothing about your implementation details, please do not be insulted if what I am telling you here is something you already know ;)


    1. DwmGetCompositionTimingInfo (...), it is (sort of) suitable for use on single-monitor systems, though even in their case, rateRefresh is not constant. This might be a suitable API for saving power or something in a desktop application, but not for tearline steering.


    2. Games move between monitors. Shocking, I know... :p NVIDIA's Reflex API even gets this wrong and will cache the monitor's refresh rate long after it has been moved by the game (or user) to a different monitor.


    I do not think that RTSS traditionally has a reason to pay attention to the location of a game window, but to do this the correct way you are going to need to start.


    What I do is the following:

    At application start, I enumerate all of the active display paths (QueryDisplayConfig function (winuser.h) - Win32 apps | Microsoft Docs) and I make note of the desktop dimensions and position of the target path (DISPLAYCONFIG_DESKTOP_IMAGE_INFO) and the signal timing (DISPLAYCONFIG_TARGET_MODE).

    Whenever a game window moves, I test its intersection against an array of these active display paths. Whatever display path has the greatest intersection is the one whose timing you must use for the game.

    There is a gotcha, you cannot cache these display paths forever. They become invalidated when a monitor goes to sleep or the user changes config settings and when they are invalidated Windows will broadcast WM_DISPLAYCHANGE. Re-enumerate display paths in response to that message, and re-run any window intersection tests to figure out the monitor timings to use for Scanline Sync.


    It should really be that simple, you can avoid messing around with any graphics API such as GDI, D3D9, DXGI, etc. and cut straight to the chase with WM_DISPLAYCHANGE and QueryDisplayConfig (...).

    Those other APIs have various ways of changing resolution/refresh or moving a game to a different monitor, but they all physically move the game's window and perform operations that will broadcast WM_DISPLAYCHANGE.

    Incidentally, do not use a DXGI Output or a DXGI Factory older than any WM_DISPLAYCHANGE message, DXGI caches stuff and you will chronically get wrong information without creating a new factory. Other APIs are less confusing and more likely to give you accurate info when display settings change; best to avoid DX APIs entirely.​

    When enumerating paths, be sure to confirm DISPLAYCONFIG_PATH_ACTIVE, toss out any that are not flagged that way. Likewise, validate the target info for DISPLAYCONFIG_TARGET_IN_USE, if it is not flagged the monitor is probably turned off.

    Doing both of these things will save you a lot of time and avoid problems like RTSS currently has with getting timings from a monitor that is not even powered on.​


    Tl;Dr: Even NVIDIA gets this wrong, but multi-monitor desktops are pretty common and easy to handle.
     
    Last edited: Oct 26, 2021
  2. TheRealKaldaien

    TheRealKaldaien Member

    Messages:
    21
    Likes Received:
    10
    GPU:
    RTX 2080 Ti K|NGP|N
    Almost slipped my mind.

    If you use any D3DKMT APIs for timing, they want a VidPn index. You need to open the active GDI display's adapter to get the correct VidPn id when a window moves to a different monitor.


    Code:
    typedef struct _D3DKMT_OPENADAPTERFROMGDIDISPLAYNAME {
      WCHAR                          DeviceName [32]; // (i.e. \\.\DISPLAY3\)
      D3DKMT_HANDLE                  hAdapter;
      LUID                           AdapterLuid;
      D3DDDI_VIDEO_PRESENT_SOURCE_ID VidPnSourceId;
    } D3DKMT_OPENADAPTERFROMGDIDISPLAYNAME;
    
    using  D3DKMTOpenAdapterFromGdiDisplayName_pfn = NTSTATUS (WINAPI *)(D3DKMT_OPENADAPTERFROMGDIDISPLAYNAME* unnamedParam1);
    static D3DKMTOpenAdapterFromGdiDisplayName_pfn
           D3DKMTOpenAdapterFromGdiDisplayName = nullptr;
     
  3. Unwinder

    Unwinder Ancient Guru Staff Member

    Messages:
    17,123
    Likes Received:
    6,686
    Excellent, more alternate implementations available for end users = better for everyone.

    There is SyncDisplay entry in RTSS profiles, which defines target display for scanline sync in my implementation. SyncDisplay translates to device index for EnumDisplayDevices. By default it is 0, so it is not the primary device but the first device enumerated by OS.

    As far as I can see DwmGetCompositionTimintInfo is kinda broken in Win11 now for refresh rate reporting. It returns composition rate instead of refresh on my system, that is why it is not constant. On Win10 rateRefresh is static and ideally matches expected result.
     
  4. TheRealKaldaien

    TheRealKaldaien Member

    Messages:
    21
    Likes Received:
    10
    GPU:
    RTX 2080 Ti K|NGP|N
    Finally,

    Could I request the active monitor, native refresh and resolution as a performance counter / stat in MSI Afterburner? This is a stat that would be helpful to a lot of software like CapFrameX and it's easy to get if you implement the stuff I discussed already.

    Code:
      DISPLAYCONFIG_TARGET_DEVICE_NAME
        getTargetName                     = { };
        getTargetName.header.type         = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME;
        getTargetName.header.size         =  sizeof (DISPLAYCONFIG_TARGET_DEVICE_NAME);
        getTargetName.header.adapterId    = display.vidpn.targetInfo.adapterId;
        getTargetName.header.id           = display.vidpn.targetInfo.id;
    
      if ( ERROR_SUCCESS == DisplayConfigGetDeviceInfo ( (DISPLAYCONFIG_DEVICE_INFO_HEADER *)&getTargetName ) )
      {
        wcsncpy_s ( display.name,                                   64,
                    getTargetName.monitorFriendlyDeviceName, _TRUNCATE );
    
    
        // Didn't get a name using the Windows APIs, let's fallback to EDID
        if (*display.name == L'\0')
        {
          // ... You may have to handle this
        }
      }
    
    
      DISPLAYCONFIG_TARGET_PREFERRED_MODE
        getPreferredMode                  = { };
        getPreferredMode.header.type      = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_PREFERRED_MODE;
        getPreferredMode.header.size      =         sizeof (DISPLAYCONFIG_TARGET_PREFERRED_MODE);
        getPreferredMode.header.adapterId = display.vidpn.targetInfo.adapterId;
        getPreferredMode.header.id        = display.vidpn.targetInfo.id;
    
      if ( ERROR_SUCCESS == DisplayConfigGetDeviceInfo ( (DISPLAYCONFIG_DEVICE_INFO_HEADER *)&getPreferredMode ) )
      {
        display.native.width   = getPreferredMode.width;
        display.native.height  = getPreferredMode.height;
    
        display.native.refresh = {
          getPreferredMode.targetMode.targetVideoSignalInfo.vSyncFreq.Numerator,
          getPreferredMode.targetMode.targetVideoSignalInfo.vSyncFreq.Denominator
        };
      }
      //
      // Those are not active resolution or refresh, don't mistake them for that, but still useful info
    
    In rare cases, Windows doesn't have an actual display name available. An implementation for parsing display name from EDID can be found in Special K's GitLab repo:

    https://gitlab.special-k.info/Kaldaien/SpecialK/-/blob/21.x.y/src/render/render_backend.cpp#L2218


    Native resolution and display name are both available through Windows APIs and through the EDID itself. Both are handy for people using MSI Afterburner for monitoring.
     

  5. Unwinder

    Unwinder Ancient Guru Staff Member

    Messages:
    17,123
    Likes Received:
    6,686
    Thanks for sharing. Not sure if I'll automate target display detection instead of manual target display selection as it is now, but if I were doing that I'd probably do it slightly different. Probably I mislook something, but why do wee need that overcomplexity with manual saving monitor rectangles, manual testing for maximum intersection etc? All that is already inside multimonitor API, MonitorFromWindow should automate the trick with intersection testing, then you can use this HMONITOR to reconcile it with GDI display name enumarated by EnumDisplayDevices and finally D3DKMTOpenAdapterFromGdiDisplayName for that GDI display name.
     
  6. Unwinder

    Unwinder Ancient Guru Staff Member

    Messages:
    17,123
    Likes Received:
    6,686
    Something close to native resolution is already available, <RES> tag introduced a few months ago allows displaying current framebuffer/viewport resolution in overlay, sample overlay included into OverlayEditor uses that:

    https://forums.guru3d.com/threads/msi-ab-rtss-development-news-thread.412822/page-161#post-5948662

    I'm not that optimistic about adding native refresh rate reporting in such form. I see it a jar of worms feature in case of reporting it as OS refresh rate. VRR display owners and owners of monitors with physical button for refresh rate switching will be unhappy to see it static and will start demanding to fix it with no doubts.
     
  7. Unwinder

    Unwinder Ancient Guru Staff Member

    Messages:
    17,123
    Likes Received:
    6,686
    Had some spare time to try the way I offered. Latching monitor handle returned by MonitorFromWindow(GetForegroundWindow()) and recalibrating scanline sync on monitor handle change does the trick. On recalibration that monitor handle maps to associated GDI display name used for D3DKMT with GetMonitorInfo. I'll map such autodetection mode to SyncDisplay=-1 profile setting. Positive values will still allow defining target scanline sync device manually like before.

    MonitorFromWindow(GetForegroundWindow()) is rather lightweight (5-30 microseconds on my dual monitor setup) so it can be performed per frame instead of tracking actual window position changes. I left it that way, just limited interval between two consequent monitor tracking events by 1000 ms.
     
    Last edited: Oct 26, 2021

Share This Page