function t = gpuDeviceTable(props)
%GPUDEVICETABLE  Table of properties of detected GPU devices
%   T = GPUDEVICETABLE returns a table indicating the name, index, and
%   several other properties of each GPU detected in your system.
%
%   T = GPUDEVICETABLE(PROPS) returns a GPU device table with the device
%   properties specified by PROPS as table variables. PROPS must be a
%   string array or a cell array of character vectors where each entry is
%   one of the properties returned by gpuDevice.
%
%   Examples:
%   gpuDeviceTable
%   ans =
%     2×5 table
%
%    Index      Name       ComputeCapability  DeviceAvailable  DeviceSelected
%    _____  _____________  _________________  _______________  ______________
%
%      1    "Quadro P620"        "6.1"             true            true
%      2    "Tesla K20c"         "3.5"             true            false
%
%   gpuDeviceTable(["Name", "MultiprocessorCount", "TotalMemory"])
%   ans =
%     2×3 table
%
%        Name       MultiprocessorCount  TotalMemory
%    _____________  ___________________  ___________
%
%    "Quadro P620"           4           2.0971e+09
%    "Tesla K20c"           13           4.9743e+09
%
%   See also gpuDevice, gpuDeviceCount.

% Copyright 2020-2023 The MathWorks, Inc.

arguments
    props (1,:) string ...
        = ["Index","Name","ComputeCapability","DeviceAvailable","DeviceSelected"]
end

nGpus = gpuDeviceCount();
vals = cellfun(@(x) iGetDefaultVals(x,nGpus), props, 'UniformOutput', false);

t = table(vals{:}, 'VariableNames', props);

% We want to avoid loading the main GPU libraries just to get basic
% information unless we really have to. See if the requested properties can
% be determined from just the driver.

if iIsBasicInfo(props)
    % Use the "basic" device information.
    for ii=1:nGpus
        gpuInfo = parallel.internal.gpudeviceinfo.basicDeviceInfo(ii);
        for ff=1:numel(props)
            t{ii,ff} = iGetVal(gpuInfo, props{ff});
        end
    end
else
    % Use the "real" device information.
    for ii=1:nGpus
        gpuInfo = parallel.gpu.GPUDevice.getDevice(ii);
        for ff=1:numel(props)
            t{ii,ff} = iGetVal(gpuInfo, props{ff});
        end
    end
end
end

function val = iGetVal(gpuInfo, propname)
% Extract a property value from a device info struct

switch string(propname)
    case {"Name", "CachePolicy", "ComputeCapability", "ComputeMode", ...
            "DriverModel", "GraphicsDriverVersion", "UUID"}
        val = string(gpuInfo.(propname));
    otherwise
        % Assume everything else can be copied directly
        val = gpuInfo.(propname);
end
end

function vals = iGetDefaultVals(propname, n)
% Create an nx1 vector of default values for a device property
switch string(propname)
    case {"DeviceSupported", "DeviceAvailable", "DeviceSelected", ...
            "GPUOverlapsTransfers", "KernelExecutionTimeout", ...
            "CanMapHostMemory", "SupportsDouble"}
        vals = false(n, 1);
    case {"Name", "ComputeCapability", "ComputeMode", "DriverModel", ...
            "CachePolicy", "GraphicsDriverVersion", "UUID"}
        vals = repmat("", n, 1);
    case {"MaxGridSize","MaxThreadBlockSize"}
        % Grid and block dimensions are 3-vectors
        vals = nan(n, 3);
    case {"LastAccessed"}
        vals = NaT(n, 1, "TimeZone", "Local");
    otherwise
        % Assume everything else is double
        vals = nan(n, 1);
end
end

function tf = iIsBasicInfo(props)
% Check if the requested fields are all basic info that can be obtained
% from the driver alone without loading other libraries.
basicProps = [
    "Name"
    "Index"
    "UUID"
    "ComputeCapability"
    "SupportsDouble"
    "DriverModel"
    "MaxThreadsPerBlock"
    "MaxShmemPerBlock"
    "MaxThreadBlockSize"
    "MaxGridSize"
    "SIMDWidth"
    "TotalMemory"
    "MultiprocessorCount"
    "ClockRateKHz"
    "ComputeMode"
    "GPUOverlapsTransfers"
    "KernelExecutionTimeout"
    "CanMapHostMemory"
    "DeviceSupported"
    "DeviceAvailable"
    "DeviceSelected"];
tf = all(ismember(props, basicProps));
end
