콘텐츠로 건너뛰기

ID3D11Texture2D를 이미지로 저장하기

보통 DirectX에서 Texture를 처리하는데 DirectXTex 라이브러리를 사용합니다. DirectXTex 라이브러리는 Visual Studio 2019 이상이 필요하기 때문에 구버전에서 사용할 수 없습니다. 이번 포스팅에서는 DirectXTex 라이브러리없이 ID3D11Texture2D를 이미지로 저장하는 방법에 대해서 알아보도록 하겠습니다.

개발에 앞서

저는 업무 특성 상 애플리케이션을 후킹하여 보안을 적용하기 위한 다양한 작업을 합니다. 그 중 하나는 캡쳐 시 화면 데이터를 보호하는 것으로 애플리케이션이 화면을 캡처하여 메모리로 저장할 때 데이터를 변조하여 화면의 특정 영역을 가리거나 복사를 불가능하게 만듭니다.

일반적으로 DirectX를 사용하여 화면을 캡처하는 경우 IDXGIOutputDuplication Interface를 사용합니다. CreateDXGIFactory 부터 IDXGIOutputDuplication 까지의 과정을 잘 파악하고 있으면 캡처 데이터를 제어하는데 어려움은 없습니다.

하지만 일부 앱은 Shared Resource 기술을 사용하여 리소스를 공유하고 메모리에 저장하는 방식을 사용하고 있어 IDXGIOutputDuplication에서 제어가 불가능한 경우가 존재합니다. 이 경우는 Shared Resource 영역을 후킹하여 캡처 데이터를 확보해야 하는데 Shared Resource 관련 함수는 리소스와 관련한 부분에서 다 사용할 수 있어 관련 함수를 분석하고 적용하는데 입력 리소스 또는 출력 리소스 그리고 변조한 데이터가 정상적으로 반영되었는지 화면이 아니라 파일로 확인하는 경우가 존재합니다.

ID3D11Texture2D를 이미지로 저장하기

아래 소스코드는 ID3D11Texture2D를 이미지로 저장하는 소스코드입니다. DDSDirectDraw Surface 포맷으로 이미지를 저장하고 있습니다.

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

#include <memory>
#include <string>
#include <algorithm>

#pragma pack(push,1)

#define DDS_MAGIC 0x20534444 // "DDS "

#define DDS_FOURCC      0x00000004  // DDPF_FOURCC
#define DDS_RGB         0x00000040  // DDPF_RGB
#define DDS_RGBA        0x00000041  // DDPF_RGB | DDPF_ALPHAPIXELS
#define DDS_LUMINANCE   0x00020000  // DDPF_LUMINANCE
#define DDS_LUMINANCEA  0x00020001  // DDPF_LUMINANCE | DDPF_ALPHAPIXELS
#define DDS_ALPHA       0x00000002  // DDPF_ALPHA
#define DDS_BUMPDUDV    0x00080000  // DDPF_BUMPDUDV

#define DDS_HEADER_FLAGS_TEXTURE        0x00001007  // DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT
#define DDS_HEADER_FLAGS_MIPMAP         0x00020000  // DDSD_MIPMAPCOUNT
#define DDS_HEADER_FLAGS_PITCH          0x00000008  // DDSD_PITCH
#define DDS_HEADER_FLAGS_LINEARSIZE     0x00080000  // DDSD_LINEARSIZE

#define DDS_SURFACE_FLAGS_TEXTURE 0x00001000 // DDSCAPS_TEXTURE

struct DDS_PIXELFORMAT {
  DWORD dwSize;
  DWORD dwFlags;
  DWORD dwFourCC;
  DWORD dwRGBBitCount;
  DWORD dwRBitMask;
  DWORD dwGBitMask;
  DWORD dwBBitMask;
  DWORD dwABitMask;
};

typedef struct {
  DWORD           dwSize;
  DWORD           dwFlags;
  DWORD           dwHeight;
  DWORD           dwWidth;
  DWORD           dwPitchOrLinearSize;
  DWORD           dwDepth;
  DWORD           dwMipMapCount;
  DWORD           dwReserved1[11];
  DDS_PIXELFORMAT ddspf;
  DWORD           dwCaps;
  DWORD           dwCaps2;
  DWORD           dwCaps3;
  DWORD           dwCaps4;
  DWORD           dwReserved2;
} DDS_HEADER;

typedef struct {
  DXGI_FORMAT              dxgiFormat;
  D3D10_RESOURCE_DIMENSION resourceDimension;
  UINT                     miscFlag;
  UINT                     arraySize;
  UINT                     miscFlags2;
} DDS_HEADER_DXT10;

#pragma pack(pop)

const DDS_PIXELFORMAT DDSPF_A8R8G8B8 =
{ sizeof(DDS_PIXELFORMAT), DDS_RGBA, 0, 32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000 };

struct com_deleter { void operator()(IUnknown* p) { if ( p ) { p->Release(); } } };
struct handle_closer { void operator()(HANDLE h) { if ( h != INVALID_HANDLE_VALUE ) CloseHandle(h); } };

template <typename T> using ScopedInterface = std::unique_ptr<T, com_deleter>;
template <typename T> ScopedInterface<T> make_com_ptr(T* p) { return ScopedInterface<T>(p); }

using ScopedHandle = std::unique_ptr<void, handle_closer>;
inline HANDLE safe_handle(HANDLE h) { return (h == INVALID_HANDLE_VALUE) ? nullptr : h; }

HRESULT SaveDDSFile(ID3D11DeviceContext* device_context, ID3D11Resource* source, const wchar_t* file_name)
{
  if ( nullptr == device_context || nullptr == source || nullptr == file_name )
  {
    return E_INVALIDARG;
  }

  D3D11_RESOURCE_DIMENSION resource_type = D3D11_RESOURCE_DIMENSION_UNKNOWN;
  source->GetType(&resource_type);

  if ( resource_type != D3D11_RESOURCE_DIMENSION_TEXTURE2D )
  {
    return HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED);
  }

  ID3D11Texture2D* source_texture = nullptr;

  HRESULT hr = source->QueryInterface(__uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&source_texture));

  if ( FAILED(hr) )
  {
    return hr;
  }

  auto _source_texture = make_com_ptr(source_texture);

  D3D11_TEXTURE2D_DESC desc;
  source_texture->GetDesc(&desc);

  ID3D11Device* device = nullptr;
  device_context->GetDevice(&device);

  auto _device = make_com_ptr(device);

  if ( desc.SampleDesc.Count != 1 && desc.Format != DXGI_FORMAT_B8G8R8A8_UNORM )
  {
    return HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED);
  }

  ID3D11Texture2D* staging_texture = nullptr;

  ScopedInterface<ID3D11Texture2D> _staging_texture;

  if ( (desc.Usage == D3D11_USAGE_STAGING) && (desc.CPUAccessFlags & D3D11_CPU_ACCESS_READ) )
  {
    staging_texture = source_texture;
    _staging_texture = std::move(_source_texture);
  }
  else
  {
    desc.BindFlags = 0;
    desc.MiscFlags = 0;
    desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
    desc.Usage = D3D11_USAGE_STAGING;

    hr = device->CreateTexture2D(&desc, nullptr, &staging_texture);

    if ( FAILED(hr) )
    {
      return hr;
    }

    device_context->CopyResource(staging_texture, source_texture);

    _staging_texture = make_com_ptr(staging_texture);
  }

  const DWORD kMaxHeaderSize = sizeof(DWORD) + sizeof(DDS_HEADER) + sizeof(DDS_HEADER_DXT10);
  BYTE file_header[kMaxHeaderSize] = {};

  *reinterpret_cast<DWORD*>(&file_header[0]) = DDS_MAGIC;

  auto header = reinterpret_cast<DDS_HEADER*>(&file_header[0] + sizeof(DWORD));
  size_t header_size = sizeof(DWORD) + sizeof(DDS_HEADER);
  header->dwSize = sizeof(DDS_HEADER);
  header->dwFlags = DDS_HEADER_FLAGS_TEXTURE | DDS_HEADER_FLAGS_MIPMAP;
  header->dwHeight = desc.Height;
  header->dwWidth = desc.Width;
  header->dwMipMapCount = 1;
  header->dwCaps = DDS_SURFACE_FLAGS_TEXTURE;

  DDS_HEADER_DXT10* ext_header = nullptr;
  memcpy_s(&header->ddspf, sizeof(header->ddspf), &DDSPF_A8R8G8B8, sizeof(DDS_PIXELFORMAT));

  DWORD row_pitch = (DWORD(desc.Width) * 32 + 7u) / 8u;
  DWORD row_count = (DWORD(desc.Height));
  DWORD row_bytes = row_pitch * row_count;

  header->dwFlags |= DDS_HEADER_FLAGS_PITCH;
  header->dwPitchOrLinearSize = static_cast<DWORD>(row_pitch);

  std::unique_ptr<BYTE[]> pixels(new (std::nothrow) BYTE[row_bytes]);

  if ( !pixels )
  {
    return E_OUTOFMEMORY;
  }

  D3D11_MAPPED_SUBRESOURCE resource;

  hr = device_context->Map(staging_texture, 0, D3D11_MAP_READ, 0, &resource);

  if ( FAILED(hr) )
  {
    return hr;
  }

  auto src_data = static_cast<const BYTE*>(resource.pData);

  if ( !src_data )
  {
    device_context->Unmap(staging_texture, 0);
    return E_POINTER;
  }

  BYTE* dst_ptr = pixels.get();

  const DWORD min_size = std::min<DWORD>(row_pitch, resource.RowPitch);

  for ( DWORD h = 0; h < row_count; ++h )
  {
    memcpy_s(dst_ptr, row_pitch, src_data, min_size);
    src_data += resource.RowPitch;
    dst_ptr += row_pitch;
  }

  device_context->Unmap(staging_texture, 0);

  ScopedHandle hFile(safe_handle(CreateFileW(file_name, GENERIC_WRITE | DELETE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)));

  if ( !hFile )
  {
    return HRESULT_FROM_WIN32(GetLastError());
  }

  DWORD bytesWritten;
  if ( !WriteFile(hFile.get(), file_header, static_cast<DWORD>(header_size), &bytesWritten, nullptr) )
  {
    return HRESULT_FROM_WIN32(GetLastError());
  }

  if ( bytesWritten != header_size )
  {
    return E_FAIL;
  }

  if ( !WriteFile(hFile.get(), pixels.get(), static_cast<DWORD>(row_bytes), &bytesWritten, nullptr) )
  {
    return HRESULT_FROM_WIN32(GetLastError());
  }

  if ( bytesWritten != row_bytes )
  {
    return E_FAIL;
  }

  return S_OK;
}

마치며

ID3D11Texture2D를 이미지로 저장하는 소스코드에 대해서 알아보았습니다. DDS 포맷은 별도 이미지 뷰어를 설치해서 확인할 수 있으며, JPEG, PNG 등의 이미지로 변경하는 부분에 대해서는 차후 기회가 된다면 추가하도록 하겠습니다.

참고 자료

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

목차 보기