To get the same look with SDR and HDR
Last time, try to render the R10G10B10A2 format on an HDR display, For now, we have confirmed that the display is brighter than R8G8B8A8.
Now we would like to try to see how we can adjust it to bring the SDR and HDR looks closer together.
First, we will simply write PixelShader so that we can set the scaling in linear color.
struct PSInput {
float4 position : SV_POSITION;
float4 color : COLOR;
};
static const float3x3 Rec709ToRec2020Matrix = {
1.2249, -0.0420, -0.0197,
-0.0184, 1.2045, -0.0158,
-0.0051, -0.0249, 1.2796
};
// Conversion from sRGB to linear color
float3 SRGBToLinear(float3 color) {
return (color <= 0.04045) ? (color / 12.92) : pow((color + 0.055) / 1.055, 2.4);
}
// PQ Correction Function (ST 2084, Simplified)
float3 PQCorrect(float3 color) {
// PQ curve constants (Simplified version)
float m1 = 0.1593;
float m2 = 78.8438;
float c1 = 0.8359;
float c2 = 18.8516;
float c3 = 18.6875;
// PQ Curve Encoding Formula
return pow(max((c1 + c2 * pow(color, m1)) / (1.0 + c3 * pow(color, m1)), 0.0), m2);
}
float4 PSMain(PSInput input) : SV_TARGET {
// Scale
float scale = 0.01;
// Converts sRGB to linear
float3 linearColor = SRGBToLinear(input.color.rgb) * scale;
// Convert Linear Color to Rec. 2020
float3 rec2020Color = mul(Rec709ToRec2020Matrix, linearColor);
// PQ Curve Correction (Rec. 2020)
float3 finalColor = PQCorrect(rec2020Color);
return float4(finalColor, input.color.a);
}
Display on HDR600 display with Direct3D12
Continuing from the last issue, this is display-related.
As I mentioned last time, this display I recently bought supports HDR600.
Dell AW2724DM 27-inch Alienware Gaming Monitor
The following article and others are a good explanation of HDR600.
https://chimolog.co/bto-gaming-monitor-vesa-hdr/
The number 600 represents a brightness of 600 nits.(nit=cd/m2)
A conventional SDR display is about 100 nits, which means that it can express a much brighter image.
Let’s check it with Direct3D12.
First, change the frame buffer format to 10-bit RGBA.
//#define FRAME_BUFFER_FORMAT (DXGI_FORMAT_R8G8B8A8_UNORM)
#define FRAME_BUFFER_FORMAT (DXGI_FORMAT_R10G10B10A2_UNORM)
Try G-SYNC Compatible with Direct3D12
Previously, we were able to freely change the refresh rate in full-screen mode,
This time, we would like to check the operation of variable refresh rate.
I have two displays that I use, one of which is this display that I recently bought.
Dell AW2724DM 27-inch Alienware gaming monitor
- NVIDIA G-SYNC Compatible
- AMD FreeSync Premium Pro
- VESA AdaptiveSync
It states that the product is compatible with
The other is a 28" display from iiyama.
This one is older and does not support variable refresh rate.
First of all, what is the refresh rate of a display? If it says 60Hz, it means it displays images 60 times per second.
How the image is displayed is by scanning image data one line at a time, starting from the top left corner, to light up the pixels of the LCD or OLED.
Displaying that one image is repeated at an interval of once every1/60seconds.
Difference between double buffer and triple buffer in Direct3D12
Previously, we were able to freely set the refresh rate,
This time, we will be able to freely set the drawing fps.
To do so, we need to change the double buffer to a triple buffer, eliminate the VSync wait, and execute Present.
Roughly speaking, the differences are as follows.
- Double buffer
- A method of switching between two buffers, a front buffer and a back buffer. VSync wait is required to avoid tearing.
- Triple buffer
- Triple buffer: Switching between three buffers (front buffer and back buffer x2) without VSync wait to avoid tearing.
In the case of Direct3D12, the internal processing is difficult to understand because it is handled well in the Present, and if you make a mistake in the settings, you may get an exception or tearing.
Tearing is a phenomenon in which different frames are displayed at the top and bottom of the screen, making the image appear to flicker.
Controlling the frame rate
Although not limited to Direct3D, PC applications need to control the frame rate.
If the frame rate can be determined according to a fixed refresh rate, as in a game console,
You can control the frame rate by simply waiting for VSync, but PC monitors have a variety of refresh rates, so the app itself will need to control the frame rate.
The easiest way would be to measure the time since the previous frame, and if that time is faster than the specified frame rate, then Sleep.
It looks like this
class FrameController {
public:
FrameController() : mFPS(60.0f), mSleepCounter(0) {
QueryPerformanceFrequency(&mFrequency);
QueryPerformanceCounter(&mPrevious);
mStart = mPrevious;
}
virtual ~FrameController() {}
double GetTimeFromStart() const
{
LARGE_INTEGER now;
QueryPerformanceCounter(&now);
double duration_sec = static_cast<double>(now.QuadPart - mStart.QuadPart) / mFrequency.QuadPart;
return duration_sec;
}
float Update(float fps = 0.0f)
{
// Change Sleep accuracy
timeBeginPeriod(1);
mSleepCounter = 0;
// Measure time from previous frame
LARGE_INTEGER now;
QueryPerformanceCounter(&now);
double duration_sec = static_cast<double>(now.QuadPart - mPrevious.QuadPart) / mFrequency.QuadPart;
if (fps > 0.0f)
{
// Sleep to control frame interval
double limit_sec = 1.0 / fps;
while (duration_sec < limit_sec)
{
// Sleep until half of the remaining time
const double SLEEP_RATIO = 0.5;
int sleep_msec = static_cast<int>(1000.0 * (limit_sec - duration_sec) * SLEEP_RATIO);
Sleep(sleep_msec);
++mSleepCounter;
// Remeasure time
QueryPerformanceCounter(&now);
duration_sec = static_cast<double>(now.QuadPart - mPrevious.QuadPart) / mFrequency.QuadPart;
}
}
// Return to Sleep Accuracy
timeEndPeriod(1);
// Update frame rate (multiply by WEIGHT since averaging is tedious)
float current_fps = float(1.0 / duration_sec);
float FPS_WEIGHT = 0.1f;
mFPS = mFPS * (1.0f - FPS_WEIGHT) + current_fps * FPS_WEIGHT;
// Update the time of the previous frame
mPrevious = now;
return mFPS;
}
// For debugging
int GetSleepCounter() const
{
return mSleepCounter;
}
private:
LARGE_INTEGER mFrequency;
LARGE_INTEGER mPrevious;
LARGE_INTEGER mStart;
float mFPS;
// For debugging
int mSleepCounter;
};
Set resolution and refresh rate in Direct3D12
In windowed mode, the resolution can be set by changing the window size. (Of course, the RTV will have to be rebuilt.)
However, the refresh rate cannot be set on a per-window basis.
To change the refresh rate, you must be running in full screen.
In my previous article, I wrote about how to switch to full screen, so this will be a continuation of that article.
First, I asked Dr. ChatGPT how to change the refresh rate, and he told me about an API called ResizeTarget.
https://learn.microsoft.com/ja-jp/windows/win32/api/dxgi/nf-dxgi-idxgiswapchain-resizetarget
However, this method does not change the actual display settings, but only the display mode of SwapChain.
It does not change the Window settings, etc., so the imgui settings are not reflected, and it did not work.
Switch to full screen with Direct3D12
While listening to Dr. ChatGPT, I implemented switching between full screen mode and windowed mode in Direct3D12.
The idea is to switch between the two by pressing the F11 key.
SampleApp g_app;
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
// ImGui Window Processing
if (ImGui_ImplWin32_WndProcHandler(hWnd, message, wParam, lParam)) {
return true;
}
static bool isFullscreen = false;
static bool isFullscreenInActive = false;
static WINDOWPLACEMENT windowPlacement = { sizeof(WINDOWPLACEMENT) };// save position and size in window mode
switch (message) {
case WM_DESTROY:
g_app.SetFullscreenState(FALSE);
PostQuitMessage(0);
return 0;
case WM_KEYDOWN:
if (wParam == VK_F11) {// Switch with F11 key
if (isFullscreen) {
// Return to window mode
g_app.SetFullscreenState(FALSE);
SetWindowLong(hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW);
SetWindowPlacement(hWnd, &windowPlacement);// Restore previous window position and size
SetWindowPos(hWnd, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
isFullscreen = false;
}
else {
// Put the window in full screen mode
GetWindowPlacement(hWnd, &windowPlacement);// Save current window position and size
SetWindowLong(hWnd, GWL_STYLE, WS_POPUP | WS_VISIBLE);// Remove window border and title bar
SetWindowPos(hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
//SetWindowPos(hWnd, HWND_TOP, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_FRAMECHANGED);
g_app.SetFullscreenState(TRUE);
isFullscreen = true;
}
}
return 0;
case WM_SIZE:
if (wParam != SIZE_MINIMIZED) {
UINT width = LOWORD(lParam);
UINT height = HIWORD(lParam);
g_app.SetScreenSize(width, height);
}
break;
case WM_ACTIVATE:
if (wParam == WA_INACTIVE) {
// When a window is deactivated
if (isFullscreen) {
// Return to windowed mode from full screen mode
g_app.SetFullscreenState(FALSE);
SetWindowLong(hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW);
SetWindowPlacement(hWnd, &windowPlacement);// Restore previous position and size
SetWindowPos(hWnd, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
isFullscreenInActive = true;
}
}
else if (wParam == WA_ACTIVE || wParam == WA_CLICKACTIVE) {
// If the window becomes active again
if (isFullscreenInActive) {
// Return to full screen if necessary
SetWindowLong(hWnd, GWL_STYLE, WS_POPUP | WS_VISIBLE);
SetWindowPos(hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
g_app.SetFullscreenState(TRUE);
isFullscreenInActive = false;
}
}
return 0;
default:
break;
}
return DefWindowProc(hWnd, message, wParam, lParam);
}
Try using imgui in Direct3D12
Writing a Direct3D application requires debug drawing. Debug drawing is, for example, drawing the frame rate and various settings on the screen.
It is not used in the final product, so we want to make it so that it can be separated and compiled.
I have tried to incorporate this with the help of this website.
https://zenn.dev/norainu/articles/10856bfe120aa2
Put them together in a separate class so that they can be separated.
class ImGuiUtil {
public:
ImGuiUtil() {
}
virtual ~ImGuiUtil() {
}
// initialization
void Initialize(HWND hWnd, ID3D12Device *pDevice)
{
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
ImGui::StyleColorsLight();
D3D12_DESCRIPTOR_HEAP_DESC srvHeapDesc = {};
srvHeapDesc.NumDescriptors = 1;
srvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
srvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
pDevice->CreateDescriptorHeap(&srvHeapDesc, IID_PPV_ARGS(&m_srvHeap));
ImGui_ImplWin32_Init(hWnd);
ImGui_ImplDX12_Init(pDevice, FRAME_BUFFER_COUNT,
DXGI_FORMAT_R8G8B8A8_UNORM, m_srvHeap.Get(),
m_srvHeap->GetCPUDescriptorHandleForHeapStart(),
m_srvHeap->GetGPUDescriptorHandleForHeapStart());
}
// end processing
void Finalize()
{
ImGui_ImplDX12_Shutdown();
ImGui_ImplWin32_Shutdown();
ImGui::DestroyContext();
}
// Call at the beginning of the frame
void NewFrame()
{
ImGui_ImplDX12_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
}
// Call at the end of the drawing
void Render(ID3D12GraphicsCommandList *pCommandList)
{
ImGui::Render();
ID3D12DescriptorHeap* descriptorHeaps[] = { m_srvHeap.Get() };
pCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);
ImGui_ImplDX12_RenderDrawData(ImGui::GetDrawData(), pCommandList);
}
private:
ComPtr<ID3D12DescriptorHeap> m_srvHeap;
};
Write a Direct3D12 application (3)
Direct3D12 takes the form of buffering drawing commands in a command list and executing them together.
A simple description of the process to be executed every frame is as follows.
{
// Start of command list
commandAllocator->Reset();
commandList->Reset(commandAllocator.Get(), pipelineState.Get());
// Execute various drawing commands to create a command list
...
// Execute buffered command list
commandList->Close();
ID3D12CommandList* commandLists[] = { commandList.Get() };
commandQueue->ExecuteCommandLists(_countof(commandLists), commandLists);
// View swap chain
swapChain->Present(1, 0);
}
Write a Direct3D12 application (2)
First, before explaining the Direct3D12 program, we will write a program to display only a Window.
Basically, all you have to do is create a Window with CreateWindow and process messages in the main loop.
Incidentally, Window applications are message-driven. For example, operations such as moving, resizing, or closing a Window are stored in a queue in the form of messages.
The program processes the messages in the main loop. Therefore, when there are no messages, the program is in the same state as if it were in Sleep.
This is not suitable for programs such as game programs that process animation and other operations in real time, frame by frame.
1/2
»