1 /*
2 * Copyright (C) 2025 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 #include "vkms_tester.h"
18
19 #include <android-base/file.h>
20 #include <cassert>
21 #include <cstdio>
22 #include <cstdlib>
23 #include <cutils/properties.h>
24 #include <dirent.h>
25 #include <errno.h>
26 #include <log/log.h>
27 #include <string>
28 #include <sys/stat.h>
29 #include <sys/wait.h>
30 #include <unistd.h>
31 #include <unordered_map>
32 #include <vector>
33 #include <inttypes.h>
34
35 namespace hcct {
36
37 namespace {
38 // `/config/vkms` is the base directory for VKMS in ConfigFS. `my-vkms` is the
39 // chosen name of the VKMS instance which can be anything.
40 constexpr const char *kVkmsBaseDir = "/config/vkms/my-vkms";
41 constexpr int kPlaneTypePrimary = 1;
42
43 // https://cs.android.com/android/platform/superproject/main/+/main:external/libdrm/xf86drmMode.h;l=190
44 enum class ConnectorStatus {
45 kConnected = 1,
46 kDisconnected = 2,
47 kUnknown = 3,
48 };
49 } // namespace
50
51 // static
52 std::unique_ptr<VkmsTester>
CreateWithGenericConnectors(int displaysCount)53 VkmsTester::CreateWithGenericConnectors(int displaysCount) {
54 if (displaysCount < 0) {
55 ALOGE("Invalid number of displays: %i. At least one connector must be "
56 "specified.",
57 displaysCount);
58 return nullptr;
59 }
60
61 auto tester = std::unique_ptr<VkmsTester>(new VkmsTester(displaysCount));
62
63 if (!tester->mInitialized) {
64 ALOGE("Failed to initialize VkmsTester with Generic Connectors");
65 return nullptr;
66 }
67
68 return tester;
69 }
70
71 // static
CreateWithBuilders(const std::vector<VkmsConnectorBuilder> & builders)72 std::unique_ptr<VkmsTester> VkmsTester::CreateWithBuilders(
73 const std::vector<VkmsConnectorBuilder> &builders) {
74 if (builders.empty()) {
75 ALOGE("Empty configuration provided. At least one connector must be "
76 "specified.");
77 return nullptr;
78 }
79
80 auto tester =
81 std::unique_ptr<VkmsTester>(new VkmsTester(builders.size(), builders));
82
83 if (!tester->mInitialized) {
84 ALOGE("Failed to initialize VkmsTester with Builder Config");
85 return nullptr;
86 }
87
88 return tester;
89 }
90
91 // static
ForceDeleteVkmsDir()92 void VkmsTester::ForceDeleteVkmsDir() { ShutdownAndCleanUpVkms(); }
93
VkmsTester(size_t displaysCount,const std::vector<VkmsConnectorBuilder> & builders)94 VkmsTester::VkmsTester(size_t displaysCount,
95 const std::vector<VkmsConnectorBuilder> &builders) {
96 mInitialized = ToggleHwc3(false) && SetVkmsAsDisplayDriver() &&
97 SetupDisplays(displaysCount, builders) && ToggleVkms(true) &&
98 ToggleHwc3(true);
99 if (!mInitialized) {
100 ALOGE("Failed to set up VKMS");
101 ShutdownAndCleanUpVkms();
102 return;
103 }
104
105 mActiveConnectorsCount = displaysCount;
106 }
107
~VkmsTester()108 VkmsTester::~VkmsTester() {
109 if (mDisableCleanupOnDestruction) {
110 ALOGI("Skipping cleanup on destruction. mDisableCleanupOnDestruction is "
111 "set to true.");
112 return;
113 }
114
115 ShutdownAndCleanUpVkms();
116 }
117
ToggleConnector(int connectorIndex,bool enable)118 bool VkmsTester::ToggleConnector(int connectorIndex, bool enable) {
119 return SetConnectorStatus(connectorIndex, enable);
120 }
121
DisableCleanupOnDestruction()122 void VkmsTester::DisableCleanupOnDestruction() {
123 mDisableCleanupOnDestruction = true;
124 }
125
SetVkmsAsDisplayDriver()126 bool VkmsTester::SetVkmsAsDisplayDriver() {
127 // TODO(b/398831713): Setup an official doc for reference.
128
129 // Set HWC to use VKMS as the display driver.
130 if (property_set("vendor.hwc.drm.device", "/dev/dri/card1") != 0) {
131 ALOGE("Failed to set vendor.hwc.drm.device property");
132 return false;
133 } else {
134 ALOGI("Successfully set vendor.hwc.drm.device property");
135 }
136
137 // Create VKMS directory
138 if (mkdir(kVkmsBaseDir, 0777) == 0) {
139 ALOGI("Successfully created directory %s", kVkmsBaseDir);
140 return true;
141 }
142
143 return false;
144 }
145
SetupDisplays(int displaysCount,const std::vector<VkmsConnectorBuilder> & builders)146 bool VkmsTester::SetupDisplays(
147 int displaysCount, const std::vector<VkmsConnectorBuilder> &builders) {
148 bool isExplicitConfig = !builders.empty();
149 if (isExplicitConfig && displaysCount != builders.size()) {
150 ALOGE("Mismatch between requested displays count and builder config size");
151 return false;
152 }
153
154 for (int i = 0; i < displaysCount; ++i) {
155 CreateResource(DrmResource::kCrtc, i);
156 CreateResource(DrmResource::kEncoder, i);
157 LinkToCrtc(DrmResource::kEncoder, i, i);
158
159 CreateResource(DrmResource::kConnector, i);
160 // Unless explicitly configured, set all connectors to disconnected.
161 SetConnectorStatus(i, isExplicitConfig && builders[i].mEnabledAtStart);
162 if (isExplicitConfig) {
163 SetConnectorType(i, builders[i].mType);
164 if (builders[i].mMonitorName.type != edid::MonitorName::Type::UNSET) {
165 SetConnectorEdid(i, builders[i].mMonitorName);
166 }
167 } else {
168 // Set connector type, eDP for first one, DP for the rest
169 SetConnectorType(i, i == 0 ? ConnectorType::keDP
170 : ConnectorType::kDisplayPort);
171 }
172 LinkConnectorToEncoder(i, i);
173
174 int additionalOverlays =
175 isExplicitConfig ? builders[i].mAdditionalOverlayPlanes : 0;
176 for (int j = 0; j < 2 + additionalOverlays; ++j) {
177 CreateResource(DrmResource::kPlane, mLatestPlaneId);
178 // For each connector, create at least 2 planes, a primary and a cursor
179 // PLUS any additional overlay planes
180 PlaneType type;
181 switch (j) {
182 case 0:
183 type = PlaneType::kCursor;
184 break;
185 case 1:
186 type = PlaneType::kPrimary;
187 break;
188 default:
189 type = PlaneType::kOverlay;
190 break;
191 }
192 SetPlaneType(mLatestPlaneId, type);
193 SetPlaneFormat(mLatestPlaneId);
194 LinkToCrtc(DrmResource::kPlane, mLatestPlaneId, i);
195
196 mLatestPlaneId++;
197 }
198
199 ALOGI("Successfully set up display %i", i);
200 }
201
202 return true;
203 }
204
205 // static
ToggleVkms(bool enable)206 bool VkmsTester::ToggleVkms(bool enable) {
207 std::string path = std::string(kVkmsBaseDir) + "/enabled";
208 std::string value = enable ? "1" : "0";
209 if (!android::base::WriteStringToFile(value, path)) {
210 ALOGE("Failed to toggle VKMS: %s", strerror(errno));
211 return false;
212 }
213
214 ALOGI("Successfully toggled VKMS at %s", path.c_str());
215 return true;
216 }
217
218 // static
ToggleHwc3(bool enable)219 bool VkmsTester::ToggleHwc3(bool enable) {
220 const char *serviceName = "vendor.hwcomposer-3";
221 const char *propertyName = "ctl.start";
222 const char *propertyStopName = "ctl.stop";
223
224 if (property_set(enable ? propertyName : propertyStopName, serviceName) !=
225 0) {
226 ALOGE("Failed to set property %s to %s",
227 enable ? propertyName : propertyStopName, serviceName);
228 return false;
229 }
230
231 ALOGI("Successfully set property %s to %s",
232 enable ? propertyName : propertyStopName, serviceName);
233 return true;
234 }
235
CreateResource(DrmResource resource,int index)236 bool VkmsTester::CreateResource(DrmResource resource, int index) {
237 std::string resourceBase = kDrmResourceBase.at(resource);
238 std::string resourceDir =
239 std::string(kVkmsBaseDir) + "/" + resourceBase + std::to_string(index);
240 if (mkdir(resourceDir.c_str(), 0777) != 0) {
241 ALOGE("Failed to create directory %s: %s", resourceDir.c_str(),
242 strerror(errno));
243 return false;
244 }
245
246 return true;
247 }
248
SetConnectorStatus(int index,bool enable)249 bool VkmsTester::SetConnectorStatus(int index, bool enable) {
250 std::string connectorDir = std::string(kVkmsBaseDir) + "/" +
251 kDrmResourceBase.at(DrmResource::kConnector) +
252 std::to_string(index);
253 std::string connectedPath = connectorDir + "/status";
254 ConnectorStatus status =
255 enable ? ConnectorStatus::kConnected : ConnectorStatus::kDisconnected;
256 std::string connectedValue = std::to_string(static_cast<int>(status));
257
258 if (!android::base::WriteStringToFile(connectedValue, connectedPath)) {
259 ALOGE("Failed to toggle connector %i: %s", index, strerror(errno));
260 return false;
261 }
262
263 ALOGI("Successfully toggled connector %i: %s", index,
264 enable ? "connected" : "disconnected");
265 return true;
266 }
267
SetConnectorType(int index,ConnectorType type)268 bool VkmsTester::SetConnectorType(int index, ConnectorType type) {
269 std::string connectorDir = std::string(kVkmsBaseDir) + "/" +
270 kDrmResourceBase.at(DrmResource::kConnector) +
271 std::to_string(index);
272 std::string typePath = connectorDir + "/type";
273 std::string typeValue = std::to_string(static_cast<int>(type));
274 if (!android::base::WriteStringToFile(typeValue, typePath)) {
275 ALOGE("Failed to write connector type: %s", strerror(errno));
276 return false;
277 }
278
279 ALOGI("Successfully set connector %i type to %i", index,
280 static_cast<int>(type));
281 return true;
282 }
283
SetConnectorEdid(int index,edid::MonitorName monitorName)284 bool VkmsTester::SetConnectorEdid(int index, edid::MonitorName monitorName) {
285 std::vector<uint8_t> edidData = edid::getBinaryEdidForMonitor(monitorName);
286 if (edidData.empty()) {
287 ALOGE("Failed to get EDID data for monitor");
288 return false;
289 }
290
291 std::string connectorDir = std::string(kVkmsBaseDir) + "/" +
292 kDrmResourceBase.at(DrmResource::kConnector) +
293 std::to_string(index);
294 std::string edidPath = connectorDir + "/edid";
295
296 int fd = open(edidPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
297 if (fd == -1) {
298 ALOGE("Failed to open EDID file for writing: %s", strerror(errno));
299 return false;
300 }
301
302 bool success =
303 android::base::WriteFully(fd, edidData.data(), edidData.size());
304 close(fd);
305
306 if (success) {
307 ALOGI("Successfully wrote EDID data with size %" PRIu64 " to connector %i",
308 edidData.size(), index);
309 } else {
310 ALOGE("Failed to write complete EDID data: %s", strerror(errno));
311 }
312
313 return success;
314 }
315
SetPlaneType(int index,PlaneType type)316 bool VkmsTester::SetPlaneType(int index, PlaneType type) {
317 std::string planeDir = std::string(kVkmsBaseDir) + "/" +
318 kDrmResourceBase.at(DrmResource::kPlane) +
319 std::to_string(index);
320 std::string typePath = planeDir + "/type";
321 std::string typeValue = std::to_string(static_cast<int>(type));
322 if (!android::base::WriteStringToFile(typeValue, typePath)) {
323 ALOGE("Failed to write plane type: %s", strerror(errno));
324 return false;
325 }
326
327 ALOGI("Successfully set plane %i type to %i", index, static_cast<int>(type));
328 return true;
329 }
330
SetPlaneFormat(int index)331 bool VkmsTester::SetPlaneFormat(int index) {
332 std::string planeDir = std::string(kVkmsBaseDir) + "/" +
333 kDrmResourceBase.at(DrmResource::kPlane) +
334 std::to_string(index);
335 std::string formatPath = planeDir + "/supported_formats";
336 // TODO(markyacoub): This is now hardcoded to all formats. Extend this later.
337 std::string formatValue = "+*";
338 if (!android::base::WriteStringToFile(formatValue, formatPath)) {
339 ALOGE("Failed to write plane format: %s", strerror(errno));
340 return false;
341 }
342
343 ALOGI("Successfully set plane %i format", index);
344 return true;
345 }
346
LinkToCrtc(DrmResource resource,int resourceIdx,int crtcIdx)347 bool VkmsTester::LinkToCrtc(DrmResource resource, int resourceIdx,
348 int crtcIdx) {
349 std::string crtcName =
350 kDrmResourceBase.at(DrmResource::kCrtc) + std::to_string(crtcIdx);
351 std::string resourceDir = std::string(kVkmsBaseDir) + "/" +
352 kDrmResourceBase.at(resource) +
353 std::to_string(resourceIdx);
354 std::string possibleCrtcPath = resourceDir + "/possible_" + crtcName;
355 std::string crtcDir = std::string(kVkmsBaseDir) + "/" + crtcName;
356
357 // Now create the symlink
358 if (symlink(crtcDir.c_str(), possibleCrtcPath.c_str()) != 0) {
359 ALOGE("Failed to create symlink at %s pointing to %s: %s",
360 possibleCrtcPath.c_str(), crtcDir.c_str(), strerror(errno));
361 return false;
362 }
363
364 ALOGI("Successfully linked %s to %s", possibleCrtcPath.c_str(),
365 crtcDir.c_str());
366 return true;
367 }
368
LinkConnectorToEncoder(int connectorIdx,int encoderIdx)369 bool VkmsTester::LinkConnectorToEncoder(int connectorIdx, int encoderIdx) {
370 std::string encoderName =
371 kDrmResourceBase.at(DrmResource::kEncoder) + std::to_string(encoderIdx);
372 std::string connectorDir = std::string(kVkmsBaseDir) + "/" +
373 kDrmResourceBase.at(DrmResource::kConnector) +
374 std::to_string(connectorIdx);
375 std::string possibleEncoderPath = connectorDir + "/possible_" + encoderName;
376 std::string encoderDir = std::string(kVkmsBaseDir) + "/" + encoderName;
377
378 // Now create the symlink
379 if (symlink(encoderDir.c_str(), possibleEncoderPath.c_str()) != 0) {
380 ALOGE("Failed to create symlink at %s pointing to %s: %s",
381 possibleEncoderPath.c_str(), encoderDir.c_str(), strerror(errno));
382 return false;
383 }
384
385 ALOGI("Successfully linked %s to %s", possibleEncoderPath.c_str(),
386 encoderDir.c_str());
387 return true;
388 }
389
390 // static
391 // ConfigFS has special rules about deletion, so we need to clean up manually
392 // every layer.
ShutdownAndCleanUpVkms()393 void VkmsTester::ShutdownAndCleanUpVkms() {
394 ToggleHwc3(false);
395 ToggleVkms(false);
396 // Give the kernel a longer time to release resources
397 usleep(500000);
398
399 // Clean up manually created relationships first under
400 // possible_(crtcs/encoders). This is required before we started cleaning up
401 // the directories.
402 FindAndCleanupPossibleLinks(kVkmsBaseDir);
403 CleanUpDirAndChildren(kVkmsBaseDir);
404 }
405
406 // static
FindAndCleanupPossibleLinks(const std::string & dirPath)407 void VkmsTester::FindAndCleanupPossibleLinks(const std::string &dirPath) {
408 DIR *dir = opendir(dirPath.c_str());
409 if (!dir) {
410 return;
411 }
412
413 struct dirent *entry;
414 while ((entry = readdir(dir)) != nullptr) {
415 if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
416 continue;
417 }
418
419 std::string path = dirPath + "/" + entry->d_name;
420 struct stat statBuf;
421
422 if (lstat(path.c_str(), &statBuf) != 0) {
423 continue;
424 }
425
426 if (S_ISDIR(statBuf.st_mode)) {
427 // If this is a "possible_*" directory, process it specially
428 if (strstr(entry->d_name, "possible_") == entry->d_name) {
429 DIR *dir = opendir(path.c_str());
430 if (!dir) {
431 return;
432 }
433
434 // First try to remove any contents
435 struct dirent *entry;
436 while ((entry = readdir(dir)) != nullptr) {
437 if (strcmp(entry->d_name, ".") == 0 ||
438 strcmp(entry->d_name, "..") == 0) {
439 continue;
440 }
441
442 std::string fullPath = path + "/" + entry->d_name;
443 unlink(
444 fullPath
445 .c_str()); // Try to remove anything inside, ignoring errors
446 }
447
448 closedir(dir);
449
450 // Then try to remove the directory itself
451 rmdir(path.c_str());
452 } else {
453 // Otherwise recursively look for more possible_* directories
454 FindAndCleanupPossibleLinks(path);
455 }
456 }
457 }
458
459 closedir(dir);
460 }
461
462 // static
CleanUpDirAndChildren(const std::string & dirPath)463 void VkmsTester::CleanUpDirAndChildren(const std::string &dirPath) {
464 DIR *dir = opendir(dirPath.c_str());
465 if (!dir) {
466 ALOGW("Failed to open directory %s: %s - skipping", dirPath.c_str(),
467 strerror(errno));
468 return;
469 }
470
471 struct dirent *entry;
472 while ((entry = readdir(dir)) != nullptr) {
473 // Skip "." and ".."
474 if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
475 continue;
476 }
477
478 std::string path = dirPath + "/" + entry->d_name;
479 struct stat statBuf;
480
481 if (lstat(path.c_str(), &statBuf) != 0) {
482 ALOGW("Failed to stat %s: %s - skipping", path.c_str(), strerror(errno));
483 continue;
484 }
485
486 if (S_ISDIR(statBuf.st_mode)) {
487 CleanUpDirAndChildren(path);
488 } else {
489 }
490 }
491
492 closedir(dir);
493
494 // Remove the directory itself. Do not check for errors as directories that
495 // are auto-created can't be manually deleted. It's a no-op otherwise so we
496 // can ignore the return value.
497 rmdir(dirPath.c_str());
498 }
499
500 } // namespace hcct
501