読者です 読者をやめる 読者になる 読者になる

ゆとりーなの日記

日記的な事を書いて行くと思はれる

描画を別スレッドで行う罠

 メッセージループと描画のスレッドは分けるべしという説があったりするわけですが、DirectX9を使う場合においては描画スレッドをメッセージループと違うスレッドにすると、デバイスロストの扱いが面倒になるんですね。というのもIDirect3DDevice9::Reset()ってのは、どうもメインスレッドからしか呼べない仕様になってるんですね。というわけで次のようなコードで甘えるとデバイスロストから復旧時にこけます。

#include <array>
#include <memory>
#include <system_error>
#include <boost/noncopyable.hpp>
#include <boost/thread.hpp>
#include <Windows.h>
#include <d3d9.h>

struct vertex {
  float x;
  float y;
  float z;
  float rhw;
  DWORD c;
};

const int kWindowWidth = 640;
const int kWindowHeight = 480;
volatile bool run = true;

struct com_delete {
  void operator ()(const LPUNKNOWN com) const {
    com->Release();
  }
};

typedef std::unique_ptr<IDirect3D9, com_delete> direct3d9_handle;
typedef std::unique_ptr<IDirect3DDevice9, com_delete> direct3d9_deivce_handle;

D3DPRESENT_PARAMETERS init_present_parameters() {
  const D3DPRESENT_PARAMETERS present_parameters = {
    kWindowWidth,
    kWindowHeight,
    D3DFMT_UNKNOWN,
    1,
    D3DMULTISAMPLE_NONE,
    0,
    D3DSWAPEFFECT_DISCARD,
    nullptr,
    TRUE,
    TRUE,
    D3DFMT_D24S8,
    D3DPRESENTFLAG_LOCKABLE_BACKBUFFER,
    D3DPRESENT_RATE_DEFAULT,
    D3DPRESENT_INTERVAL_DEFAULT
  };
  return present_parameters;
}

direct3d9_handle create_base() {
  const LPDIRECT3D9 direct3d = Direct3DCreate9(D3D_SDK_VERSION);
  if (!direct3d) {
    throw std::runtime_error("Direct3Dの作成に失敗しました");
  }
  return direct3d9_handle(direct3d);
}

direct3d9_deivce_handle create_device(const HWND window, const direct3d9_handle &direct3d) {
  D3DPRESENT_PARAMETERS present_parameters = init_present_parameters();
  LPDIRECT3DDEVICE9 device;
  if (FAILED(direct3d->CreateDevice(D3DADAPTER_DEFAULT, 
                                    D3DDEVTYPE_HAL, 
                                    window, 
                                    D3DCREATE_HARDWARE_VERTEXPROCESSING,
                                    &present_parameters,
                                    &device))) {
    if (FAILED(direct3d->CreateDevice(D3DADAPTER_DEFAULT,
                                      D3DDEVTYPE_HAL, 
                                      window, 
                                      D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                                      &present_parameters,
                                      &device))) {
      if (FAILED(direct3d->CreateDevice(D3DADAPTER_DEFAULT,
                                        D3DDEVTYPE_REF,
                                        window, 
                                        D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                                        &present_parameters, 
                                        &device))) {
        throw std::runtime_error("Direct3Dデバイスの作成に失敗しました");
      }  
    }
  }
  return direct3d9_deivce_handle(device);
}

void init_device(const direct3d9_deivce_handle &device) {
  device->SetRenderState(D3DRS_LIGHTING, FALSE);
  device->SetRenderState(D3DRS_ZENABLE, FALSE);
}

LRESULT CALLBACK procedure(const HWND window_handle, const UINT message, const WPARAM wp, const LPARAM lp) {
  switch (message) {
    case WM_DESTROY:
      PostQuitMessage(0);
      break;
    default:
      return DefWindowProc(window_handle, message, wp, lp);
  }
  return 0;
}

void init_app() {
  const WNDCLASSEXA window_class  = {
    sizeof(window_class),
    CS_HREDRAW | CS_VREDRAW,
    &procedure,
    0,
    0,
    GetModuleHandle(nullptr),
    reinterpret_cast<HICON>(LoadImage(nullptr, 
                                      IDI_APPLICATION, 
                                      IMAGE_ICON, 
                                      32,
                                      32,
                                      LR_DEFAULTSIZE | LR_SHARED)),
    reinterpret_cast<HCURSOR>(LoadImage(nullptr,
                                        IDC_ARROW, 
                                        IMAGE_CURSOR, 
                                        0,
                                        0,
                                        LR_DEFAULTSIZE | LR_SHARED)),
    reinterpret_cast<HBRUSH>(GetStockObject(WHITE_BRUSH)),
    nullptr,
    "BASE",
    nullptr
  };
  if (!RegisterClassExA(&window_class)) {
    const DWORD error = GetLastError();
    throw std::system_error(std::error_code(error, std::system_category()),
                            "ウィンドウクラスの登録に失敗しました。\n詳細");
  }
}

HWND create_window() {
  const DWORD kStyle = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
  RECT rect = {0, 0, kWindowWidth, kWindowHeight};
  if (!AdjustWindowRect(&rect, kStyle, FALSE)) {
    const DWORD error = GetLastError();
    throw std::system_error(std::error_code(error, std::system_category()),
                            "ウィンドウサイズの取得に失敗しました。\n詳細");
  }
  const HWND window = CreateWindowA("BASE", 
                                     "窓",
                                     kStyle,
                                     CW_USEDEFAULT,
                                     CW_USEDEFAULT, 
                                     rect.right - rect.left,
                                     rect.bottom - rect.top,
                                     nullptr,
                                     nullptr,
                                     GetModuleHandle(nullptr),
                                     nullptr);
  if (!window) {
    const DWORD error = GetLastError();
    throw std::system_error(std::error_code(error, std::system_category()), "ウィンドウの作成に失敗しました。\n詳細");
  }
  return window;
}

void reset(const direct3d9_deivce_handle &device) {
  D3DPRESENT_PARAMETERS present_parameter = init_present_parameters();
  if (FAILED(device->Reset(&present_parameter))) {
    throw std::runtime_error("デバイスリセットに失敗しました");
  }
  init_device(device);
}

void present(const direct3d9_deivce_handle &device) {
  switch (device->Present(nullptr, nullptr, nullptr, nullptr)) {
    case D3DERR_DEVICELOST:
      if (device->TestCooperativeLevel() == D3DERR_DEVICENOTRESET) {
        reset(device);
      }
      break;
    case D3DERR_DRIVERINTERNALERROR:
      throw std::runtime_error("内部ドライバエラーが発生しました");
      break;
    default:
      break;
  }
}

class scoped_render : private boost::noncopyable {
 public:
  explicit scoped_render(const direct3d9_deivce_handle &device) : device_(device), succeeded_(SUCCEEDED(device->BeginScene())) {}
  
  ~scoped_render() {
    device_->EndScene();
  }

  bool succeeded() const {
    return succeeded_;
  }

 private:
  const direct3d9_deivce_handle &device_;
  const bool succeeded_;
};

void draw(const direct3d9_deivce_handle &device) {
  device->SetFVF(D3DFVF_XYZRHW | D3DFVF_DIFFUSE);
  const std::array<vertex, 4> v = {{
    {0.f, 0.f, 0.f, 1.f, 0xFFFFFFFF},
    {320.f, 0.f, 0.f, 1.f, 0xFFFFFFFF},
    {0.f, 240.f, 0.f, 1.f, 0xFFFFFFFF},
    {320.f, 240.f, 0.f, 1.f, 0xFFFFFFFF}
  }};
  device->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, v.data(), sizeof(v.front()));
}

void update(const direct3d9_deivce_handle &device) {
  while (run) {
    {
      const scoped_render render(device);
      if (render.succeeded()) {
        device->Clear(0, nullptr, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xFF000000, 1.f, 0);
        draw(device);
      }
    }
    present(device);
  }
}

int message_loop(const direct3d9_deivce_handle &device) {
  boost::thread update_thread([&device] {update(device);});
  MSG mes;
  for (;;) {
    if (PeekMessage(&mes, nullptr, 0, 0, PM_NOREMOVE)) {
      const BOOL result = GetMessage(&mes, nullptr, 0, 0);
      if (!(result && ~result)) {
        break;
      } 
      TranslateMessage(&mes);
      DispatchMessage(&mes);
    }
  }
  run = false;
  update_thread.join();
  return mes.wParam;
}

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
  try {
    init_app();
    const HWND window = create_window();
    const direct3d9_handle d3d9 = create_base();
    const direct3d9_deivce_handle device = create_device(window, d3d9);
    init_device(device);
    ShowWindow(window, SW_RESTORE);
    return message_loop(device);
  } catch (const std::exception &error) {
    MessageBoxA(nullptr, error.what(), "Error", MB_ICONSTOP | MB_OK);
  }
  return 0;
}

 というわけでなんとかして描画スレッドからデバイスがこけたらメインスレッドにデバイスリセットしてよ通知をしてあげないといけないみたいです。雰囲気これはウィンドウプロシージャで扱うのがよさそうですね。WM_APP以降から何個かは好き勝手にメッセージを作れますのでこいつを使うのがよさそうです。で、SendMessageは若干デッドロックフラグが立っているものの、リセット完了まで待機していて欲しいという願望があるのでこれに甘えていきます。

LRESULT CALLBACK procedure(const HWND window_handle, const UINT message, const WPARAM wp, const LPARAM lp) {
  switch (message) {
    case WM_DEVICE_RESER:
      if (FAILED(reinterpret_cast<LPDIRECT3DDEVICE9>(wp)->Reset(reinterpret_cast<D3DPRESENT_PARAMETERS *>(lp)))) {
        throw std::runtime_error("デバイスリセットに失敗しました");
      }
      break;
    case WM_DESTROY:
      PostQuitMessage(0);
      break;
    default:
      return DefWindowProc(window_handle, message, wp, lp);
  }
  return 0;
}

void reset(const HWND window, const direct3d9_deivce_handle &device) {
  D3DPRESENT_PARAMETERS present_parameter = init_present_parameters();
  SendMessage(window, WM_DEVICE_RESER, reinterpret_cast<WPARAM>(device.get()), reinterpret_cast<LPARAM>(&present_parameter));
  init_device(device);
}

 こんな感じでウィンドウプロシージャとリセット部を修正すると、とりあえずデバイスロストからの復旧時にこけるという事態は回避できます。キャスト炸裂です。
 もっといい方法があるような気はしますが・・・。