Direct3d

Difference between right-handed and left-handed systems

Whenever I write a program in Direct3D, I am always concerned about the fact that it is a left-handed system. I was used to right-handed systems because my first 3DCG learning experience was with OpenGL, and the mathematics I learned as a student must have been right-handed as well. I also think that many of the GL-like APIs for game consoles were also right-handed. And yet, I think that Direct3D’s adoption of the left-hand system has made complex 3DCG even more complex.

Many people think that the right-hand system and the left-hand system are just different in matrix calculation, but the biggest difference is that the definition of the Z buffer changes. Whereas the right-hand system is in the range-1~ 1, the left-hand system is in the range 0 ~ 1. The projective transformation matrix changes accordingly, clipping range changes, the orientation of the vectors in the outer product calculation changes, and the orientation of the polygon faces changes. Frankly, just reading the program, I am not quite sure what the correct code is.

Debugging Direct3D12 Apps with RenderDoc

Debugging even simple shaders to see if they are working correctly is difficult. At first I tried to debug using PIX, but although I could see the contents of the constant buffer, I could not see the VertexShader vertex data, I wondered if my settings were bad, but when I asked Dr. ChatGPT, his answer was to use RenderDoc when in trouble, so I decided to use RenderDoc with half a doubt.

RenderDoc worked easily. I could see the vertex data of VertexShader without any particular settings, and the step execution of shaders worked without any problem.

The debugging method for VertexShader is as follows.

Drawing a mesh created in Blender with Direct3D12

I decided to try drawing an arbitrary mesh in Direct3D12, so I first created the mesh data.

I use Maya in my work, but this time, I will create the data in Blender. Since this is my first time, I created the rocket mesh while reading this book on Kindle Unlimited.

https://amzn.to/4fcLOOz

I think this book is simple and easy to understand.

Next, I figured out how to bring the Blender data toC++. This time, the two types of data needed will be vertex data and index data. (Materials will come later.)

If we were to export the Blender data, we could use fbx, usd, and glTF formats. Each also provides a python API so you can get the data you need. However, each was more tedious than I expected when I tried to dump the vertex data in python. Is there a more user-friendly format?

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, &amp;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, &amp;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, &amp;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);
}