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.MemorySize | REG_BINARY / REG_QWORD | ビデオメモリ容量(バイト数) | 4バイトならUInt32、8バイトならUInt64として読む。単位換算はMiB/GiBがおすすめ。 |
| HardwareInformation.ChipType | REG_BINARY | チップ情報 | UTF-16(Unicode)としてデコードし、末尾のヌル文字をTrimEndする。 |
| HardwareInformation.DacType | REG_BINARY | DAC情報 | ChipTypeと同様にUnicodeデコード。 |
| HardwareInformation.AdapterString | REG_BINARY | アダプター名(ドライバーが返す名称) | Unicodeデコード。DriverDescと近いが、より詳細になることがある。 |
| HardwareInformation.BiosString | REG_BINARY | BIOS文字列 | Unicodeデコード。環境によっては空の場合もある。 |
| DriverDesc | REG_SZ | デバイスの説明(表示名) | 通常はそのままStringで取得できる。 |
| DriverVersion | REG_SZ | ドライバーバージョン | 文字列として扱う。 |
| DriverDate | REG_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になることがあります。そんなときは次の順で切り分けると原因が追いやすいです。
- 該当サブキーが本当に実GPUか(DriverDescやAdapterStringが空/仮想っぽい名称ではないか)
- 値の型が想定と違わないか(REG_QWORDなのにByte()として処理していないか)
- 統合GPUの特性(UMAで専用VRAMが小さく、共有メモリ主体のため0や小さな値に見えることがある)
- 確認先を揃える(タスクマネージャーの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情報取得ロジックになります。

コメント