2
\$\begingroup\$

I'm new to C++, been a C programmer most of my life. I wanted to get into graphics and Direct3D is C++ so I figured it was about time. I've been learning about strings, vectors, references, smart pointers, etc. Not using classes because I still struggle to understand how to apply them in design.

Before I dive any further in graphics I wanted to make a good debug layer just in case anything breaks. But it's probably super messy and there are many things I don't know how to do cleanly. I'd appreciate a review on this, I tried to comment it as much as possible. Thanks!

#include "../resource.h" // For IDI_ICON1 (get an icon to the window)
#include "../dxerr/dxerr.h" // From https://github.com/planetchili/hw3d

#include <string>
#include <vector>
#include <memory>

#include <windows.h>
#include <d3d11.h>
#include <dxgidebug.h>

#pragma warning(push, 0)
#include <fmt/format.h> // Just a string formatting library, it works
#pragma warning(pop)

typedef const char* str;

struct Window {
    HWND handle;
    int width;
    int height;
};

struct Panic {
    int line;
    str file;

    std::string desc;
};
#define PANIC_OK Panic { -1, "", "OK" }

// Globals
namespace g {
    static bool running = true;
}

// The window callback, closes the window if the close button is pressed
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
    LRESULT result = 0;
    switch (msg) {
    case WM_CLOSE: g::running = false; break;

    default: result = DefWindowProcA(hwnd, msg, wparam, lparam); break;
    }
    return result;
}

// Creates a window properly
Window create_window(str title, int width, int height) {
    HINSTANCE instance = GetModuleHandleA(0);
    WNDCLASSA winclass = {};
    winclass.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
    winclass.lpfnWndProc = WndProc;
    winclass.hInstance = instance;
    winclass.lpszClassName = "DX11 Engine Window";
    winclass.hIcon = LoadIconA(instance, MAKEINTRESOURCEA(IDI_ICON1));
    RegisterClassA(&winclass);
    // Do not allow maximizing or resizing
    DWORD style = WS_OVERLAPPEDWINDOW ^ (WS_SIZEBOX | WS_MAXIMIZEBOX);

    // Calculate the desired client region size
    RECT wr;
    wr.left = 100;
    wr.right = width + wr.left;
    wr.top = 100;
    wr.bottom = height + wr.top;
    AdjustWindowRect(&wr, style, FALSE);

    HWND handle = CreateWindowA(
        winclass.lpszClassName, title,
        style, CW_USEDEFAULT, CW_USEDEFAULT, wr.right - wr.left, wr.bottom - wr.top,
        nullptr, nullptr, instance, 0
    );

    // Position the window at the center of the screen
    RECT rc;
    GetWindowRect(handle, &rc);
    int x = (GetSystemMetrics(SM_CXSCREEN) - rc.right) / 2;
    int y = (GetSystemMetrics(SM_CYSCREEN) - rc.bottom) / 2;
    SetWindowPos(handle, 0, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE);

    UnregisterClassA(winclass.lpszClassName, instance);
    ShowWindow(handle, SW_SHOW);

    return Window { handle, width, height };
}

// Message loop
void update_window(Window& window) {
    MSG msg;
    while (PeekMessageA(&msg, window.handle, 0, 0, PM_REMOVE)) {
        TranslateMessage(&msg);
        DispatchMessageA(&msg);
    }
}

// --- The 0 at the end of the macros is so you can use the semicolon. ---
// Just create a Panic struct with proper line and file.
#define CREATE_DX_PANIC(hr) Panic { __LINE__, __FILE__, gfx::get_hr(hr, __LINE__, __FILE__) };
// Same but with info
#define CREATE_DX_PANIC_INFO(hr) Panic { __LINE__, __FILE__, gfx::get_hr_info(hr, __LINE__, __FILE__, gfx::get_info_messages(gfx.info)) }
// Create a new scope so hr doesn't leak into the code. If assigning it to the call failed, get the proper panic. Else it's just PANIC_OK.
#define PANIC_DX(call) { HRESULT hr; gfx::set_info_queue(gfx.info); if (FAILED(hr = (call))) panic = CREATE_DX_PANIC_INFO(hr); else panic = PANIC_OK; } 0

// Same thing as PANIC_DX() with the scope. For some reason comparing callr to PANIC_OK fails. Is there a cleaner way to do this?
#ifdef __DEBUG
#define CHECK_PANIC(call) { Panic callr = (call); if (callr.desc != "OK") \
    { MessageBoxA(0, callr.desc.c_str(), "Error!", MB_OK | MB_ICONERROR); __debugbreak(); } } 0
// Same as above but just with the panic struct in functions. Since DirectX is windows only anyway I don't think it's bad to use __debugbreak().
#define CHECK_PANIC_NO_CALL() if (panic.desc != "OK") { MessageBoxA(0, panic.desc.c_str(), "Error!", MB_OK | MB_ICONERROR); __debugbreak(); } 0
// Just adds up the previous macros so you don't have to call both everytime.
#define DX_CALL(call) PANIC_DX(call); CHECK_PANIC_NO_CALL();
#else
#define CHECK_PANIC(call) call
#define CHECK_PANIC_NO_CALL() 0
#define DX_CALL(call) call
#endif
namespace gfx {
    struct DebugInfo {
        unsigned long long next = 0;
        IDXGIInfoQueue* queue = nullptr;
    };

    struct Graphics {
        IDXGISwapChain* swap;
        ID3D11Device* device;
        ID3D11DeviceContext* context;
        ID3D11RenderTargetView* screen;

        DebugInfo info;
    };

    // Loads the dxgidebug functions as well
    DebugInfo create_debug_info() {
        DebugInfo info;
        // define function signature of DXGIGetDebugInterface
        typedef HRESULT (WINAPI* DXGIGetDebugInterface) (REFIID,void **);

        // load the dll that contains the function DXGIGetDebugInterface
        HMODULE dxgi_mod = LoadLibraryExA("dxgidebug.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
        if (dxgi_mod == nullptr) {
            // How to handle this?
        }

        // get address of DXGIGetDebugInterface in dll
        const auto DxgiGetDebugInterface = (DXGIGetDebugInterface) (void*) GetProcAddress(dxgi_mod, "DXGIGetDebugInterface");
        if (DxgiGetDebugInterface == nullptr) {
            // How to handle this?
        }

        DxgiGetDebugInterface(__uuidof(IDXGIInfoQueue), (void**) &info.queue);

        return info;
    }

    // Sets a point in the info queue where the messages can start to be collected
    void set_info_queue(DebugInfo info) {
        info.next = info.queue->GetNumStoredMessages(DXGI_DEBUG_ALL);
    }

    // Gets the messages from the queue
    std::vector<std::string> get_info_messages(DebugInfo info){
        std::vector<std::string> messages;
        UINT64 end = info.queue->GetNumStoredMessages( DXGI_DEBUG_ALL );
        for (unsigned long long i = info.next; i < end; i++) {
            SIZE_T msg_len;
            // get the size of message i in bytes
#pragma warning(suppress: 6001) // Use of uninitialized memory is ok here
            info.queue->GetMessageA(DXGI_DEBUG_ALL, i, nullptr, &msg_len);
            // allocate memory for message
            auto bytes = std::make_unique<unsigned char[]>(msg_len);
            auto msg = (DXGI_INFO_QUEUE_MESSAGE*) (bytes.get());
            // get the message and push its description into the vector
#pragma warning(suppress: 6001) // Use of uninitialized memory is ok here
            info.queue->GetMessageA(DXGI_DEBUG_ALL, i, msg, &msg_len);
            messages.emplace_back(msg->pDescription);
        }
        return messages;
    }

    std::string get_hr(HRESULT hr, int line, str file) {
        // Use DXErr to get error description and title. Supposedly FormatMessage() works with DX errors, but only on Windows 8 and above.
        std::string err_code = fmt::format("{:#x}", hr);
        str err_str = DXGetErrorStringA(hr);
        char desc[512];
        DXGetErrorDescriptionA(hr, desc, sizeof desc);

        return fmt::format("[Code] {}\n[String] {}\n[Desc] {}\n\n[Line] {}\n[File] {}\n", err_code, err_str, desc, line, file);
    }

    // The same but with the info queue.
    std::string get_hr_info(HRESULT hr, int line, str file, std::vector<std::string> msgs) {
        std::string err_code = fmt::format("{:#x}", hr); // Formats it as hex
        str err_str = DXGetErrorStringA(hr);
        char desc[512];
        DXGetErrorDescriptionA(hr, desc, sizeof desc);

        std::string info;
        // join all info messages with newlines into single string
        for(auto& m : msgs)
        {
            info += m;
            info.push_back('\n');
        }
        // remove final newline if exists
        if(!info.empty()) info.pop_back();

        return fmt::format("[Code] {}\n[String] {}\n[Desc] {}\n[Info] {}\n\n[Line] {}\n[File] {}\n", err_code, err_str, desc, info, line, file);
    }

    // Is there a cleaner way to do this?
    struct CreateResult {
        Graphics gfx;
        Panic panic;
    };

    CreateResult create(Window& window) {
        Graphics gfx = {};
        Panic panic;

        DXGI_SWAP_CHAIN_DESC swd = {};
        swd.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
        swd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
        swd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;

        swd.SampleDesc.Count = 1;
        swd.SampleDesc.Quality = 0;

        swd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
        swd.BufferCount = 1;

        swd.OutputWindow = window.handle;
        swd.Windowed = TRUE;

        swd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;

#ifdef __DEBUG
        UINT flags = D3D11_CREATE_DEVICE_DEBUG;
        gfx.info = create_debug_info();
#else
        UINT flags = 0;
#endif

        DX_CALL(D3D11CreateDeviceAndSwapChain(
            nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr,
            flags, nullptr, 0, D3D11_SDK_VERSION,
            &swd, &gfx.swap, &gfx.device, nullptr, &gfx.context
        ));

        ID3D11Resource* back_buffer;
        DX_CALL(gfx.swap->GetBuffer(0, __uuidof(ID3D11Resource), (void**) &back_buffer));
        DX_CALL(gfx.device->CreateRenderTargetView(back_buffer, nullptr, &gfx.screen));

        back_buffer->Release();

        return CreateResult { gfx, panic };
    }

    Panic present(Graphics& gfx) {
        HRESULT hr;
        Panic panic;
        // If the error is DXGI_ERROR_DEVICE_REMOVED you actually have to get the reason from the device.
        if (FAILED(hr = gfx.swap->Present(1, 0))) {
            if (hr == DXGI_ERROR_DEVICE_REMOVED) { panic = CREATE_DX_PANIC(gfx.device->GetDeviceRemovedReason()); }
            else { panic = CREATE_DX_PANIC(hr); }
            return panic;
        } else {
            return PANIC_OK;
        }
    }

    void clear(Graphics& gfx, float r, float g, float b) {
        f32 color[] = { r, g, b, 1.0f };
        gfx.context->ClearRenderTargetView(gfx.screen, color);
    }
}

i32 CALLBACK WinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE, _In_ LPSTR args, _In_ int) {
    Window window = create_window("DirectX 11", 800, 600);

    // Don't know a way to not handle this manually
    gfx::CreateResult gfx_r = gfx::create(window);
    if (gfx_r.panic.desc != "OK") {
        MessageBoxA(0, gfx_r.panic.desc.c_str(), "Error!", MB_OK | MB_ICONERROR);
        g::running = false;
    }
    gfx::Graphics gfx = gfx_r.gfx;
    while (g::running) {
        update_window(window);

        gfx::clear(gfx, 1.0f, 0.0f, 0.0f);
        CHECK_PANIC(gfx::present(gfx));
    }
    return 0;
}
```
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.