AMD just showed Oasis demo, presenting usage of its FreeSync 2 HDR technology. If you wonder how could you implement same features in your Windows DirectX program or game (it doesn’t matter if you use D3D11 or D3D12), here is an article for you.
But first, a disclaimer: Although I already put it on my “About” page, I’d like to stress that this is my personal blog, so all opinions presented here are my own and do not reflect that of my employer.
Radeon FreeSync (its new, official web page is here: Radeon™ FreeSync™ Technology | FreeSync™ 2 HDR Games) is an AMD technology that covers two different things, which may cause some confusion. First is variable refresh rate, second is HDR. Both of them need to be supported by a monitor. The database of FreeSync compatible monitors and their parameters is: Freesync Monitors.
Traditionally, presenting a freshly rendered frame can happen in two modes. With vertical synchronization (V-sync) off, it happens immediately. Game then renders new frames as fast as it can, but flip to a new frame may happen in the middle of scanline process, which causes unpleasant effect called “tearing”. On the other hand, when V-sync is on, graphics card has to wait with presenting the new frame until a new monitor refresh (vertical synchronization) happens, which occurs at a constant pace equal to the monitor refresh rate (typically 60 Hz). Game performance, as measured in frames per second (FPS) is then limited to that frequency. (Please note that FPS is the same unit - a frequency in Hz = 1/second). The game then works smoothly, with no tearing, as long as GPU is able to render frames on time. When it doesn’t, it needs to wait until next V-sync, which causes visible stuttering.
Variable refresh rate, in form of VESA Adaptive-Sync (as implemented in FreeSync) is an addition to DisplayPort and HDMI protocol that allows GPU to inform the monitor when a new frame is ready. Screen refresh can happen in irregular intervals, which combines benefits from both methods described above - no tearing and no stuttering, in a range of supported refresh rates (in case of my monitor: LG 32GK850F, it’s 48-144 Hz).
In order to enable it:
- Have a FreeSync compatible monitor.
- Connect it to AMD graphics card through DisplayPort or HDMI cable.
- Enable FreeSync in monitor menu, if applicable.
- Enable FreeSync in driver settings - go to Start menu → AMD Radeon Settings → Display tab → Radeon FreeSync = On.
- You don’t need to do anything special in your app to make it working! Just make sure it renders like you had V-sync on, which means
SyncInterval= 1, not 0. Otherwise my monitor reports variable refresh rate still working, but tearing is visible, so it doesn’t make much sense.
What happens is that:
- If your program is able to deliver frames at the pace of the refresh rate you’ve chosen when setting up fullscreen parameters (whether it’s 60, 100, or 144) or higher, then the FPS is capped to that frequency and the program runs smoothly, just like it had V-sync on.
- If your program works at the lower framerate, but still above the minimum for your monitor (like 48 in my case), then that’s the framerate you get - monitor adjusts to it, presents each frame when it’s ready and everything still works and looks great, without much stuttering.
- When your framerate drops below that, then of course animation becomes less smooth, but there is still some mechanism working that tries to ensure best possible experience, called Low Framerate Compensation (LFC).
I tried many different display settings. None of them disabled variable refresh rate.
- Entered exclusive fullscreen using function
IDXGISwapChain::SetFullscreenState(TRUE, NULL), but also tried not to do it, just create a borderless window covering whole screen.
- Used AMD AGS library and called
mode = AGSDisplaySettings::Mode_Freesync2_scRGB, but also skipped doing this.
DXGI_MODE_DESC::ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIEDor
DXGI_SWAPCHAIN_DESC::SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD,
Another thing is displaying colors in high dynamic range (HDR) of brightness. There are various HDR-supporting monitors and TVs on the market and they can be controlled from various GPUs. I researched the topic of Programming HDR monitor support in my previous blog post, where I made experiments on AMD, Nvidia, and Intel.
Now as I know a bit more about it, I’d like to describe steps recommended to activate HDR as supported by FreeSync 2. With this technology, you can query the monitor for its capabilities (maximum luminance in nits, XY primaries of red/green/blue color etc.) and adjust the tone mapping in the postprocessing pass of your game accordingly. This way you have more information and full control over the process, and over the final result! To do that:
- Follow steps 1-4 as described above - use compatible monitor, enable FreeSync.
- In your program, use AMD AGS library:
- Link with “amd_ags_x64.lib”.
- Bundle file “amd_ags_x64.dll” with your program.
- Call function
agsInitto initialize the library.
- Inspect structure
agsInit. Find the GPU and then the display of your interest. Remember its describing
AGSDisplayInfostructure. Let’s call it
- Make sure FreeSync 2 is supported by checking if
(dispInfo.displayFlags & AGS_DISPLAYFLAG_FREESYNC_2) != 0.
- Create D3D swapchain in
- Enter exclusive fullscreen mode by calling
AGSDisplaySettingsstructure with following parameters and submit it using
AGSDisplaySettings settings; settings.mode = AGSDisplaySettings::Mode_Freesync2_scRGB; settings.chromaticityRedX = dispInfo.chromaticityRedX; // Just copy them all. settings.chromaticityRedY = dispInfo.chromaticityRedY; settings.chromaticityGreenX = dispInfo.chromaticityGreenX; settings.chromaticityGreenY = dispInfo.chromaticityGreenY; settings.chromaticityBlueX = dispInfo.chromaticityBlueX; settings.chromaticityBlueY = dispInfo.chromaticityBlueY; settings.chromaticityWhitePointX = dispInfo.chromaticityWhitePointX; settings.chromaticityWhitePointY = dispInfo.chromaticityWhitePointY; settings.minLuminance = dispInfo.minLuminance; settings.maxLuminance = dispInfo.maxLuminance; settings.maxContentLightLevel = dispInfo.maxLuminance; // !! settings.maxFrameAverageLightLevel = dispInfo.maxLuminance * 0.5; // !! settings.flags = 0; agsSetDisplayMode(agsContext, deviceIndex, displayIndex, &settings);
- Write color values to your swap chain in linear space, not gamma corrected. Scale them and adjust your tone mapping so that maximum brightness is maxLuminance/80. This is because scRGB standard defines value 1 as 80 nits. In case of my monitor, where maxLuminance = 496, I need to output values in range from 0 to 496/80 = 6.2.
By using these specific values for the filled structure, you make sure that your colors are displayed as-is, with no additional tone mapping or other postprocessing taking place in the monitor, which can improve quality and reduce latency. In practice I can’t see any difference comparing to just enabling HDR in a simple way, as I described in my previous post, but that’s the theory.
Just like in the previous post, I focus solely on high dynamic range of brightness here, not on color gamut. That’s because most content pipelines today still use traditional SDR sRGB color space for textures and all the input graphics. Authoring them in wide color gamut or remapping the image to the color gamut reported by the monitor is another, complex topic, which will become more important in the future.
Finally, let me repeat that again: Everything I described here is just my personal knowledge and anecdotal evidence of what worked in my case, not an official guide. But as I couldn’t find any other publicly available information about this technology for developers, I guess it’s better than nothing :)
Platform used in my tests: OS = Windows 10 64-bit version 1809 (OD Build 17763.316), GPU = Radeon RX Vega 56, driver = 19.2.3, monitor = LG 32GK850F, cable = DisplayPort.