← SEARCHABLE ARCHIVE
DirectX 12 · 2026-07-18

DX12 디버그 레이어와 GPU 어댑터 선택

검은 화면 전에 검증 계층을 켜고 실행 장치 확인하기

INTERACTIVE READING MODE →

본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.

DirectX 12에서 검은 화면을 만난 뒤 로그를 추가하면 너무 늦다. 장치를 만들기 전에 Debug Layer를 켜고, 어느 GPU를 선택했는지 기록해야 첫 프레임부터 증거를 남길 수 있다.

DXGI와 D3D12의 역할을 나눈다

DXGI는 그래픽 어댑터, 모니터 출력, Swap Chain을 다룬다. D3D12 Device는 선택한 어댑터에서 Resource, Command Queue, Pipeline State 같은 GPU 객체를 만드는 논리적 인터페이스다.

DXGI Factory
  └─ Adapter 선택
       └─ D3D12 Device 생성
            ├─ Command Queue
            ├─ Resource
            └─ Root Signature / PSO

Device가 GPU 자체는 아니다. 같은 어댑터에 대해 프로세스에 이미 Device가 있다면 D3D12CreateDevice가 기존 Device를 반환할 수도 있다. 중요한 것은 Device를 통해 이 실행 환경이 어떤 기능을 제공하는지 질의하는 것이다.

Device보다 Debug Layer를 먼저 켠다

ComPtr<ID3D12Debug> debugController;

#if defined(_DEBUG)
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
{
    debugController->EnableDebugLayer();
}
#endif

Debug Layer는 매개변수, Resource State, Descriptor Binding, Shader 연결 같은 규칙을 검사하고 디버그 출력에 오류를 남긴다. 반드시 Device를 만들기 전에 켜야 한다. Device 생성 뒤에 켜면 이미 만든 객체에 원하는 검증이 적용된다는 보장이 없다.

Windows에서 D3D12GetDebugInterface가 실패한다면 선택 기능의 Graphics Tools 설치 상태를 확인한다. 배포 빌드에서는 비용을 피하되, 개발과 CI 검증 빌드에서는 경고까지 읽는 편이 좋다.

어댑터를 열거하고 선택 근거를 남긴다

ComPtr<IDXGIFactory6> factory;
ThrowIfFailed(CreateDXGIFactory2(
#if defined(_DEBUG)
    DXGI_CREATE_FACTORY_DEBUG,
#else
    0,
#endif
    IID_PPV_ARGS(&factory)));

ComPtr<IDXGIAdapter1> adapter;

for (UINT index = 0;
     factory->EnumAdapterByGpuPreference(
         index,
         DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE,
         IID_PPV_ARGS(&adapter)) != DXGI_ERROR_NOT_FOUND;
     ++index)
{
    DXGI_ADAPTER_DESC1 desc{};
    ThrowIfFailed(adapter->GetDesc1(&desc));

    if ((desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) != 0)
    {
        adapter.Reset();
        continue;
    }

    if (SUCCEEDED(D3D12CreateDevice(
            adapter.Get(), D3D_FEATURE_LEVEL_11_0,
            __uuidof(ID3D12Device), nullptr)))
    {
        break;
    }

    adapter.Reset();
}

EnumAdapterByGpuPreference의 high-performance 순서는 선택 정책일 뿐 성능 순위의 영구적 보장이 아니다. 노트북은 전력 정책과 사용자 설정에 따라 달라질 수 있다. 실행 로그에는 어댑터 이름, Vendor/Device ID, Dedicated Video Memory, LUID, Driver 정보를 남겨야 다른 PC의 문제를 비교할 수 있다.

WARP는 실패을 분리하는 기준선이다

ComPtr<IDXGIAdapter> warpAdapter;
ThrowIfFailed(factory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter)));
ThrowIfFailed(D3D12CreateDevice(
    warpAdapter.Get(),
    D3D_FEATURE_LEVEL_11_0,
    IID_PPV_ARGS(&device)));

WARP는 CPU에서 돌아가는 소프트웨어 래스터라이저다. 실시간 성능 시험용은 아니지만, 동일한 코드가 WARP에서는 정상이고 특정 GPU에서만 깨진다면 Shader 결과, Driver, Capability 가정을 분리해 조사할 수 있다.

WARP가 정상이라고 하드웨어 코드가 자동으로 올바른 것은 아니다. Timing, Memory Budget, Hardware Feature 차이는 남는다. WARP는 재현 축 하나를 추가할 뿐이다.

Feature Level과 Feature Query를 구분한다

D3D_FEATURE_LEVEL_11_0로 Device를 만들었다고 Mesh Shader, Raytracing, 특정 Shader Model을 쓸 수 있는 것은 아니다. Feature Level은 기본 하드웨어 기능 집합의 하한선이다. 선택 기능은 CheckFeatureSupport로 따로 묻고 결과를 저장해야 한다.

D3D12_FEATURE_DATA_D3D12_OPTIONS options{};
ThrowIfFailed(device->CheckFeatureSupport(
    D3D12_FEATURE_D3D12_OPTIONS,
    &options,
    sizeof(options)));

좋은 초기화 코드는 supportsX 불리언을 여기저기 만들지 않고 DeviceCapabilities 구조체로 한 번에 수집한다. 렌더러는 이 스냅샷으로 기능 경로를 결정하고 크래시 보고서에도 같은 값을 남긴다.

HRESULT와 객체 이름을 버리지 않는다

void SetDebugName(ID3D12Object* object, std::wstring_view name)
{
#if defined(_DEBUG)
    if (object != nullptr)
    {
        object->SetName(std::wstring(name).c_str());
    }
#endif
}

HRESULT를 검사하지 않고 다음 줄에서 null pointer로 죽으면 실제 원인이 사라진다. ThrowIfFailed는 최소한 작업 이름, HRESULT, 파일과 줄을 남겨야 한다. Resource와 Command List에 SceneColor, Frame[2] Allocator 같은 이름을 붙이면 PIX, DRED, Debug Layer 메시지가 사람이 읽을 수 있는 증거가 된다.

점검 실험

  1. 로컬 PC의 모든 어댑터 정보와 선택된 이유를 출력한다.
  2. 하드웨어와 WARP로 같은 첫 프레임을 생성한다.
  3. Resource State 전환 하나를 의도적으로 틀리고 Debug Layer 메시지를 저장한다.
  4. 모든 영구 GPU 객체에 이름을 붙인 뒤 종료 시 생존 객체 보고를 확인한다.

첫 삼각형의 성공 기준은 화면에 색이 나오는 것만이 아니다. 어느 어댑터에서 실행되었고, 어떤 기능을 택했으며, Debug Layer에 오류가 없는지까지 설명할 수 있어야 한다.

공식 자료

확인일: 2026-08-15