VB.NETでGPU情報をレジストリから取得する方法|4GB超VRAM対応・複数アダプター列挙

GPUのビデオメモリが4GB以上のPCでは、WMI(Win32_VideoController.AdapterRAM)が上限に引っ掛かり正しいVRAM容量を取得できないことがあります。そこで本記事では、レジストリを直接参照して複数GPUをループし、名称・VRAM・ドライバー情報をVB.NETで取得する実装例と注意点をまとめます。

目次

WMIのWin32_VideoController.AdapterRAMが4GB以上で正しく取れない理由

WindowsのWMIクラス Win32_VideoController は、GPUの基本情報を手軽に取得できる一方で、AdapterRAM が大容量VRAM環境で正確にならないケースがあります。主な理由は、AdapterRAMが「32bit(32ビット)幅の数値」として扱われることが多く、バイト数をそのまま表現すると約4GB付近で上限に達してしまうためです。

結果として、8GB/12GB/16GBなどのビデオメモリを搭載したGPUでも、値が切り捨てられたり、0や不自然に小さい値として返ってきたりします。アプリ側で「Integer(Int32)」に変換している場合は、2GBを超えると符号付きのオーバーフローで負の値になるなど、さらに分かりづらい症状が出ることもあります。

方法メリットデメリット(今回の論点)向いている場面
WMI(Win32_VideoController)実装が簡単/他の属性も一括で取れるAdapterRAMが4GB超で正しく出ない場合があるざっくりした情報でよい、古いPC中心
レジストリ(Display Class配下)ドライバーが持つ詳細値を直接参照できるドライバー依存で値が欠けることがある/型の扱いが難しいVRAM容量をできるだけ正確に取りたい、複数GPUを列挙したい

GPU情報が並ぶレジストリの場所とサブキー構造

今回の回避策としてよく使われるのが、次のレジストリキーです。Display(ディスプレイ)クラスに紐づく情報がまとまっており、0000, 0001, 0002… のようなサブキーが、概ね「GPUアダプターごとの設定」を表します。

項目内容
ベースキーHKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}
よくある別名HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}(実運用ではこちらが無難)
サブキー例0000 / 0001 / 0002 …(先頭が0のものがアダプター候補になりやすい)
ポイントドライバーや環境により、実GPU以外(リモート表示、仮想アダプター)が混ざることがある

質問で挙がっている ControlSet001 を読めば目的の値が取れることは多いですが、PCが起動時に採用している制御セットが必ずしも001とは限りません。実装を堅牢にするなら、CurrentControlSet を優先し、見つからない場合にControlSet001へフォールバックする形にしておくと安全です。

VB.NETでサブキーを列挙してGPUごとに読み取る(基本形)

まずは「Display Class配下のサブキーを列挙し、0000/0001/0002…を順に開く」という骨格を押さえます。GetSubKeyNames() でサブキー名を取り、StartsWith(“0”) のものだけを対象にします。

Imports Microsoft.Win32
Imports System.Diagnostics

Dim basePath As String =
    "SYSTEM\ControlSet001\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}"

Using classKey As RegistryKey = Registry.LocalMachine.OpenSubKey(basePath, False)
    If classKey Is Nothing Then
        Debug.WriteLine("レジストリパスが見つかりませんでした。")
        Return
    End If

    For Each subKeyName As String In classKey.GetSubKeyNames()
        If Not subKeyName.StartsWith("0"c) Then
            Continue For
        End If

        Using subKey As RegistryKey = classKey.OpenSubKey(subKeyName, False)
            If subKey Is Nothing Then Continue For

            ' ここでアダプター情報を読み取る
            Dim driverDesc As Object = subKey.GetValue("DriverDesc", "N/A")
            Debug.WriteLine($"{subKeyName}: {driverDesc}")
        End Using
    Next
End Using

ここまでで「複数GPU(複数サブキー)をループして値を読む」部分は完成です。次は、取得する値の一覧と型(REG_BINARYなど)への対処を詰めます。

取得対象の値と、ドライバー依存の「型」を整理する

レジストリに入っている値は、すべてが文字列(REG_SZ)とは限りません。特に HardwareInformation.* 系は、見た目は文字列情報でも REG_BINARY(Byte配列) として保存されていることがよくあります。

値の名前典型的な型意味扱い方の要点
HardwareInformation.MemorySizeREG_BINARY / REG_QWORDビデオメモリ容量(バイト数)4バイトならUInt32、8バイトならUInt64として読む。単位換算はMiB/GiBがおすすめ。
HardwareInformation.ChipTypeREG_BINARYチップ情報UTF-16(Unicode)としてデコードし、末尾のヌル文字をTrimEndする。
HardwareInformation.DacTypeREG_BINARYDAC情報ChipTypeと同様にUnicodeデコード。
HardwareInformation.AdapterStringREG_BINARYアダプター名(ドライバーが返す名称)Unicodeデコード。DriverDescと近いが、より詳細になることがある。
HardwareInformation.BiosStringREG_BINARYBIOS文字列Unicodeデコード。環境によっては空の場合もある。
DriverDescREG_SZデバイスの説明(表示名)通常はそのままStringで取得できる。
DriverVersionREG_SZドライバーバージョン文字列として扱う。
DriverDateREG_SZドライバー日付文字列として扱い、必要ならDateTimeへ変換する(書式は環境で差が出る)。

REG_BINARYの扱い:value.ToString()だけでは「本来の文字列」にならない

VB.NETで RegistryKey.GetValue を呼ぶと、戻り値は Object です。値の種類がREG_SZならStringとして返ってきますが、REG_BINARYの場合は Byte() が返ってきます。

「文字列値なら value.ToString() だけでいい?」

この疑問は自然ですが、REG_BINARY(Byte配列)に対して ToString() を呼ぶと、多くの場合 System.Byte[] のような型名になるだけで、レジストリに入っている“本来の文字列”にはなりません。また、BitConverter.ToString(byteArray) はバイト列を16進に並べるだけなので、人間が読める文字列にはなりにくいです。

そこで、GPU関連のREG_BINARYは「Unicode文字列(UTF-16LE)+末尾にヌル終端」という形式が多いこと理解した上で、次のようにデコードします。

Dim bytes As Byte() = CType(value, Byte())
Dim s As String = Encoding.Unicode.GetString(bytes)
s = s.TrimEnd(ChrW(0))

この一手間で、チップ名やアダプター名などが期待通りの形で取り出せます。

実践:複数GPUをまとめて取得するVB.NETサンプル(堅牢版)

ここからは、実用寄りの形でサンプルをまとめます。ポイントは次の通りです。

  • CurrentControlSet を優先し、見つからない場合に ControlSet001 を読む
  • サブキーは 0000/0001… を対象にし、並び順を安定させるためにソートする
  • REG_BINARYは Unicodeデコード、VRAMサイズは UInt32/UInt64 に分岐して読む
  • 値が存在しない・型が違うケースに備え、Nothingチェックを入れる
Imports Microsoft.Win32
Imports System.Text
Imports System.Collections.Generic

Public NotInheritable Class GpuRegistryReader

    Private Sub New()
    End Sub

    Private Const DisplayClassGuid As String =
        "{4d36e968-e325-11ce-bfc1-08002be10318}"

    Public Shared Function ReadAll() As List(Of GpuAdapterInfo)
        Dim result As New List(Of GpuAdapterInfo)()

        Dim view As RegistryView =
            If(Environment.Is64BitOperatingSystem, RegistryView.Registry64, RegistryView.Registry32)

        Using baseKey As RegistryKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view)

            Dim candidatePaths As String() = {
                $"SYSTEM\CurrentControlSet\Control\Class\{DisplayClassGuid}",
                $"SYSTEM\ControlSet001\Control\Class\{DisplayClassGuid}"
            }

            Dim classKey As RegistryKey = Nothing
            Dim classPathUsed As String = ""

            For Each p As String In candidatePaths
                classKey = baseKey.OpenSubKey(p, False)
                If classKey IsNot Nothing Then
                    classPathUsed = p
                    Exit For
                End If
            Next

            If classKey Is Nothing Then
                Return result
            End If

            Using classKey
                Dim subNames As String() = classKey.GetSubKeyNames()
                Array.Sort(subNames, StringComparer.OrdinalIgnoreCase)

                Dim index As Integer = 0

                For Each subName As String In subNames
                    If Not subName.StartsWith("0"c) Then
                        Continue For
                    End If

                    Using subKey As RegistryKey = classKey.OpenSubKey(subName, False)
                        If subKey Is Nothing Then Continue For

                        Dim info As New GpuAdapterInfo()
                        info.Index = index
                        info.RegistrySubKey = $"{classPathUsed}\{subName}"

                        info.DriverDesc = ReadString(subKey, "DriverDesc")
                        info.DriverVersion = ReadString(subKey, "DriverVersion")
                        info.DriverDate = ReadString(subKey, "DriverDate")

                        info.AdapterString =
                            ReadUnicodeFromBinary(subKey.GetValue("HardwareInformation.AdapterString"))
                        info.ChipType =
                            ReadUnicodeFromBinary(subKey.GetValue("HardwareInformation.ChipType"))
                        info.DacType =
                            ReadUnicodeFromBinary(subKey.GetValue("HardwareInformation.DacType"))
                        info.BiosString =
                            ReadUnicodeFromBinary(subKey.GetValue("HardwareInformation.BiosString"))

                        info.MemoryBytes =
                            ReadMemoryBytes(subKey.GetValue("HardwareInformation.MemorySize"))

                        ' DriverDescが空のものは「実GPUではない」可能性があるので除外する例
                        If Not String.IsNullOrWhiteSpace(info.DriverDesc) Then
                            result.Add(info)
                            index += 1
                        End If
                    End Using
                Next
            End Using
        End Using

        Return result
    End Function

    Private Shared Function ReadString(key As RegistryKey, name As String) As String
        Dim v As Object = key.GetValue(name)
        If v Is Nothing Then Return ""
        Return Convert.ToString(v)
    End Function

    Private Shared Function ReadUnicodeFromBinary(value As Object) As String
        If value Is Nothing Then Return ""
        If TypeOf value Is String Then Return CType(value, String)

        If TypeOf value Is Byte() Then
            Dim bytes As Byte() = CType(value, Byte())
            Dim s As String = Encoding.Unicode.GetString(bytes)
            Return s.TrimEnd(ChrW(0))
        End If

        Return Convert.ToString(value)
    End Function

    Private Shared Function ReadMemoryBytes(value As Object) As Nullable(Of ULong)
        If value Is Nothing Then Return Nothing

        If TypeOf value Is Integer Then Return CULng(CInt(value))
        If TypeOf value Is Long Then Return CULng(CLng(value))
        If TypeOf value Is ULong Then Return CType(value, ULong)

        If TypeOf value Is Byte() Then
            Dim bytes As Byte() = CType(value, Byte())

            If bytes.Length >= 8 Then
                Return BitConverter.ToUInt64(bytes, 0)
            ElseIf bytes.Length >= 4 Then
                Return BitConverter.ToUInt32(bytes, 0)
            End If
        End If

        If TypeOf value Is String Then
            Dim s As String = CType(value, String).Trim()
            Dim n As ULong
            If ULong.TryParse(s, n) Then Return n
        End If

        Return Nothing
    End Function

End Class

Public Class GpuAdapterInfo
    Public Property Index As Integer
    Public Property RegistrySubKey As String
    Public Property DriverDesc As String
    Public Property AdapterString As String
    Public Property ChipType As String
    Public Property DacType As String
    Public Property BiosString As String
    Public Property DriverVersion As String
    Public Property DriverDate As String
    Public Property MemoryBytes As Nullable(Of ULong)

    Public ReadOnly Property MemoryMiB As Nullable(Of Double)
        Get
            If Not MemoryBytes.HasValue Then Return Nothing
            Return MemoryBytes.Value / 1024.0 / 1024.0
        End Get
    End Property

    Public ReadOnly Property MemoryGiB As Nullable(Of Double)
        Get
            If Not MemoryBytes.HasValue Then Return Nothing
            Return MemoryBytes.Value / 1024.0 / 1024.0 / 1024.0
        End Get
    End Property
End Class

上記は「取得に失敗しても落ちにくい」ように、型の分岐やNothingチェックを入れています。特にVRAM容量は、4バイト(UInt32)か8バイト(UInt64)かで読み方が変わる可能性があるため、バイト配列の長さで分岐させています。

取得結果を確認するための出力例(Console / Debug)

取得した List(Of GpuAdapterInfo) は、コンソールやログに整形して出しておくと、実機で「何が取れて、何が空なのか」を判断しやすくなります。

Dim gpus As List(Of GpuAdapterInfo) = GpuRegistryReader.ReadAll()

For Each g As GpuAdapterInfo In gpus
    Console.WriteLine($"[{g.Index}] {g.DriverDesc}")

    If g.MemoryGiB.HasValue Then
        Console.WriteLine($"  VRAM: {g.MemoryGiB.Value:F2} GiB ({g.MemoryBytes.Value:N0} bytes)")
    Else
        Console.WriteLine("  VRAM: N/A")
    End If

    If Not String.IsNullOrWhiteSpace(g.AdapterString) Then
        Console.WriteLine($"  AdapterString: {g.AdapterString}")
    End If

    If Not String.IsNullOrWhiteSpace(g.ChipType) Then
        Console.WriteLine($"  ChipType: {g.ChipType}")
    End If

    Console.WriteLine($"  Driver: {g.DriverVersion} ({g.DriverDate})")
    Console.WriteLine($"  Registry: {g.RegistrySubKey}")
    Console.WriteLine("")
Next

実行結果の見え方としては、Intel内蔵GPU + NVIDIA/AMDの外部GPU のような構成でも、サブキーを順番に舐めることで複数行に分かれて表示されます。もしRemote Desktop利用時に仮想アダプターが混じる場合は、DriverDescやChipTypeでフィルタリングする方針が取りやすいです。

「ControlSet001固定」で困るケースと、CurrentControlSetを使う理由

開発中の検証ではControlSet001が常に存在するように見えますが、Windowsは起動状況や復旧などの都合で、採用されるControlSetが変わることがあります。アプリが参照するべき“現在の設定”は、一般的には CurrentControlSet と考えて差し支えありません。

ただし、質問の文脈ではControlSet001を読んで目的達成できることも多いので、「CurrentControlSetを優先しつつ、ControlSet001へフォールバック」という実装が、再現性と実運用のバランスを取りやすいです。

64bit/32bitアプリとレジストリ参照の注意点

GPU情報を取る処理をサービスや常駐アプリに組み込むときに、意外とハマるのが「どのビット数で実行されているか」です。基本的にSYSTEM配下はSoftwareほど強烈なリダイレクトが起きにくいものの、環境差をゼロにするなら、RegistryKey.OpenBaseKey と RegistryView を使って参照ビューを明示しておくのが無難です。

  • 64bit OS + 32bitアプリ:既定ビューが32bit側になる可能性がある
  • 64bit OS + 64bitアプリ:通常は64bit側を参照
  • AnyCPU:実行環境で変わるため、意図したビューを指定した方が事故が減る

サンプルでは Environment.Is64BitOperatingSystem を見て、OSが64bitならRegistry64、そうでなければRegistry32を選んでいます。さらに厳密にしたい場合は「アプリを常に64bitで動かす(Prefer 32-bitを外す)」も検討すると、検証コストが下がります。

VRAM値が取れない・0になる場合のチェックポイント

レジストリにある値は、GPUドライバーが書き込んでいる情報です。したがって、機種やドライバーの種類によっては HardwareInformation.MemorySize が存在しない、または0になることがあります。そんなときは次の順で切り分けると原因が追いやすいです。

  1. 該当サブキーが本当に実GPUか(DriverDescやAdapterStringが空/仮想っぽい名称ではないか)
  2. 値の型が想定と違わないか(REG_QWORDなのにByte()として処理していないか)
  3. 統合GPUの特性(UMAで専用VRAMが小さく、共有メモリ主体のため0や小さな値に見えることがある)
  4. 確認先を揃える(タスクマネージャーのGPU欄/dxdiag/GPU-Zなどで「Dedicated」と「Shared」を区別して確認する)

特にノートPCの内蔵GPUは「専用VRAM」と「共有システムメモリ」が混在します。レジストリのMemorySizeが返すのは、必ずしもタスクマネージャーの“合計”と一致しない場合があるので、表示したい指標(専用のみ/合計)を先に決めておくと設計がブレません。

UIに載せるなら:DataGridViewに出すための最小例

取得したリストをそのまま表示するだけでも便利ですが、Windows Formsなどで一覧表示したい場合は、DataTableを作ってバインドすると作業が楽です(必要に応じて Imports System.Data を追加してください)。

Dim gpus As List(Of GpuAdapterInfo) = GpuRegistryReader.ReadAll()

Dim dt As New DataTable()
dt.Columns.Add("Name")
dt.Columns.Add("VRAM(GiB)")
dt.Columns.Add("DriverVersion")
dt.Columns.Add("DriverDate")

For Each g As GpuAdapterInfo In gpus
    Dim vram As String = ""
    If g.MemoryGiB.HasValue Then
        vram = g.MemoryGiB.Value.ToString("F2")
    Else
        vram = "N/A"
    End If

    dt.Rows.Add(g.DriverDesc, vram, g.DriverVersion, g.DriverDate)
Next

DataGridView1.DataSource = dt

「ユーザーに見せる」用途なら、VRAMはバイトではなくGiB表記にし、ドライバーはバージョンと日付を並べるだけでも、現場のトラブルシュートがかなり楽になります。

まとめ:WMIの制限を回避しつつ、VB.NETで複数GPUを安全に列挙する

WMIのAdapterRAMが4GB以上で正しく取れない問題は、VRAMが大容量化した現在では遭遇しやすい落とし穴です。Display Class配下のレジストリを列挙する方法なら、ドライバーが保持している値を参照でき、複数GPUもループで拾えます。

実装では、サブキー列挙とREG_BINARYのデコード、そしてVRAMサイズのUInt32/UInt64分岐が要点です。紹介したサンプルをベースに、取得できない値がある環境でも落ちないようにチェックを追加し、用途に合わせてフィルタリングや整形出力を行えば、実務でも使えるGPU情報取得ロジックになります。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次