Previous parts of this series covered NVAPI initialization, GPU information queries, and EDID reading and writing. This part tackles the project’s core feature: custom resolutions. Rather than simply calling NVAPI functions, the goal is to calculate and apply display timing directly — the language that monitors actually understand.
When a monitor and GPU exchange pixel data, they don’t just pass around resolution numbers. The analog signal conventions from the CRT era carried over into the digital world, so beyond the visible pixels that make up the image, there are defined blanking intervals on all sides.
[StructLayout(LayoutKind.Sequential)]publicstructNvTiming{publicushort HVisible;// H Active pixel countpublicushort HBorder;publicushort HFrontPorch;publicushort HSyncWidth;publicushort HTotal;publicbyte HSyncPol;// 1 = Positive, 0 = Negativepublicushort VVisible;// V Active line countpublicushort VBorder;publicushort VFrontPorch;publicushort VSyncWidth;publicushort VTotal;publicbyte VSyncPol;publicushort Interlaced;publicuint Pclk;// Pixel clock (10 kHz units)public NvTimingExt Etc;publicdouble RefreshRate
{get{if(HTotal ==0|| VTotal ==0)return0;return(Pclk *10000.0)/(HTotal * VTotal);}}}
The RefreshRate property back-calculates the actual refresh rate. Pclk × 10000 gives the pixel clock in Hz, and dividing by HTotal × VTotal yields the frame rate.
CVT (Coordinated Video Timings) is the timing calculation standard published by VESA. Designed for the LCD era, it comes in two variants: Standard and Reduced Blanking.
GTF (Generalized Timing Formula) is the older VESA standard that predates CVT. It mathematically models the physical characteristics of CRT monitors and is used when compatibility with legacy displays is needed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
publicstatic NvTiming CalculateGTF(int hActive,int vActive,double refreshRate){// GTF constants (fixed VESA specification values)constdouble MIN_PORCH =1;constdouble V_SYNC_RQD =3;constdouble H_SYNC_PCT =8.0;constdouble MIN_V_BACK_PORCH =550.0;// µsconstdouble M =600.0;constdouble C =40.0;constdouble K =128.0;constdouble J =20.0;double CPrime =((C - J)* K /256.0)+ J;// = 30.0double MPrime = K /256.0* M;// = 300.0
The core of GTF lies in the C’ and M’ constants. These are empirically derived values that model the retrace speed of a CRT electron beam.
While the GTF and CVT Standard formulas look similar, their calculation paths differ. GTF first computes hPeriod precisely and then back-calculates the pixel clock, whereas CVT estimates the ideal pclk upfront.
1
2
3
4
5
// GTF: back-calculate pclk from hPerioddouble pclk =(double)hTotal /(hPeriodEstimate *1000000.0)/1000000.0;// Convert to 10 kHz units timing.Pclk =(uint)(pclk *100000000.0/10000.0);
In the UI’s Custom ResolutionPanel, when cboTimingMode is set to “Manual”, the user enters each value directly into numHFP, numHSW, numHTotal, numVFP, numVSW, numVTotal, and numPixelClock.
[StructLayout(LayoutKind.Sequential)]publicstructNvCustomDisplay{publicuint Version;publicuint Width;// Resolution widthpublicuint Height;// Resolution heightpublicuint Depth;// Color depth (typically 32)publicuint ColorFormat;// Color formatpublic NvViewportF SourcePartition;// Source partition (usually full: 0,0,1,1)publicfloat XRatio;// Scale ratio (usually 1.0)publicfloat YRatio;public NvTiming Timing;// Timing parameterspublicuint HwModeSetOnly;// 0: persistent, 1: hardware mode onlypublicstatic NvCustomDisplay Create(){var cd =new NvCustomDisplay(); cd.Version =(uint)(Marshal.SizeOf(typeof(NvCustomDisplay))|(1<<16)); cd.Timing = NvTiming.Create(); cd.SourcePartition =new NvViewportF { X =0, Y =0, W =1.0f, H =1.0f}; cd.XRatio =1.0f; cd.YRatio =1.0f;return cd;}}
The Version field follows a common NVAPI pattern where the structure size and version number are combined via a bitwise OR. When HwModeSetOnly = 0, the mode is registered with the Windows Display Driver Model (WDDM) and persists across reboots. Setting it to 1 only changes the hardware registers — faster, but the setting is lost on reboot.
TryCustomDisplay applies the resolution immediately but only holds it in NVAPI’s internal temporary state. When the driver restarts or RevertCustomDisplay is called, it reverts. The “Test (15s)” button in the UI follows this path.
NvTimingExt.Rr holds the refresh rate as an integer in Hz, while Rrx1k holds it in units of 0.001 Hz. Both fields must be populated for the driver to recognize the correct refresh rate.
publicstatic NvStatus SaveCustomResolution(uint displayId, NvDisplayHandle displayHandle){var ids =newuint[]{ displayId }; NvStatus status;// Attempt 1: displayId + (0,0) status = NvApiWrapper.DISP_SaveCustomDisplay(ids,1,0,0);if(status == NvStatus.OK){ Logger.Info("Saved via displayId (0,0)");return status;}// Attempt 2: displayId + (1,0) status = NvApiWrapper.DISP_SaveCustomDisplay(ids,1,1,0);if(status == NvStatus.OK){ Logger.Info("Saved via displayId (1,0)");return status;}// Attempts 3–4: retry with NvDisplayHandleif(displayHandle.IsValid){var handles =new NvDisplayHandle[]{ displayHandle }; status = NvApiWrapper.DISP_SaveCustomDisplayByHandle(handles,1,0,0);if(status == NvStatus.OK)return status; status = NvApiWrapper.DISP_SaveCustomDisplayByHandle(handles,1,1,0);if(status == NvStatus.OK)return status;}
The third and fourth arguments to SaveCustomDisplay are flags whose exact meaning is not documented in the NVAPI reference. Experimentally, (0,0) and (1,0) combinations are tried in order, and both the DisplayId and DisplayHandle API overloads are attempted. Driver behavior varies between versions, which is why this fallback chain is necessary.
Saved custom resolutions survive driver restarts and become selectable in the Windows Display Settings app.
If a custom resolution is incompatible with the monitor, the screen can go completely black. To handle this, the “Test” feature includes an auto-revert timer:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// In BtnTest_Click:revertCountdown =15;revertTimer.Start();btnSave.Enabled =false;privatevoid RevertTimer_Tick(object sender, EventArgs e){ revertCountdown--; lblStatus.Text =$"Testing... reverting in {revertCountdown}s";if(revertCountdown <=0){ revertTimer.Stop(); NvCustomDisplayManager.RevertCustomResolution(displayId);}}
RevertCustomResolution uses a two-stage fallback strategy:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
publicstatic NvStatus RevertCustomResolution(uint displayId){// Primary: NVAPI RevertCustomDisplayvar ids =newuint[]{ displayId };var status = NvApiWrapper.DISP_RevertCustomDisplay(ids,1);if(status == NvStatus.OK){ Logger.Info("Custom display reverted via NVAPI");return status;}// Fallback: Windows ChangeDisplaySettingsEx Logger.Warn("NVAPI RevertCustomDisplay not available, using Windows API fallback");int result = ChangeDisplaySettingsExA(null, IntPtr.Zero, IntPtr.Zero,0, IntPtr.Zero);if(result ==0)// DISP_CHANGE_SUCCESSFUL{ Logger.Info("Display reverted via Windows API");return NvStatus.OK;}
If NVAPI DISP_RevertCustomDisplay fails, the code calls Win32 ChangeDisplaySettingsEx with a null device name and an empty DEVMODE to force the OS back to its default resolution. Even if the screen goes dark, the 15-second timer guarantees automatic recovery — the user never has to wait blindly.
NVAPI allows specifying the timing generation method via the NvTimingOverride enum:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
publicenum NvTimingOverride :int{ Current =0,// Keep current setting Auto =1,// Driver auto-select EDID =2,// Timings from monitor EDID DMT =3,// VESA DMT standard timings DMTRb =4,// DMT Reduced Blanking CVT =5,// CVT Standard CVTRb =6,// CVT Reduced Blanking GTF =7,// GTF EIA861 =8,// CEA/EIA-861 (HDMI standard timings) AnalogTV =9,// Analog TV CEA861CVT =10, AsiaTV =11,// Asian TV standard Custom =255,// Fully manual timing}
When applying a custom resolution, NvTimingOverride.Custom is used. In this mode, the driver does not calculate timings on its own — it passes the values from our NvTiming structure directly to the hardware.
ResolutionPreset bundles resolution, refresh rate, timing parameters, and pixel clock into a single record. These are displayed in the DataGridView on the right panel of the UI. Double-clicking a row automatically populates the input form on the left.
Select resolution, refresh rate, and timing mode in the UI
Click Calculate → calls TimingCalculator.CalculateCVT() or CalculateGTF(), results appear in the form
Click Test (15s) → calls TryCustomResolution(), auto-reverts after 15 seconds
If the image looks good, click Save → calls SaveCustomResolution() (tries four fallback methods)
If something goes wrong, click Restore Original → calls RevertCustomResolution() immediately
The key principle is the three-stage separation: apply → verify → save. This prevents accidentally persisting a timing that the monitor cannot display.
Display timing is a complex world hiding behind the simple resolution numbers we see on screen. CVT and GTF are the crystallization of decades of display engineering knowledge, and NVAPI provides a direct window into that world at the driver level.
Part 5 will combine this custom resolution feature with color space control (Color Space, HDR), covering how to switch between SDR and HDR using the same NVAPI.