├── .gitignore
├── LICENSE
├── README.md
├── bin
└── .gitignore
├── build.bat
├── build.ps1
├── cloud
└── aws
│ ├── README.md
│ ├── cluster
│ ├── .gitignore
│ └── test-cluster.template
│ ├── deploy.bat
│ ├── deploy.ps1
│ └── node
│ ├── .gitignore
│ ├── eks-worker-node.pkr.hcl.template
│ ├── generate-setup-script.py
│ └── scripts
│ ├── cleanup.ps1
│ ├── setup.ps1
│ └── startup.ps1
├── deployments
├── default-daemonsets.yml
├── multitenancy-configmap.yml
└── multitenancy-inline.yml
├── examples
├── cuda-devicequery
│ ├── cuda-devicequery-mcdm.yml
│ └── cuda-devicequery-wddm.yml
├── cuda-montecarlo
│ ├── cuda-montecarlo-mcdm.yml
│ └── cuda-montecarlo-wddm.yml
├── device-discovery
│ ├── device-discovery-mcdm.yml
│ └── device-discovery-wddm.yml
├── directml
│ ├── directml-mcdm.yml
│ └── directml-wddm.yml
├── ffmpeg-amf
│ └── ffmpeg-amf.yml
├── ffmpeg-autodetect
│ ├── autodetect-encoder.ps1
│ └── ffmpeg-autodetect.yml
├── ffmpeg-nvenc
│ └── ffmpeg-nvenc.yml
├── ffmpeg-quicksync
│ └── ffmpeg-quicksync.yml
├── nvidia-smi
│ ├── nvidia-smi-mcdm.yml
│ └── nvidia-smi-wddm.yml
├── opencl-enum
│ ├── opencl-enum-mcdm.yml
│ └── opencl-enum-wddm.yml
└── vulkaninfo
│ └── vulkaninfo.yml
├── external
└── .gitignore
├── library
├── CMakeLists.txt
├── include
│ ├── DeviceDiscovery.h
│ └── DeviceFilter.h
├── src
│ ├── Adapter.h
│ ├── AdapterEnumeration.cpp
│ ├── AdapterEnumeration.h
│ ├── D3DHelpers.cpp
│ ├── D3DHelpers.h
│ ├── Device.h
│ ├── DeviceDiscovery.cpp
│ ├── DeviceDiscoveryImp.cpp
│ ├── DeviceDiscoveryImp.h
│ ├── DllMain.cpp
│ ├── ErrorHandling.cpp
│ ├── ErrorHandling.h
│ ├── ObjectHelpers.h
│ ├── RegistryQuery.cpp
│ ├── RegistryQuery.h
│ ├── SafeArray.cpp
│ ├── SafeArray.h
│ ├── WmiQuery.cpp
│ ├── WmiQuery.h
│ └── pch.h
├── test
│ └── test-device-discovery-cpp.cpp
└── vcpkg.json
├── plugins
├── cmd
│ ├── device-plugin-mcdm
│ │ └── main.go
│ ├── device-plugin-wddm
│ │ └── main.go
│ ├── gen-device-mounts
│ │ └── main.go
│ ├── query-hcs-capabilities
│ │ └── main.go
│ └── test-device-discovery-go
│ │ └── main.go
├── go.mod
├── go.sum
└── internal
│ ├── discovery
│ ├── device.go
│ ├── device_discovery.go
│ ├── device_filter.go
│ └── runtime_file.go
│ ├── mount
│ ├── default_mounts.go
│ ├── device_mounts.go
│ └── vendors.go
│ └── plugin
│ ├── common_main.go
│ ├── deletion_watcher.go
│ ├── device_plugin.go
│ ├── device_watcher.go
│ └── plugin_configuration.go
├── update-version.bat
└── update-version.ps1
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | build
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022-2023 TensorWorks Pty Ltd
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/bin/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/build.bat:
--------------------------------------------------------------------------------
1 | @powershell -ExecutionPolicy Bypass -File "%~dp0.\build.ps1" %*
2 |
--------------------------------------------------------------------------------
/cloud/aws/README.md:
--------------------------------------------------------------------------------
1 | # Amazon EKS demo deployment
2 |
3 | This directory contains scripts that can be used to deploy the Kubernetes device plugins for DirectX to an [Amazon EKS](https://aws.amazon.com/eks/) Kubernetes cluster for demonstration purposes. Note that the deployment created by these scripts **is not intended for production use**, and lacks important functionality such as auto-scaling the Windows node group based on requests for DirectX devices.
4 |
5 | The [main deployment script](./deploy.ps1) performs the following steps:
6 |
7 | - Builds a custom [Amazon Machine Image (AMI)](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html) based on Windows Server 2022 for use by Kubernetes worker nodes, with the NVIDIA GPU drivers and containerd v1.7.0 installed. The supporting scripts for building the AMI are located in the [node](./node) subdirectory.
8 |
9 | - Creates a EKS cluster with a Windows node group of `g4dn.xlarge` instances that is configured to use the custom AMI. The supporting configuration files for creating the cluster are located in the [cluster](./cluster) subdirectory.
10 |
11 | - Deploys the Kubernetes device plugins for DirectX to the EKS cluster using the [default HostProcess DaemonSets for the MCDM device plugin and the WDDM device plugin](../../deployments/default-daemonsets.yml).
12 |
13 |
14 | ## Contents
15 |
16 | - [Requirements](#requirements)
17 | - [Running the deployment script](#running-the-deployment-script)
18 | - [Testing the cluster](#testing-the-cluster)
19 | - [Cleaning up](#cleaning-up)
20 |
21 |
22 | ## Requirements
23 |
24 | To use the deployment scripts, the following requirements must be met:
25 |
26 | - The AWS region that you are using needs to have sufficient quota to run at least one `g4dn.xlarge` EC2 instance. To view or change the relevant limit, login to the AWS web console and navigate to the [*Running On-Demand G and VT instances*](https://console.aws.amazon.com/servicequotas/home/services/ec2/quotas/L-DB2E81BA) service quota page. The minimum required value is 4 vCPUs.
27 |
28 | - The AWS region that you are using needs to have a default VPC configured with at least one subnet. If you have deleted the default VPC for the target region then you will need to [create a new one](https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html#create-default-vpc).
29 |
30 | - [Microsoft PowerShell](https://github.com/PowerShell/PowerShell) needs to be installed when running the deployment scripts under Linux or macOS systems. (Under Windows, the built-in Windows PowerShell is used instead.)
31 |
32 | - The [AWS CLI](https://docs.aws.amazon.com/cli/) needs to be installed and configured with credentials that permit the creation of AMIs and EKS clusters. For details, see [*Configuring the AWS CLI*](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html).
33 |
34 | - [eksctl](https://eksctl.io/) version [0.111.0](https://github.com/weaveworks/eksctl/blob/v0.111.0/docs/release_notes/0.110.0.md) or newer needs to be installed (older versions will refuse to create Windows node groups with GPUs).
35 |
36 | - [HashiCorp Packer](https://www.packer.io/) needs to be installed.
37 |
38 | - [kubectl](https://kubernetes.io/docs/reference/kubectl/) needs to be installed.
39 |
40 |
41 | ## Running the deployment script
42 |
43 | Under Windows, run the main deployment script using the following command:
44 |
45 | ```
46 | deploy.bat
47 | ```
48 |
49 | Under Linux and macOS, use this command instead:
50 |
51 | ```bash
52 | pwsh deploy.ps1
53 | ```
54 |
55 | The following optional flags can be used to control the deployment options:
56 |
57 | - `-Region`: specifies the AWS region into which resources will be deployed. The default region is `us-east-1`.
58 |
59 | - `-AmiName`: specifies the name to use for the custom worker node AMI. The default name is `eks-worker-node`.
60 |
61 | - `-ClusterName`: specifies the name to use for the EKS cluster. The default name is `demo-cluster`.
62 |
63 | An example usage of these flags is shown below:
64 |
65 | ```bash
66 | # Deploys to the Sydney (ap-southeast-2) AWS region and uses custom names for both the AMI and the EKS cluster
67 | pwsh deploy.ps1 -Region "ap-southeast-2" -AmiName "my-custom-ami" -ClusterName "my-test-cluster"
68 | ```
69 |
70 |
71 | ## Testing the cluster
72 |
73 | Once the EKS cluster has been created, eksctl will configure kubectl to communicate with that cluster by default. This means you can start using kubectl to deploy examples from the top-level [examples](../../examples) directory without the need for any additional configuration steps:
74 |
75 | 1. The first example you should deploy is the [**device-discovery**](../../examples/device-discovery/) test, which acts as a sanity check to verify that GPUs are being exposed to containers correctly:
76 |
77 | ```bash
78 | kubectl apply -f '../../examples/device-discovery/device-discovery-wddm.yml'
79 | ```
80 |
81 | Once the Job has been created, wait for the Pod to be assigned to a Windows worker node and then run to completion. If the Job finishes with a status of "Succeeded" then you should check the Pod logs to verify that the NVIDIA Tesla T4 GPU is listed in the output. If the Job finishes with a status of "Failed" or if the log output lists zero devices then something has gone wrong. A failure here could indicate an issue with the Kubernetes device plugins for DirectX themselves, or with some aspect of the EKS cluster configuration.
82 |
83 | 2. Since the EKS worker nodes are using NVIDIA GPUs, the second example you should deploy is the [**nvidia-smi**](../../examples/nvidia-smi/) test, which acts as a sanity check to verify that the NVIDIA GPU drivers are able to communicate with the GPU:
84 |
85 | ```bash
86 | kubectl apply -f '../../examples/nvidia-smi/nvidia-smi-wddm.yml'
87 | ```
88 |
89 | If the Job finishes with a status of "Succeeded" then `nvidia-smi` was able to communicate with the GPU, and you can check the Pod logs to verify that the output is as expected. If the Job finishes with a status of "Failed" then this indicates either an issue with the Kubernetes device plugins for DirectX themselves or with the NVIDIA GPU drivers on the worker node.
90 |
91 | 3. With these basic sanity checks out of the way, you can then try out any of the other examples that work on NVIDIA GPUs. Since the NVIDIA Tesla T4 GPUs provided by `g4dn.xlarge` EC2 instances support both compute and display, you will need to deploy examples that request a `directx.microsoft.com/display` resource. Note that some examples include YAML files for both compute-only (MCDM) and compute+display (WDDM) requests, so be sure to use the version with a filename suffix of `-wddm.yml`.
92 |
93 | Note that if you attempt to run two tests at once, one of them will wait for the other to complete before it can be scheduled. This is because the Windows node group will not automatically scale up in response to requests for DirectX devices, so only one node (and thus one GPU) will be available to allocate to containers at any given time.
94 |
95 |
96 | ## Cleaning up
97 |
98 | To delete all previously deployed AWS resources, run the main deployment script with the `-Clean` flag. Under Windows:
99 |
100 | ```
101 | deploy.bat -Clean
102 | ```
103 |
104 | Under Linux and macOS:
105 |
106 | ```bash
107 | pwsh deploy.ps1 -Clean
108 | ```
109 |
110 | If you specified flags for the AWS region or custom resource names when deploying the resources then be sure to include these flags when deleting them as well. For example:
111 |
112 | ```bash
113 | # Deletes the AWS resources that were deployed in the example from the earlier section
114 | pwsh deploy.ps1 -Region "ap-southeast-2" -AmiName "my-custom-ami" -ClusterName "my-test-cluster" -Clean
115 | ```
116 |
--------------------------------------------------------------------------------
/cloud/aws/cluster/.gitignore:
--------------------------------------------------------------------------------
1 | test-cluster.yml
2 |
--------------------------------------------------------------------------------
/cloud/aws/cluster/test-cluster.template:
--------------------------------------------------------------------------------
1 | apiVersion: eksctl.io/v1alpha5
2 | kind: ClusterConfig
3 |
4 | metadata:
5 | name: "__CLUSTER_NAME__"
6 | region: "__AWS_REGION__"
7 | version: "1.24"
8 |
9 | nodeGroups:
10 | - name: windows
11 | ami: "__AMI_ID__"
12 | amiFamily: WindowsServer2022FullContainer
13 | preBootstrapCommands: ["net user Administrator \"Passw0rd!\""]
14 | instanceType: g4dn.xlarge
15 | containerRuntime: containerd
16 | volumeSize: 100
17 | minSize: 1
18 | maxSize: 3
19 |
20 | managedNodeGroups:
21 | - name: linux
22 | instanceType: t2.large
23 | minSize: 2
24 | maxSize: 3
25 |
--------------------------------------------------------------------------------
/cloud/aws/deploy.bat:
--------------------------------------------------------------------------------
1 | @powershell -ExecutionPolicy Bypass -File "%~dp0.\deploy.ps1" %*
2 |
--------------------------------------------------------------------------------
/cloud/aws/deploy.ps1:
--------------------------------------------------------------------------------
1 | Param (
2 | [parameter(HelpMessage = "Remove existing resources created by a previous run")]
3 | [switch] $Clean,
4 |
5 | [parameter(HelpMessage = "The AWS region in which to deploy resources")]
6 | $Region = 'us-east-1',
7 |
8 | [parameter(HelpMessage = "The name to use for the custom worker node AMI")]
9 | $AmiName = 'eks-worker-node',
10 |
11 | [parameter(HelpMessage = "The name to use for the EKS cluster")]
12 | $ClusterName = 'demo-cluster'
13 | )
14 |
15 |
16 | # Halt execution if we encounter an error
17 | $ErrorActionPreference = 'Stop'
18 |
19 |
20 | # Replaces the placeholders in a template file with values and writes the output to a new file
21 | function FillTemplate
22 | {
23 | Param (
24 | $Template,
25 | $Rendered,
26 | $Values
27 | )
28 |
29 | $filled = Get-Content -Path $Template -Raw
30 | $Values.GetEnumerator() | ForEach-Object {
31 | $filled = $filled.Replace($_.Key, $_.Value)
32 | }
33 | Set-Content -Path $Rendered -Value $filled -NoNewline
34 | }
35 |
36 | # Represents the output of a native process
37 | class ProcessOutput
38 | {
39 | ProcessOutput([string] $stdout, [string] $stderr)
40 | {
41 | $this.StandardOutput = $stdout
42 | $this.StandardError = $stderr
43 | }
44 |
45 | [string] $StandardOutput
46 | [string] $StandardError
47 | }
48 |
49 | # Helper functions for executing native commands
50 | class ExecutionHelpers
51 | {
52 | # Escapes command-line arguments for passing to a native command
53 | static [string] EscapeArguments([string[]] $arguments)
54 | {
55 | $escaped = @()
56 |
57 | foreach ($arg in $arguments)
58 | {
59 | if ($arg.Contains(' ')) {
60 | $escaped += @("`"$arg`"")
61 | }
62 | else {
63 | $escaped += @($arg)
64 | }
65 | }
66 |
67 | return $escaped -join ' '
68 | }
69 |
70 | # Executes a command and throws an error if it returns a non-zero exit code
71 | static [ProcessOutput] RunCommand([string] $command, [string[]] $arguments, [bool] $captureStdOut, [bool] $captureStdErr)
72 | {
73 | # Log the command
74 | $escapedArgs = [ExecutionHelpers]::EscapeArguments($arguments)
75 | $formatted = "[$command $escapedArgs]"
76 | Write-Host "$formatted" -ForegroundColor DarkYellow
77 |
78 | # Execute the command and wait for it to complete, retrieving the exit code, stdout and stderr
79 | $info = New-Object System.Diagnostics.ProcessStartInfo
80 | $info.FileName = $command
81 | $info.Arguments = $escapedArgs
82 | $info.RedirectStandardError = $captureStdErr
83 | $info.RedirectStandardOutput = $captureStdOut
84 | $info.UseShellExecute = $false
85 | $info.WorkingDirectory = (Get-Location).ToString()
86 | $process = New-Object System.Diagnostics.Process
87 | $process.StartInfo = $info
88 | $process.Start()
89 | $process.WaitForExit()
90 | $exitCode = $process.ExitCode
91 | $stdout = if ($captureStdOut) { $process.StandardOutput.ReadToEnd() } else { '' }
92 | $stderr = if ($captureStdErr) { $process.StandardError.ReadToEnd() } else { '' }
93 |
94 | # If the command terminated with a non-zero exit code then throw an error
95 | if ($exitCode -ne 0) {
96 | throw "Command $formatted terminated with exit code $exitCode, stdout $stdout and stderr $stderr"
97 | }
98 |
99 | # Return the output
100 | return [ProcessOutput]::new($stdout, $stderr)
101 | }
102 |
103 | # Do not capture stdout and stderr of child processes unless the caller explicitly requests it
104 | static [void] RunCommand([string] $command, [string[]] $arguments) {
105 | [ExecutionHelpers]::RunCommand($command, $arguments, $false, $false)
106 | }
107 |
108 | # Tests whether the specified command exists, by attempting to execute it with the supplied arguments
109 | static [bool] CommandExists([string] $command, [string[]] $testArguments)
110 | {
111 | try
112 | {
113 | [ExecutionHelpers]::RunCommand($command, $testArguments, $true, $true)
114 | return $true
115 | }
116 | catch {
117 | return $false
118 | }
119 | }
120 | }
121 |
122 | # Represents the Packer manifest data for our EKS worker node AMI
123 | class PackerManifest
124 | {
125 | PackerManifest([string] $path) {
126 | $this.ManifestPath = $path
127 | }
128 |
129 | [bool] Exists() {
130 | return (Test-Path -Path $this.ManifestPath)
131 | }
132 |
133 | [void] Parse()
134 | {
135 | # Parse the Packer manifest JSON and validate the AMI details
136 | $manifestDetails = Get-Content -Path $this.ManifestPath -Raw | ConvertFrom-Json
137 | $amiDetails = ($manifestDetails.builds[0].artifact_id -split ':')
138 | if ($amiDetails.Length -lt 2) {
139 | throw "Malformed 'artifact_id' field in Packer build manifest: '$amiDetails'"
140 | }
141 |
142 | # Extract the region and AMI ID
143 | $this.AmiRegion = $amiDetails[0]
144 | $this.AmiID = $amiDetails[1]
145 |
146 | # If the manifest data doesn't contain the snapshot ID for the AMI then populate it
147 | $this.SnapshotID = $manifestDetails.builds[0].custom_data.snapshot_id
148 | if ($this.SnapshotID.Length -lt 1)
149 | {
150 | # Attempt to retrieve the snapshot ID from the AWS API
151 | Write-Host 'Retrieving the snapshot ID for the AMI...' -ForegroundColor Green
152 | $queryOutput = [ExecutionHelpers]::RunCommand('aws', @('ec2', 'describe-images', "--region=$($this.AmiRegion)", "--image-ids=$($this.AmiID)"), $true, $true)
153 | $snapshotDetails = $queryOutput.StandardOutput | ConvertFrom-Json
154 | $this.SnapshotID = $snapshotDetails.Images[0].BlockDeviceMappings[0].Ebs.SnapshotId
155 | if ($amiDetails.Length -lt 1) {
156 | throw "Failed to retrieve snapshot ID for AMI: '$this.AmiID'"
157 | }
158 |
159 | # Inject the snapshot ID into the manifest data
160 | $manifestDetails.builds[0].custom_data.snapshot_id = $this.SnapshotID
161 |
162 | # Write the updated manifest data back to the JSON file
163 | $manifestJson = ConvertTo-Json $manifestDetails -Depth 32
164 | Set-Content -Path $this.ManifestPath -Value $manifestJson -NoNewline
165 | }
166 | }
167 |
168 | [void] Delete()
169 | {
170 | # De-register the AMI
171 | [ExecutionHelpers]::RunCommand('aws', @('ec2', 'deregister-image', "--region=$($this.AmiRegion)", "--image-id=$($this.AmiID)"))
172 |
173 | # Remove the snapshot
174 | [ExecutionHelpers]::RunCommand('aws', @('ec2', 'delete-snapshot', "--region=$($this.AmiRegion)", "--snapshot-id=$($this.SnapshotID)"))
175 |
176 | # Delete the manifest JSON file
177 | Remove-Item -Force $this.ManifestPath
178 | }
179 |
180 | [string] $ManifestPath
181 | [string] $AmiID
182 | [string] $AmiRegion
183 | [string] $SnapshotID
184 | }
185 |
186 | # Represents an EKS cluster managed by eksctl
187 | class EksCluster
188 | {
189 | EksCluster([string] $name) {
190 | $this.Name = $name
191 | }
192 |
193 | [bool] Exists()
194 | {
195 | try
196 | {
197 | [ExecutionHelpers]::RunCommand('eksctl', @('get', 'cluster', "--name=$($this.Name)", "--region=$($global:Region)"), $true, $true)
198 | return $true
199 | }
200 | catch {
201 | return $false
202 | }
203 | }
204 |
205 | [void] Create([string] $yamlFile) {
206 | [ExecutionHelpers]::RunCommand('eksctl', @('create', 'cluster', '-f', $yamlFile.Replace('\', '/')))
207 | }
208 |
209 | [void] Delete() {
210 | [ExecutionHelpers]::RunCommand('eksctl', @('delete', 'cluster', "--name=$($this.Name)", "--region=$($global:Region)"))
211 | }
212 |
213 | [string] $Name
214 | }
215 |
216 |
217 | # Verify that all of the native commands we require are available
218 | $requiredCommands = @{
219 | 'the AWS CLI' = [ExecutionHelpers]::CommandExists('aws', @('help'));
220 | 'eksctl' = [ExecutionHelpers]::CommandExists('eksctl', @('version'));
221 | 'kubectl' = [ExecutionHelpers]::CommandExists('kubectl', @('help'));
222 | 'HashiCorp Packer' = [ExecutionHelpers]::CommandExists('packer', @('version'))
223 | }
224 | foreach ($command in $requiredCommands.GetEnumerator())
225 | {
226 | if ($command.Value -eq $false) {
227 | throw "Error: $($command.Name) must be installed to run this script!"
228 | }
229 | }
230 |
231 | # Resolve the path to the Packer manifest file and create a helper object to represent the manifest data
232 | $packerDir = "$PSScriptRoot\node"
233 | $packerManifest = [PackerManifest]::new("$packerDir\manifest.json")
234 |
235 | # Create a helper object to represent our test EKS cluster
236 | $eksCluster = [EksCluster]::new($global:ClusterName)
237 |
238 | # Determine whether we are removing existing resources created by a previous run
239 | if ($Clean)
240 | {
241 | # Remove the EKS cluster if it exists
242 | if ($eksCluster.Exists())
243 | {
244 | Write-Host 'Removing existing EKS cluster...' -ForegroundColor Green
245 | $eksCluster.Delete()
246 | }
247 |
248 | # Delete the AMI and its accompanying snapshot if they exist
249 | if ($packerManifest.Exists())
250 | {
251 | Write-Host 'Removing AMI and its accompanying snapshot...' -ForegroundColor Green
252 | $packerManifest.Parse()
253 | $packerManifest.Delete()
254 | }
255 |
256 | Exit
257 | }
258 |
259 | # Build the custom worker node AMI if it doesn't already exist
260 | if ($packerManifest.Exists() -eq $false)
261 | {
262 | # Populate the Packer template
263 | $packerfile = "$packerDir\eks-worker-node.pkr.hcl"
264 | FillTemplate `
265 | -Template "$packerDir\eks-worker-node.pkr.hcl.template" `
266 | -Rendered $packerfile `
267 | -Values @{
268 | '__AWS_REGION__' = $global:Region;
269 | '__AMI_NAME__' = $global:AmiName
270 | }
271 |
272 | # Build the AMI
273 | Write-Host 'Building the EKS custom worker node AMI...' -ForegroundColor Green
274 | Push-Location "$packerDir"
275 | [ExecutionHelpers]::RunCommand('packer', @('init', 'eks-worker-node.pkr.hcl'))
276 | [ExecutionHelpers]::RunCommand('packer', @('build', 'eks-worker-node.pkr.hcl'))
277 | Pop-Location
278 | }
279 |
280 | # Parse the Packer manifest JSON and validate the AMI details
281 | $packerManifest.Parse()
282 |
283 | # Populate the cluster template YAML with the values for the AMI
284 | $clusterDir = "$PSScriptRoot\cluster"
285 | $configFile = "$clusterDir\test-cluster.yml"
286 | FillTemplate `
287 | -Template "$clusterDir\test-cluster.template" `
288 | -Rendered $configFile `
289 | -Values @{
290 | '__CLUSTER_NAME__' = $global:ClusterName;
291 | '__AWS_REGION__' = $packerManifest.AmiRegion;
292 | '__AMI_ID__' = $packerManifest.AmiID
293 | }
294 |
295 | # Deploy the test EKS cluster if it doesn't already exist
296 | if ($eksCluster.Exists() -eq $false)
297 | {
298 | Write-Host 'Deploying a test EKS cluster with a Windows worker node group using the custom AMI...' -ForegroundColor Green
299 | $eksCluster.Create($configFile)
300 | }
301 |
302 | # Deploy the device plugin DaemonSets to the test cluster
303 | Write-Host 'Deploying the DirectX device plugin DaemonSets to the test EKS cluster...' -ForegroundColor Green
304 | $deploymentsYaml = "$PSScriptRoot\..\..\deployments\default-daemonsets.yml"
305 | [ExecutionHelpers]::RunCommand('kubectl', @('apply', '-f', $deploymentsYaml.Replace('\', '/')))
306 |
--------------------------------------------------------------------------------
/cloud/aws/node/.gitignore:
--------------------------------------------------------------------------------
1 | eks-worker-node.pkr.hcl
2 | manifest.json
3 |
--------------------------------------------------------------------------------
/cloud/aws/node/eks-worker-node.pkr.hcl.template:
--------------------------------------------------------------------------------
1 | packer {
2 | required_plugins {
3 | amazon = {
4 | version = ">= 1.0.9"
5 | source = "github.com/hashicorp/amazon"
6 | }
7 | }
8 | }
9 |
10 | source "amazon-ebs" "eks-worker-node" {
11 | ami_name = "__AMI_NAME__"
12 | instance_type = "g4dn.xlarge"
13 | region = "__AWS_REGION__"
14 |
15 | # Use the latest version of the official Windows Server 2022 base image
16 | source_ami_filter {
17 | filters = {
18 | name = "Windows_Server-2022-English-Full-Base-*"
19 | root-device-type = "ebs"
20 | virtualization-type = "hvm"
21 | }
22 |
23 | most_recent = true
24 | owners = ["amazon"]
25 | }
26 |
27 | # Expand the boot disk to 100GB
28 | launch_block_device_mappings {
29 | device_name = "/dev/sda1"
30 | volume_size = 100
31 | volume_type = "gp3"
32 | delete_on_termination = true
33 | }
34 |
35 | # Allow S3 access for the VM
36 | temporary_iam_instance_profile_policy_document {
37 | Version = "2012-10-17"
38 | Statement {
39 | Action = ["s3:Get*", "s3:List*"]
40 | Effect = "Allow"
41 | Resource = ["*"]
42 | }
43 | }
44 |
45 | # Use our startup script to enable SSH access
46 | user_data_file = "${path.root}/scripts/startup.ps1"
47 |
48 | # Use SSH for running commands in the VM
49 | communicator = "ssh"
50 | ssh_username = "Administrator"
51 | ssh_timeout = "30m"
52 |
53 | # Don't automatically stop the instance, since sysprep will perform the shutdown
54 | disable_stop_instance = true
55 | }
56 |
57 | build {
58 | name = "eks-worker-node"
59 | sources = ["source.amazon-ebs.eks-worker-node"]
60 |
61 | # Run our EKS worker node setup script
62 | provisioner "powershell" {
63 | script = "${path.root}/scripts/setup.ps1"
64 | }
65 |
66 | # Perform cleanup and shut down the VM
67 | provisioner "powershell" {
68 | script = "${path.root}/scripts/cleanup.ps1"
69 | valid_exit_codes = [0, 2300218]
70 | }
71 |
72 | # Store the AMI ID in a manifest file when the build completes
73 | post-processor "manifest" {
74 | output = "manifest.json"
75 | custom_data = {
76 | snapshot_id = ""
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/cloud/aws/node/generate-setup-script.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | #
3 | # This script automates the generation of `setup.ps1` in the `scripts` subdirectory
4 | #
5 | import json, re, subprocess, yaml, sys
6 | from pathlib import Path
7 |
8 |
9 | class Utility:
10 |
11 | @staticmethod
12 | def log(message):
13 | """
14 | Logs a message to stderr
15 | """
16 | print('[generate-setup-script.py]: {}'.format(message), flush=True, file=sys.stderr)
17 |
18 | @staticmethod
19 | def capture(command, **kwargs):
20 | """
21 | Executes the specified command and captures its output
22 | """
23 |
24 | # Log the command being executed
25 | Utility.log(command)
26 |
27 | # Attempt to execute the specified command
28 | result = subprocess.run(
29 | command,
30 | check = True,
31 | capture_output = True,
32 | universal_newlines = True,
33 | **kwargs
34 | )
35 |
36 | # Return the contents of stdout
37 | return result.stdout.strip()
38 |
39 | @staticmethod
40 | def writeFile(filename, data):
41 | """
42 | Writes data to the specified file
43 | """
44 | return Path(filename).write_bytes(data.encode('utf-8'))
45 |
46 | @staticmethod
47 | def commentForStep(name):
48 | """
49 | Returns a descriptive comment for the build step with the specified name
50 | """
51 | return {
52 |
53 | 'ConfigureDirectories': '# Create each of our directories',
54 | 'DownloadKubernetes': '# Download the Kubernetes components',
55 | 'DownloadEKSArtifacts': '# Download the EKS artifacts archive',
56 | 'ExtractEKSArtifacts': '# Extract the EKS artifacts archive',
57 | 'MoveEKSArtifacts': '# Move the EKS files into place',
58 | 'ExecuteBuildScripts': '# Perform EKS worker node setup',
59 | 'RemoveEKSArtifactDownloadDirectory': '# Perform cleanup',
60 |
61 | 'InstallContainers': '\n'.join([
62 | '# Install the Windows Containers feature',
63 | '# (Note: this is actually a no-op here, since we install the feature beforehand in startup.ps1)'
64 | ])
65 |
66 | }.get(name, None)
67 |
68 | @staticmethod
69 | def parseConstants(constants):
70 | """
71 | Parses an EC2 ImageBuilder component's constants list
72 | """
73 | parsed = {}
74 | for entry in constants:
75 | for key, values in entry.items():
76 | parsed[key] = values['value']
77 | return parsed
78 |
79 | @staticmethod
80 | def replaceConstants(string, constants):
81 | """
82 | Converts EC2 ImageBuilder constant references to PowerShell variable references
83 | """
84 |
85 | # If the value of a constant is used as a magic value rather than a reference,
86 | # replace it with a reference to the variable representing the constant instead
87 | transformed = string
88 | for key, value in constants.items():
89 | transformed = transformed.replace(value, '${}'.format(key))
90 |
91 | # Convert `{{ variable }}` syntax to PowerShell `$variable` syntax
92 | # (Note that we don't bother to wrap the variable names in curly braces, since we know that none
93 | # of the variable names contain special characters, and they're only ever interpolated as either
94 | # part of a filesystem path surrounded by separators, or as a parameter surrounded by whitespace)
95 | return re.sub('{{ (.+?) }}', '$\\1', transformed)
96 |
97 | @staticmethod
98 | def replaceSystemPaths(path):
99 | """
100 | Replaces hard-coded system paths with the equivalent environment variables
101 | """
102 | replaced = path
103 | replaced = replaced.replace('C:\\Program Files', '$env:ProgramFiles')
104 | replaced = replaced.replace('C:\\ProgramData', '$env:ProgramData')
105 | return replaced
106 |
107 | @staticmethod
108 | def s3UriToHttpsUrl(s3Uri):
109 | """
110 | Converts an `s3://` URI to an HTTPS URL
111 | """
112 | url = s3Uri.replace('s3://', '')
113 | components = url.split('/', 1)
114 | return 'https://{}.s3.amazonaws.com/{}'.format(components[0], components[1])
115 |
116 |
117 | # Retrieve the contents of the "Amazon EKS Optimized Windows AMI" EC2 ImageBuilder component
118 | componentData = json.loads(Utility.capture([
119 | 'aws',
120 | 'imagebuilder',
121 | 'get-component',
122 | '--region=us-east-1',
123 | '--component-build-version-arn',
124 | 'arn:aws:imagebuilder:us-east-1:aws:component/eks-optimized-ami-windows/1.24.0'
125 | ]))
126 |
127 | # Parse the pipeline YAML data and extract the list of constants
128 | pipelineData = yaml.load(componentData['component']['data'], Loader=yaml.Loader)
129 | constants = Utility.parseConstants(pipelineData['constants'])
130 |
131 | # Extract the steps for the "build" phase
132 | buildSteps = [p['steps'] for p in pipelineData['phases'] if p['name'] == 'build'][0]
133 |
134 | print('CONSTANTS:')
135 | print(json.dumps(constants, indent=4))
136 |
137 | print()
138 | print('BUILD STEPS:')
139 | print(json.dumps(buildSteps, indent=4))
140 |
141 | # Prepend our header to the generated PowerShell code
142 | generated = '''<#
143 | THIS FILE IS AUTOMATICALLY GENERATED, DO NOT EDIT!
144 |
145 | This script is based on the logic from the "Amazon EKS Optimized Windows AMI"
146 | EC2 ImageBuilder component, with modifications to use containerd 1.7.0.
147 |
148 | The original ImageBuilder component logic is Copyright Amazon.com, Inc. or
149 | its affiliates, and is licensed under the MIT License.
150 | #>
151 |
152 | # Halt execution if we encounter an error
153 | $ErrorActionPreference = 'Stop'
154 |
155 |
156 | # Applies in-place patches to a file
157 | function PatchFile
158 | {
159 | Param (
160 | $File,
161 | $Patches
162 | )
163 |
164 | $patched = Get-Content -Path $File -Raw
165 | $Patches.GetEnumerator() | ForEach-Object {
166 | $patched = $patched.Replace($_.Key, $_.Value)
167 | }
168 | Set-Content -Path $File -Value $patched -NoNewline
169 | }
170 |
171 |
172 | '''
173 |
174 | # Inject an additional constant for the parent of the temp directory, immediately before the child directory
175 | tempPath = {k:v for k,v in constants.items() if k == 'TempPath'}
176 | otherConstants = {k:v for k,v in constants.items() if k != 'TempPath'}
177 | constants = {**otherConstants, 'TempRoot': 'C:\\TempEKSArtifactDir', **tempPath}
178 |
179 | # Define variables for each of our constants
180 | generated += '# Constants\n'
181 | existingConstants = {}
182 | for key, value in constants.items():
183 | transformed = Utility.replaceConstants(value, existingConstants)
184 | transformed = Utility.replaceSystemPaths(transformed)
185 | generated += '${} = "{}"\n'.format(key, transformed)
186 | existingConstants[key] = value
187 |
188 | # Process each build step in turn
189 | for step in buildSteps:
190 |
191 | # Determine whether we have custom preprocessing logic for the step
192 | name = step['name']
193 | if name == 'ConfigureDirectories':
194 |
195 | # Add the temp directory to the list of directories to be created
196 | step['loop']['forEach'] += [constants['TempRoot']]
197 |
198 | elif name == 'DownloadKubernetes':
199 |
200 | # Inject the driver installation step immediately prior to the Kubernetes download step
201 | generated += '\n'.join([
202 | '',
203 | '# Install the NVIDIA GPU drivers',
204 | "$driverBucket = 'ec2-windows-nvidia-drivers'",
205 | "$driver = Get-S3Object -BucketName $driverBucket -KeyPrefix 'latest' -Region 'us-east-1' | Where-Object {$_.Key.Contains('server2022')}",
206 | 'Copy-S3Object -BucketName $driverBucket -Key $driver.Key -LocalFile "$TempRoot\driver.exe" -Region \'us-east-1\'',
207 | "Start-Process -FilePath \"$TempRoot\driver.exe\" -ArgumentList @('-s', '-noreboot') -NoNewWindow -Wait",
208 | ''
209 | ])
210 |
211 | elif name == 'ExtractEKSArtifacts':
212 |
213 | # Remove the redundant directory creation command
214 | step['inputs']['commands'] = [
215 | c for c in step['inputs']['commands']
216 | if not c.startswith('New-Item')
217 | ]
218 |
219 | # Use absolute file and directory paths rather than relative paths
220 | step['inputs']['commands'] = [
221 | c.replace('EKS-Artifacts.zip', '"C:\\EKS-Artifacts.zip"').replace('TempEKSArtifactDir', 'C:\\TempEKSArtifactDir')
222 | for c in step['inputs']['commands']
223 | ]
224 |
225 | elif name == 'InstallContainerRuntimes':
226 |
227 | # Inject the containerd 1.7.0 download step, along with our configuration patching steps, immediately prior to the containerd installation step
228 | generated += '\n'.join([
229 | '',
230 | '# -------',
231 | '',
232 | '# TEMPORARY UNTIL EKS ADDS SUPPORT FOR CONTAINERD v1.7.0:',
233 | '# Download and extract the containerd 1.7.0 release build',
234 | '$containerdTarball = "$TempPath\\containerd-1.7.0.tar.gz"',
235 | '$containerdFiles = "$TempPath\\containerd-1.7.0"',
236 | '$webClient.DownloadFile(\'https://github.com/containerd/containerd/releases/download/v1.7.0/containerd-1.7.0-windows-amd64.tar.gz\', $containerdTarball)',
237 | 'New-Item -Path "$containerdFiles" -ItemType Directory -Force | Out-Null',
238 | 'tar.exe -xvzf "$containerdTarball" -C "$containerdFiles"',
239 | '',
240 | '# Move the containerd files into place',
241 | 'Move-Item -Path "$containerdFiles\\bin\\containerd.exe" -Destination "$ContainerdPath\\containerd.exe" -Force',
242 | 'Move-Item -Path "$containerdFiles\\bin\\containerd-shim-runhcs-v1.exe" -Destination "$ContainerdPath\\containerd-shim-runhcs-v1.exe" -Force',
243 | 'Move-Item -Path "$containerdFiles\\bin\\ctr.exe" -Destination "$ContainerdPath\\ctr.exe" -Force',
244 | '',
245 | '# Clean up the containerd intermediate files',
246 | 'Remove-Item -Path "$containerdFiles" -Recurse -Force',
247 | 'Remove-Item -Path "$containerdTarball" -Force',
248 | '',
249 | '# -------',
250 | '',
251 | '# Patch the containerd setup script to configure a log file (rather than just discarding log output) and to use the upstream pause',
252 | '# container image rather than the EKS version, since the latter appears to cause errors when attempting to create Windows Pods',
253 | 'PatchFile -File "$TempPath\Add-ContainerdRuntime.ps1" -Patches @{',
254 | ' "containerd --register-service" = "containerd --register-service --log-file \'C:\\ProgramData\\containerd\\root\\output.log\'";',
255 | ' "amazonaws.com/eks/pause-windows:latest" = "registry.k8s.io/pause:3.9"',
256 | '}',
257 | '',
258 | '# Add the full Windows Server 2022 base image and the pause image to the list of images to pre-pull',
259 | '$baseLayersFile = "$TempPath\eks.baselayers.config"',
260 | '$baseLayers = Get-Content -Path $baseLayersFile -Raw | ConvertFrom-Json',
261 | '$baseLayers.2022 += "mcr.microsoft.com/windows/server:ltsc2022"',
262 | '$baseLayers.2022 += "registry.k8s.io/pause:3.9"',
263 | '$patchedJson = ConvertTo-Json -Depth 100 -InputObject $baseLayers',
264 | 'Set-Content -Path $baseLayersFile -Value $patchedJson -NoNewline',
265 | '',
266 | ])
267 |
268 | # Simplify the containerd installation command
269 | step['inputs']['commands'] = [
270 | '',
271 | '# Register containerd as the EKS container runtime',
272 | 'Push-Location $TempPath',
273 | '& .\Add-ContainerdRuntime.ps1 -Path "$ContainerdPath"',
274 | 'Pop-Location'
275 | ]
276 |
277 | elif name == 'ExecuteBuildScripts':
278 |
279 | # Prefix each script invocation with the call operator
280 | step['loop']['forEach'] = [
281 | '& {}'.format(command)
282 | for command in step['loop']['forEach']
283 | ]
284 |
285 | # Strip away the boilerplate code surrounding each script invocation
286 | step['inputs']['commands'] = ['Push-Location $TempPath'] + step['loop']['forEach'] + ['Pop-Location']
287 |
288 | # -------
289 |
290 | # If we have a descriptive comment for the step then include it above its generated code
291 | comment = Utility.commentForStep(name)
292 | if comment != None:
293 | generated += '\n{}\n'.format(comment)
294 |
295 | # -------
296 |
297 | # Generate code for the step based on its action type
298 | action = step['action']
299 |
300 | if action == 'CreateFolder':
301 | directories = [Utility.replaceConstants(d, constants) for d in step['loop']['forEach']]
302 | generated += '\n'.join([
303 | 'foreach ($dir in @({})) {{'.format(', '.join(directories)),
304 | '\tNew-Item -Path $dir -ItemType Directory -Force | Out-Null',
305 | '}'
306 | ])
307 |
308 | elif action == 'DeleteFolder':
309 | generated += '\n'.join([
310 | 'Remove-Item -Path "{}" -Recurse -Force'.format(Utility.replaceConstants(input['path'], constants))
311 | for input in step['inputs']
312 | ])
313 |
314 | elif action == 'MoveFile':
315 | generated += '\n'.join([
316 | 'Move-Item -Path "{}" -Destination "{}" -Force'.format(
317 | Utility.replaceConstants(input['source'], constants),
318 | Utility.replaceConstants(input['destination'], constants)
319 | )
320 | for input in step['inputs']
321 | ])
322 |
323 | elif action == 'S3Download':
324 | generated += '\n'.join([
325 | '$webClient.DownloadFile("{}", "{}")'.format(
326 | Utility.s3UriToHttpsUrl(input['source']),
327 | Utility.replaceConstants(input['destination'], constants)
328 | )
329 | for input in step['inputs']
330 | ])
331 |
332 | elif action == 'ExecutePowerShell':
333 | generated += '\n'.join([
334 | Utility.replaceConstants(c, constants).replace("'", '"')
335 | for c in step['inputs']['commands']
336 | if not c.startswith('$ErrorActionPreference')
337 | ])
338 |
339 | elif action == 'Reboot':
340 | Utility.log('Ignoring reboot step.')
341 | continue
342 |
343 | else:
344 | raise RuntimeError('Unknown build step action: {}'.format(action))
345 |
346 | # -------
347 |
348 | # Add a trailing newline after each non-ignored step
349 | generated += '\n'
350 |
351 | # Write the generated code to the output script file
352 | outfile = Path(__file__).parent / 'scripts' / 'setup.ps1'
353 | Utility.writeFile(outfile, generated)
354 | Utility.log('Wrote generated code to {}'.format(outfile))
355 |
--------------------------------------------------------------------------------
/cloud/aws/node/scripts/cleanup.ps1:
--------------------------------------------------------------------------------
1 | # Perform cleanup
2 | Set-Service -Name sshd -StartupType 'Manual'
3 | Remove-Item -Path 'C:\ProgramData\ssh\administrators_authorized_keys' -Force
4 |
5 | # Remove the file for this script, since Packer won't have a chance to perform its own cleanup
6 | Remove-Item -Path $PSCommandPath -Force
7 |
8 | # Perform sysprep and shut down the VM
9 | & "$Env:ProgramFiles\Amazon\EC2Launch\EC2Launch.exe" sysprep --shutdown=true
10 |
--------------------------------------------------------------------------------
/cloud/aws/node/scripts/setup.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | THIS FILE IS AUTOMATICALLY GENERATED, DO NOT EDIT!
3 |
4 | This script is based on the logic from the "Amazon EKS Optimized Windows AMI"
5 | EC2 ImageBuilder component, with modifications to use containerd 1.7.0.
6 |
7 | The original ImageBuilder component logic is Copyright Amazon.com, Inc. or
8 | its affiliates, and is licensed under the MIT License.
9 | #>
10 |
11 | # Halt execution if we encounter an error
12 | $ErrorActionPreference = 'Stop'
13 |
14 |
15 | # Applies in-place patches to a file
16 | function PatchFile
17 | {
18 | Param (
19 | $File,
20 | $Patches
21 | )
22 |
23 | $patched = Get-Content -Path $File -Raw
24 | $Patches.GetEnumerator() | ForEach-Object {
25 | $patched = $patched.Replace($_.Key, $_.Value)
26 | }
27 | Set-Content -Path $File -Value $patched -NoNewline
28 | }
29 |
30 |
31 | # Constants
32 | $KubernetesPath = "$env:ProgramFiles\Kubernetes"
33 | $KubernetesDownload = "https://amazon-eks.s3.amazonaws.com/1.24.7/2022-10-31/bin/windows/amd64"
34 | $ContainerdPath = "$env:ProgramFiles\containerd"
35 | $EKSPath = "$env:ProgramFiles\Amazon\EKS"
36 | $CNIPath = "$EKSPath\cni"
37 | $CSIProxyPath = "$EKSPath\bin"
38 | $EKSLogsPath = "$env:ProgramData\Amazon\EKS\logs"
39 | $TempRoot = "C:\TempEKSArtifactDir"
40 | $TempPath = "$TempRoot\EKS-Artifacts"
41 |
42 | # Create each of our directories
43 | foreach ($dir in @($ContainerdPath, $KubernetesPath, $EKSPath, $CNIPath, $CSIProxyPath, $EKSLogsPath, $TempRoot)) {
44 | New-Item -Path $dir -ItemType Directory -Force | Out-Null
45 | }
46 |
47 | # Install the NVIDIA GPU drivers
48 | $driverBucket = 'ec2-windows-nvidia-drivers'
49 | $driver = Get-S3Object -BucketName $driverBucket -KeyPrefix 'latest' -Region 'us-east-1' | Where-Object {$_.Key.Contains('server2022')}
50 | Copy-S3Object -BucketName $driverBucket -Key $driver.Key -LocalFile "$TempRoot\driver.exe" -Region 'us-east-1'
51 | Start-Process -FilePath "$TempRoot\driver.exe" -ArgumentList @('-s', '-noreboot') -NoNewWindow -Wait
52 |
53 | # Download the Kubernetes components
54 | $webClient = New-Object System.Net.WebClient
55 | $webClient.DownloadFile("$KubernetesDownload/kubelet.exe", "$KubernetesPath\kubelet.exe")
56 | $webClient.DownloadFile("$KubernetesDownload/kube-proxy.exe", "$KubernetesPath\kube-proxy.exe")
57 | $webClient.DownloadFile("$KubernetesDownload/aws-iam-authenticator.exe", "$EKSPath\aws-iam-authenticator.exe")
58 |
59 | # Download the EKS artifacts archive
60 | $webClient.DownloadFile("https://ec2imagebuilder-managed-resources-us-east-1-prod.s3.amazonaws.com/components/eks-optimized-ami-windows/1.24.0/EKS-Artifacts.zip", "C:\EKS-Artifacts.zip")
61 |
62 | # Extract the EKS artifacts archive
63 | Expand-Archive -Path "C:\EKS-Artifacts.zip" -DestinationPath $TempRoot
64 | Remove-Item -Path "C:\EKS-Artifacts.zip" -Force
65 |
66 | # Move the EKS files into place
67 | Move-Item -Path "$TempPath\ctr.exe" -Destination "$ContainerdPath\ctr.exe" -Force
68 | Move-Item -Path "$TempPath\containerd.exe" -Destination "$ContainerdPath\containerd.exe" -Force
69 | Move-Item -Path "$TempPath\containerd-shim-runhcs-v1.exe" -Destination "$ContainerdPath\containerd-shim-runhcs-v1.exe" -Force
70 | Move-Item -Path "$TempPath\Start-EKSBootstrap.ps1" -Destination "$EKSPath\Start-EKSBootstrap.ps1" -Force
71 | Move-Item -Path "$TempPath\EKS-StartupTask.ps1" -Destination "$EKSPath\EKS-StartupTask.ps1" -Force
72 | Move-Item -Path "$TempPath\vpc-shared-eni.exe" -Destination "$CNIPath\vpc-shared-eni.exe" -Force
73 | Move-Item -Path "$TempPath\csi-proxy.exe" -Destination "$CSIProxyPath\csi-proxy.exe" -Force
74 |
75 | # Install the Windows Containers feature
76 | # (Note: this is actually a no-op here, since we install the feature beforehand in startup.ps1)
77 | Install-WindowsFeature -Name Containers
78 |
79 | # -------
80 |
81 | # TEMPORARY UNTIL EKS ADDS SUPPORT FOR CONTAINERD v1.7.0:
82 | # Download and extract the containerd 1.7.0 release build
83 | $containerdTarball = "$TempPath\containerd-1.7.0.tar.gz"
84 | $containerdFiles = "$TempPath\containerd-1.7.0"
85 | $webClient.DownloadFile('https://github.com/containerd/containerd/releases/download/v1.7.0/containerd-1.7.0-windows-amd64.tar.gz', $containerdTarball)
86 | New-Item -Path "$containerdFiles" -ItemType Directory -Force | Out-Null
87 | tar.exe -xvzf "$containerdTarball" -C "$containerdFiles"
88 |
89 | # Move the containerd files into place
90 | Move-Item -Path "$containerdFiles\bin\containerd.exe" -Destination "$ContainerdPath\containerd.exe" -Force
91 | Move-Item -Path "$containerdFiles\bin\containerd-shim-runhcs-v1.exe" -Destination "$ContainerdPath\containerd-shim-runhcs-v1.exe" -Force
92 | Move-Item -Path "$containerdFiles\bin\ctr.exe" -Destination "$ContainerdPath\ctr.exe" -Force
93 |
94 | # Clean up the containerd intermediate files
95 | Remove-Item -Path "$containerdFiles" -Recurse -Force
96 | Remove-Item -Path "$containerdTarball" -Force
97 |
98 | # -------
99 |
100 | # Patch the containerd setup script to configure a log file (rather than just discarding log output) and to use the upstream pause
101 | # container image rather than the EKS version, since the latter appears to cause errors when attempting to create Windows Pods
102 | PatchFile -File "$TempPath\Add-ContainerdRuntime.ps1" -Patches @{
103 | "containerd --register-service" = "containerd --register-service --log-file 'C:\ProgramData\containerd\root\output.log'";
104 | "amazonaws.com/eks/pause-windows:latest" = "registry.k8s.io/pause:3.9"
105 | }
106 |
107 | # Add the full Windows Server 2022 base image and the pause image to the list of images to pre-pull
108 | $baseLayersFile = "$TempPath\eks.baselayers.config"
109 | $baseLayers = Get-Content -Path $baseLayersFile -Raw | ConvertFrom-Json
110 | $baseLayers.2022 += "mcr.microsoft.com/windows/server:ltsc2022"
111 | $baseLayers.2022 += "registry.k8s.io/pause:3.9"
112 | $patchedJson = ConvertTo-Json -Depth 100 -InputObject $baseLayers
113 | Set-Content -Path $baseLayersFile -Value $patchedJson -NoNewline
114 |
115 | # Register containerd as the EKS container runtime
116 | Push-Location $TempPath
117 | & .\Add-ContainerdRuntime.ps1 -Path "$ContainerdPath"
118 | Pop-Location
119 |
120 | # Perform EKS worker node setup
121 | Push-Location $TempPath
122 | & .\create-windows-pause-image.ps1 -ContainerRuntime containerd
123 | & .\Get-EKSBaseLayers.ps1 -ConfigFile eks.baselayers.config -ContainerRuntime containerd
124 | & .\Add-CSIProxy.ps1 -Path "$CSIProxyPath" -LogPath "$EKSLogsPath"
125 | & .\EKS-WindowsServiceHost.ps1
126 | & .\Install-EKSWorkerNode.ps1
127 | Pop-Location
128 |
129 | # Perform cleanup
130 | Remove-Item -Path "$TempRoot" -Recurse -Force
131 |
--------------------------------------------------------------------------------
/cloud/aws/node/scripts/startup.ps1:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Install the OpenSSH server and set the sshd service to start automatically at system startup
4 | Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
5 | Set-Service -Name sshd -StartupType 'Automatic'
6 |
7 | # Create the OpenSSH configuration directory if it doesn't already exist
8 | $sshDir = 'C:\ProgramData\ssh'
9 | if ((Test-Path -Path $sshDir) -eq $false) {
10 | New-Item -Path $sshDir -ItemType Directory -Force | Out-Null
11 | }
12 |
13 | # Retrieve the SHH public key from the EC2 metadata service
14 | $authorisedKeys = "$sshDir\administrators_authorized_keys"
15 | curl.exe 'http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key' -o "$authorisedKeys"
16 |
17 | # Set the required ACLs for the authorised keys file
18 | icacls.exe "$authorisedKeys" /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F"
19 |
20 | # Install the Windows feature for containers, which will require a reboot
21 | Install-WindowsFeature -Name Containers -IncludeAllSubFeature
22 |
23 | # Restart the VM
24 | Restart-Computer
25 |
26 |
--------------------------------------------------------------------------------
/deployments/default-daemonsets.yml:
--------------------------------------------------------------------------------
1 | # HostProcess DaemonSets for the MCDM device plugin and the WDDM device plugin, using default settings
2 |
3 | apiVersion: apps/v1
4 | kind: DaemonSet
5 | metadata:
6 | name: device-plugin-mcdm
7 | spec:
8 | selector:
9 | matchLabels:
10 | app: device-plugin-mcdm
11 | template:
12 | metadata:
13 | labels:
14 | app: device-plugin-mcdm
15 | spec:
16 | nodeSelector:
17 | kubernetes.io/os: 'windows'
18 | kubernetes.io/arch: 'amd64'
19 | node.kubernetes.io/windows-build: '10.0.20348'
20 | securityContext:
21 | windowsOptions:
22 | hostProcess: true
23 | runAsUserName: "NT AUTHORITY\\SYSTEM"
24 | hostNetwork: true
25 | containers:
26 | - name: device-plugin-mcdm
27 | image: "index.docker.io/tensorworks/mcdm-device-plugin:0.0.1"
28 | imagePullPolicy: Always
29 |
30 | ---
31 |
32 | apiVersion: apps/v1
33 | kind: DaemonSet
34 | metadata:
35 | name: device-plugin-wddm
36 | spec:
37 | selector:
38 | matchLabels:
39 | app: device-plugin-wddm
40 | template:
41 | metadata:
42 | labels:
43 | app: device-plugin-wddm
44 | spec:
45 | nodeSelector:
46 | kubernetes.io/os: 'windows'
47 | kubernetes.io/arch: 'amd64'
48 | node.kubernetes.io/windows-build: '10.0.20348'
49 | securityContext:
50 | windowsOptions:
51 | hostProcess: true
52 | runAsUserName: "NT AUTHORITY\\SYSTEM"
53 | hostNetwork: true
54 | containers:
55 | - name: device-plugin-wddm
56 | image: "index.docker.io/tensorworks/wddm-device-plugin:0.0.1"
57 | imagePullPolicy: Always
58 |
--------------------------------------------------------------------------------
/deployments/multitenancy-configmap.yml:
--------------------------------------------------------------------------------
1 | # Example HostProcess DaemonSets for the MCDM device plugin and the WDDM device plugin, using settings that enable multitenancy
2 | #
3 | # This version of the DaemonSets uses a ConfigMap to provide configuration values. For a version that sets environment variable
4 | # values directly in the Pod spec, see the file `multitenancy-inline.yml`
5 |
6 | apiVersion: v1
7 | kind: ConfigMap
8 | metadata:
9 | name: device-plugin-config
10 | data:
11 |
12 | # Configure the device plugins to allow 4 containers to mount each device simultaneously
13 | multitenancy: '4'
14 |
15 | ---
16 |
17 | apiVersion: apps/v1
18 | kind: DaemonSet
19 | metadata:
20 | name: device-plugin-mcdm
21 | spec:
22 | selector:
23 | matchLabels:
24 | app: device-plugin-mcdm
25 | template:
26 | metadata:
27 | labels:
28 | app: device-plugin-mcdm
29 | spec:
30 | nodeSelector:
31 | kubernetes.io/os: 'windows'
32 | kubernetes.io/arch: 'amd64'
33 | node.kubernetes.io/windows-build: '10.0.20348'
34 | securityContext:
35 | windowsOptions:
36 | hostProcess: true
37 | runAsUserName: "NT AUTHORITY\\SYSTEM"
38 | hostNetwork: true
39 | containers:
40 | - name: device-plugin-mcdm
41 | image: "index.docker.io/tensorworks/mcdm-device-plugin:0.0.1"
42 | imagePullPolicy: Always
43 |
44 | # Use the configuration values from the ConfigMap
45 | env:
46 | - name: MCDM_DEVICE_PLUGIN_MULTITENANCY
47 | valueFrom:
48 | configMapKeyRef:
49 | name: device-plugin-config
50 | key: multitenancy
51 |
52 | ---
53 |
54 | apiVersion: apps/v1
55 | kind: DaemonSet
56 | metadata:
57 | name: device-plugin-wddm
58 | spec:
59 | selector:
60 | matchLabels:
61 | app: device-plugin-wddm
62 | template:
63 | metadata:
64 | labels:
65 | app: device-plugin-wddm
66 | spec:
67 | nodeSelector:
68 | kubernetes.io/os: 'windows'
69 | kubernetes.io/arch: 'amd64'
70 | node.kubernetes.io/windows-build: '10.0.20348'
71 | securityContext:
72 | windowsOptions:
73 | hostProcess: true
74 | runAsUserName: "NT AUTHORITY\\SYSTEM"
75 | hostNetwork: true
76 | containers:
77 | - name: device-plugin-wddm
78 | image: "index.docker.io/tensorworks/wddm-device-plugin:0.0.1"
79 | imagePullPolicy: Always
80 |
81 | # Use the configuration values from the ConfigMap
82 | env:
83 | - name: WDDM_DEVICE_PLUGIN_MULTITENANCY
84 | valueFrom:
85 | configMapKeyRef:
86 | name: device-plugin-config
87 | key: multitenancy
88 |
--------------------------------------------------------------------------------
/deployments/multitenancy-inline.yml:
--------------------------------------------------------------------------------
1 | # Example HostProcess DaemonSets for the MCDM device plugin and the WDDM device plugin, using settings that enable multitenancy
2 | #
3 | # This version of the DaemonSets sets environment variable values directly in the Pod spec. For a version that uses a ConfigMap
4 | # to provide configuration values, see the file `multitenancy-configmap.yml`
5 |
6 | apiVersion: apps/v1
7 | kind: DaemonSet
8 | metadata:
9 | name: device-plugin-mcdm
10 | spec:
11 | selector:
12 | matchLabels:
13 | app: device-plugin-mcdm
14 | template:
15 | metadata:
16 | labels:
17 | app: device-plugin-mcdm
18 | spec:
19 | nodeSelector:
20 | kubernetes.io/os: 'windows'
21 | kubernetes.io/arch: 'amd64'
22 | node.kubernetes.io/windows-build: '10.0.20348'
23 | securityContext:
24 | windowsOptions:
25 | hostProcess: true
26 | runAsUserName: "NT AUTHORITY\\SYSTEM"
27 | hostNetwork: true
28 | containers:
29 | - name: device-plugin-mcdm
30 | image: "index.docker.io/tensorworks/mcdm-device-plugin:0.0.1"
31 | imagePullPolicy: Always
32 |
33 | # Configure the MCDM device plugin to allow 4 containers to mount each compute-only device simultaneously
34 | env:
35 | - name: MCDM_DEVICE_PLUGIN_MULTITENANCY
36 | value: "4"
37 |
38 | ---
39 |
40 | apiVersion: apps/v1
41 | kind: DaemonSet
42 | metadata:
43 | name: device-plugin-wddm
44 | spec:
45 | selector:
46 | matchLabels:
47 | app: device-plugin-wddm
48 | template:
49 | metadata:
50 | labels:
51 | app: device-plugin-wddm
52 | spec:
53 | nodeSelector:
54 | kubernetes.io/os: 'windows'
55 | kubernetes.io/arch: 'amd64'
56 | node.kubernetes.io/windows-build: '10.0.20348'
57 | securityContext:
58 | windowsOptions:
59 | hostProcess: true
60 | runAsUserName: "NT AUTHORITY\\SYSTEM"
61 | hostNetwork: true
62 | containers:
63 | - name: device-plugin-wddm
64 | image: "index.docker.io/tensorworks/wddm-device-plugin:0.0.1"
65 | imagePullPolicy: Always
66 |
67 | # Configure the WDDM device plugin to allow 4 containers to mount each display device simultaneously
68 | env:
69 | - name: WDDM_DEVICE_PLUGIN_MULTITENANCY
70 | value: "4"
71 |
--------------------------------------------------------------------------------
/examples/cuda-devicequery/cuda-devicequery-mcdm.yml:
--------------------------------------------------------------------------------
1 | # Example Job for running the CUDA deviceQuery sample program inside a container
2 | #
3 | # This version of the Job requests a compute-only device from the MCDM device plugin. For a version that
4 | # requests a display device from the WDDM device plugin, see the file `cuda-devicequery-wddm.yml`
5 | #
6 | # NOTE: this Job will only work when the device allocated by the MCDM device plugin is an NVIDIA GPU,
7 | # otherwise the DLL files required by `deviceQuery.exe` won't exist and the Pod will fail to start.
8 |
9 | apiVersion: batch/v1
10 | kind: Job
11 | metadata:
12 | name: example-cuda-devicequery-mcdm
13 | spec:
14 | template:
15 | spec:
16 | containers:
17 | - name: example-cuda-devicequery-mcdm
18 | image: "index.docker.io/tensorworks/example-cuda-devicequery:0.0.1"
19 | resources:
20 | limits:
21 | directx.microsoft.com/compute: 1
22 | nodeSelector:
23 | "kubernetes.io/os": windows
24 | restartPolicy: Never
25 | backoffLimit: 0
26 |
--------------------------------------------------------------------------------
/examples/cuda-devicequery/cuda-devicequery-wddm.yml:
--------------------------------------------------------------------------------
1 | # Example Job for running the CUDA deviceQuery sample program inside a container
2 | #
3 | # This version of the Job requests a display device from the WDDM device plugin. For a version that
4 | # requests a compute-only device from the MCDM device plugin, see the file `cuda-devicequery-mcdm.yml`
5 | #
6 | # NOTE: this Job will only work when the device allocated by the WDDM device plugin is an NVIDIA GPU,
7 | # otherwise the DLL files required by `deviceQuery.exe` won't exist and the Pod will fail to start.
8 |
9 | apiVersion: batch/v1
10 | kind: Job
11 | metadata:
12 | name: example-cuda-devicequery-wddm
13 | spec:
14 | template:
15 | spec:
16 | containers:
17 | - name: example-cuda-devicequery-wddm
18 | image: "index.docker.io/tensorworks/example-cuda-devicequery:0.0.1"
19 | resources:
20 | limits:
21 | directx.microsoft.com/display: 1
22 | nodeSelector:
23 | "kubernetes.io/os": windows
24 | restartPolicy: Never
25 | backoffLimit: 0
26 |
--------------------------------------------------------------------------------
/examples/cuda-montecarlo/cuda-montecarlo-mcdm.yml:
--------------------------------------------------------------------------------
1 | # Example Job for running the CUDA MC_EstimatePiP sample program inside a container
2 | #
3 | # This version of the Job requests a compute-only device from the MCDM device plugin. For a version that
4 | # requests a display device from the WDDM device plugin, see the file `cuda-montecarlo-wddm.yml`
5 | #
6 | # NOTE: this Job will only work when the device allocated by the MCDM device plugin is an NVIDIA GPU,
7 | # otherwise the DLL files required by `MC_EstimatePiP.exe` won't exist and the Pod will fail to start.
8 |
9 | apiVersion: batch/v1
10 | kind: Job
11 | metadata:
12 | name: example-cuda-montecarlo-mcdm
13 | spec:
14 | template:
15 | spec:
16 | containers:
17 | - name: example-cuda-montecarlo-mcdm
18 | image: "index.docker.io/tensorworks/example-cuda-montecarlo:0.0.1"
19 | resources:
20 | limits:
21 | directx.microsoft.com/compute: 1
22 | nodeSelector:
23 | "kubernetes.io/os": windows
24 | restartPolicy: Never
25 | backoffLimit: 0
26 |
--------------------------------------------------------------------------------
/examples/cuda-montecarlo/cuda-montecarlo-wddm.yml:
--------------------------------------------------------------------------------
1 | # Example Job for running the CUDA MC_EstimatePiP sample program inside a container
2 | #
3 | # This version of the Job requests a display device from the WDDM device plugin. For a version that
4 | # requests a compute-only device from the MCDM device plugin, see the file `cuda-montecarlo-mcdm.yml`
5 | #
6 | # NOTE: this Job will only work when the device allocated by the WDDM device plugin is an NVIDIA GPU,
7 | # otherwise the DLL files required by `MC_EstimatePiP.exe` won't exist and the Pod will fail to start.
8 |
9 | apiVersion: batch/v1
10 | kind: Job
11 | metadata:
12 | name: example-cuda-montecarlo-wddm
13 | spec:
14 | template:
15 | spec:
16 | containers:
17 | - name: example-cuda-montecarlo-wddm
18 | image: "index.docker.io/tensorworks/example-cuda-montecarlo:0.0.1"
19 | resources:
20 | limits:
21 | directx.microsoft.com/display: 1
22 | nodeSelector:
23 | "kubernetes.io/os": windows
24 | restartPolicy: Never
25 | backoffLimit: 0
26 |
--------------------------------------------------------------------------------
/examples/device-discovery/device-discovery-mcdm.yml:
--------------------------------------------------------------------------------
1 | # Example Job for running the device discovery test program inside a container
2 | #
3 | # This version of the Job requests a compute-only device from the MCDM device plugin. For a version that
4 | # requests a display device from the WDDM device plugin, see the file `device-discovery-wddm.yml`
5 |
6 | apiVersion: batch/v1
7 | kind: Job
8 | metadata:
9 | name: example-device-discovery-mcdm
10 | spec:
11 | template:
12 | spec:
13 | containers:
14 | - name: example-device-discovery-mcdm
15 | image: "index.docker.io/tensorworks/example-device-discovery:0.0.1"
16 | resources:
17 | limits:
18 | directx.microsoft.com/compute: 1
19 | nodeSelector:
20 | "kubernetes.io/os": windows
21 | restartPolicy: Never
22 | backoffLimit: 0
23 |
--------------------------------------------------------------------------------
/examples/device-discovery/device-discovery-wddm.yml:
--------------------------------------------------------------------------------
1 | # Example Job for running the device discovery test program inside a container
2 | #
3 | # This version of the Job requests a display device from the WDDM device plugin. For a version that
4 | # requests a compute-only device from the MCDM device plugin, see the file `device-discovery-mcdm.yml`
5 |
6 | apiVersion: batch/v1
7 | kind: Job
8 | metadata:
9 | name: example-device-discovery-wddm
10 | spec:
11 | template:
12 | spec:
13 | containers:
14 | - name: example-device-discovery-wddm
15 | image: "index.docker.io/tensorworks/example-device-discovery:0.0.1"
16 | resources:
17 | limits:
18 | directx.microsoft.com/display: 1
19 | nodeSelector:
20 | "kubernetes.io/os": windows
21 | restartPolicy: Never
22 | backoffLimit: 0
23 |
--------------------------------------------------------------------------------
/examples/directml/directml-mcdm.yml:
--------------------------------------------------------------------------------
1 | # Example Job for running a DirectML sample inside a container
2 | #
3 | # This version of the Job requests a compute-only device from the MCDM device plugin. For a version that
4 | # requests a display device from the WDDM device plugin, see the file `directml-wddm.yml`
5 |
6 | apiVersion: batch/v1
7 | kind: Job
8 | metadata:
9 | name: example-directml-mcdm
10 | spec:
11 | template:
12 | spec:
13 | containers:
14 | - name: example-directml-mcdm
15 | image: "index.docker.io/tensorworks/example-directml:0.0.1"
16 | resources:
17 | limits:
18 | directx.microsoft.com/compute: 1
19 | nodeSelector:
20 | "kubernetes.io/os": windows
21 | restartPolicy: Never
22 | backoffLimit: 0
23 |
--------------------------------------------------------------------------------
/examples/directml/directml-wddm.yml:
--------------------------------------------------------------------------------
1 | # Example Job for running a DirectML sample inside a container
2 | #
3 | # This version of the Job requests a display device from the WDDM device plugin. For a version that
4 | # requests a compute-only device from the MCDM device plugin, see the file `directml-mcdm.yml`
5 |
6 | apiVersion: batch/v1
7 | kind: Job
8 | metadata:
9 | name: example-directml-wddm
10 | spec:
11 | template:
12 | spec:
13 | containers:
14 | - name: example-directml-wddm
15 | image: "index.docker.io/tensorworks/example-directml:0.0.1"
16 | resources:
17 | limits:
18 | directx.microsoft.com/display: 1
19 | nodeSelector:
20 | "kubernetes.io/os": windows
21 | restartPolicy: Never
22 | backoffLimit: 0
23 |
--------------------------------------------------------------------------------
/examples/ffmpeg-amf/ffmpeg-amf.yml:
--------------------------------------------------------------------------------
1 | # Example Job for running an AMD AMF transcode operation with FFmpeg inside a container
2 | #
3 | # NOTE: this Job will only work when the device allocated by the WDDM device plugin is an AMD GPU,
4 | # otherwise the DLL files for AMF won't exist and FFmpeg will fail when it tries to load them.
5 |
6 | apiVersion: batch/v1
7 | kind: Job
8 | metadata:
9 | name: example-ffmpeg-amf
10 | spec:
11 | template:
12 | spec:
13 | containers:
14 | - name: example-ffmpeg-amf
15 | image: "index.docker.io/tensorworks/example-ffmpeg:0.0.1"
16 | args: ["-i", "C:\\sample-video.mp4", "-c:v", "h264_amf", "-preset", "default", "C:\\output.mp4"]
17 | resources:
18 | limits:
19 | directx.microsoft.com/display: 1
20 | nodeSelector:
21 | "kubernetes.io/os": windows
22 | restartPolicy: Never
23 | backoffLimit: 0
24 |
--------------------------------------------------------------------------------
/examples/ffmpeg-autodetect/autodetect-encoder.ps1:
--------------------------------------------------------------------------------
1 | # Attempt to detect the availability of a hardware video encoder
2 | $encoder = ''
3 | if ((Get-ChildItem "C:\Windows\System32\amfrt64.dll" -ErrorAction SilentlyContinue))
4 | {
5 | Write-Host 'Detected an AMD GPU, using the AMF video encoder'
6 | $encoder = 'h264_amf'
7 | }
8 | elseif ((Get-ChildItem "C:\Windows\System32\intel_gfx_api-x64.dll" -ErrorAction SilentlyContinue))
9 | {
10 | Write-Host 'Detected an Intel GPU, using the Quick Sync video encoder'
11 | $encoder = 'h264_qsv'
12 | }
13 | elseif ((Get-ChildItem "C:\Windows\System32\nvEncodeAPI64.dll" -ErrorAction SilentlyContinue))
14 | {
15 | Write-Host 'Detected an NVIDIA GPU, using the NVENC video encoder'
16 | $encoder = 'h264_nvenc'
17 | }
18 | else {
19 | throw "Failed to detect the availability of a supported hardware video encoder"
20 | }
21 |
22 | # Invoke FFmpeg with the detected hardware video encoder
23 | & C:\ffmpeg.exe -i C:\sample-video.mp4 -c:v "$encoder" -preset default C:\output.mp4
24 | if ($LastExitCode -ne 0) {
25 | throw "FFmpeg terminated with exit code $LastExitCode"
26 | }
27 |
--------------------------------------------------------------------------------
/examples/ffmpeg-autodetect/ffmpeg-autodetect.yml:
--------------------------------------------------------------------------------
1 | # Example Job for running a hardware accelerated transcode operation with FFmpeg inside a container
2 | #
3 | # The transcode script will attempt to detect the availability of the following encoders:
4 | #
5 | # - AMD AMF
6 | # - Intel Quick Sync
7 | # - NVIDIA NVENC
8 | #
9 | # If a hardware encoder is detected then it will be used, otherwise the script will fail.
10 | #
11 | # NOTE: this Job will only work when the device allocated by the WDDM device plugin is an AMD, Intel or NVIDIA GPU,
12 | # otherwise the DLL files for the hardware encoders won't exist and the script will fail when no encoder is detected.
13 |
14 | apiVersion: batch/v1
15 | kind: Job
16 | metadata:
17 | name: example-ffmpeg-autodetect
18 | spec:
19 | template:
20 | spec:
21 | containers:
22 | - name: example-ffmpeg-autodetect
23 | image: "index.docker.io/tensorworks/example-ffmpeg:0.0.1"
24 | command: ["powershell"]
25 | args: ["-ExecutionPolicy", "Bypass", "-File", "C:\\autodetect-encoder.ps1"]
26 | resources:
27 | limits:
28 | directx.microsoft.com/display: 1
29 | nodeSelector:
30 | "kubernetes.io/os": windows
31 | restartPolicy: Never
32 | backoffLimit: 0
33 |
--------------------------------------------------------------------------------
/examples/ffmpeg-nvenc/ffmpeg-nvenc.yml:
--------------------------------------------------------------------------------
1 | # Example Job for running an NVIDIA NVENC transcode operation with FFmpeg inside a container
2 | #
3 | # NOTE: this Job will only work when the device allocated by the WDDM device plugin is an NVIDIA GPU,
4 | # otherwise the DLL files for CUDA and NVENC won't exist and FFmpeg will fail when it tries to load them.
5 |
6 | apiVersion: batch/v1
7 | kind: Job
8 | metadata:
9 | name: example-ffmpeg-nvenc
10 | spec:
11 | template:
12 | spec:
13 | containers:
14 | - name: example-ffmpeg-nvenc
15 | image: "index.docker.io/tensorworks/example-ffmpeg:0.0.1"
16 | args: ["-i", "C:\\sample-video.mp4", "-c:v", "h264_nvenc", "-preset", "default", "C:\\output.mp4"]
17 | resources:
18 | limits:
19 | directx.microsoft.com/display: 1
20 | nodeSelector:
21 | "kubernetes.io/os": windows
22 | restartPolicy: Never
23 | backoffLimit: 0
24 |
--------------------------------------------------------------------------------
/examples/ffmpeg-quicksync/ffmpeg-quicksync.yml:
--------------------------------------------------------------------------------
1 | # Example Job for running an Intel Quick Sync transcode operation with FFmpeg inside a container
2 | #
3 | # NOTE: this Job will only work when the device allocated by the WDDM device plugin is an Intel GPU,
4 | # otherwise the DLL files for Quick Sync won't exist and FFmpeg will fail when it tries to load them.
5 |
6 | apiVersion: batch/v1
7 | kind: Job
8 | metadata:
9 | name: example-ffmpeg-quicksync
10 | spec:
11 | template:
12 | spec:
13 | containers:
14 | - name: example-ffmpeg-quicksync
15 | image: "index.docker.io/tensorworks/example-ffmpeg:0.0.1"
16 | args: ["-i", "C:\\sample-video.mp4", "-c:v", "h264_qsv", "-preset", "default", "C:\\output.mp4"]
17 | resources:
18 | limits:
19 | directx.microsoft.com/display: 1
20 | nodeSelector:
21 | "kubernetes.io/os": windows
22 | restartPolicy: Never
23 | backoffLimit: 0
24 |
--------------------------------------------------------------------------------
/examples/nvidia-smi/nvidia-smi-mcdm.yml:
--------------------------------------------------------------------------------
1 | # Example Job for running the NVIDIA SMI tool inside a container
2 | #
3 | # This version of the Job requests a compute-only device from the MCDM device plugin. For a version that
4 | # requests a display device from the WDDM device plugin, see the file `nvidia-smi-wddm.yml`
5 | #
6 | # NOTE: this Job will only work when the device allocated by the MCDM device plugin is an NVIDIA GPU,
7 | # otherwise the executable `nvidia-smi.exe` won't exist and the Pod will fail to start.
8 |
9 | apiVersion: batch/v1
10 | kind: Job
11 | metadata:
12 | name: example-nvidia-smi
13 | spec:
14 | template:
15 | spec:
16 | containers:
17 | - name: example-nvidia-smi
18 | image: "mcr.microsoft.com/windows/servercore:ltsc2022"
19 | command: ["nvidia-smi.exe"]
20 | resources:
21 | limits:
22 | directx.microsoft.com/compute: 1
23 | nodeSelector:
24 | "kubernetes.io/os": windows
25 | restartPolicy: Never
26 | backoffLimit: 0
27 |
--------------------------------------------------------------------------------
/examples/nvidia-smi/nvidia-smi-wddm.yml:
--------------------------------------------------------------------------------
1 | # Example Job for running the NVIDIA SMI tool inside a container
2 | #
3 | # This version of the Job requests a display device from the WDDM device plugin. For a version that
4 | # requests a compute-only device from the MCDM device plugin, see the file `nvidia-smi-mcdm.yml`
5 | #
6 | # NOTE: this Job will only work when the device allocated by the WDDM device plugin is an NVIDIA GPU,
7 | # otherwise the executable `nvidia-smi.exe` won't exist and the Pod will fail to start.
8 |
9 | apiVersion: batch/v1
10 | kind: Job
11 | metadata:
12 | name: example-nvidia-smi
13 | spec:
14 | template:
15 | spec:
16 | containers:
17 | - name: example-nvidia-smi
18 | image: "mcr.microsoft.com/windows/servercore:ltsc2022"
19 | command: ["nvidia-smi.exe"]
20 | resources:
21 | limits:
22 | directx.microsoft.com/display: 1
23 | nodeSelector:
24 | "kubernetes.io/os": windows
25 | restartPolicy: Never
26 | backoffLimit: 0
27 |
--------------------------------------------------------------------------------
/examples/opencl-enum/opencl-enum-mcdm.yml:
--------------------------------------------------------------------------------
1 | # Example Job for running the OpenCL enumopencl sample program inside a container
2 | #
3 | # This version of the Job requests a compute-only device from the MCDM device plugin. For a version that
4 | # requests a display device from the WDDM device plugin, see the file `opencl-enum-wddm.yml`
5 | #
6 | # NOTE: this Job will only work when the device allocated by the MCDM device plugin is is a GPU that supports
7 | # OpenCL, otherwise the DLL files required by `enumopencl.exe` won't exist and the Pod will fail to start.
8 |
9 | apiVersion: batch/v1
10 | kind: Job
11 | metadata:
12 | name: example-opencl-enum-mcdm
13 | spec:
14 | template:
15 | spec:
16 | containers:
17 | - name: example-opencl-enum-mcdm
18 | image: "index.docker.io/tensorworks/example-opencl-enum:0.0.1"
19 | resources:
20 | limits:
21 | directx.microsoft.com/compute: 1
22 | nodeSelector:
23 | "kubernetes.io/os": windows
24 | restartPolicy: Never
25 | backoffLimit: 0
26 |
--------------------------------------------------------------------------------
/examples/opencl-enum/opencl-enum-wddm.yml:
--------------------------------------------------------------------------------
1 | # Example Job for running the OpenCL enumopencl sample program inside a container
2 | #
3 | # This version of the Job requests a display device from the WDDM device plugin. For a version that
4 | # requests a compute-only device from the MCDM device plugin, see the file `opencl-enum-mcdm.yml`
5 | #
6 | # NOTE: this Job will only work when the device allocated by the WDDM device plugin is is a GPU that supports
7 | # OpenCL, otherwise the DLL files required by `enumopencl.exe` won't exist and the Pod will fail to start.
8 |
9 | apiVersion: batch/v1
10 | kind: Job
11 | metadata:
12 | name: example-opencl-enum-wddm
13 | spec:
14 | template:
15 | spec:
16 | containers:
17 | - name: example-opencl-enum-wddm
18 | image: "index.docker.io/tensorworks/example-opencl-enum:0.0.1"
19 | resources:
20 | limits:
21 | directx.microsoft.com/display: 1
22 | nodeSelector:
23 | "kubernetes.io/os": windows
24 | restartPolicy: Never
25 | backoffLimit: 0
26 |
--------------------------------------------------------------------------------
/examples/vulkaninfo/vulkaninfo.yml:
--------------------------------------------------------------------------------
1 | # Example Job for running the Vulkan information tool inside a container
2 | #
3 | # NOTE: this Job will only work when the device allocated by the WDDM device plugin is a GPU that supports
4 | # Vulkan, otherwise the executable `vulkaninfo.exe` won't exist and the Pod will fail to start.
5 |
6 | apiVersion: batch/v1
7 | kind: Job
8 | metadata:
9 | name: example-vulkaninfo
10 | spec:
11 | template:
12 | spec:
13 | containers:
14 | - name: example-vulkaninfo
15 | image: "mcr.microsoft.com/windows/server:ltsc2022"
16 | command: ["vulkaninfo.exe"]
17 | resources:
18 | limits:
19 | directx.microsoft.com/display: 1
20 | nodeSelector:
21 | "kubernetes.io/os": windows
22 | restartPolicy: Never
23 | backoffLimit: 0
24 |
--------------------------------------------------------------------------------
/external/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/library/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.22)
2 | project(directx-device-discovery)
3 |
4 | # Set the C++ standard to C++17
5 | set(CMAKE_CXX_STANDARD 17)
6 | set(CMAKE_CXX_STANDARD_REQUIRED ON)
7 | set(CMAKE_CXX_EXTENSIONS OFF)
8 |
9 | # Locate our dependencies (these will be provided by vcpkg)
10 | find_package(cppwinrt CONFIG REQUIRED)
11 | find_package(fmt CONFIG REQUIRED)
12 | find_package(spdlog CONFIG REQUIRED)
13 | find_package(wil CONFIG REQUIRED)
14 |
15 | # Build our shared library
16 | add_library(directx-device-discovery SHARED
17 | src/AdapterEnumeration.cpp
18 | src/D3DHelpers.cpp
19 | src/DeviceDiscovery.cpp
20 | src/DeviceDiscoveryImp.cpp
21 | src/DllMain.cpp
22 | src/ErrorHandling.cpp
23 | src/RegistryQuery.cpp
24 | src/SafeArray.cpp
25 | src/WmiQuery.cpp
26 | )
27 | target_link_libraries(directx-device-discovery PRIVATE
28 | dxcore.lib
29 | dxguid.lib
30 | fmt::fmt-header-only
31 | gdi32.lib
32 | Microsoft::CppWinRT
33 | spdlog::spdlog_header_only
34 | wbemuuid.lib
35 | WIL::WIL
36 | WindowsApp.lib
37 | )
38 | set_property(TARGET directx-device-discovery PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded")
39 | target_include_directories(directx-device-discovery PUBLIC include)
40 | target_precompile_headers(directx-device-discovery PRIVATE src/pch.h)
41 |
42 | # Build our test executable
43 | add_executable(test-device-discovery-cpp test/test-device-discovery-cpp.cpp)
44 | set_property(TARGET test-device-discovery-cpp PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded")
45 | target_link_libraries(test-device-discovery-cpp PRIVATE Microsoft::CppWinRT directx-device-discovery)
46 |
47 | # Install the shared library and the test executable to the top-level bin directory
48 | install(
49 | TARGETS directx-device-discovery test-device-discovery-cpp
50 | RUNTIME DESTINATION bin
51 | )
52 |
--------------------------------------------------------------------------------
/library/include/DeviceDiscovery.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include "DeviceFilter.h"
3 |
4 | #define DLLEXPORT __declspec(dllexport)
5 |
6 | #ifdef __cplusplus
7 | extern "C" {
8 | #endif
9 |
10 | // Opaque pointer type for DeviceDiscovery instances
11 | typedef void* DeviceDiscoveryInstance;
12 |
13 | // Returns the version string for the device discovery library
14 | DLLEXPORT const wchar_t* GetDiscoveryLibraryVersion();
15 |
16 | // Disables verbose logging for the device discovery library (this is the default)
17 | DLLEXPORT void DisableDiscoveryLogging();
18 |
19 | // Enables verbose logging for the device discovery library
20 | DLLEXPORT void EnableDiscoveryLogging();
21 |
22 | // Creates a new DeviceDiscovery instance
23 | DLLEXPORT DeviceDiscoveryInstance CreateDeviceDiscoveryInstance();
24 |
25 | // Frees the memory for a DeviceDiscovery instance
26 | DLLEXPORT void DestroyDeviceDiscoveryInstance(DeviceDiscoveryInstance instance);
27 |
28 | // Retrieves the error message for the last operation performed by the DeviceDiscovery instance.
29 | // If the last operation succeeded then an empty string will be returned.
30 | DLLEXPORT const wchar_t* DeviceDiscovery_GetLastErrorMessage(DeviceDiscoveryInstance instance);
31 |
32 | // Determines whether the current device list is stale and needs to be refreshed by performing device discovery again
33 | DLLEXPORT int DeviceDiscovery_IsRefreshRequired(DeviceDiscoveryInstance instance);
34 |
35 | // Performs device discovery. Returns 0 on success and -1 on failure.
36 | // Call GetLastErrorMessage to retrieve the error details for a failure.
37 | DLLEXPORT int DeviceDiscovery_DiscoverDevices(DeviceDiscoveryInstance instance, int filter, int includeIntegrated, int includeDetachable);
38 |
39 | // Returns the number of devices found by the last device discovery, or -1 if device discovery has not been performed
40 | DLLEXPORT int DeviceDiscovery_GetNumDevices(DeviceDiscoveryInstance instance);
41 |
42 | DLLEXPORT long long DeviceDiscovery_GetDeviceAdapterLUID(DeviceDiscoveryInstance instance, unsigned int device);
43 |
44 | // Returns the unique ID of the device with the specified index, or a NULL pointer if the specified device index is invalid
45 | DLLEXPORT const wchar_t* DeviceDiscovery_GetDeviceID(DeviceDiscoveryInstance instance, unsigned int device);
46 |
47 | DLLEXPORT const wchar_t* DeviceDiscovery_GetDeviceDescription(DeviceDiscoveryInstance instance, unsigned int device);
48 |
49 | DLLEXPORT const wchar_t* DeviceDiscovery_GetDeviceDriverRegistryKey(DeviceDiscoveryInstance instance, unsigned int device);
50 |
51 | DLLEXPORT const wchar_t* DeviceDiscovery_GetDeviceDriverStorePath(DeviceDiscoveryInstance instance, unsigned int device);
52 |
53 | DLLEXPORT const wchar_t* DeviceDiscovery_GetDeviceLocationPath(DeviceDiscoveryInstance instance, unsigned int device);
54 |
55 | DLLEXPORT const wchar_t* DeviceDiscovery_GetDeviceVendor(DeviceDiscoveryInstance instance, unsigned int device);
56 |
57 | DLLEXPORT int DeviceDiscovery_GetNumRuntimeFiles(DeviceDiscoveryInstance instance, unsigned int device);
58 |
59 | DLLEXPORT const wchar_t* DeviceDiscovery_GetRuntimeFileSource(DeviceDiscoveryInstance instance, unsigned int device, unsigned int file);
60 |
61 | DLLEXPORT const wchar_t* DeviceDiscovery_GetRuntimeFileDestination(DeviceDiscoveryInstance instance, unsigned int device, unsigned int file);
62 |
63 | DLLEXPORT int DeviceDiscovery_GetNumRuntimeFilesWow64(DeviceDiscoveryInstance instance, unsigned int device);
64 |
65 | DLLEXPORT const wchar_t* DeviceDiscovery_GetRuntimeFileSourceWow64(DeviceDiscoveryInstance instance, unsigned int device, unsigned int file);
66 |
67 | DLLEXPORT const wchar_t* DeviceDiscovery_GetRuntimeFileDestinationWow64(DeviceDiscoveryInstance instance, unsigned int device, unsigned int file);
68 |
69 | DLLEXPORT int DeviceDiscovery_IsDeviceIntegrated(DeviceDiscoveryInstance instance, unsigned int device);
70 |
71 | DLLEXPORT int DeviceDiscovery_IsDeviceDetachable(DeviceDiscoveryInstance instance, unsigned int device);
72 |
73 | DLLEXPORT int DeviceDiscovery_DoesDeviceSupportDisplay(DeviceDiscoveryInstance instance, unsigned int device);
74 |
75 | DLLEXPORT int DeviceDiscovery_DoesDeviceSupportCompute(DeviceDiscoveryInstance instance, unsigned int device);
76 |
77 | #ifdef __cplusplus
78 | } // extern "C"
79 |
80 |
81 | #include
82 | #include
83 |
84 | // API wrapper classes for C++ clients
85 |
86 | class DeviceDiscoveryException
87 | {
88 | public:
89 | DeviceDiscoveryException(const wchar_t* message) {
90 | this->message = message;
91 | }
92 |
93 | DeviceDiscoveryException(const DeviceDiscoveryException& other) = default;
94 | DeviceDiscoveryException(DeviceDiscoveryException&& other) = default;
95 | DeviceDiscoveryException& operator=(const DeviceDiscoveryException& other) = default;
96 | DeviceDiscoveryException& operator=(DeviceDiscoveryException&& other) = default;
97 |
98 | std::wstring what() const {
99 | return this->message;
100 | }
101 |
102 | private:
103 | std::wstring message;
104 | };
105 |
106 | class DeviceDiscovery
107 | {
108 | private:
109 | DeviceDiscoveryInstance instance;
110 |
111 | public:
112 |
113 | inline DeviceDiscovery() {
114 | this->instance = CreateDeviceDiscoveryInstance();
115 | }
116 |
117 | inline ~DeviceDiscovery()
118 | {
119 | DestroyDeviceDiscoveryInstance(this->instance);
120 | this->instance = nullptr;
121 | }
122 |
123 | inline const wchar_t* GetLastErrorMessage() {
124 | return DeviceDiscovery_GetLastErrorMessage(this->instance);
125 | }
126 |
127 | inline bool IsRefreshRequired() {
128 | return DeviceDiscovery_IsRefreshRequired(this->instance);
129 | }
130 |
131 | #define THROW_IF_ERROR(sentinel) if (result == sentinel) { throw DeviceDiscoveryException(DeviceDiscovery_GetLastErrorMessage(this->instance)); }
132 |
133 | inline bool DiscoverDevices(DeviceFilter filter, bool includeIntegrated, bool includeDetachable)
134 | {
135 | int result = DeviceDiscovery_DiscoverDevices(this->instance, static_cast(filter), includeIntegrated, includeDetachable);
136 | THROW_IF_ERROR(-1);
137 | return (result == 0);
138 | }
139 |
140 | inline int GetNumDevices()
141 | {
142 | int result = DeviceDiscovery_GetNumDevices(this->instance);
143 | THROW_IF_ERROR(-1);
144 | return result;
145 | }
146 |
147 | inline long long GetDeviceAdapterLUID(unsigned int device)
148 | {
149 | long long result = DeviceDiscovery_GetDeviceAdapterLUID(this->instance, device);
150 | THROW_IF_ERROR(-1);
151 | return result;
152 | }
153 |
154 | inline const wchar_t* GetDeviceID(unsigned int device)
155 | {
156 | const wchar_t* result = DeviceDiscovery_GetDeviceID(this->instance, device);
157 | THROW_IF_ERROR(nullptr);
158 | return result;
159 | }
160 |
161 | inline const wchar_t* GetDeviceDescription(unsigned int device)
162 | {
163 | const wchar_t* result = DeviceDiscovery_GetDeviceDescription(this->instance, device);
164 | THROW_IF_ERROR(nullptr);
165 | return result;
166 | }
167 |
168 | inline const wchar_t* GetDeviceDriverRegistryKey(unsigned int device)
169 | {
170 | const wchar_t* result = DeviceDiscovery_GetDeviceDriverRegistryKey(this->instance, device);
171 | THROW_IF_ERROR(nullptr);
172 | return result;
173 | }
174 |
175 | inline const wchar_t* GetDeviceDriverStorePath(unsigned int device)
176 | {
177 | const wchar_t* result = DeviceDiscovery_GetDeviceDriverStorePath(this->instance, device);
178 | THROW_IF_ERROR(nullptr);
179 | return result;
180 | }
181 |
182 | inline const wchar_t* GetDeviceLocationPath(unsigned int device)
183 | {
184 | const wchar_t* result = DeviceDiscovery_GetDeviceLocationPath(this->instance, device);
185 | THROW_IF_ERROR(nullptr);
186 | return result;
187 | }
188 |
189 | inline const wchar_t* GetDeviceVendor(unsigned int device)
190 | {
191 | const wchar_t* result = DeviceDiscovery_GetDeviceVendor(this->instance, device);
192 | THROW_IF_ERROR(nullptr);
193 | return result;
194 | }
195 |
196 | inline int GetNumRuntimeFiles(unsigned int device)
197 | {
198 | int result = DeviceDiscovery_GetNumRuntimeFiles(this->instance, device);
199 | THROW_IF_ERROR(-1);
200 | return result;
201 | }
202 |
203 | inline const wchar_t* GetRuntimeFileSource(unsigned int device, unsigned int file)
204 | {
205 | const wchar_t* result = DeviceDiscovery_GetRuntimeFileSource(this->instance, device, file);
206 | THROW_IF_ERROR(nullptr);
207 | return result;
208 | }
209 |
210 | inline const wchar_t* GetRuntimeFileDestination(unsigned int device, unsigned int file)
211 | {
212 | const wchar_t* result = DeviceDiscovery_GetRuntimeFileDestination(this->instance, device, file);
213 | THROW_IF_ERROR(nullptr);
214 | return result;
215 | }
216 |
217 | inline int GetNumRuntimeFilesWow64(unsigned int device)
218 | {
219 | int result = DeviceDiscovery_GetNumRuntimeFilesWow64(this->instance, device);
220 | THROW_IF_ERROR(-1);
221 | return result;
222 | }
223 |
224 | inline const wchar_t* GetRuntimeFileSourceWow64(unsigned int device, unsigned int file)
225 | {
226 | const wchar_t* result = DeviceDiscovery_GetRuntimeFileSourceWow64(this->instance, device, file);
227 | THROW_IF_ERROR(nullptr);
228 | return result;
229 | }
230 |
231 | inline const wchar_t* GetRuntimeFileDestinationWow64(unsigned int device, unsigned int file)
232 | {
233 | const wchar_t* result = DeviceDiscovery_GetRuntimeFileDestinationWow64(this->instance, device, file);
234 | THROW_IF_ERROR(nullptr);
235 | return result;
236 | }
237 |
238 | inline bool IsDeviceIntegrated(unsigned int device)
239 | {
240 | int result = DeviceDiscovery_IsDeviceIntegrated(this->instance, device);
241 | THROW_IF_ERROR(-1);
242 | return result;
243 | }
244 |
245 | inline bool IsDeviceDetachable(unsigned int device)
246 | {
247 | int result = DeviceDiscovery_IsDeviceDetachable(this->instance, device);
248 | THROW_IF_ERROR(-1);
249 | return result;
250 | }
251 |
252 | inline bool DoesDeviceSupportDisplay(unsigned int device)
253 | {
254 | int result = DeviceDiscovery_DoesDeviceSupportDisplay(this->instance, device);
255 | THROW_IF_ERROR(-1);
256 | return result;
257 | }
258 |
259 | inline bool DoesDeviceSupportCompute(unsigned int device)
260 | {
261 | int result = DeviceDiscovery_DoesDeviceSupportCompute(this->instance, device);
262 | THROW_IF_ERROR(-1);
263 | return result;
264 | }
265 |
266 | #undef THROW_IF_ERROR
267 | };
268 |
269 | #endif
270 |
--------------------------------------------------------------------------------
/library/include/DeviceFilter.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | // Enumerate all devices
4 | #define DEVICEFILTER_ALL 0
5 |
6 | // Enumerate devices that support display, irrespective of whether they also support compute
7 | #define DEVICEFILTER_DISPLAY_SUPPORTED 1
8 |
9 | // Enumerate devices that support compute, irrespective of whether they also support display
10 | #define DEVICEFILTER_COMPUTE_SUPPORTED 2
11 |
12 | // Enumerate devices that support display and do not support compute (e.g. legacy DirectX 11 devices)
13 | #define DEVICEFILTER_DISPLAY_ONLY 3
14 |
15 | // Enumerate devices that support compute and do not support display (i.e. compute-only DirectX 12 devices)
16 | #define DEVICEFILTER_COMPUTE_ONLY 4
17 |
18 | // Enumerate devices that support both display and compute (i.e. fully-featured DirectX 12 devices)
19 | #define DEVICEFILTER_DISPLAY_AND_COMPUTE 5
20 |
21 |
22 | #ifdef __cplusplus
23 |
24 | #include
25 |
26 | // Device filter enum for C++ clients
27 | enum class DeviceFilter : int
28 | {
29 | AllDevices = DEVICEFILTER_ALL,
30 | DisplaySupported = DEVICEFILTER_DISPLAY_SUPPORTED,
31 | ComputeSupported = DEVICEFILTER_COMPUTE_SUPPORTED,
32 | DisplayOnly = DEVICEFILTER_DISPLAY_ONLY,
33 | ComputeOnly = DEVICEFILTER_COMPUTE_ONLY,
34 | DisplayAndCompute = DEVICEFILTER_DISPLAY_AND_COMPUTE
35 | };
36 |
37 | // Returns a string representation of a device filter
38 | inline std::wstring DeviceFilterName(DeviceFilter filter)
39 | {
40 | switch (filter)
41 | {
42 | case DeviceFilter::AllDevices:
43 | return L"AllDevices";
44 |
45 | case DeviceFilter::DisplaySupported:
46 | return L"DisplaySupported";
47 |
48 | case DeviceFilter::ComputeSupported:
49 | return L"ComputeSupported";
50 |
51 | case DeviceFilter::DisplayOnly:
52 | return L"DisplayOnly";
53 |
54 | case DeviceFilter::ComputeOnly:
55 | return L"ComputeOnly";
56 |
57 | case DeviceFilter::DisplayAndCompute:
58 | return L"DisplayAndCompute";
59 |
60 | default:
61 | return L"";
62 | }
63 | }
64 |
65 | #endif
66 |
--------------------------------------------------------------------------------
/library/src/Adapter.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | // Represents a DirectX adapter as enumerated by DXCore
4 | // (For additional details, see: )
5 | struct Adapter
6 | {
7 | inline Adapter() :
8 | InstanceLuid(0),
9 | IsHardware(false),
10 | IsIntegrated(false),
11 | IsDetachable(false),
12 | SupportsDisplay(false),
13 | SupportsCompute(false)
14 | {}
15 |
16 | // The locally unique identifier (LUID) for the adapter
17 | int64_t InstanceLuid;
18 |
19 | // The PnP hardware ID information for the adapter
20 | DXCoreHardwareID HardwareID;
21 |
22 | // Specifies whether the adapter is a hardware device (as opposed to a software device)
23 | bool IsHardware;
24 |
25 | // Specifies whether the adapter is an integrated GPU (as opposed to a discrete GPU)
26 | bool IsIntegrated;
27 |
28 | // Specifies whether the adapter is a detachable device (i.e. the device can be removed at runtime)
29 | bool IsDetachable;
30 |
31 | // Specifies whether the adapter supports display
32 | // (i.e. supports either the DXCORE_ADAPTER_ATTRIBUTE_D3D11_GRAPHICS or DXCORE_ADAPTER_ATTRIBUTE_D3D12_GRAPHICS attributes)
33 | bool SupportsDisplay;
34 |
35 | // Specifies whether the adapter supports compute (i.e. supports the DXCORE_ADAPTER_ATTRIBUTE_D3D12_CORE_COMPUTE attribute)
36 | bool SupportsCompute;
37 | };
38 |
--------------------------------------------------------------------------------
/library/src/AdapterEnumeration.cpp:
--------------------------------------------------------------------------------
1 | #include "AdapterEnumeration.h"
2 | #include "ErrorHandling.h"
3 | #include "ObjectHelpers.h"
4 |
5 | #include
6 |
7 | AdapterEnumeration::AdapterEnumeration()
8 | {
9 | // Create our DXCore adapter factory
10 | auto error = CheckHresult(DXCoreCreateAdapterFactory(this->adapterFactory.put()));
11 | if (error) {
12 | throw error.Wrap(L"DXCoreCreateAdapterFactory failed");
13 | }
14 | }
15 |
16 | void AdapterEnumeration::EnumerateAdapters(const DeviceFilter& filter, bool includeIntegrated, bool includeDetachable)
17 | {
18 | // Log our enumeration parameters
19 | LOG(
20 | L"Enumerating DirectX adapters using parameters: {{ filter:{}, includeIntegrated:{}, includeDetachable:{} }}",
21 | DeviceFilterName(filter),
22 | includeIntegrated,
23 | includeDetachable
24 | );
25 |
26 | // Clear our adapter lists and our set of unique adapters
27 | this->adapterLists.clear();
28 | this->uniqueAdapters.clear();
29 |
30 | #define ENUMERATE_ADAPTERS(attribute)\
31 | {\
32 | GUID attributes[]{ attribute };\
33 | this->adapterLists.push_back(nullptr); \
34 | auto error = CheckHresult(this->adapterFactory->CreateAdapterList(_countof(attributes), attributes, this->adapterLists.back().put()));\
35 | if (error) { \
36 | throw error.Wrap(L"IDXCoreAdapterFactory::CreateAdapterList() failed for attribute " + wstring(L#attribute));\
37 | }\
38 | }
39 |
40 | // Enumerate adapters that support Direct3D 11
41 | if (filter != DeviceFilter::ComputeOnly && filter != DeviceFilter::DisplayAndCompute) {
42 | ENUMERATE_ADAPTERS(DXCORE_ADAPTER_ATTRIBUTE_D3D11_GRAPHICS);
43 | }
44 |
45 | // Enumerate adapters that support Direct3D 12
46 | if (filter != DeviceFilter::ComputeOnly) {
47 | ENUMERATE_ADAPTERS(DXCORE_ADAPTER_ATTRIBUTE_D3D12_GRAPHICS);
48 | }
49 |
50 | // Enumerate adapters that support Direct3D 12 Core
51 | if (filter != DeviceFilter::DisplayOnly) {
52 | ENUMERATE_ADAPTERS(DXCORE_ADAPTER_ATTRIBUTE_D3D12_CORE_COMPUTE);
53 | }
54 |
55 | #undef ENUMERATE_ADAPTERS
56 |
57 | // Process each of the enumerated adapters and apply our filtering criteria
58 | for (auto const& adapters : this->adapterLists)
59 | {
60 | const uint32_t count = adapters->GetAdapterCount();
61 | for (uint32_t index = 0; index < count; ++index)
62 | {
63 | // Extract the details for the current adapter
64 | com_ptr adapter;
65 | auto error = CheckHresult(adapters->GetAdapter(index, adapter.put()));
66 | if (error) {
67 | throw error.Wrap(L"IDXCoreAdapterList::GetAdapter() failed for index " + std::to_wstring(index));
68 | }
69 | Adapter details = this->ExtractAdapterDetails(adapter);
70 |
71 | // Ignore software devices
72 | if (!details.IsHardware) {
73 | continue;
74 | }
75 |
76 | // If the adapter does not match our filter mode then ignore it
77 | if ((filter == DeviceFilter::DisplayOnly && details.SupportsCompute) ||
78 | (filter == DeviceFilter::ComputeOnly && details.SupportsDisplay) ||
79 | (filter == DeviceFilter::DisplayAndCompute && (!details.SupportsDisplay || !details.SupportsCompute))) {
80 | continue;
81 | }
82 |
83 | // If the adapter is integrated and we are not including integrated devices then ignore it
84 | if (details.IsIntegrated && !includeIntegrated) {
85 | continue;
86 | }
87 |
88 | // If the adapter is detachable and we are not including detachable devices then ignore it
89 | if (details.IsDetachable && !includeDetachable) {
90 | continue;
91 | }
92 |
93 | // Add the adapter to our set of unique adapters
94 | this->uniqueAdapters.insert(std::make_pair(details.InstanceLuid, details));
95 | }
96 | }
97 |
98 | // Log the list of unique adapter LUIDs
99 | LOG(L"Enumerated DirectX adapters with LUIDs: {}", FMT(ObjectHelpers::GetMappingKeys(this->uniqueAdapters)));
100 | }
101 |
102 | const map& AdapterEnumeration::GetUniqueAdapters() const {
103 | return this->uniqueAdapters;
104 | }
105 |
106 | bool AdapterEnumeration::IsStale() const
107 | {
108 | // If we have not yet performed enumeration then report that our data is stale
109 | if (this->adapterLists.empty())
110 | {
111 | LOG(L"No adapter lists yet, need to perform enumeration");
112 | return true;
113 | }
114 |
115 | // If any of our adapter lists are stale then our data is stale
116 | for (auto const& list : this->adapterLists)
117 | {
118 | if (list->IsStale())
119 | {
120 | LOG(L"Found stale adapter list");
121 | return true;
122 | }
123 | }
124 |
125 | return false;
126 | }
127 |
128 | Adapter AdapterEnumeration::ExtractAdapterDetails(const com_ptr& adapter) const
129 | {
130 | Adapter details;
131 | DeviceDiscoveryError error;
132 |
133 | // Extract the adapter LUID and convert it to an int64_t
134 | LUID instanceLuid;
135 | error = CheckHresult(adapter->GetProperty(DXCoreAdapterProperty::InstanceLuid, &instanceLuid));
136 | if (error) {
137 | throw error.Wrap(L"IDXCoreAdapter::GetProperty() failed for property InstanceLuid");
138 | }
139 | details.InstanceLuid = Int64FromLuid(instanceLuid);
140 |
141 | // Extract the PnP hardware ID information
142 | error = CheckHresult(adapter->GetProperty(DXCoreAdapterProperty::HardwareID, &details.HardwareID));
143 | if (error) {
144 | throw error.Wrap(L"IDXCoreAdapter::GetProperty() failed for property HardwareID");
145 | }
146 |
147 | // Extract the boolean specifying whether the adapter is a hardware device
148 | error = CheckHresult(adapter->GetProperty(DXCoreAdapterProperty::IsHardware, &details.IsHardware));
149 | if (error) {
150 | throw error.Wrap(L"IDXCoreAdapter::GetProperty() failed for property IsHardware");
151 | }
152 |
153 | // Extract the boolean specifying whether the adapter is an integrated GPU
154 | error = CheckHresult(adapter->GetProperty(DXCoreAdapterProperty::IsIntegrated, &details.IsIntegrated));
155 | if (error) {
156 | throw error.Wrap(L"IDXCoreAdapter::GetProperty() failed for property IsIntegrated");
157 | }
158 |
159 | // Extract the boolean specifying whether the adapter is detachable
160 | error = CheckHresult(adapter->GetProperty(DXCoreAdapterProperty::IsDetachable, &details.IsDetachable));
161 | if (error) {
162 | throw error.Wrap(L"IDXCoreAdapter::GetProperty() failed for property IsDetachable");
163 | }
164 |
165 | // Determine whether the adapter supports display
166 | details.SupportsDisplay =
167 | adapter->IsAttributeSupported(DXCORE_ADAPTER_ATTRIBUTE_D3D11_GRAPHICS) ||
168 | adapter->IsAttributeSupported(DXCORE_ADAPTER_ATTRIBUTE_D3D12_GRAPHICS);
169 |
170 | // Determine whether the adapter supports compute
171 | details.SupportsCompute = adapter->IsAttributeSupported(DXCORE_ADAPTER_ATTRIBUTE_D3D12_GRAPHICS);
172 |
173 | return details;
174 | }
175 |
--------------------------------------------------------------------------------
/library/src/AdapterEnumeration.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "Adapter.h"
4 | #include "DeviceFilter.h"
5 |
6 | using std::map;
7 | using std::vector;
8 | using winrt::com_ptr;
9 |
10 | class AdapterEnumeration
11 | {
12 | public:
13 | AdapterEnumeration();
14 |
15 | // Enumerates the DirectX adapters that meet the specified filtering criteria
16 | void EnumerateAdapters(const DeviceFilter& filter, bool includeIntegrated, bool includeDetachable);
17 |
18 | // Retrieves the list of unique adapters retrieved during the last enumeration operation
19 | const map& GetUniqueAdapters() const;
20 |
21 | // Determines whether the list of adapters is stale and needs to be refreshed by performing enumeration again
22 | bool IsStale() const;
23 |
24 | private:
25 |
26 | // Extracts the details from a DXCore adapter object
27 | Adapter ExtractAdapterDetails(const com_ptr& adapter) const;
28 |
29 | // Our DXCore adapter factory
30 | com_ptr adapterFactory;
31 |
32 | // Our collection of DXCore adapter lists, used for enumerating adapters with various capabilities
33 | vector< com_ptr > adapterLists;
34 |
35 | // The list of unique adapters retrieved during the last enumeration operation, keyed by adapter LUID
36 | map uniqueAdapters;
37 | };
38 |
--------------------------------------------------------------------------------
/library/src/D3DHelpers.cpp:
--------------------------------------------------------------------------------
1 | #include "D3DHelpers.h"
2 | #include "ErrorHandling.h"
3 | #include "ObjectHelpers.h"
4 |
5 | QueryD3DRegistryInfo::QueryD3DRegistryInfo()
6 | {
7 | this->Resize(0);
8 | this->RegistryInfo->PhysicalAdapterIndex = 0;
9 | }
10 |
11 | void QueryD3DRegistryInfo::SetFilesystemQuery(D3DDDI_QUERYREGISTRY_TYPE queryType)
12 | {
13 | ZeroMemory(this->RegistryInfo->ValueName, sizeof(wchar_t) * MAX_PATH);
14 | this->RegistryInfo->QueryFlags.TranslatePath = 0;
15 | this->RegistryInfo->QueryType = queryType;
16 | this->RegistryInfo->ValueType = 0;
17 | }
18 |
19 | void QueryD3DRegistryInfo::SetAdapterKeyQuery(wstring_view name, ULONG valueType, bool translatePaths)
20 | {
21 | memcpy(this->RegistryInfo->ValueName, name.data(), sizeof(wchar_t) * name.size());
22 | this->RegistryInfo->QueryFlags.TranslatePath = (translatePaths ? 1 : 0);
23 | this->RegistryInfo->QueryType = D3DDDI_QUERYREGISTRY_ADAPTERKEY;
24 | this->RegistryInfo->ValueType = valueType;
25 | }
26 |
27 | void QueryD3DRegistryInfo::Resize(size_t trailingBuffer)
28 | {
29 | // Allocate memory for the new struct + buffer
30 | this->PrivateDataSize = sizeof(D3DDDI_QUERYREGISTRY_INFO) + trailingBuffer;
31 | auto newData = std::make_unique(this->PrivateDataSize);
32 |
33 | // If we have existing struct values then copy them over to the new struct
34 | if (this->PrivateData) {
35 | memcpy(newData.get(), this->PrivateData.get(), sizeof(D3DDDI_QUERYREGISTRY_INFO));
36 | }
37 |
38 | // Release the existing data (if any) and update our struct pointer
39 | this->PrivateData = std::move(newData);
40 | this->RegistryInfo = reinterpret_cast(this->PrivateData.get());
41 | }
42 |
43 | void QueryD3DRegistryInfo::PerformQuery(unique_adapter_handle& adapter)
44 | {
45 | while (true)
46 | {
47 | // Attempt to perform the query
48 | auto adapterQuery = this->CreateAdapterQuery(adapter);
49 | auto error = CheckNtStatus(D3DKMTQueryAdapterInfo(&adapterQuery));
50 | if (error) {
51 | throw error.Wrap(L"D3DKMTQueryAdapterInfo failed");
52 | }
53 |
54 | // Determine whether we need to resize the trailing buffer and try again
55 | if (this->RegistryInfo->Status == D3DDDI_QUERYREGISTRY_STATUS_BUFFER_OVERFLOW) {
56 | this->Resize(this->RegistryInfo->OutputValueSize);
57 | }
58 | else {
59 | return;
60 | }
61 | }
62 | }
63 |
64 | D3DKMT_QUERYADAPTERINFO QueryD3DRegistryInfo::CreateAdapterQuery(unique_adapter_handle& adapter)
65 | {
66 | auto adapterQuery = ObjectHelpers::GetZeroedStruct();
67 | adapterQuery.hAdapter = adapter.get();
68 | adapterQuery.Type = KMTQAITYPE_QUERYREGISTRY;
69 | adapterQuery.pPrivateDriverData = this->PrivateData.get();
70 | adapterQuery.PrivateDriverDataSize = this->PrivateDataSize;
71 | return adapterQuery;
72 | }
73 |
--------------------------------------------------------------------------------
/library/src/D3DHelpers.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | using std::wstring_view;
4 |
5 |
6 | // Closes the supplied DirectX adapter handle
7 | inline NTSTATUS CloseAdapter(D3DKMT_HANDLE adapter)
8 | {
9 | D3DKMT_CLOSEADAPTER close;
10 | close.hAdapter = adapter;
11 | return D3DKMTCloseAdapter(&close);
12 | }
13 |
14 | // Auto-releasing resource wrapper type for DirectX adapter handles
15 | typedef wil::unique_any unique_adapter_handle;
16 |
17 |
18 | // Encapsulates a D3DDDI_QUERYREGISTRY_INFO struct, along with its trailing buffer for receiving output data
19 | class QueryD3DRegistryInfo
20 | {
21 | public:
22 |
23 | // Use this to access the struct's member fields
24 | D3DDDI_QUERYREGISTRY_INFO* RegistryInfo;
25 |
26 | QueryD3DRegistryInfo();
27 |
28 | // Populates the struct fields for querying a filesystem path
29 | void SetFilesystemQuery(D3DDDI_QUERYREGISTRY_TYPE queryType);
30 |
31 | // Populates the struct fields for querying a registry value from the adapter key
32 | void SetAdapterKeyQuery(wstring_view name, ULONG valueType, bool translatePaths);
33 |
34 | // Resizes the trailing buffer
35 | void Resize(size_t trailingBuffer);
36 |
37 | // Performs a registry query against the specified adapter, resizing the trailing buffer to accommodate the output data size as needed
38 | void PerformQuery(unique_adapter_handle& adapter);
39 |
40 | private:
41 |
42 | // The underlying data and size for the struct along with its trailing buffer
43 | std::unique_ptr PrivateData;
44 | size_t PrivateDataSize;
45 |
46 | // Creates a D3DKMT_QUERYADAPTERINFO struct that wraps struct and its trailing buffer
47 | D3DKMT_QUERYADAPTERINFO CreateAdapterQuery(unique_adapter_handle& adapter);
48 | };
49 |
--------------------------------------------------------------------------------
/library/src/Device.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "Adapter.h"
4 |
5 | using std::vector;
6 | using std::wstring;
7 |
8 |
9 | // Represents an additional file that needs to be copied from the driver store to the system directory in order to use a device with non-DirectX runtimes
10 | // (For details, see: )
11 | struct RuntimeFile
12 | {
13 | RuntimeFile(wstring SourcePath, wstring DestinationFilename)
14 | {
15 | this->SourcePath = SourcePath;
16 | this->DestinationFilename = DestinationFilename;
17 |
18 | // If no destination filename was specified then use the filename from the source path
19 | if (this->DestinationFilename.empty()) {
20 | this->DestinationFilename = std::filesystem::path(this->SourcePath).filename().wstring();
21 | }
22 | }
23 |
24 | // The relative path to the file in the driver store
25 | wstring SourcePath;
26 |
27 | // The filename that the file should be given when copied to the destination directory
28 | wstring DestinationFilename;
29 | };
30 |
31 |
32 | // Represents the underlying PnP device associated with a DirectX adapter
33 | struct Device
34 | {
35 | // The DirectX adapter associated with the PnP device
36 | Adapter DeviceAdapter;
37 |
38 | // The unique PNP hardware identifier for the device
39 | wstring ID;
40 |
41 | // A human-readable description of the device (e.g. the model name)
42 | wstring Description;
43 |
44 | // The registry key that contains the driver details for the device
45 | wstring DriverRegistryKey;
46 |
47 | // The absolute path to the directory in the driver store that contains the driver files for the device
48 | wstring DriverStorePath;
49 |
50 | // The path to the physical location of the device in the system
51 | wstring LocationPath;
52 |
53 | // The list of additional files that need to be copied from the driver store to the System32 directory in order to use the device with non-DirectX runtimes
54 | vector RuntimeFiles;
55 |
56 | // The list of additional files that need to be copied from the driver store to the SysWOW64 directory in order to use the device with non-DirectX runtimes
57 | vector RuntimeFilesWow64;
58 |
59 | // The vendor of the device (e.g. AMD, Intel, NVIDIA)
60 | wstring Vendor;
61 | };
62 |
--------------------------------------------------------------------------------
/library/src/DeviceDiscovery.cpp:
--------------------------------------------------------------------------------
1 | #include "DeviceDiscovery.h"
2 | #include "DeviceDiscoveryImp.h"
3 |
4 | #define LIBRARY_VERSION L"0.0.1"
5 |
6 | #define INSTANCE (reinterpret_cast(instance))
7 |
8 | const wchar_t* GetDiscoveryLibraryVersion() {
9 | return LIBRARY_VERSION;
10 | }
11 |
12 | void DisableDiscoveryLogging() {
13 | spdlog::set_level(spdlog::level::off);
14 | }
15 |
16 | void EnableDiscoveryLogging()
17 | {
18 | spdlog::set_pattern("%^[directx-device-discovery.dll %Y-%m-%dT%T%z]%$ [%s:%# %!] %v", spdlog::pattern_time_type::local);
19 | spdlog::set_level(spdlog::level::info);
20 | spdlog::flush_on(spdlog::level::info);
21 | }
22 |
23 | DeviceDiscoveryInstance CreateDeviceDiscoveryInstance() {
24 | return new DeviceDiscoveryImp();
25 | }
26 |
27 | void DestroyDeviceDiscoveryInstance(DeviceDiscoveryInstance instance) {
28 | delete INSTANCE;
29 | }
30 |
31 | const wchar_t* DeviceDiscovery_GetLastErrorMessage(DeviceDiscoveryInstance instance) {
32 | return INSTANCE->GetLastErrorMessage();
33 | }
34 |
35 | int DeviceDiscovery_IsRefreshRequired(DeviceDiscoveryInstance instance) {
36 | return INSTANCE->IsRefreshRequired();
37 | }
38 |
39 | int DeviceDiscovery_DiscoverDevices(DeviceDiscoveryInstance instance, int filter, int includeIntegrated, int includeDetachable)
40 | {
41 | bool success = INSTANCE->DiscoverDevices(static_cast(filter), includeIntegrated, includeDetachable);
42 | return (success ? 0 : -1);
43 | }
44 |
45 | int DeviceDiscovery_GetNumDevices(DeviceDiscoveryInstance instance) {
46 | return INSTANCE->GetNumDevices();
47 | }
48 |
49 | long long DeviceDiscovery_GetDeviceAdapterLUID(DeviceDiscoveryInstance instance, unsigned int device) {
50 | return INSTANCE->GetDeviceAdapterLUID(device);
51 | }
52 |
53 | const wchar_t* DeviceDiscovery_GetDeviceID(DeviceDiscoveryInstance instance, unsigned int device) {
54 | return INSTANCE->GetDeviceID(device);
55 | }
56 |
57 | const wchar_t* DeviceDiscovery_GetDeviceDescription(DeviceDiscoveryInstance instance, unsigned int device) {
58 | return INSTANCE->GetDeviceDescription(device);
59 | }
60 |
61 | const wchar_t* DeviceDiscovery_GetDeviceDriverRegistryKey(DeviceDiscoveryInstance instance, unsigned int device) {
62 | return INSTANCE->GetDeviceDriverRegistryKey(device);
63 | }
64 |
65 | const wchar_t* DeviceDiscovery_GetDeviceDriverStorePath(DeviceDiscoveryInstance instance, unsigned int device) {
66 | return INSTANCE->GetDeviceDriverStorePath(device);
67 | }
68 |
69 | const wchar_t* DeviceDiscovery_GetDeviceLocationPath(DeviceDiscoveryInstance instance, unsigned int device) {
70 | return INSTANCE->GetDeviceLocationPath(device);
71 | }
72 |
73 | const wchar_t* DeviceDiscovery_GetDeviceVendor(DeviceDiscoveryInstance instance, unsigned int device) {
74 | return INSTANCE->GetDeviceVendor(device);
75 | }
76 |
77 | int DeviceDiscovery_GetNumRuntimeFiles(DeviceDiscoveryInstance instance, unsigned int device) {
78 | return INSTANCE->GetNumRuntimeFiles(device);
79 | }
80 |
81 | const wchar_t* DeviceDiscovery_GetRuntimeFileSource(DeviceDiscoveryInstance instance, unsigned int device, unsigned int file) {
82 | return INSTANCE->GetRuntimeFileSource(device, file);
83 | }
84 |
85 | const wchar_t* DeviceDiscovery_GetRuntimeFileDestination(DeviceDiscoveryInstance instance, unsigned int device, unsigned int file) {
86 | return INSTANCE->GetRuntimeFileDestination(device, file);
87 | }
88 |
89 | int DeviceDiscovery_GetNumRuntimeFilesWow64(DeviceDiscoveryInstance instance, unsigned int device) {
90 | return INSTANCE->GetNumRuntimeFilesWow64(device);
91 | }
92 |
93 | const wchar_t* DeviceDiscovery_GetRuntimeFileSourceWow64(DeviceDiscoveryInstance instance, unsigned int device, unsigned int file) {
94 | return INSTANCE->GetRuntimeFileSourceWow64(device, file);
95 | }
96 |
97 | const wchar_t* DeviceDiscovery_GetRuntimeFileDestinationWow64(DeviceDiscoveryInstance instance, unsigned int device, unsigned int file) {
98 | return INSTANCE->GetRuntimeFileDestinationWow64(device, file);
99 | }
100 |
101 | int DeviceDiscovery_IsDeviceIntegrated(DeviceDiscoveryInstance instance, unsigned int device) {
102 | return INSTANCE->IsDeviceIntegrated(device);
103 | }
104 |
105 | int DeviceDiscovery_IsDeviceDetachable(DeviceDiscoveryInstance instance, unsigned int device) {
106 | return INSTANCE->IsDeviceDetachable(device);
107 | }
108 |
109 | int DeviceDiscovery_DoesDeviceSupportDisplay(DeviceDiscoveryInstance instance, unsigned int device) {
110 | return INSTANCE->DoesDeviceSupportDisplay(device);
111 | }
112 |
113 | int DeviceDiscovery_DoesDeviceSupportCompute(DeviceDiscoveryInstance instance, unsigned int device) {
114 | return INSTANCE->DoesDeviceSupportCompute(device);
115 | }
116 |
--------------------------------------------------------------------------------
/library/src/DeviceDiscoveryImp.cpp:
--------------------------------------------------------------------------------
1 | #include "DeviceDiscoveryImp.h"
2 | #include "ErrorHandling.h"
3 | #include "RegistryQuery.h"
4 |
5 | #include
6 | #include
7 |
8 | #define RETURN_ERROR(sentinel, message) this->SetLastErrorMessage(message); return sentinel
9 | #define RETURN_SUCCESS(value) this->SetLastErrorMessage(L""); return value
10 |
11 | #define VERIFY_DEVICE(sentinel) try { this->ValidateRequestedDevice(device); } catch (const DeviceDiscoveryError& err) { RETURN_ERROR(sentinel, err.message); }
12 | #define VERIFY_FILE() if (file >= files.size()) { RETURN_ERROR(nullptr, L"requested runtime file index is invalid: " + std::to_wstring(file)); }
13 |
14 | const wchar_t* DeviceDiscoveryImp::GetLastErrorMessage() const {
15 | return this->lastError.c_str();
16 | }
17 |
18 | bool DeviceDiscoveryImp::IsRefreshRequired()
19 | {
20 | // Make sure WinRT is initialised for the calling thread
21 | Windows::Foundation::Initialize(RO_INIT_MULTITHREADED);
22 |
23 | // We require a refresh if we have no data or we have stale data
24 | return (this->HaveDevices()) ? this->enumeration->IsStale() : true;
25 | }
26 |
27 | bool DeviceDiscoveryImp::DiscoverDevices(DeviceFilter filter, bool includeIntegrated, bool includeDetachable)
28 | {
29 | // Make sure WinRT is initialised for the calling thread
30 | Windows::Foundation::Initialize(RO_INIT_MULTITHREADED);
31 |
32 | try
33 | {
34 | // If this is the first time we're performing device discovery then create our helper objects
35 | if (!this->HaveDevices())
36 | {
37 | this->enumeration = std::make_unique();
38 | this->wmi = std::make_unique();
39 | }
40 |
41 | // Enumerate the DirectX adapters that meet the supplied filtering criteria
42 | this->enumeration->EnumerateAdapters(filter, includeIntegrated, includeDetachable);
43 |
44 | // Retrieve the PnP device details from WMI for each of the enumerated adapters
45 | this->devices = this->wmi->GetDevicesForAdapters(this->enumeration->GetUniqueAdapters());
46 |
47 | // Retrieve the driver details from the registry for each of the devices
48 | for (auto& device : this->devices) {
49 | RegistryQuery::FillDriverDetails(device);
50 | }
51 |
52 | RETURN_SUCCESS(true);
53 | }
54 | catch (const DeviceDiscoveryError& err) {
55 | RETURN_ERROR(false, err.Pretty());
56 | }
57 | catch (const std::runtime_error& err) {
58 | RETURN_ERROR(false, winrt::to_hstring(err.what()));
59 | }
60 | }
61 |
62 | int DeviceDiscoveryImp::GetNumDevices()
63 | {
64 | // Verify that we have a device list
65 | if (!this->HaveDevices()) {
66 | RETURN_ERROR(-1, L"attempted to retrieve device count before performing device discovery");
67 | }
68 |
69 | RETURN_SUCCESS(this->devices.size());
70 | }
71 |
72 | long long DeviceDiscoveryImp::GetDeviceAdapterLUID(unsigned int device)
73 | {
74 | // Verify that the requested device exists
75 | VERIFY_DEVICE(-1);
76 |
77 | // Retrieve the adapter LUID of the specified device
78 | RETURN_SUCCESS(this->devices[device].DeviceAdapter.InstanceLuid);
79 | }
80 |
81 | const wchar_t* DeviceDiscoveryImp::GetDeviceID(unsigned int device)
82 | {
83 | // Verify that the requested device exists
84 | VERIFY_DEVICE(nullptr);
85 |
86 | // Retrieve the ID of the specified device
87 | RETURN_SUCCESS(this->devices[device].ID.c_str());
88 | }
89 |
90 | const wchar_t* DeviceDiscoveryImp::GetDeviceDescription(unsigned int device)
91 | {
92 | // Verify that the requested device exists
93 | VERIFY_DEVICE(nullptr);
94 |
95 | // Retrieve the human-readable description of the specified device
96 | RETURN_SUCCESS(this->devices[device].Description.c_str());
97 | }
98 |
99 | const wchar_t* DeviceDiscoveryImp::GetDeviceDriverRegistryKey(unsigned int device)
100 | {
101 | // Verify that the requested device exists
102 | VERIFY_DEVICE(nullptr);
103 |
104 | // Retrieve the path of the registry key with the driver details for the specified device
105 | RETURN_SUCCESS(this->devices[device].DriverRegistryKey.c_str());
106 | }
107 |
108 | const wchar_t* DeviceDiscoveryImp::GetDeviceDriverStorePath(unsigned int device)
109 | {
110 | // Verify that the requested device exists
111 | VERIFY_DEVICE(nullptr);
112 |
113 | // Retrieve the absolute path to the driver store directory for the specified device
114 | RETURN_SUCCESS(this->devices[device].DriverStorePath.c_str());
115 | }
116 |
117 | const wchar_t* DeviceDiscoveryImp::GetDeviceLocationPath(unsigned int device)
118 | {
119 | // Verify that the requested device exists
120 | VERIFY_DEVICE(nullptr);
121 |
122 | // Retrieve the physical location path of the specified device
123 | RETURN_SUCCESS(this->devices[device].LocationPath.c_str());
124 | }
125 |
126 | const wchar_t* DeviceDiscoveryImp::GetDeviceVendor(unsigned int device)
127 | {
128 | // Verify that the requested device exists
129 | VERIFY_DEVICE(nullptr);
130 |
131 | // Retrieve the vendor of the specified device
132 | RETURN_SUCCESS(this->devices[device].Vendor.c_str());
133 | }
134 |
135 | int DeviceDiscoveryImp::GetNumRuntimeFiles(unsigned int device)
136 | {
137 | // Verify that the requested device exists
138 | VERIFY_DEVICE(-1);
139 |
140 | // Retrieve the number of additional runtime files for the device
141 | RETURN_SUCCESS(this->devices[device].RuntimeFiles.size());
142 | }
143 |
144 | const wchar_t* DeviceDiscoveryImp::GetRuntimeFileSource(unsigned int device, unsigned int file)
145 | {
146 | // Verify that the requested device exists
147 | VERIFY_DEVICE(nullptr);
148 |
149 | // Verify that the requested file entry exists
150 | const vector& files = this->devices[device].RuntimeFiles;
151 | VERIFY_FILE();
152 |
153 | // Retrieve the source path for the file
154 | RETURN_SUCCESS(files[file].SourcePath.c_str());
155 | }
156 |
157 | const wchar_t* DeviceDiscoveryImp::GetRuntimeFileDestination(unsigned int device, unsigned int file)
158 | {
159 | // Verify that the requested device exists
160 | VERIFY_DEVICE(nullptr);
161 |
162 | // Verify that the requested file entry exists
163 | const vector& files = this->devices[device].RuntimeFiles;
164 | VERIFY_FILE();
165 |
166 | // Retrieve the destination filename for the file
167 | RETURN_SUCCESS(files[file].DestinationFilename.c_str());
168 | }
169 |
170 | int DeviceDiscoveryImp::GetNumRuntimeFilesWow64(unsigned int device)
171 | {
172 | // Verify that the requested device exists
173 | VERIFY_DEVICE(-1);
174 |
175 | // Retrieve the number of additional SysWOW64 runtime files for the device
176 | RETURN_SUCCESS(this->devices[device].RuntimeFilesWow64.size());
177 | }
178 |
179 | const wchar_t* DeviceDiscoveryImp::GetRuntimeFileSourceWow64(unsigned int device, unsigned int file)
180 | {
181 | // Verify that the requested device exists
182 | VERIFY_DEVICE(nullptr);
183 |
184 | // Verify that the requested file entry exists
185 | const vector& files = this->devices[device].RuntimeFilesWow64;
186 | VERIFY_FILE();
187 |
188 | // Retrieve the source path for the file
189 | RETURN_SUCCESS(files[file].SourcePath.c_str());
190 | }
191 |
192 | const wchar_t* DeviceDiscoveryImp::GetRuntimeFileDestinationWow64(unsigned int device, unsigned int file)
193 | {
194 | // Verify that the requested device exists
195 | VERIFY_DEVICE(nullptr);
196 |
197 | // Verify that the requested file entry exists
198 | const vector& files = this->devices[device].RuntimeFilesWow64;
199 | VERIFY_FILE();
200 |
201 | // Retrieve the destination filename for the file
202 | RETURN_SUCCESS(files[file].DestinationFilename.c_str());
203 | }
204 |
205 | int DeviceDiscoveryImp::IsDeviceIntegrated(unsigned int device)
206 | {
207 | // Verify that the requested device exists
208 | VERIFY_DEVICE(-1);
209 |
210 | // Determine whether the specified device is an integrated GPU
211 | RETURN_SUCCESS(this->devices[device].DeviceAdapter.IsIntegrated);
212 | }
213 |
214 | int DeviceDiscoveryImp::IsDeviceDetachable(unsigned int device)
215 | {
216 | // Verify that the requested device exists
217 | VERIFY_DEVICE(-1);
218 |
219 | // Determine whether the specified device is detachable
220 | RETURN_SUCCESS(this->devices[device].DeviceAdapter.IsDetachable);
221 | }
222 |
223 | int DeviceDiscoveryImp::DoesDeviceSupportDisplay(unsigned int device)
224 | {
225 | // Verify that the requested device exists
226 | VERIFY_DEVICE(-1);
227 |
228 | // Determine whether the specified device supports display
229 | RETURN_SUCCESS(this->devices[device].DeviceAdapter.SupportsDisplay);
230 | }
231 |
232 | int DeviceDiscoveryImp::DoesDeviceSupportCompute(unsigned int device)
233 | {
234 | // Verify that the requested device exists
235 | VERIFY_DEVICE(-1);
236 |
237 | // Determine whether the specified device supports compute
238 | RETURN_SUCCESS(this->devices[device].DeviceAdapter.SupportsCompute);
239 | }
240 |
241 | bool DeviceDiscoveryImp::HaveDevices() const {
242 | return (this->enumeration && this->wmi);
243 | }
244 |
245 | void DeviceDiscoveryImp::SetLastErrorMessage(std::wstring_view message) {
246 | this->lastError = message;
247 | }
248 |
249 | void DeviceDiscoveryImp::ValidateRequestedDevice(unsigned int device)
250 | {
251 | // Verify that we have a device list
252 | if (!this->HaveDevices()) {
253 | throw CreateError(L"attempted to retrieve device details before performing device discovery");
254 | }
255 |
256 | // Verify that the specified device index is valid
257 | if (device >= this->GetNumDevices()) {
258 | throw CreateError(L"requested device index is invalid: " + std::to_wstring(device));
259 | }
260 | }
261 |
--------------------------------------------------------------------------------
/library/src/DeviceDiscoveryImp.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "AdapterEnumeration.h"
4 | #include "Device.h"
5 | #include "DeviceFilter.h"
6 | #include "WmiQuery.h"
7 |
8 | using std::wstring;
9 | using std::wstring_view;
10 | using std::unique_ptr;
11 | using std::vector;
12 |
13 | class DeviceDiscoveryImp
14 | {
15 | public:
16 |
17 | DeviceDiscoveryImp() {}
18 | const wchar_t* GetLastErrorMessage() const;
19 | bool IsRefreshRequired();
20 | bool DiscoverDevices(DeviceFilter filter, bool includeIntegrated, bool includeDetachable);
21 | int GetNumDevices();
22 | long long GetDeviceAdapterLUID(unsigned int device);
23 | const wchar_t* GetDeviceID(unsigned int device);
24 | const wchar_t* GetDeviceDescription(unsigned int device);
25 | const wchar_t* GetDeviceDriverRegistryKey(unsigned int device);
26 | const wchar_t* GetDeviceDriverStorePath(unsigned int device);
27 | const wchar_t* GetDeviceLocationPath(unsigned int device);
28 | const wchar_t* GetDeviceVendor(unsigned int device);
29 | int GetNumRuntimeFiles(unsigned int device);
30 | const wchar_t* GetRuntimeFileSource(unsigned int device, unsigned int file);
31 | const wchar_t* GetRuntimeFileDestination(unsigned int device, unsigned int file);
32 | int GetNumRuntimeFilesWow64(unsigned int device);
33 | const wchar_t* GetRuntimeFileSourceWow64(unsigned int device, unsigned int file);
34 | const wchar_t* GetRuntimeFileDestinationWow64(unsigned int device, unsigned int file);
35 | int IsDeviceIntegrated(unsigned int device);
36 | int IsDeviceDetachable(unsigned int device);
37 | int DoesDeviceSupportDisplay(unsigned int device);
38 | int DoesDeviceSupportCompute(unsigned int device);
39 |
40 | private:
41 |
42 | bool HaveDevices() const;
43 | void SetLastErrorMessage(wstring_view message);
44 | void ValidateRequestedDevice(unsigned int device);
45 |
46 | vector devices;
47 | wstring lastError;
48 |
49 | unique_ptr enumeration;
50 | unique_ptr wmi;
51 | };
52 |
--------------------------------------------------------------------------------
/library/src/DllMain.cpp:
--------------------------------------------------------------------------------
1 | BOOL APIENTRY DllMain(HINSTANCE hModule, DWORD dwReason, PVOID lpReserved)
2 | {
3 | if (dwReason == DLL_PROCESS_ATTACH)
4 | {
5 | // Disable logging by default
6 | spdlog::set_level(spdlog::level::off);
7 | }
8 |
9 | return TRUE;
10 | }
11 |
--------------------------------------------------------------------------------
/library/src/ErrorHandling.cpp:
--------------------------------------------------------------------------------
1 | #include "ErrorHandling.h"
2 |
3 | DeviceDiscoveryError ErrorHandling::ErrorForNtStatus(NTSTATUS status, wstring_view file, wstring_view function, size_t line)
4 | {
5 | if (status < 0)
6 | {
7 | // Allocate a buffer to hold the error message
8 | size_t bufSize = 1024;
9 | auto buffer = std::make_unique(bufSize);
10 |
11 | // Attempt to retrieve the error message for the status code
12 | DWORD length = FormatMessageW(
13 | FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS,
14 | GetModuleHandleW(L"ntdll.dll"),
15 | status,
16 | 0,
17 | buffer.get(),
18 | bufSize,
19 | nullptr
20 | );
21 |
22 | if (length > 0)
23 | {
24 | // If the message has a trailing newline then remove it
25 | wstring message(buffer.get(), length);
26 | size_t newline = message.find_last_of(L"\r\n");
27 | if (newline != wstring::npos) {
28 | message = message.substr(0, newline - 1);
29 | }
30 |
31 | // Return an error with the retrieved message
32 | return DeviceDiscoveryError(message, file, function, line);
33 | }
34 | else
35 | {
36 | // Return an error with the hexadecimal representation of the NTSTATUS code
37 | return DeviceDiscoveryError(
38 | fmt::format(
39 | L"Unable to retrieve error message for NTSTATUS code 0x{:0>8X}",
40 | static_cast(status)
41 | ),
42 | file,
43 | function,
44 | line
45 | );
46 | }
47 | }
48 |
49 | return DeviceDiscoveryError(L"", file, function, line);
50 | }
51 |
--------------------------------------------------------------------------------
/library/src/ErrorHandling.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | using std::wstring;
4 | using std::wstring_view;
5 |
6 |
7 | // Define Unicode versions of __FILE__ and __FUNCTION__
8 | #define __WIDE2(x) L##x
9 | #define __WIDE1(x) __WIDE2(x)
10 | #define __WFILE__ __WIDE1(__FILE__)
11 | #define __WFUNCTION__ __WIDE1(__FUNCTION__)
12 |
13 |
14 | // The exception type used to represent all errors inside the device discovery library
15 | class DeviceDiscoveryError
16 | {
17 | public:
18 | inline DeviceDiscoveryError() : message(L""), file(L""), function(L""), line(0) {}
19 |
20 | inline DeviceDiscoveryError(wstring_view message, wstring_view file, wstring_view function, size_t line) :
21 | message(message), file(file), function(function), line(line)
22 | {}
23 |
24 | inline DeviceDiscoveryError(wstring_view message, const DeviceDiscoveryError& inner) :
25 | file(inner.file), function(inner.function), line(inner.line)
26 | {
27 | this->message = wstring(message) + L": " + inner.message;
28 | }
29 |
30 | DeviceDiscoveryError(const DeviceDiscoveryError& other) = default;
31 | DeviceDiscoveryError(DeviceDiscoveryError&& other) = default;
32 | DeviceDiscoveryError& operator=(const DeviceDiscoveryError& other) = default;
33 | DeviceDiscoveryError& operator=(DeviceDiscoveryError&& other) = default;
34 |
35 | inline operator bool() const {
36 | return (!this->message.empty());
37 | }
38 |
39 | // Wraps this error in a surrounding error message
40 | inline DeviceDiscoveryError Wrap(wstring_view message) const {
41 | return DeviceDiscoveryError(message, *this);
42 | }
43 |
44 | // Formats the error details as a pretty string
45 | inline wstring Pretty() const
46 | {
47 | // Extract the filename from the file path
48 | wstring filename = std::filesystem::path(this->file).filename().wstring();
49 |
50 | // Append the filename, line number and function name to the error message
51 | wstring fileAndLine = filename + L":" + std::to_wstring(this->line);
52 | return this->message + L" [" + fileAndLine + L" " + this->function + L"]";
53 | }
54 |
55 | wstring message;
56 | wstring file;
57 | wstring function;
58 | size_t line;
59 | };
60 |
61 |
62 | // Provides functionality related to managing errors
63 | namespace ErrorHandling
64 | {
65 | // Returns an error object representing the supplied NTSTATUS code
66 | DeviceDiscoveryError ErrorForNtStatus(NTSTATUS status, wstring_view file, wstring_view function, size_t line);
67 |
68 | // Returns an error object representing the supplied HRESULT code
69 | inline DeviceDiscoveryError ErrorForHresult(const winrt::hresult& result, wstring_view file, wstring_view function, size_t line)
70 | {
71 | try
72 | {
73 | winrt::check_hresult(result);
74 | return DeviceDiscoveryError(L"", file, function, line);
75 | }
76 | catch (const winrt::hresult_error& err) {
77 | return DeviceDiscoveryError(err.message(), file, function, line);
78 | }
79 | }
80 |
81 | // Returns an error object representing the supplied Win32 error code
82 | template
83 | inline DeviceDiscoveryError ErrorForWin32(T error, wstring_view file, wstring_view function, size_t line)
84 | {
85 | try
86 | {
87 | winrt::check_win32(error);
88 | return DeviceDiscoveryError(L"", file, function, line);
89 | }
90 | catch (const winrt::hresult_error& err) {
91 | return DeviceDiscoveryError(err.message(), file, function, line);
92 | }
93 | }
94 |
95 | // Convenience macros for automatically filling out error file, function and line details
96 | #define CreateError(message) DeviceDiscoveryError(message, __WFILE__, __WFUNCTION__, __LINE__)
97 | #define CheckNtStatus(status) ErrorHandling::ErrorForNtStatus(status, __WFILE__, __WFUNCTION__, __LINE__)
98 | #define CheckHresult(status) ErrorHandling::ErrorForHresult(status, __WFILE__, __WFUNCTION__, __LINE__)
99 | #define CheckWin32(status) ErrorHandling::ErrorForWin32(status, __WFILE__, __WFUNCTION__, __LINE__)
100 |
101 | // Catches a winrt::hresult_error object and converts it to a DeviceDiscoveryError object
102 | #define CatchHresult(error, operation) try { operation; error = DeviceDiscoveryError(); } catch (const winrt::hresult_error & err) { error = CreateError(err.message()); }
103 | }
104 |
--------------------------------------------------------------------------------
/library/src/ObjectHelpers.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | namespace ObjectHelpers {
4 |
5 |
6 | // Retrieves the list of keys for an STL associative container type (maps, sets, etc.)
7 | // For the full list of supported container types, see:
8 | // -
9 | // -
10 | template
11 | std::vector GetMappingKeys(const MappingType& mapping)
12 | {
13 | std::vector keys;
14 |
15 | for (const auto& pair : mapping) {
16 | keys.push_back(pair.first);
17 | }
18 |
19 | return keys;
20 | }
21 |
22 | // Returns a zeroed-out instance of the specified struct type
23 | template T GetZeroedStruct()
24 | {
25 | T instance;
26 | ZeroMemory(&instance, sizeof(T));
27 | return instance;
28 | }
29 |
30 |
31 | } // namespace ObjectHelpers
32 |
--------------------------------------------------------------------------------
/library/src/RegistryQuery.cpp:
--------------------------------------------------------------------------------
1 | #include "RegistryQuery.h"
2 | #include "D3DHelpers.h"
3 | #include "ErrorHandling.h"
4 | #include "ObjectHelpers.h"
5 |
6 | #include
7 | #include
8 | #include
9 |
10 | map< wstring, vector > RegistryQuery::EnumerateMultiStringValues(unique_hkey& key)
11 | {
12 | map< wstring, vector > values;
13 |
14 | LSTATUS result = ERROR_SUCCESS;
15 | for (int i = 0; ; ++i)
16 | {
17 | // Receives the type of the enumerated value
18 | DWORD valueType = 0;
19 |
20 | // Receives the name of the enumerated value
21 | DWORD nameBufsize = 256;
22 | auto valueName = std::make_unique(nameBufsize);
23 |
24 | // Receives the data of the enumerated value
25 | DWORD dataBufsize = 1024;
26 | auto valueData = std::make_unique(dataBufsize);
27 |
28 | // Retrieve the next value and check to see if we have processed all available values
29 | result = RegEnumValueW(key.get(), i, valueName.get(), &nameBufsize, nullptr, &valueType, valueData.get(), &dataBufsize);
30 | if (result == ERROR_NO_MORE_ITEMS) {
31 | break;
32 | }
33 |
34 | // Report any errors
35 | auto error = CheckWin32(result);
36 | if (error) {
37 | throw error.Wrap(L"RegEnumValueW failed");
38 | }
39 |
40 | // Verify that the value data is of type REG_MULTI_SZ
41 | wstring name = wstring(valueName.get(), nameBufsize);
42 | if (valueType != REG_MULTI_SZ) {
43 | throw CreateError(L"enumerated value was not of type REG_MULTI_SZ: " + name);
44 | }
45 |
46 | // Parse the value data and add it to our mapping
47 | auto strings = RegistryQuery::ExtractMultiStringValue((wchar_t*)(valueData.get()), dataBufsize);
48 | values.insert(std::make_pair(name, strings));
49 | }
50 |
51 | return values;
52 | }
53 |
54 | vector RegistryQuery::ExtractMultiStringValue(const wchar_t* data, size_t numBytes)
55 | {
56 | vector strings;
57 |
58 | size_t offset = 0;
59 | size_t upperBound = numBytes / sizeof(wchar_t);
60 | while (offset < upperBound)
61 | {
62 | // Extract the next string and check that it's not empty
63 | wstring nextString(data + offset);
64 | if (nextString.size() == 0) { break; }
65 |
66 | // Add the string to our list and proceed to the next one
67 | strings.push_back(nextString);
68 | offset += strings.back().size() + 1;
69 | }
70 |
71 | return strings;
72 | }
73 |
74 | unique_hkey RegistryQuery::OpenKeyFromString(wstring_view key)
75 | {
76 | // Our list of supported root keys
77 | static map rootKeys = {
78 | { L"HKEY_CLASSES_ROOT", HKEY_CLASSES_ROOT },
79 | { L"HKEY_CURRENT_CONFIG", HKEY_CURRENT_CONFIG },
80 | { L"HKEY_CURRENT_USER", HKEY_CURRENT_USER },
81 | { L"HKEY_LOCAL_MACHINE", HKEY_LOCAL_MACHINE },
82 | { L"HKEY_PERFORMANCE_DATA", HKEY_PERFORMANCE_DATA },
83 | { L"HKEY_USERS", HKEY_USERS }
84 | };
85 |
86 | // Verify that the supplied key path is well-formed
87 | size_t backslash = key.find_first_of(L"\\");
88 | if (backslash == wstring_view::npos || backslash >= (key.size()-1)) {
89 | throw CreateError(L"invalid registry key path: " + wstring(key));
90 | }
91 |
92 | // Split the root key name from the rest of the path
93 | wstring rootKeyName = wstring(key.substr(0, backslash));
94 | wstring keyPath = wstring(key.substr(backslash + 1));
95 |
96 | // Identify the handle for the specified root key
97 | auto rootKey = rootKeys.find(rootKeyName);
98 | if (rootKey == rootKeys.end()) {
99 | throw CreateError(L"unknown registry root key: " + rootKeyName);
100 | }
101 |
102 | // Attempt to open the key
103 | unique_hkey keyHandle;
104 | auto error = CheckWin32(RegOpenKeyExW(rootKey->second, keyPath.c_str(), 0, KEY_READ, keyHandle.put()));
105 | if (error) {
106 | throw error.Wrap(L"failed to open registry key " + wstring(key));
107 | }
108 |
109 | return keyHandle;
110 | }
111 |
112 | void RegistryQuery::ProcessRuntimeFiles(Device& device, wstring_view key, bool isWow64)
113 | {
114 | try
115 | {
116 | // Determine whether we are adding runtime files to the device's System32 list or SysWOW64 list
117 | auto& list = (isWow64) ? device.RuntimeFilesWow64 : device.RuntimeFiles;
118 |
119 | // Attempt to open the specified registry key and enumerate its REG_MULTI_SZ values
120 | unique_hkey registryKey = RegistryQuery::OpenKeyFromString(device.DriverRegistryKey + L"\\" + wstring(key));
121 | auto files = RegistryQuery::EnumerateMultiStringValues(registryKey);
122 | for (const auto& pair : files)
123 | {
124 | if (!pair.second.empty())
125 | {
126 | // Construct a RuntimeFile from the string values
127 | RuntimeFile newFile(pair.second[0], ((pair.second.size() == 2) ? pair.second[1] : L""));
128 |
129 | // Check whether the destination filename for the runtime file clashes with an existing file
130 | auto existing = std::find_if(list.begin(), list.end(), [newFile](RuntimeFile f) {
131 | return f.DestinationFilename == newFile.DestinationFilename;
132 | });
133 |
134 | // Only add the new runtime file to the list if there's no clash
135 | if (existing == list.end()) {
136 | list.push_back(newFile);
137 | }
138 | else {
139 | LOG(L"{}: ignoring runtime file with duplicate destination filename {}", key, newFile.DestinationFilename);
140 | }
141 | }
142 | }
143 | }
144 | catch (const DeviceDiscoveryError& err) {
145 | LOG(L"Could not enumerate runtime files for the {} key: {}", key, err.message);
146 | }
147 | }
148 |
149 | void RegistryQuery::FillDriverDetails(Device& device)
150 | {
151 | // Log the device ID to provide context for any subsequent log messages and errors
152 | LOG(L"Querying device driver registry details for device {}", device.ID);
153 |
154 | // Attempt to open the DirectX adapter for the device
155 | auto adapterDetails = ObjectHelpers::GetZeroedStruct();
156 | adapterDetails.AdapterLuid = LuidFromInt64(device.DeviceAdapter.InstanceLuid);
157 | auto error = CheckNtStatus(D3DKMTOpenAdapterFromLuid(&adapterDetails));
158 | if (error)
159 | {
160 | throw error.Wrap(
161 | L"D3DKMTOpenAdapterFromLuid failed to open adapter with LUID " +
162 | std::to_wstring(device.DeviceAdapter.InstanceLuid)
163 | );
164 | }
165 |
166 | // Ensure we automatically close the adapter handle when we finish
167 | unique_adapter_handle adapter(adapterDetails.hAdapter);
168 |
169 | // Retrieve the path to the driver store directory for the adapter
170 | QueryD3DRegistryInfo queryDriverStore;
171 | queryDriverStore.SetFilesystemQuery(D3DDDI_QUERYREGISTRY_DRIVERSTOREPATH);
172 | queryDriverStore.PerformQuery(adapter);
173 | device.DriverStorePath = wstring(queryDriverStore.RegistryInfo->OutputString);
174 |
175 | // If the driver store path begins with the "\SystemRoot" prefix then expand it
176 | wstring prefix = L"\\SystemRoot";
177 | wstring systemRoot = wstring(wil::GetEnvironmentVariableW(L"SystemRoot").get());
178 | if (device.DriverStorePath.find(prefix, 0) == 0) {
179 | device.DriverStorePath = device.DriverStorePath.replace(0, prefix.size(), systemRoot);
180 | }
181 |
182 | // Determine whether we're running on the host or inside a container
183 | // (e.g. when using a client tool to verify that a device has been mounted correctly)
184 | if (device.DriverStorePath.find(L"HostDriverStore", 0) != wstring::npos)
185 | {
186 | // We have no way of enumerating the CopyToVmWhenNewer subkey inside a container, so stop processing here
187 | LOG(L"Running inside a container, skipping runtime file enumeration");
188 | return;
189 | }
190 |
191 | // Retrieve the list of additional runtime files that need to be copied to the System32 directory
192 | RegistryQuery::ProcessRuntimeFiles(device, L"CopyToVmOverwrite", false);
193 | RegistryQuery::ProcessRuntimeFiles(device, L"CopyToVmWhenNewer", false);
194 |
195 | // Retrieve the list of additional runtime files that need to be copied to the SysWOW64 directory
196 | RegistryQuery::ProcessRuntimeFiles(device, L"CopyToVmOverwriteWow64", true);
197 | RegistryQuery::ProcessRuntimeFiles(device, L"CopyToVmWhenNewerWow64", true);
198 | }
199 |
--------------------------------------------------------------------------------
/library/src/RegistryQuery.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "Device.h"
4 |
5 | using std::map;
6 | using std::vector;
7 | using std::wstring;
8 | using std::wstring_view;
9 | using wil::unique_hkey;
10 |
11 | // Provides functionality for querying the Windows registry
12 | namespace RegistryQuery
13 | {
14 | // Enumerates the values of the supplied registry key and parses their data as REG_MULTI_SZ
15 | map< wstring, vector > EnumerateMultiStringValues(unique_hkey& key);
16 |
17 | // Extracts the individual strings of a REG_MULTI_SZ registry value
18 | vector ExtractMultiStringValue(const wchar_t* data, size_t numBytes);
19 |
20 | // Parses a registry key path and opens it using the appropriate root key
21 | unique_hkey OpenKeyFromString(wstring_view key);
22 |
23 | // Enumerates the runtime files for a device as listed under the specified registry key
24 | void ProcessRuntimeFiles(Device& device, wstring_view key, bool isWow64);
25 |
26 | // Queries the registry to retrieve driver-related details for the supplied PnP device
27 | void FillDriverDetails(Device& device);
28 | }
29 |
--------------------------------------------------------------------------------
/library/src/SafeArray.cpp:
--------------------------------------------------------------------------------
1 | #include "SafeArray.h"
2 |
3 | unique_variant SafeArrayFactory::CreateStringArray(initializer_list elems)
4 | {
5 | // Create our array bounds descriptor
6 | SAFEARRAYBOUND bounds;
7 | bounds.lLbound = 0;
8 | bounds.cElements = elems.size();
9 |
10 | // Create a VARIANT to hold our array
11 | unique_variant vtArray;
12 | vtArray.vt = VT_ARRAY | VT_BSTR;
13 |
14 | // Create the SAFEARRAY and lock it for data access
15 | vtArray.parray = SafeArrayCreate(VT_BSTR, 1, &bounds);
16 | auto error = CheckHresult(SafeArrayLock(vtArray.parray));
17 | if (error) {
18 | throw error.Wrap(L"SafeArrayLock failed");
19 | }
20 |
21 | // Populate the array with the supplied elements
22 | BSTR* array = reinterpret_cast(vtArray.parray->pvData);
23 | int index = 0;
24 | for (const auto& elem : elems)
25 | {
26 | // Note that a SAFEARRAY owns the memory of its elements, so we transfer ownership of each BSTR
27 | array[index] = wil::make_bstr(elem.c_str()).release();
28 | index++;
29 | }
30 |
31 | // Unlock the array
32 | SafeArrayUnlock(vtArray.parray);
33 | return vtArray;
34 | }
35 |
--------------------------------------------------------------------------------
/library/src/SafeArray.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "ErrorHandling.h"
4 |
5 | using std::initializer_list;
6 | using std::wstring;
7 | using wil::unique_variant;
8 |
9 |
10 | // Provides functionality for iterating over the contents of a one-dimensional SAFEARRAY
11 | template
12 | class SafeArrayIterator
13 | {
14 | public:
15 | SafeArrayIterator(SAFEARRAY* array)
16 | {
17 | // Lock the array for data access
18 | this->array = array;
19 | this->reinterpretedArray = reinterpret_cast(this->array->pvData);
20 | auto error = CheckHresult(SafeArrayLock(this->array));
21 | if (error) {
22 | throw error.Wrap(L"SafeArrayLock failed");
23 | }
24 |
25 | // Retrieve the array bounds and compute the number of elements
26 | long lowerBound = 0;
27 | long upperBound = 0;
28 | SafeArrayGetLBound(this->array, 1, &lowerBound);
29 | SafeArrayGetUBound(this->array, 1, &upperBound);
30 | this->numElements = (upperBound - lowerBound) + 1;
31 | }
32 |
33 | ~SafeArrayIterator()
34 | {
35 | // Unlock the array
36 | SafeArrayUnlock(this->array);
37 | this->array = nullptr;
38 | this->reinterpretedArray = nullptr;
39 | }
40 |
41 | T* begin() {
42 | return this->reinterpretedArray;
43 | }
44 |
45 | const T* begin() const {
46 | return this->reinterpretedArray;
47 | }
48 |
49 | T* end() {
50 | return this->reinterpretedArray + this->numElements;
51 | }
52 |
53 | const T* end() const {
54 | return this->reinterpretedArray + this->numElements;
55 | }
56 |
57 | private:
58 | SAFEARRAY* array;
59 | T* reinterpretedArray;
60 | long numElements;
61 | };
62 |
63 |
64 | // Provides functionality for creating one-dimensional SAFEARRAY instances for specific element types
65 | class SafeArrayFactory
66 | {
67 | public:
68 |
69 | // Creates a SAFEARRAY of BSTR strings and wraps it in a VARIANT
70 | static unique_variant CreateStringArray(initializer_list elems);
71 | };
72 |
--------------------------------------------------------------------------------
/library/src/WmiQuery.cpp:
--------------------------------------------------------------------------------
1 | #include "WmiQuery.h"
2 | #include "ErrorHandling.h"
3 | #include "SafeArray.h"
4 |
5 | #include
6 | #include
7 |
8 | using std::set;
9 | using wil::unique_variant;
10 | using winrt::hstring;
11 |
12 | namespace
13 | {
14 | // Device property key for retrieving the DirectX adapter LUID
15 | const DEVPROPKEY DEVPKEY_Device_AdapterLuid = {
16 | { 0x60b193cb, 0x5276, 0x4d0f, { 0x96, 0xfc, 0xf1, 0x73, 0xab, 0xad, 0x3e, 0xc6 } },
17 | 2
18 | };
19 |
20 | // Formats a DEVPROPKEY as a string in the form "{00000000-0000-0000-0000-000000000000} 0"
21 | wstring DevPropKeyToString(const DEVPROPKEY& key)
22 | {
23 | return fmt::format(
24 | L"{{{:0>8X}-{:0>4X}-{:0>4X}-{:0>2X}{:0>2X}-{:0>2X}{:0>2X}{:0>2X}{:0>2X}{:0>2X}{:0>2X}}} {}",
25 | key.fmtid.Data1,
26 | key.fmtid.Data2,
27 | key.fmtid.Data3,
28 | key.fmtid.Data4[0],
29 | key.fmtid.Data4[1],
30 | key.fmtid.Data4[2],
31 | key.fmtid.Data4[3],
32 | key.fmtid.Data4[4],
33 | key.fmtid.Data4[5],
34 | key.fmtid.Data4[6],
35 | key.fmtid.Data4[7],
36 | key.pid
37 | );
38 | }
39 |
40 | // Formats a PnP hardware ID for use in a WQL query
41 | wstring FormatHardwareID(const DXCoreHardwareID& dxHardwareID)
42 | {
43 | // Build a PCI hardware identifier string as per:
44 | //
45 | // and insert a trailing wildcard for the device instance
46 | return fmt::format(
47 | L"PCI\\\\VEN_{:0>4X}&DEV_{:0>4X}&SUBSYS_{:0>8X}&REV_{:0>2X}%",
48 | dxHardwareID.vendorID,
49 | dxHardwareID.deviceID,
50 | dxHardwareID.subSysID,
51 | dxHardwareID.revision
52 | );
53 | }
54 | }
55 |
56 | WmiQuery::WmiQuery()
57 | {
58 | // Create a reusable error object
59 | DeviceDiscoveryError error;
60 |
61 | // Generate the string identifier for the DEVPKEY_Device_AdapterLuid device property key
62 | this->devPropKeyLUID = DevPropKeyToString(DEVPKEY_Device_AdapterLuid);
63 |
64 | // Create our IWbemLocator instance
65 | CatchHresult(error, this->wbemLocator = winrt::create_instance(CLSID_WbemLocator));
66 | if (error) {
67 | throw error.Wrap(L"failed to create an IWbemLocator instance");
68 | }
69 |
70 | // Connect to the WMI service and retrieve a service proxy object
71 | error = CheckHresult(this->wbemLocator->ConnectServer(
72 | wil::make_bstr(L"ROOT\\CIMV2").get(),
73 | nullptr,
74 | nullptr,
75 | nullptr,
76 | 0,
77 | nullptr,
78 | nullptr,
79 | this->wbemServices.put()
80 | ));
81 | if (error) {
82 | throw error.Wrap(L"failed to connect to the WMI service");
83 | }
84 |
85 | // Set the security level for the service proxy
86 | error = CheckHresult(CoSetProxyBlanket(
87 | this->wbemServices.get(),
88 | RPC_C_AUTHN_WINNT,
89 | RPC_C_AUTHZ_NONE,
90 | nullptr,
91 | RPC_C_AUTHN_LEVEL_CALL,
92 | RPC_C_IMP_LEVEL_IMPERSONATE,
93 | nullptr,
94 | EOAC_NONE
95 | ));
96 | if (error) {
97 | throw error.Wrap(L"failed to set the security level for the WMI service proxy");
98 | }
99 |
100 | // Retrieve the CIM class definition for the Win32_PnPEntity class
101 | error = CheckHresult(this->wbemServices->GetObject(
102 | wil::make_bstr(L"Win32_PnPEntity").get(),
103 | 0,
104 | nullptr,
105 | this->pnpEntityClass.put(),
106 | nullptr
107 | ));
108 | if (error) {
109 | throw error.Wrap(L"failed to retrieve the CIM class definition for the Win32_PnPEntity class");
110 | }
111 |
112 | // Retrieve the input parameters class for the `GetDeviceProperties` method of the CIM class definition
113 | error = CheckHresult(this->pnpEntityClass->GetMethod(L"GetDeviceProperties", 0, this->inputParameters.put(), nullptr));
114 | if (error) {
115 | throw error.Wrap(L"failed to retrieve the input parameters class for Win32_PnPEntity::GetDeviceProperties");
116 | }
117 | }
118 |
119 | vector WmiQuery::GetDevicesForAdapters(const map& adapters)
120 | {
121 | // If we don't have any adapters then don't query WMI
122 | if (adapters.empty())
123 | {
124 | LOG(L"Empty adapter list provided, skipping WMI query");
125 | return {};
126 | }
127 |
128 | // Gather the unique PnP hardware IDs from the DirectX adapters for use in our WQL query string
129 | set hardwareIDs;
130 | for (auto const& adapter : adapters) {
131 | hardwareIDs.insert(FormatHardwareID(adapter.second.HardwareID));
132 | }
133 |
134 | // Build the WQL query string to retrieve the PnP devices associated with the adapters
135 | wstring query = L"SELECT * FROM Win32_PnPEntity WHERE Present = TRUE AND (";
136 | int index = 0;
137 | int last = hardwareIDs.size() - 1;
138 | for (auto const& id : hardwareIDs)
139 | {
140 | query += L"DeviceID LIKE \"" + id + L"\"" + ((index < last) ? L" OR " : L"");
141 | index++;
142 | }
143 | query += L")";
144 |
145 | // Log the query string
146 | LOG(L"Executing WQL query: {}", query);
147 |
148 | // Execute the query
149 | com_ptr enumerator;
150 | auto error = CheckHresult(wbemServices->ExecQuery(
151 | wil::make_bstr(L"WQL").get(),
152 | wil::make_bstr(query.c_str()).get(),
153 | 0,
154 | nullptr,
155 | enumerator.put()
156 | ));
157 | if (error) {
158 | throw error.Wrap(L"WQL query execution failed");
159 | }
160 |
161 | // Iterate over the retrieved PnP devices and match them to their corresponding DirectX adapters
162 | vector devices;
163 | for (int index = 0; ; index++)
164 | {
165 | // Retrieve the device for the current loop iteration
166 | ULONG numReturned = 0;
167 | com_ptr device;
168 | auto error = CheckHresult(enumerator->Next(WBEM_INFINITE, 1, device.put(), &numReturned));
169 | if (error) {
170 | throw error.Wrap(L"enumerating PnP devices failed");
171 | }
172 | if (numReturned == 0) {
173 | break;
174 | }
175 |
176 | // Extract the details for the device and determine whether it matches any of our adapters
177 | Device details = this->ExtractDeviceDetails(device);
178 | auto matchingAdapter = adapters.find(details.DeviceAdapter.InstanceLuid);
179 | if (matchingAdapter != adapters.end())
180 | {
181 | // Log the match
182 | LOG(L"Matched adapter LUID {} to PnP device {}", details.DeviceAdapter.InstanceLuid, details.ID);
183 |
184 | // Replace the device's adapter details with the matching adapter
185 | details.DeviceAdapter = matchingAdapter->second;
186 |
187 | // Include the device in our results
188 | devices.push_back(details);
189 | }
190 | }
191 |
192 | return devices;
193 | }
194 |
195 | Device WmiQuery::ExtractDeviceDetails(const com_ptr& device) const
196 | {
197 | Device details;
198 | DeviceDiscoveryError error;
199 |
200 | // Retrieve the unique PnP device ID of the device
201 | unique_variant vtDeviceID;
202 | error = CheckHresult(device->Get(L"DeviceID", 0, &vtDeviceID, nullptr, nullptr));
203 | if (error) {
204 | throw error.Wrap(L"failed to retrieve DeviceID property of PnP device");
205 | }
206 | details.ID = winrt::to_hstring(vtDeviceID.bstrVal);
207 |
208 | // Retrieve the human-readable description of the device
209 | unique_variant vtDescription;
210 | error = CheckHresult(device->Get(L"Description", 0, &vtDescription, nullptr, nullptr));
211 | if (error) {
212 | throw error.Wrap(L"failed to retrieve Description property of PnP device");
213 | }
214 | details.Description = winrt::to_hstring(vtDescription.bstrVal);
215 |
216 | // Retrieve the vendor of the device
217 | unique_variant vtVendor;
218 | error = CheckHresult(device->Get(L"Manufacturer", 0, &vtVendor, nullptr, nullptr));
219 | if (error) {
220 | throw error.Wrap(L"failed to retrieve Manufacturer property of PnP device");
221 | }
222 | details.Vendor = winrt::to_hstring(vtVendor.bstrVal);
223 |
224 | // Retrieve the object path for the instance so we can call instance methods with it
225 | unique_variant vtPath;
226 | error = CheckHresult(device->Get(L"__Path", 0, &vtPath, nullptr, nullptr));
227 | if (error) {
228 | throw error.Wrap(L"failed to retrieve __Path property of PnP device");
229 | }
230 |
231 | // Create an instance of the input parameters type for the `GetDeviceProperties` instance method
232 | com_ptr inputArgs;
233 | error = CheckHresult(this->inputParameters->SpawnInstance(0, inputArgs.put()));
234 | if (error) {
235 | throw error.Wrap(L"failed to spawn input parameters instance for Win32_PnPEntity::GetDeviceProperties");
236 | }
237 |
238 | // Populate the input parameters with the list of decive property keys we want to retrieve
239 | unique_variant vtPropertyKeys = SafeArrayFactory::CreateStringArray({
240 | L"DEVPKEY_Device_Driver",
241 | L"DEVPKEY_Device_LocationPaths",
242 | this->devPropKeyLUID
243 | });
244 | error = CheckHresult(inputArgs->Put(L"devicePropertyKeys", 0, &vtPropertyKeys, CIM_FLAG_ARRAY | CIM_STRING));
245 | if (error) {
246 | throw error.Wrap(L"failed to assign input parameters array for Win32_PnPEntity::GetDeviceProperties");
247 | }
248 |
249 | // Call the `GetDeviceProperties` instance method
250 | com_ptr callResult;
251 | error = CheckHresult(this->wbemServices->ExecMethod(
252 | vtPath.bstrVal,
253 | wil::make_bstr(L"GetDeviceProperties").get(),
254 | 0,
255 | nullptr,
256 | inputArgs.get(),
257 | nullptr,
258 | callResult.put()
259 | ));
260 | if (error) {
261 | throw error.Wrap(L"failed to invoke Win32_PnPEntity::GetDeviceProperties()");
262 | }
263 |
264 | // Retrieve the return value
265 | com_ptr returnValue;
266 | error = CheckHresult(callResult->GetResultObject(WBEM_INFINITE, returnValue.put()));
267 | if (error) {
268 | throw error.Wrap(L"failed to retrieve return value for Win32_PnPEntity::GetDeviceProperties");
269 | }
270 |
271 | // Extract the device properties array and verify that it matches the expected type
272 | unique_variant vtPropertiesArray;
273 | error = CheckHresult(returnValue->Get(L"deviceProperties", 0, &vtPropertiesArray, nullptr, nullptr));
274 | if (error) {
275 | throw error.Wrap(L"failed to retrieve deviceProperties property of Win32_PnPEntity::GetDeviceProperties return value");
276 | }
277 | if (vtPropertiesArray.vt != (VT_ARRAY | VT_UNKNOWN)) {
278 | throw CreateError(L"deviceProperties value was not an array of IUnknown objects");
279 | }
280 |
281 | // Iterate over the device properties array
282 | SafeArrayIterator propertiesIterator(vtPropertiesArray.parray);
283 | for (auto element : propertiesIterator)
284 | {
285 | // Cast the property object to an IWbemClassObject
286 | IWbemClassObject* object = nullptr;
287 | error = CheckHresult(element->QueryInterface(&object));
288 | if (error) {
289 | throw error.Wrap(L"IUnknown::QueryInterface() failed for Win32_PnPDeviceProperty object");
290 | }
291 |
292 | // Retrieve the key name of the property
293 | unique_variant vtKeyName;
294 | error = CheckHresult(object->Get(L"KeyName", 0, &vtKeyName, nullptr, nullptr));
295 | if (error) {
296 | throw error.Wrap(L"failed to retrieve KeyName property of PnP device property");
297 | }
298 | wstring keyName(winrt::to_hstring(vtKeyName.bstrVal));
299 |
300 | // Attempt to retrieve the value of the property
301 | unique_variant data;
302 | HRESULT result = object->Get(L"Data", 0, &data, nullptr, nullptr);
303 | if (FAILED(result))
304 | {
305 | // The property has no value, so ignore it
306 | continue;
307 | }
308 |
309 | // Determine which device property we are dealing with
310 | if (keyName == L"DEVPKEY_Device_Driver")
311 | {
312 | // Verify that the device driver value is of the expected type
313 | if (data.vt != VT_BSTR) {
314 | throw CreateError(L"DeviceDriver value was not a string");
315 | }
316 |
317 | // Construct the full path to the registry key for the device's driver
318 | details.DriverRegistryKey =
319 | L"HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Class\\" +
320 | winrt::to_hstring(data.bstrVal);
321 | }
322 | else if (keyName == L"DEVPKEY_Device_LocationPaths")
323 | {
324 | // Verify that the LocationPaths array is of the expected type
325 | if (data.vt != (VT_ARRAY | VT_BSTR)) {
326 | throw CreateError(L"LocationPaths value was not an array of strings");
327 | }
328 |
329 | // Retrieve the first element from the LocationPaths array
330 | SafeArrayIterator locationIterator(data.parray);
331 | details.LocationPath = winrt::to_hstring(*locationIterator.begin());
332 | }
333 | else if (keyName == this->devPropKeyLUID)
334 | {
335 | // Determine whether the LUID value is represented as a raw 64-bit integer or a string representation
336 | if (data.vt == VT_I8) {
337 | details.DeviceAdapter.InstanceLuid = data.llVal;
338 | }
339 | else if (data.vt == VT_BSTR)
340 | {
341 | // Parse the string back into a 64-bit integer
342 | details.DeviceAdapter.InstanceLuid = std::stoll(winrt::to_string(data.bstrVal));
343 | }
344 | else {
345 | throw CreateError(L"LUID value was not a 64-bit integer or a string");
346 | }
347 | }
348 | }
349 |
350 | return details;
351 | }
352 |
--------------------------------------------------------------------------------
/library/src/WmiQuery.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "Adapter.h"
4 | #include "Device.h"
5 |
6 | using std::map;
7 | using std::vector;
8 | using std::wstring;
9 | using winrt::com_ptr;
10 |
11 | // Provides functionality for querying Windows Management Instrumentation (WMI)
12 | class WmiQuery
13 | {
14 | public:
15 |
16 | WmiQuery();
17 |
18 | // Retrieves the device details for the underlying PnP devices associated with the supplied DirectX adapters
19 | vector GetDevicesForAdapters(const map& adapters);
20 |
21 | private:
22 |
23 | // Extracts the details from a PnP device
24 | Device ExtractDeviceDetails(const com_ptr& device) const;
25 |
26 | // Our COM objects for communicating with WMI
27 | com_ptr wbemLocator;
28 | com_ptr wbemServices;
29 | com_ptr pnpEntityClass;
30 | com_ptr inputParameters;
31 |
32 | // The string identifier for the DEVPROPKEY_GPU_LUID device property key
33 | wstring devPropKeyLUID;
34 | };
35 |
--------------------------------------------------------------------------------
/library/src/pch.h:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | // Prevent the GetObject => GetObjectW macro definition from the Windows headers from interfering with the IDL for IWbemServices
12 | #ifdef GetObject
13 | #undef GetObject
14 | #endif
15 | #include
16 |
17 | // Enable wchar_t support for filenames in spdlog
18 | #define SPDLOG_WCHAR_FILENAMES
19 | #include
20 | #define LOG(...) SPDLOG_INFO(__VA_ARGS__)
21 |
22 | // Include the range formatting support from fmt to facilitate logging container types
23 | #include
24 | #define FMT(x) fmt::format(L"{}", x)
25 |
26 | #include
27 | #include