Skip to main content
The Virtual Display Driver supports precise fractional refresh rates through a multiplier system, enabling accurate NTSC rates (59.94Hz, 29.97Hz, 23.976Hz) and other non-integer refresh rates.

Refresh Rate Multiplier System

Floating-point refresh rates use a multiplier to represent fractional values accurately:
Actual Refresh Rate = Nominal Rate × (Multiplier / 1000)

Configuration Format

<MonitorMode>
  <Width>1920</Width>
  <Height>1080</Height>
  <RefreshRate>59.940</RefreshRate>
  <RefreshRateMultiplier>999</RefreshRateMultiplier>
  <NominalRefreshRate>60</NominalRefreshRate>
</MonitorMode>
RefreshRate
double
Actual refresh rate in Hz (for display purposes)
RefreshRateMultiplier
int
default:"1000"
Multiplier value (0-1000):
  • 1000 = Exact rate (no fractional component)
  • 999 = NTSC rate (×0.999)
  • Custom values for other fractional rates
NominalRefreshRate
int
Base refresh rate before multiplier is applied

Standard NTSC Rates

NTSC fractional rates use the 999/1000 multiplier:

59.94 Hz (NTSC 60Hz)

<MonitorMode>
  <Width>1920</Width>
  <Height>1080</Height>
  <RefreshRate>59.940</RefreshRate>
  <RefreshRateMultiplier>999</RefreshRateMultiplier>
  <NominalRefreshRate>60</NominalRefreshRate>
</MonitorMode>
Calculation: 60 × (999/1000) = 59.94 Hz

29.97 Hz (NTSC 30Hz)

<MonitorMode>
  <Width>3840</Width>
  <Height>2160</Height>
  <RefreshRate>29.970</RefreshRate>
  <RefreshRateMultiplier>999</RefreshRateMultiplier>
  <NominalRefreshRate>30</NominalRefreshRate>
</MonitorMode>
Calculation: 30 × (999/1000) = 29.97 Hz

23.976 Hz (Film Rate)

<MonitorMode>
  <Width>4096</Width>
  <Height>2160</Height>
  <RefreshRate>23.976</RefreshRate>
  <RefreshRateMultiplier>999</RefreshRateMultiplier>
  <NominalRefreshRate>24</NominalRefreshRate>
</MonitorMode>
Calculation: 24 × (999/1000) = 23.976 Hz

Custom Fractional Rates

For non-NTSC fractional rates, calculate custom multipliers:

47.952 Hz (Double Film Rate)

<MonitorMode>
  <Width>1920</Width>
  <Height>1080</Height>
  <RefreshRate>47.952</RefreshRate>
  <RefreshRateMultiplier>999</RefreshRateMultiplier>
  <NominalRefreshRate>48</NominalRefreshRate>
</MonitorMode>

119.88 Hz (NTSC 120Hz)

<MonitorMode>
  <Width>1920</Width>
  <Height>1080</Height>
  <RefreshRate>119.880</RefreshRate>
  <RefreshRateMultiplier>999</RefreshRateMultiplier>
  <NominalRefreshRate>120</NominalRefreshRate>
</MonitorMode>

Internal V-Sync Calculation

The driver converts fractional rates to v-sync ratios using GCD (Greatest Common Divisor) reduction:
// Driver.cpp:1445-1453
void float_to_vsync(float refresh_rate, int& num, int& den) {
    den = 10000;
    num = static_cast<int>(round(refresh_rate * den));
    
    int divisor = gcd(num, den);
    num /= divisor;
    den /= divisor;
}
Example: 59.94 Hz
Input: 59.94 Hz
1. num = 59.94 × 10000 = 599400
2. den = 10000
3. GCD(599400, 10000) = 600
4. Reduced: num = 999, den = 16.67
5. Final ratio: 999:1000

Complete Configuration Examples

Multi-Rate 4K Configuration

<MonitorModes>
  <!-- Exact rates -->
  <MonitorMode>
    <Width>3840</Width>
    <Height>2160</Height>
    <RefreshRate>24.000</RefreshRate>
    <RefreshRateMultiplier>1000</RefreshRateMultiplier>
    <NominalRefreshRate>24</NominalRefreshRate>
  </MonitorMode>
  
  <!-- NTSC film rate -->
  <MonitorMode>
    <Width>3840</Width>
    <Height>2160</Height>
    <RefreshRate>23.976</RefreshRate>
    <RefreshRateMultiplier>999</RefreshRateMultiplier>
    <NominalRefreshRate>24</NominalRefreshRate>
  </MonitorMode>
  
  <!-- Exact 30Hz -->
  <MonitorMode>
    <Width>3840</Width>
    <Height>2160</Height>
    <RefreshRate>30.000</RefreshRate>
    <RefreshRateMultiplier>1000</RefreshRateMultiplier>
    <NominalRefreshRate>30</NominalRefreshRate>
  </MonitorMode>
  
  <!-- NTSC 30Hz -->
  <MonitorMode>
    <Width>3840</Width>
    <Height>2160</Height>
    <RefreshRate>29.970</RefreshRate>
    <RefreshRateMultiplier>999</RefreshRateMultiplier>
    <NominalRefreshRate>30</NominalRefreshRate>
  </MonitorMode>
  
  <!-- Exact 60Hz -->
  <MonitorMode>
    <Width>3840</Width>
    <Height>2160</Height>
    <RefreshRate>60.000</RefreshRate>
    <RefreshRateMultiplier>1000</RefreshRateMultiplier>
    <NominalRefreshRate>60</NominalRefreshRate>
  </MonitorMode>
  
  <!-- NTSC 60Hz -->
  <MonitorMode>
    <Width>3840</Width>
    <Height>2160</Height>
    <RefreshRate>59.940</RefreshRate>
    <RefreshRateMultiplier>999</RefreshRateMultiplier>
    <NominalRefreshRate>60</NominalRefreshRate>
  </MonitorMode>
</MonitorModes>

High Refresh Gaming Setup

<MonitorModes>
  <!-- 144Hz exact -->
  <MonitorMode>
    <Width>2560</Width>
    <Height>1440</Height>
    <RefreshRate>144.000</RefreshRate>
    <RefreshRateMultiplier>1000</RefreshRateMultiplier>
    <NominalRefreshRate>144</NominalRefreshRate>
  </MonitorMode>
  
  <!-- 165Hz exact -->
  <MonitorMode>
    <Width>2560</Width>
    <Height>1440</Height>
    <RefreshRate>165.000</RefreshRate>
    <RefreshRateMultiplier>1000</RefreshRateMultiplier>
    <NominalRefreshRate>165</NominalRefreshRate>
  </MonitorMode>
  
  <!-- 240Hz exact -->
  <MonitorMode>
    <Width>1920</Width>
    <Height>1080</Height>
    <RefreshRate>240.000</RefreshRate>
    <RefreshRateMultiplier>1000</RefreshRateMultiplier>
    <NominalRefreshRate>240</NominalRefreshRate>
  </MonitorMode>
</MonitorModes>

EDID Profile Integration

EDID profiles automatically include fractional rates with correct multipliers:
<!-- From monitor_profile.xml -->
<MonitorMode>
  <Width>720</Width>
  <Height>480</Height>
  <RefreshRate>59.940</RefreshRate>
  <RefreshRateMultiplier>999</RefreshRateMultiplier>
  <NominalRefreshRate>60</NominalRefreshRate>
</MonitorMode>
The EDID parser (edid_parser.txt:80-87) automatically calculates multipliers:
int calculate_z_multiplier(double actual_refresh, int nominal_refresh) {
    // For standard NTSC fractional rates (59.94, 29.97, 23.976)
    if (std::abs(actual_refresh - nominal_refresh * 1000.0/1001.0) < 0.01) {
        return 999; // NTSC factor
    }
    return 1000; // Exact refresh rates
}

Filtering Fractional Rates

When using auto resolutions, you can exclude fractional rates:
<auto_resolutions>
  <enabled>true</enabled>
  <edid_mode_filtering>
    <exclude_fractional_rates>true</exclude_fractional_rates>
  </edid_mode_filtering>
</auto_resolutions>
This filters out all modes where RefreshRateMultiplier ≠ 1000.

Global Refresh Rate Application

Apply specific refresh rates globally to all resolutions:
<global>
  <g_refresh_rate>60</g_refresh_rate>
  <g_refresh_rate>120</g_refresh_rate>
  <g_refresh_rate>144</g_refresh_rate>
</global>
Global refresh rates always use exact rates (multiplier = 1000). For fractional rates, define them explicitly in individual resolution entries.

Refresh Rate Limits

Constrain refresh rates when using EDID or auto-resolution features:
<auto_resolutions>
  <edid_mode_filtering>
    <min_refresh_rate>24</min_refresh_rate>
    <max_refresh_rate>240</max_refresh_rate>
  </edid_mode_filtering>
</auto_resolutions>
Filtering applies to the NominalRefreshRate value, so both 60Hz and 59.94Hz would be included if the range includes 60.

Common Refresh Rates Reference

RateTypeMultiplierNominalRefreshRate
23.976 HzFilm (NTSC)9992423.976
24.000 HzFilm (Exact)10002424.000
25.000 HzPAL10002525.000
29.970 HzNTSC9993029.970
30.000 HzExact10003030.000
47.952 HzFilm 2×9994847.952
50.000 HzPAL10005050.000
59.940 HzNTSC9996059.940
60.000 HzExact10006060.000
119.880 HzNTSC 2×999120119.880
120.000 HzExact1000120120.000
144.000 HzGaming1000144144.000
165.000 HzGaming1000165165.000
240.000 HzHigh-refresh1000240240.000

VIC Mode Support

The EDID parser includes VIC (Video Identification Code) lookup tables for standard fractional rates (edid_parser.txt:436-481):
static const std::map<uint8_t, VICMode> vic_table = {
    {2, {720, 480, 60, true}},   // 720p NTSC (59.94Hz)
    {4, {1280, 720, 60, true}},  // 720p NTSC
    {5, {1920, 1080, 60, true}}, // 1080p NTSC
    {32, {1920, 1080, 24, true}}, // 1080p Film
    // ...
};
The true flag indicates fractional rate support (multiplier = 999).

Troubleshooting

Refresh rate not showing correctly:
  • Verify RefreshRateMultiplier is set correctly (999 for NTSC, 1000 for exact)
  • Check that NominalRefreshRate matches the base rate
  • Ensure RefreshRate calculation is accurate
Application doesn’t detect fractional rate:
  • Some applications only detect nominal rates
  • Provide both exact and fractional versions of the same rate
Performance issues with fractional rates:
  • Fractional rates require more precise timing
  • Use exact rates (multiplier=1000) for better performance if fractional accuracy isn’t needed

Build docs developers (and LLMs) love