DevLog/D2D11 프로젝트

자체엔진 DWrite 클래스 최적화 및 기능 확장 (D3D11 텍스트 출력 담당 클래스)

뽀또치즈맛 2025. 5. 11. 11:48

 
최적화 및 기능 확장 DWrite 클래스

// DWrite.h
#pragma once

#include <d2d1_1.h>
#include <dwrite.h>
#include <dxgi1_2.h>
#include <d3d11.h>
#include <wrl/client.h>
#include <unordered_map>
#include <string>
#include <functional>
#include <stdexcept>
#include <sstream>
#include <iomanip>

using Microsoft::WRL::ComPtr;

// 간단한 RGBA 색상 구조체
typedef struct Color {
    float x, y, z, a;
    uint32_t toARGB() const {
        uint32_t r = static_cast<uint32_t>(x * 255) & 0xFF;
        uint32_t g = static_cast<uint32_t>(y * 255) & 0xFF;
        uint32_t b = static_cast<uint32_t>(z * 255) & 0xFF;
        uint32_t aa = static_cast<uint32_t>(a * 255) & 0xFF;
        return (aa << 24) | (r << 16) | (g << 8) | b;
    }
} TextColor;

// 지원할 기본 폰트 매핑 (locale -> font 이름)
static const std::unordered_map<std::wstring, std::wstring> DefaultLocaleFonts = {
    {L"en-US", L"Segoe UI"},
    {L"zh-CN", L"Microsoft YaHei"},
    {L"ko-KR", L"맑은 고딕"},
    {L"ja-JP", L"Meiryo"}
};


class DWrite {
public:
    DWrite(ID3D11Device* d3dDevice, IDXGISwapChain* swapChain);
    ~DWrite();

    // 리사이즈 시에만 호출
    void OnResize(UINT width, UINT height);

    // 텍스트 렌더링 (locale: en-US, zh-CN, ko-KR, ja-JP 등)
    void RenderText(
        const std::wstring& text,
        const D2D1_RECT_F& rect,
        float size = 20.0f,
        const std::wstring& font = L"",           // 빈 문자열 시 locale 기본 폰트 사용
        const std::wstring& locale = L"en-US",
        TextColor color = { 1,1,1,1 },
        DWRITE_FONT_WEIGHT weight = DWRITE_FONT_WEIGHT_NORMAL,
        DWRITE_FONT_STYLE style = DWRITE_FONT_STYLE_NORMAL,
        DWRITE_FONT_STRETCH stretch = DWRITE_FONT_STRETCH_NORMAL
    );

private:
    // DirectWrite/2D 팩토리 및 컨텍스트
    ComPtr<IDWriteFactory>       writeFactory;
    ComPtr<ID2D1Factory1>        d2dFactory;
    ComPtr<ID2D1Device>          d2dDevice;
    ComPtr<ID2D1DeviceContext>   d2dContext;
    ComPtr<ID2D1Bitmap1>         targetBitmap;
    ComPtr<ID3D11Device>         d3dDevice;
    ComPtr<IDXGISwapChain>       swapChain;

    // 캐시 키 정의 및 해시
    struct BrushKey {
        TextColor c;
        bool operator==(BrushKey const& o) const noexcept { return c.toARGB() == o.c.toARGB(); }
    };
    struct BrushKeyHash {
        size_t operator()(BrushKey const& k) const noexcept {
            return std::hash<uint32_t>()(k.c.toARGB());
        }
    };

    struct TextFormatKey {
        std::wstring font;
        std::wstring locale;
        float size;
        DWRITE_FONT_WEIGHT weight;
        DWRITE_FONT_STYLE style;
        DWRITE_FONT_STRETCH stretch;
        bool operator==(TextFormatKey const& o) const noexcept {
            return font == o.font && locale == o.locale && size == o.size
                && weight == o.weight && style == o.style && stretch == o.stretch;
        }
    };
    struct TextFormatKeyHash {
        size_t operator()(TextFormatKey const& k) const noexcept {
            size_t h = std::hash<std::wstring>()(k.font);
            h ^= std::hash<std::wstring>()(k.locale) + 0x9e3779b9 + (h << 6) + (h >> 2);
            h ^= std::hash<float>()(k.size) + 0x9e3779b9 + (h << 6) + (h >> 2);
            h ^= std::hash<uint32_t>()((uint32_t)k.weight) + 0x9e3779b9 + (h << 6) + (h >> 2);
            h ^= std::hash<uint32_t>()((uint32_t)k.style) + 0x9e3779b9 + (h << 6) + (h >> 2);
            h ^= std::hash<uint32_t>()((uint32_t)k.stretch) + 0x9e3779b9 + (h << 6) + (h >> 2);
            return h;
        }
    };

    struct TextLayoutKey {
        std::wstring text;
        ComPtr<IDWriteTextFormat> format;
        float maxW, maxH;
        bool operator==(TextLayoutKey const& o) const noexcept {
            return text == o.text && format.Get() == o.format.Get()
                && maxW == o.maxW && maxH == o.maxH;
        }
    };
    struct TextLayoutKeyHash {
        size_t operator()(TextLayoutKey const& k) const noexcept {
            size_t h = std::hash<std::wstring>()(k.text);
            h ^= std::hash<void*>()((void*)k.format.Get()) + 0x9e3779b9 + (h << 6) + (h >> 2);
            h ^= std::hash<float>()(k.maxW) + 0x9e3779b9 + (h << 6) + (h >> 2);
            h ^= std::hash<float>()(k.maxH) + 0x9e3779b9 + (h << 6) + (h >> 2);
            return h;
        }
    };

    // 캐시 컨테이너
    std::unordered_map<BrushKey, ComPtr<ID2D1SolidColorBrush>, BrushKeyHash> brushCache;
    std::unordered_map<TextFormatKey, ComPtr<IDWriteTextFormat>, TextFormatKeyHash> formatCache;
    std::unordered_map<TextLayoutKey, ComPtr<IDWriteTextLayout>, TextLayoutKeyHash> layoutCache;

    // 헬퍼 함수
    ComPtr<ID2D1SolidColorBrush> GetBrush(TextColor color);
    ComPtr<IDWriteTextFormat>    GetTextFormat(
        const std::wstring& font,
        const std::wstring& locale,
        float size,
        DWRITE_FONT_WEIGHT,
        DWRITE_FONT_STYLE,
        DWRITE_FONT_STRETCH
    );
    ComPtr<IDWriteTextLayout>    GetTextLayout(
        const std::wstring& text,
        ComPtr<IDWriteTextFormat> format,
        float maxW, float maxH
    );
    void ThrowIfFailed(HRESULT hr, const std::wstring& msg);
};



#include "DWrite.h"
#include <d2d1_1helper.h>

DWrite::DWrite(ID3D11Device* d3dDev, IDXGISwapChain* sc)
    : d3dDevice(d3dDev), swapChain(sc)
{
    HRESULT hr = DWriteCreateFactory(
        DWRITE_FACTORY_TYPE_SHARED,
        __uuidof(IDWriteFactory),
        reinterpret_cast<IUnknown**>(writeFactory.GetAddressOf())
    ); ThrowIfFailed(hr, L"DWriteCreateFactory");

    D2D1_FACTORY_OPTIONS opts = {};
    hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, opts, d2dFactory.GetAddressOf());
    ThrowIfFailed(hr, L"D2D1CreateFactory");

    ComPtr<IDXGIDevice> dxgiDev;
    hr = d3dDevice.As(&dxgiDev); ThrowIfFailed(hr, L"QueryInterface IDXGIDevice");

    hr = d2dFactory->CreateDevice(dxgiDev.Get(), d2dDevice.GetAddressOf());
    ThrowIfFailed(hr, L"CreateDevice");

    hr = d2dDevice->CreateDeviceContext(
        D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
        d2dContext.GetAddressOf()
    ); ThrowIfFailed(hr, L"CreateDeviceContext");

    // 초기 설정
    DXGI_SWAP_CHAIN_DESC desc;
    swapChain->GetDesc(&desc);
    OnResize(desc.BufferDesc.Width, desc.BufferDesc.Height);
}

DWrite::~DWrite() {}

void DWrite::OnResize(UINT width, UINT height) {
    targetBitmap.Reset();
    ComPtr<IDXGISurface> surf;
    ThrowIfFailed(swapChain->GetBuffer(0, __uuidof(IDXGISurface), reinterpret_cast<void**>(surf.GetAddressOf())), L"GetBuffer");

    D2D1_BITMAP_PROPERTIES1 props = D2D1::BitmapProperties1(
        D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW,
        D2D1::PixelFormat(DXGI_FORMAT_R8G8B8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),
        96.f, 96.f
    );
    ThrowIfFailed(d2dContext->CreateBitmapFromDxgiSurface(surf.Get(), &props, targetBitmap.GetAddressOf()), L"CreateBitmap");
    d2dContext->SetTarget(targetBitmap.Get());
}

ComPtr<ID2D1SolidColorBrush> DWrite::GetBrush(TextColor color) {
    BrushKey key{ color };
    auto it = brushCache.find(key);
    if (it != brushCache.end()) return it->second;
    ComPtr<ID2D1SolidColorBrush> brush;
    ThrowIfFailed(d2dContext->CreateSolidColorBrush(
        D2D1::ColorF(color.x, color.y, color.z, color.a),
        brush.GetAddressOf()), L"CreateSolidBrush");
    brushCache[key] = brush;
    return brush;
}

ComPtr<IDWriteTextFormat> DWrite::GetTextFormat(
    const std::wstring& font, const std::wstring& locale,
    float size, DWRITE_FONT_WEIGHT weight,
    DWRITE_FONT_STYLE style, DWRITE_FONT_STRETCH stretch
) {
    // 빈 폰트 시 locale 기본 폰트 사용
    std::wstring actualFont = font.empty()
        ? DefaultLocaleFonts.at(locale)
        : font;
    TextFormatKey key{ actualFont, locale, size, weight, style, stretch };
    auto it = formatCache.find(key);
    if (it != formatCache.end()) return it->second;

    ComPtr<IDWriteTextFormat> fmt;
    ThrowIfFailed(writeFactory->CreateTextFormat(
        actualFont.c_str(), nullptr,
        weight, style, stretch,
        size, locale.c_str(),
        fmt.GetAddressOf()), L"CreateTextFormat");
    formatCache[key] = fmt;
    return fmt;
}

ComPtr<IDWriteTextLayout> DWrite::GetTextLayout(
    const std::wstring& text,
    ComPtr<IDWriteTextFormat> format,
    float maxW, float maxH
) {
    TextLayoutKey key{ text, format, maxW, maxH };
    auto it = layoutCache.find(key);
    if (it != layoutCache.end()) return it->second;

    ComPtr<IDWriteTextLayout> layout;
    ThrowIfFailed(writeFactory->CreateTextLayout(
        text.c_str(), (UINT32)text.size(),
        format.Get(), maxW, maxH,
        layout.GetAddressOf()), L"CreateTextLayout");
    layoutCache[key] = layout;
    return layout;
}

void DWrite::RenderText(
    const std::wstring& text,
    const D2D1_RECT_F& rect,
    float size,
    const std::wstring& font,
    const std::wstring& locale,
    TextColor color,
    DWRITE_FONT_WEIGHT weight,
    DWRITE_FONT_STYLE style,
    DWRITE_FONT_STRETCH stretch
) {
    auto brush = GetBrush(color);
    auto fmt = GetTextFormat(font, locale, size, weight, style, stretch);
    auto layout = GetTextLayout(text, fmt, rect.right - rect.left, rect.bottom - rect.top);
    d2dContext->DrawTextLayout(D2D1::Point2F(rect.left, rect.top), layout.Get(), brush.Get());
}

void DWrite::ThrowIfFailed(HRESULT hr, const std::wstring& msg) {
    if (FAILED(hr)) {
        std::ostringstream oss;
        oss << "0x" << std::hex << static_cast<uint32_t>(hr);
        throw std::runtime_error("DWrite error " + oss.str() + ": " + std::string(msg.begin(), msg.end()));
    }
}

 




추후에 기술소개서에
어느 부분이 확장되었는지,
의도가 뭔지
최적화 된 부분은 어디인지
최적화가 왜 필요했는지
그 의도대로 되었는지 적기

확장 전 후 부분 코드 비교하며 설명하기