1 /*
2  * Copyright 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.graphics.cts;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertNotNull;
21 import static org.junit.Assert.assertNull;
22 import static org.junit.Assert.assertTrue;
23 import static org.junit.Assume.assumeTrue;
24 
25 import android.content.pm.FeatureInfo;
26 import android.content.pm.PackageManager;
27 import android.util.Log;
28 
29 import androidx.test.InstrumentationRegistry;
30 import androidx.test.filters.SmallTest;
31 import androidx.test.runner.AndroidJUnit4;
32 
33 import com.android.compatibility.common.util.CddTest;
34 import com.android.compatibility.common.util.PropertyUtil;
35 
36 import org.json.JSONArray;
37 import org.json.JSONException;
38 import org.json.JSONObject;
39 import org.junit.Before;
40 import org.junit.Test;
41 import org.junit.runner.RunWith;
42 
43 /**
44  * Test that the Vulkan loader is present, supports the required extensions, and that system
45  * features accurately indicate the capabilities of the Vulkan driver if one exists.
46  */
47 @SmallTest
48 @RunWith(AndroidJUnit4.class)
49 public class VulkanFeaturesTest {
50 
51     static {
52         System.loadLibrary("ctsgraphics_jni");
53     }
54 
55     private static final String TAG = VulkanFeaturesTest.class.getSimpleName();
56     private static final boolean DEBUG = false;
57 
58     // Require patch version 3 for Vulkan 1.0: It was the first publicly available version,
59     // and there was an important bugfix relative to 1.0.2.
60     private static final int VULKAN_1_0 = 0x00400003; // 1.0.3
61     private static final int VULKAN_1_1 = 0x00401000; // 1.1.0
62 
63     private static final String VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME =
64             "VK_ANDROID_external_memory_android_hardware_buffer";
65     private static final int VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_SPEC_VERSION = 2;
66     private static final int VK_EXTERNAL_FENCE_HANDLE_TYPE_SYNC_FD_BIT = 0x8;
67     private static final int VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT = 0x10;
68     private static final int VK_PHYSICAL_DEVICE_TYPE_CPU = 4;
69 
70     private static final int API_LEVEL_BEFORE_ANDROID_HARDWARE_BUFFER_REQ = 28;
71 
72     private PackageManager mPm;
73     private FeatureInfo mVulkanHardwareLevel = null;
74     private FeatureInfo mVulkanHardwareVersion = null;
75     private FeatureInfo mVulkanHardwareCompute = null;
76     private JSONObject mVkJSON = null;
77     private JSONObject mVulkanDevices[];
78     private JSONObject mBestDevice = null;
79 
80     @Before
setup()81     public void setup() throws Throwable {
82         mPm = InstrumentationRegistry.getTargetContext().getPackageManager();
83         FeatureInfo features[] = mPm.getSystemAvailableFeatures();
84         if (features != null) {
85             for (FeatureInfo feature : features) {
86                 if (PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL.equals(feature.name)) {
87                     mVulkanHardwareLevel = feature;
88                     if (DEBUG) {
89                         Log.d(TAG, feature.name + "=" + feature.version);
90                     }
91                 } else if (PackageManager.FEATURE_VULKAN_HARDWARE_VERSION.equals(feature.name)) {
92                     mVulkanHardwareVersion = feature;
93                     if (DEBUG) {
94                         Log.d(TAG, feature.name + "=0x" + Integer.toHexString(feature.version));
95                     }
96                 } else if (PackageManager.FEATURE_VULKAN_HARDWARE_COMPUTE.equals(feature.name)) {
97                     mVulkanHardwareCompute = feature;
98                     if (DEBUG) {
99                         Log.d(TAG, feature.name + "=" + feature.version);
100                     }
101                 }
102             }
103         }
104 
105         mVkJSON = new JSONObject(nativeGetVkJSON());
106         mVulkanDevices = getVulkanDevices(mVkJSON);
107         mBestDevice = getBestDevice();
108     }
109     @CddTest(requirement = "7.1.4.2/C-1-1,C-2-1")
110     @Test
testVulkanHardwareFeatures()111     public void testVulkanHardwareFeatures() throws JSONException {
112         if (DEBUG) {
113             Log.d(TAG, "Inspecting " + mVulkanDevices.length + " devices");
114         }
115         if (mVulkanDevices.length == 0) {
116             assertNull("System feature " + PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL +
117                        " is supported, but no Vulkan physical devices are available",
118                        mVulkanHardwareLevel);
119             assertNull("System feature " + PackageManager.FEATURE_VULKAN_HARDWARE_VERSION +
120                        " is supported, but no Vulkan physical devices are available",
121                        mVulkanHardwareLevel);
122             assertNull("System feature " + PackageManager.FEATURE_VULKAN_HARDWARE_COMPUTE +
123                        " is supported, but no Vulkan physical devices are available",
124                        mVulkanHardwareCompute);
125             return;
126         }
127 
128         if (hasOnlyCpuDevice()) {
129             return;
130         }
131 
132         assertNotNull("Vulkan physical devices are available, but system feature " +
133                       PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL + " is not supported",
134                       mVulkanHardwareLevel);
135         assertNotNull("Vulkan physical devices are available, but system feature " +
136                       PackageManager.FEATURE_VULKAN_HARDWARE_VERSION + " is not supported",
137                       mVulkanHardwareVersion);
138         if (mVulkanHardwareLevel == null || mVulkanHardwareVersion == null) {
139             return;
140         }
141 
142         assertTrue("System feature " + PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL +
143                    " version " + mVulkanHardwareLevel.version + " is not one of the defined " +
144                    " versions [0..1]",
145                    mVulkanHardwareLevel.version >= 0 && mVulkanHardwareLevel.version <= 1);
146         assertTrue("System feature " + PackageManager.FEATURE_VULKAN_HARDWARE_VERSION +
147                    " version 0x" + Integer.toHexString(mVulkanHardwareVersion.version) + " is not" +
148                    " one of the versions allowed",
149                    isHardwareVersionAllowed(mVulkanHardwareVersion.version));
150         if (mVulkanHardwareCompute != null) {
151             assertTrue("System feature " + PackageManager.FEATURE_VULKAN_HARDWARE_COMPUTE +
152                        " version " + mVulkanHardwareCompute.version +
153                        " is not one of the versions allowed",
154                        mVulkanHardwareCompute.version == 0);
155         }
156 
157         int bestDeviceLevel = determineHardwareLevel(mBestDevice);
158         int bestComputeLevel = determineHardwareCompute(mBestDevice);
159         int bestDeviceVersion = determineHardwareVersion(mBestDevice);
160 
161         assertEquals("System feature " + PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL +
162             " version " + mVulkanHardwareLevel.version + " doesn't match best physical device " +
163             " hardware level " + bestDeviceLevel,
164             bestDeviceLevel, mVulkanHardwareLevel.version);
165         assertTrue(
166             "System feature " + PackageManager.FEATURE_VULKAN_HARDWARE_VERSION +
167             " version 0x" + Integer.toHexString(mVulkanHardwareVersion.version) +
168             " isn't close enough (same major and minor version, less or equal patch version)" +
169             " to best physical device version 0x" + Integer.toHexString(bestDeviceVersion),
170             isVersionCompatible(bestDeviceVersion, mVulkanHardwareVersion.version));
171         if (mVulkanHardwareCompute == null) {
172             assertEquals("System feature " + PackageManager.FEATURE_VULKAN_HARDWARE_COMPUTE +
173                 " not present, but required features are supported",
174                 bestComputeLevel, -1);
175         } else {
176             assertEquals("System feature " + PackageManager.FEATURE_VULKAN_HARDWARE_COMPUTE +
177                 " version " + mVulkanHardwareCompute.version +
178                 " doesn't match best physical device (version: " + bestComputeLevel + ")",
179                 bestComputeLevel, mVulkanHardwareCompute.version);
180         }
181     }
182 
183     @CddTest(requirement = "7.1.4.2/C-3-1")
184     @Test
testVulkan1_1Requirements()185     public void testVulkan1_1Requirements() throws JSONException {
186         if (mVulkanHardwareVersion == null || mVulkanHardwareVersion.version < VULKAN_1_1
187                 || !PropertyUtil.isVendorApiLevelNewerThan(
188                         API_LEVEL_BEFORE_ANDROID_HARDWARE_BUFFER_REQ)) {
189             return;
190         }
191         assertTrue("Devices with Vulkan 1.1 must support sampler YCbCr conversion",
192                 mBestDevice.getJSONObject("samplerYcbcrConversionFeatures")
193                            .getInt("samplerYcbcrConversion") != 0);
194 
195         if (hasOnlyCpuDevice()) {
196             return;
197         }
198         assertTrue("Devices with Vulkan 1.1 must support " +
199                 VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME +
200                 " (version >= " + VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_SPEC_VERSION +
201                 ")",
202                 hasDeviceExtension(mBestDevice,
203                     VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME,
204                     VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_SPEC_VERSION));
205         assertTrue("Devices with Vulkan 1.1 must support SYNC_FD external semaphores",
206                 hasHandleType(mBestDevice.getJSONArray("externalSemaphoreProperties"),
207                     VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT,
208                     "externalSemaphoreFeatures", 0x3 /* importable + exportable */));
209         assertTrue("Devices with Vulkan 1.1 must support SYNC_FD external fences",
210                 hasHandleType(mBestDevice.getJSONArray("externalFenceProperties"),
211                     VK_EXTERNAL_FENCE_HANDLE_TYPE_SYNC_FD_BIT,
212                     "externalFenceFeatures", 0x3 /* importable + exportable */));
213     }
214 
215     @CddTest(requirement = "7.1.4.2/C-1-7")
216     @Test
testVulkanRequiredExtensions()217     public void testVulkanRequiredExtensions() throws JSONException {
218         assumeTrue("Skipping because Vulkan is not supported", mVulkanDevices.length > 0);
219 
220         assertVulkanInstanceExtension("VK_KHR_surface", 25);
221         assertVulkanInstanceExtension("VK_KHR_android_surface", 6);
222         assertVulkanDeviceExtension("VK_KHR_swapchain", 68);
223         assertVulkanDeviceExtension("VK_KHR_incremental_present", 1);
224     }
225 
226     @CddTest(requirement = "7.9.2/C-1-5")
227     @Test
testVulkanVersionForVrHighPerformance()228     public void testVulkanVersionForVrHighPerformance() {
229         if (!mPm.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE))
230             return;
231         assertTrue(
232             "VR high-performance devices must support Vulkan 1.0 with Hardware Level 0, " +
233             "but this device does not.",
234             mVulkanHardwareVersion != null && mVulkanHardwareVersion.version >= VULKAN_1_0 &&
235             mVulkanHardwareLevel != null && mVulkanHardwareLevel.version >= 0);
236     }
237 
238     @CddTest(requirement = "7.1.4.2/C-1-11")
239     @Test
testVulkanBlockedExtensions()240     public void testVulkanBlockedExtensions() throws JSONException {
241         assertNoVulkanDeviceExtension("VK_KHR_performance_query");
242         assertNoVulkanDeviceExtension("VK_KHR_video_queue");
243         assertNoVulkanDeviceExtension("VK_KHR_video_decode_queue");
244         assertNoVulkanDeviceExtension("VK_KHR_video_encode_queue");
245     }
246 
getBestDevice()247     private JSONObject getBestDevice() throws JSONException {
248         JSONObject bestDevice = null;
249         int bestDeviceLevel = -1;
250         int bestComputeLevel = -1;
251         int bestDeviceVersion = -1;
252         for (JSONObject device : mVulkanDevices) {
253             int level = determineHardwareLevel(device);
254             int compute = determineHardwareCompute(device);
255             int version = determineHardwareVersion(device);
256             if (DEBUG) {
257                 Log.d(TAG, device.getJSONObject("properties").getString("deviceName") +
258                     ": level=" + level + " compute=" + compute +
259                     " version=0x" + Integer.toHexString(version));
260             }
261             if (level >= bestDeviceLevel && compute >= bestComputeLevel &&
262                     version >= bestDeviceVersion) {
263                 bestDevice = device;
264                 bestDeviceLevel = level;
265                 bestComputeLevel = compute;
266                 bestDeviceVersion = version;
267             }
268         }
269         return bestDevice;
270     }
271 
hasOnlyCpuDevice()272     private boolean hasOnlyCpuDevice() throws JSONException {
273         for (JSONObject device : mVulkanDevices) {
274             if (device.getJSONObject("properties").getInt("deviceType")
275                     != VK_PHYSICAL_DEVICE_TYPE_CPU) {
276                 return false;
277             }
278         }
279         return true;
280     }
281 
determineHardwareLevel(JSONObject device)282     private int determineHardwareLevel(JSONObject device) throws JSONException {
283         JSONObject features = device.getJSONObject("features");
284         boolean textureCompressionETC2 = features.getInt("textureCompressionETC2") != 0;
285         boolean fullDrawIndexUint32 = features.getInt("fullDrawIndexUint32") != 0;
286         boolean imageCubeArray = features.getInt("imageCubeArray") != 0;
287         boolean independentBlend = features.getInt("independentBlend") != 0;
288         boolean geometryShader = features.getInt("geometryShader") != 0;
289         boolean tessellationShader = features.getInt("tessellationShader") != 0;
290         boolean sampleRateShading = features.getInt("sampleRateShading") != 0;
291         boolean textureCompressionASTC_LDR = features.getInt("textureCompressionASTC_LDR") != 0;
292         boolean fragmentStoresAndAtomics = features.getInt("fragmentStoresAndAtomics") != 0;
293         boolean shaderImageGatherExtended = features.getInt("shaderImageGatherExtended") != 0;
294         boolean shaderUniformBufferArrayDynamicIndexing = features.getInt("shaderUniformBufferArrayDynamicIndexing") != 0;
295         boolean shaderSampledImageArrayDynamicIndexing = features.getInt("shaderSampledImageArrayDynamicIndexing") != 0;
296         if (!textureCompressionETC2) {
297             return -1;
298         }
299         if (!fullDrawIndexUint32 ||
300             !imageCubeArray ||
301             !independentBlend ||
302             !geometryShader ||
303             !tessellationShader ||
304             !sampleRateShading ||
305             !textureCompressionASTC_LDR ||
306             !fragmentStoresAndAtomics ||
307             !shaderImageGatherExtended ||
308             !shaderUniformBufferArrayDynamicIndexing ||
309             !shaderSampledImageArrayDynamicIndexing) {
310             return 0;
311         }
312         return 1;
313     }
314 
determineHardwareCompute(JSONObject device)315     private int determineHardwareCompute(JSONObject device) throws JSONException {
316         boolean variablePointers = false;
317         try {
318             variablePointers = device.getJSONObject("variablePointerFeatures")
319                                              .getInt("variablePointers") != 0;
320         } catch (JSONException exp) {
321             try {
322                 variablePointers = device.getJSONObject("VK_KHR_variable_pointers")
323                                                  .getJSONObject("variablePointerFeaturesKHR")
324                                                  .getInt("variablePointers") != 0;
325             }  catch (JSONException exp2) {
326                 variablePointers = false;
327             }
328         }
329         JSONObject limits = device.getJSONObject("properties").getJSONObject("limits");
330         int maxPerStageDescriptorStorageBuffers = limits.getInt("maxPerStageDescriptorStorageBuffers");
331         if (DEBUG) {
332             Log.d(TAG, device.getJSONObject("properties").getString("deviceName") +
333                 ": variablePointers=" + variablePointers +
334                 " maxPerStageDescriptorStorageBuffers=" + maxPerStageDescriptorStorageBuffers);
335         }
336         if (!variablePointers || maxPerStageDescriptorStorageBuffers < 16)
337             return -1;
338         return 0;
339     }
340 
determineHardwareVersion(JSONObject device)341     private int determineHardwareVersion(JSONObject device) throws JSONException {
342         return device.getJSONObject("properties").getInt("apiVersion");
343     }
344 
isVersionCompatible(int expected, int actual)345     private boolean isVersionCompatible(int expected, int actual) {
346         int expectedMajor = (expected >> 22) & 0x3FF;
347         int expectedMinor = (expected >> 12) & 0x3FF;
348         int expectedPatch = (expected >>  0) & 0xFFF;
349         int actualMajor = (actual >> 22) & 0x3FF;
350         int actualMinor = (actual >> 12) & 0x3FF;
351         int actualPatch = (actual >>  0) & 0xFFF;
352         return (actualMajor == expectedMajor) &&
353                (actualMinor == expectedMinor) &&
354                (actualPatch <= expectedPatch);
355     }
356 
isHardwareVersionAllowed(int actual)357     private boolean isHardwareVersionAllowed(int actual) {
358         // Limit which system feature hardware versions are allowed. If a new major/minor version
359         // is released, we don't want devices claiming support for it until tests for the new
360         // version are available. And only claiming support for a base patch level per major/minor
361         // pair reduces fragmentation seen by developers. Patch-level changes are supposed to be
362         // forwards and backwards compatible; if a developer *really* needs to alter behavior based
363         // on the patch version, they can do so at runtime, but must be able to handle previous
364         // patch versions.
365         final int[] ALLOWED_HARDWARE_VERSIONS = {
366             VULKAN_1_0,
367             VULKAN_1_1,
368         };
369         for (int expected : ALLOWED_HARDWARE_VERSIONS) {
370             if (actual == expected) {
371                 return true;
372             }
373         }
374         return false;
375     }
376 
assertVulkanDeviceExtension(final String name, final int minVersion)377     private void assertVulkanDeviceExtension(final String name, final int minVersion)
378             throws JSONException {
379         assertTrue(
380                 String.format(
381                         "Devices with Vulkan must support device extension %s (version >= %d)",
382                         name,
383                         minVersion),
384                 hasDeviceExtension(mBestDevice, name, minVersion));
385     }
386 
assertNoVulkanDeviceExtension(final String name)387     private void assertNoVulkanDeviceExtension(final String name)
388             throws JSONException {
389         for (JSONObject device : mVulkanDevices) {
390             assertTrue(
391                     String.format("Devices must not support Vulkan device extension %s", name),
392                     !hasDeviceExtension(device, name, 0));
393         }
394     }
395 
assertVulkanInstanceExtension(final String name, final int minVersion)396     private void assertVulkanInstanceExtension(final String name, final int minVersion)
397             throws JSONException {
398         assertTrue(
399                 String.format(
400                         "Devices with Vulkan must support instance extension %s (version >= %d)",
401                         name,
402                         minVersion),
403                 hasInstanceExtension(name, minVersion));
404     }
405 
hasDeviceExtension( final JSONObject device, final String name, final int minVersion)406     private static boolean hasDeviceExtension(
407             final JSONObject device,
408             final String name,
409             final int minVersion) throws JSONException {
410         final JSONArray deviceExtensions = device.getJSONArray("extensions");
411         return hasExtension(deviceExtensions, name, minVersion);
412     }
413 
hasInstanceExtension( final String name, final int minVersion)414     private boolean hasInstanceExtension(
415             final String name,
416             final int minVersion) throws JSONException {
417         // Instance extensions are in the top-level vkjson object.
418         final JSONArray instanceExtensions = mVkJSON.getJSONArray("extensions");
419         return hasExtension(instanceExtensions, name, minVersion);
420     }
421 
hasExtension( final JSONArray extensions, final String name, final int minVersion)422     private static boolean hasExtension(
423             final JSONArray extensions,
424             final String name,
425             final int minVersion) throws JSONException {
426         for (int i = 0; i < extensions.length(); i++) {
427             JSONObject ext = extensions.getJSONObject(i);
428             if (ext.getString("extensionName").equals(name) &&
429                     ext.getInt("specVersion") >= minVersion)
430                 return true;
431         }
432         return false;
433     }
434 
hasHandleType(JSONArray handleTypes, int type, String featuresName, int requiredFeatures)435     private boolean hasHandleType(JSONArray handleTypes, int type,
436             String featuresName, int requiredFeatures) throws JSONException {
437         for (int i = 0; i < handleTypes.length(); i++) {
438             JSONArray typeRecord = handleTypes.getJSONArray(i);
439             if (typeRecord.getInt(0) == type) {
440                 JSONObject typeInfo = typeRecord.getJSONObject(1);
441                 if ((typeInfo.getInt(featuresName) & requiredFeatures) == requiredFeatures)
442                     return true;
443             }
444         }
445         return false;
446     }
447 
nativeGetVkJSON()448     private static native String nativeGetVkJSON();
449 
getVulkanDevices(final JSONObject vkJSON)450     private static JSONObject[] getVulkanDevices(final JSONObject vkJSON) throws JSONException {
451         JSONArray devicesArray = vkJSON.getJSONArray("devices");
452         JSONObject[] devices = new JSONObject[devicesArray.length()];
453         for (int i = 0; i < devicesArray.length(); i++) {
454             devices[i] = devicesArray.getJSONObject(i);
455         }
456         return devices;
457     }
458 }
459