wrongwrongな開発日記

しんまいさんの忘備録

【DxLib】描画スレッドで宣言したハンドルへの描画が止まる問題への対処【.Net】

別スレッドにおいてDxLibを動かして描画を行うプログラムで、exeから起動時に描画が停止する状況と、対応策をまとめます。
プロジェクト全体はGitHubに有ります。
github.com
※このサンプルは描画はUIスレッドから行うという原則に反しているので良くないです。形としてはTimerなりと組み合わせてデリゲートでメインスレッドから描画するのが正しいです。メインスレッドだけで触ればこの問題も発生しません。

状況

問題の発生するプログラムの構成と、問題の内容です。

構成

プログラムは以下のような構成です。

  • メインスレッド
  • 描画スレッド
    • 無限ループするスレッド

メイン画面のPictureBoxと、別画面として用意したFormに同時に描画をしています。Formへの描画は関連記事にまとめてあります。

プログラム

動かない状態を再現したプロジェクトはこちら

Imports System.Threading
Imports DxLibDLL

Public Class Form1
    Private isEnd As Boolean
    Private targetForm As Form
    Private drawThread As Thread
    Private pictureHandle As Integer

    'DxLibのオプション設定と初期化
    Private Sub InitDxLib(ByRef handle As Integer)
        DX.SetUserWindow(handle) 'ハンドルセット
        'ウィンドウがアクティブじゃない時も処理を続ける
        DX.SetAlwaysRunFlag(DX.TRUE)
        'DxLib_Init()を呼んだwindow以外からも操作を可能にする
        DX.SetMultiThreadFlag(DX.TRUE)
        'pngなどの透過チャンネルを有効に設定
        DX.SetMovieColorA8R8G8B8Flag(DX.TRUE)
        '一度裏画面に描画を行い、Flipすると描画が行われるように設定(ダブルバッファリング)
        DX.SetDrawScreen(DX.DX_SCREEN_BACK)
        '2枚以上の画面にScreen.flipする場合デフォルトではVSync待ちで遅くなるのでVSyncは切る
        DX.SetWaitVSyncFlag(DX.FALSE)
        'これが無いとプログラム本体のIME関連動作がおかしくなる
        DX.SetUseIMEFlag(DX.TRUE)
        '透過色を指定しない、これが無いとデフォルトでは黒が透過されてしまう
        DX.SetUseTransColor(DX.FALSE)

        If DX.DxLib_Init() = -1 Then MsgBox("初期化に失敗しました。", MsgBoxStyle.Critical)
    End Sub

    '描画ループ
    Private Sub DrawLoop()
        targetForm = New Form
        With targetForm
            .FormBorderStyle = FormBorderStyle.None '枠無
            .Size = New Size(192, 108)
            .Show() '表示
            .DesktopLocation = New Point(0, 0)
        End With
        Dim formHandle = targetForm.Handle

        InitDxLib(formHandle)
        Dim i As Integer = 0
        Dim sw = Stopwatch.StartNew()
        Dim color = DX.GetColor(100, 0, 0)

        While Not isEnd
            sw.Restart()

            DX.SetDrawScreen(formHandle)
            DX.ClearDrawScreen() '描画先初期化

            i += 1
            If i = 100 Then i = 0
            DX.DrawBox(i, 0, 100, 100, color, DX.TRUE)

            DX.SetDrawScreen(DX.DX_SCREEN_BACK) '描画先を裏画面へ
            DX.DrawGraph(0, 0, formHandle, DX.TRUE) '裏画面へ描画
            DX.ScreenFlip() 'メインウィンドウへ表示
            'サブウィンドウへ表示し、その後メインウィンドウへ処理を戻す
            DX.DrawGraph(0, 0, pictureHandle, DX.TRUE)
            DX.SetScreenFlipTargetWindow(pictureHandle) '描画先をサブ画面へ
            DX.ScreenFlip() 'サブ画面へ描画
            '後処理
            DX.ClearDrawScreen() '裏画面クリア
            DX.SetScreenFlipTargetWindow(0) '描画先をメイン画面(0番のハンドルの画面がそれ)へ戻す

            sw.Stop()
            Thread.Sleep(Math.Max(0, 16 - sw.ElapsedMilliseconds)) '簡単な垂直同期
        End While
    End Sub

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        isEnd = False
        targetForm = New Form
        With targetForm
            .FormBorderStyle = FormBorderStyle.None '枠無
            .Size = New Size(192, 108)
            .Show() '表示
            .DesktopLocation = New Point(0, 0)
        End With

        pictureHandle = PictureBox1.Handle

        drawThread = New Thread(New ParameterizedThreadStart(AddressOf DrawLoop)) With {
            .IsBackground = True
        }
        drawThread.Start(targetForm.Handle)
    End Sub
End Class
問題

以下のような問題が発生します。

  • 起動から不定な時間の経過/何らかの操作でフォームへの描画が停止する
    • メインスレッドから渡したPicture Boxへの描画は続く
  • VisualStudioから開始しても問題は発生しない
    • exeからプログラムを開始すると発生する

対策

描画先を描画スレッドではなくメインスレッドで行うようにすると解決します。

プログラム

動く状態を再現したプロジェクトはこちら

Imports System.Threading
Imports DxLibDLL

Public Class Form1
    Private isEnd As Boolean
    Private targetForm As Form
    Private drawThread As Thread
    Private pictureHandle As Integer

    'DxLibのオプション設定と初期化
    Private Sub InitDxLib(ByRef handle As Integer)
        DX.SetUserWindow(handle) 'ハンドルセット
        'ウィンドウがアクティブじゃない時も処理を続ける
        DX.SetAlwaysRunFlag(DX.TRUE)
        'DxLib_Init()を呼んだwindow以外からも操作を可能にする
        DX.SetMultiThreadFlag(DX.TRUE)
        'pngなどの透過チャンネルを有効に設定
        DX.SetMovieColorA8R8G8B8Flag(DX.TRUE)
        '一度裏画面に描画を行い、Flipすると描画が行われるように設定(ダブルバッファリング)
        DX.SetDrawScreen(DX.DX_SCREEN_BACK)
        '2枚以上の画面にScreen.flipする場合デフォルトではVSync待ちで遅くなるのでVSyncは切る
        DX.SetWaitVSyncFlag(DX.FALSE)
        'これが無いとプログラム本体のIME関連動作がおかしくなる
        DX.SetUseIMEFlag(DX.TRUE)
        '透過色を指定しない、これが無いとデフォルトでは黒が透過されてしまう
        DX.SetUseTransColor(DX.FALSE)

        If DX.DxLib_Init() = -1 Then MsgBox("初期化に失敗しました。", MsgBoxStyle.Critical)
    End Sub

    '描画ループ
    Private Sub DrawLoop(ByVal handleObj As Object)
        Dim formHandle = handleObj.ToInt32
        InitDxLib(formHandle)
        Dim i As Integer = 0
        Dim sw = Stopwatch.StartNew()
        Dim color = DX.GetColor(100, 0, 0)

        While Not isEnd
            sw.Restart()

            DX.SetDrawScreen(formHandle)
            DX.ClearDrawScreen() '描画先初期化

            i += 1
            If i = 100 Then i = 0
            DX.DrawBox(i, 0, 100, 100, color, DX.TRUE)

            DX.SetDrawScreen(DX.DX_SCREEN_BACK) '描画先を裏画面へ
            DX.DrawGraph(0, 0, formHandle, DX.TRUE) '裏画面へ描画
            DX.ScreenFlip() 'メインウィンドウへ表示
            'サブウィンドウへ表示し、その後メインウィンドウへ処理を戻す
            DX.DrawGraph(0, 0, pictureHandle, DX.TRUE)
            DX.SetScreenFlipTargetWindow(pictureHandle) '描画先をサブ画面へ
            DX.ScreenFlip() 'サブ画面へ描画
            '後処理
            DX.ClearDrawScreen() '裏画面クリア
            DX.SetScreenFlipTargetWindow(0) '描画先をメイン画面(0番のハンドルの画面がそれ)へ戻す

            sw.Stop()
            Thread.Sleep(Math.Max(0, 16 - sw.ElapsedMilliseconds)) '簡単な垂直同期
        End While
    End Sub

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        isEnd = False
        targetForm = New Form
        With targetForm
            .FormBorderStyle = FormBorderStyle.None '枠無
            .Size = New Size(192, 108)
            .Show() '表示
            .DesktopLocation = New Point(0, 0)
        End With

        pictureHandle = PictureBox1.Handle

        drawThread = New Thread(New ParameterizedThreadStart(AddressOf DrawLoop)) With {
            .IsBackground = True
        }
        drawThread.Start(targetForm.Handle)
    End Sub
End Class
原因

はっきり特定したわけではありませんが、描画スレッドのリソースが勝手に解放されてしまうのが原因のようです。
Visual Studioからの起動時に問題が発生しないのは、Visual Studioからの動作の監視によってリソース開放が防がれているものと考えられます。

感想

VSから始めると発生しないってのが厄介でした。これWindowsの方のバグってことでいいんですかね?