Vuo 2.4.4
Loading...
Searching...
No Matches
VuoScreenCommon.mm
Go to the documentation of this file.
1
10#include "VuoScreenCommon.h"
11#include "VuoTriggerSet.hh"
12#include "VuoOsStatus.h"
13#include "VuoPnpId.h"
14#include "VuoApp.h"
15
16#include <sstream>
17#include <string>
18using namespace std;
19
21#include <IOKit/graphics/IOGraphicsLib.h>
22#include <AppKit/AppKit.h>
23
24#ifdef VUO_COMPILER
26 "title" : "VuoScreenCommon",
27 "dependencies" : [
28 "VuoApp",
29 "VuoOsStatus",
30 "VuoPnpId",
31 "VuoScreen",
32 "VuoList_VuoScreen",
33 "IOKit.framework",
34 "AppKit.framework"
35 ]
36 });
37#endif
38
40unsigned int VuoScreen_useCount = 0;
41
45@interface NSScreen (VuoAdditions)
46- (NSInteger)deviceId;
47@end
48
52static VuoText VuoScreen_getName(CGDirectDisplayID displayID)
53{
54 NSString *modelName = nil;
55 uint32_t displayModelNumber = CGDisplayModelNumber(displayID);
56 NSString *manufacturerName = nil;
57 uint32_t displayVendorNumber = CGDisplayVendorNumber(displayID);
58 NSString *transport = nil;
59 NSString *serialString = nil;
60 uint32_t serialNumber = CGDisplaySerialNumber(displayID);
61 uint8_t manufacturedWeek = 0;
62 uint16_t manufacturedYear = 0;
63
64
65 // First, try `IODisplayCreateInfoDictionary`, which provides the most information,
66 // but is only supported on x86_64.
67 // https://b33p.net/kosada/vuo/vuo/-/issues/18596
68
69#if __x86_64__
70
71 // Deprecated on 10.9, but there's no alternative.
72 #pragma clang diagnostic push
73 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
74 io_service_t port = CGDisplayIOServicePort(displayID);
75 #pragma clang diagnostic pop
76
77 NSDictionary *deviceInfo = (NSDictionary *)IODisplayCreateInfoDictionary(port, kIODisplayOnlyPreferredName);
78
79 NSData *edid = [deviceInfo objectForKey:@kIODisplayEDIDKey];
80 NSString *displayLocation = [deviceInfo objectForKey:@kIODisplayLocationKey];
81 if (edid)
82 {
83 const unsigned char *bytes = (const unsigned char *)[edid bytes];
84
85 uint16_t manufacturerId = (bytes[8] << 8) + bytes[9];
86 char *manufacturer = VuoPnpId_getString(manufacturerId);
87 manufacturerName = [NSString stringWithUTF8String:manufacturer];
88 free(manufacturer);
89
90 serialNumber = (bytes[15] << 24) + (bytes[14] << 16) + (bytes[13] << 8) + bytes[12];
91
92 manufacturedWeek = bytes[16];
93 manufacturedYear = bytes[17] + 1990;
94 }
95 else
96 {
97 // If no EDID manufacturer is provided, inspect the device path for some known unique keys.
98 if ([displayLocation rangeOfString:@"SRXDisplayCard" options:0].location != NSNotFound)
99 manufacturerName = @"Splashtop XDisplay";
100 else if ([displayLocation rangeOfString:@"info_ennowelbers_proxyframebuffer_fbuffer" options:0].location != NSNotFound)
101 manufacturerName = @"GoodDual Display";
102 }
103
104 // Yam Display reports "DIS" as its EDID manufacturer.
105 // I assume they meant that as short for "Display"
106 // but it's actually the apparently-unrelated company "Diseda S.A.", so ignore it.
107 if ([displayLocation rangeOfString:@"com_yamstu_YamDisplayDriver" options:0].location != NSNotFound)
108 manufacturerName = nil;
109
110
111 [deviceInfo autorelease];
112 NSDictionary *localizedNames = [deviceInfo objectForKey:@kDisplayProductName];
113 if ([localizedNames count] > 0)
114 modelName = [localizedNames objectForKey:[[localizedNames allKeys] objectAtIndex:0]];
115
116#endif
117
118 if (!manufacturerName && !modelName)
119 {
120 // If we're running on Apple Silicon (ARM64),
121 // or on x86_64 and `IODisplayCreateInfoDictionary` didn't provide any information,
122 // see if there's any information on this display in the IORegistry.
123
124 CFMutableDictionaryRef matching = IOServiceMatching("IOMobileFramebuffer");
125
126 io_iterator_t it = 0;
127 kern_return_t ret = IOServiceGetMatchingServices(kIOMasterPortDefault, matching, &it);
128 if (ret == noErr)
129 {
130 while (io_object_t ioo = IOIteratorNext(it))
131 {
132 NSDictionary *properties = nil;
133 kern_return_t ret = IORegistryEntryCreateCFProperties(ioo, (CFMutableDictionaryRef *)&properties, NULL, 0);
134 if (ret == noErr)
135 {
136 uint32_t ioregSerialNumber = ((NSNumber *)properties[@"DisplayAttributes"][@"ProductAttributes"][@"SerialNumber"]).unsignedLongValue;
137 uint32_t ioregVendorNumber = ((NSNumber *)properties[@"DisplayAttributes"][@"ProductAttributes"][@"LegacyManufacturerID"]).unsignedLongValue;
138 uint32_t ioregModelNumber = ((NSNumber *)properties[@"DisplayAttributes"][@"ProductAttributes"][@"ProductID"]).unsignedLongValue;
139 if (ioregSerialNumber == serialNumber
140 && ioregVendorNumber == displayVendorNumber
141 && ioregModelNumber == displayModelNumber)
142 {
143 modelName = [[properties[@"DisplayAttributes"][@"ProductAttributes"][@"ProductName"] retain] autorelease];
144 manufacturerName = [[properties[@"DisplayAttributes"][@"ProductAttributes"][@"ManufacturerID"] retain] autorelease];
145 transport = [[properties[@"Transport"][@"Downstream"] retain] autorelease];
146 serialString = [[properties[@"DisplayAttributes"][@"ProductAttributes"][@"AlphanumericSerialNumber"] retain] autorelease];
147 manufacturedWeek = ((NSNumber *)properties[@"DisplayAttributes"][@"ProductAttributes"][@"WeekOfManufacture"]).unsignedIntValue;
148 manufacturedYear = ((NSNumber *)properties[@"DisplayAttributes"][@"ProductAttributes"][@"YearOfManufacture"]).unsignedIntValue;
149 }
150 }
151 [properties release];
152 IOObjectRelease(ioo);
153 }
154 IOObjectRelease(it);
155 }
156 }
157
158
159 // Hardcode info for displays that aren't available via the IORegistry or EDID.
160 if (!manufacturerName && !modelName)
161 {
162 if (displayVendorNumber == 0x0610
163 || displayVendorNumber == 'aapl' /* 0x6161706c */)
164 {
165 manufacturerName = @"Apple";
166 if (displayModelNumber == 0xa050)
167 modelName = @"Built-in Liquid Retina XDR Display";
168 else if (displayModelNumber == 'airp' /* 0x61697270 */)
169 modelName = @"AirPlay";
170 else
171 modelName = @"Display";
172 }
173 else if (displayVendorNumber == kDisplayVendorIDUnknown /* 0x756e6b6e 'unkn' */ && displayModelNumber == 'virt' /* 0x76697274 */)
174 modelName = @"Virtual Display";
175 else if (displayVendorNumber == 0x046d && displayModelNumber == 0x100)
176 modelName = @"Yam Display";
177 else if (displayVendorNumber == 0xf0f0) // https://github.com/waydabber/BetterDisplay/blob/0aa8394a3c04762d41268fa9c35053ef24d1ef1e/BetterDummy/Model/Dummy.swift#L109
178 modelName = [NSString stringWithFormat:@"BetterDummy %d:%d", displayModelNumber / 256 + 1, displayModelNumber % 256 + 1];
179 }
180
181
182 // Coalesce the various attributes into a single descriptive string.
183
184 NSString *name = nil;
185 if (modelName.length > 0)
186 {
187 if (manufacturerName.length > 0)
188 name = [NSString stringWithFormat:@"%@: %@", manufacturerName, modelName];
189 else
190 name = modelName;
191 }
192
193 if (name.length == 0)
194 {
195 if (manufacturerName)
196 name = manufacturerName;
197 else
198 {
199 // As a last resort, upgrade to an NSApplication-app and check whether `NSScreen` reports anything.
200 // Disabled since `NSScreen.localizedName` is crashy.
201 // https://b33p.net/kosada/vuo/vuo/-/issues/19231
202// VuoApp_init(false);
203// for (NSScreen *nsscreen in [NSScreen screens])
204// if (nsscreen.deviceId == displayID)
205// name = [nsscreen.localizedName autorelease];
206
207 name = [NSString stringWithFormat:@"Unknown display (vendor 0x%04x, model 0x%04x)", displayVendorNumber, displayModelNumber];
208 }
209 }
210
211
212 NSMutableArray *parentheticalComponents = [NSMutableArray new];
213
214 if (transport.length > 0)
215 [parentheticalComponents addObject:transport];
216
217 if (serialString.length > 0)
218 [parentheticalComponents addObject:serialString];
219 else if (serialNumber)
220 [parentheticalComponents addObject:[NSString stringWithFormat:@"%u", serialNumber]];
221
222 if (manufacturedYear)
223 [parentheticalComponents addObject:[NSString stringWithFormat:@"%d-W%02d", manufacturedYear, manufacturedWeek]];
224
225
226 NSString *compositeName = name;
227 NSString *parenthetical = [parentheticalComponents componentsJoinedByString:@", "];
228 [parentheticalComponents release];
229 if (parenthetical.length > 0)
230 compositeName = [name stringByAppendingFormat:@" (%@)", parenthetical];
231
232 if (VuoIsDebugEnabled())
233 {
234#pragma clang diagnostic push
235#pragma clang diagnostic ignored "-Wdeprecated-declarations"
236 io_service_t port = CGDisplayIOServicePort(displayID);
237 bool captured = CGDisplayIsCaptured(displayID);
238#pragma clang diagnostic pop
239
240 CGGammaValue r0 = 0, r1 = 0, rg = 0, g0 = 0, g1 = 0, gg = 0, b0 = 0, b1 = 0, bg = 0;
241 CGGetDisplayTransferByFormula(displayID, &r0, &r1, &rg, &g0, &g1, &gg, &b0, &b1, &bg);
242
243 uint32_t gammaCap = CGDisplayGammaTableCapacity(displayID);
244 CGGammaValue *gamma = (CGGammaValue *)malloc(sizeof(CGGammaValue) * gammaCap * 3);
245 uint32_t gammaCount = 0;
246 CGGetDisplayTransferByTable(displayID, gammaCap, gamma, gamma + gammaCap, gamma + gammaCap * 2, &gammaCount);
247
248 VUserLog("[%-55s] %5zux%-5zu (%4.0fx%-4.0f mm) @ %5gx%-5g id=%02x port=%x vendor=0x%08x (%10u) model=0x%08x (%10u) serial=0x%08x (%10u) unit=0x%04x (%5u) active=%d asleep=%d online=%d main=%d builtin=%d mirror=%s%s gl=%d stereo=%d captured=%d shield=%x rotation=%g cgcontext=%p transfer=r(%g %g %g %7.5f) g(%g %g %g %7.5f) b(%g %g %g %7.5f)",
249 compositeName.UTF8String,
250 CGDisplayPixelsWide(displayID),
251 CGDisplayPixelsHigh(displayID),
252 CGDisplayScreenSize(displayID).width,
253 CGDisplayScreenSize(displayID).height,
254 CGDisplayBounds(displayID).origin.x,
255 CGDisplayBounds(displayID).origin.y,
256 displayID,
257 port,
258 displayVendorNumber, displayVendorNumber,
259 displayModelNumber, displayModelNumber,
260 serialNumber, serialNumber,
261 CGDisplayUnitNumber(displayID), CGDisplayUnitNumber(displayID),
262 CGDisplayIsActive(displayID),
263 CGDisplayIsAsleep(displayID),
264 CGDisplayIsOnline(displayID),
265 CGDisplayIsMain(displayID),
266 CGDisplayIsBuiltin(displayID),
267 CGDisplayIsInMirrorSet(displayID) ? (CGDisplayIsAlwaysInMirrorSet(displayID) ? "always" : "yes") : "no",
268 CGDisplayIsInHWMirrorSet(displayID) ? " hw" : "",
269 CGDisplayUsesOpenGLAcceleration(displayID),
270 CGDisplayIsStereo(displayID),
271 captured,
272 CGShieldingWindowID(displayID),
273 CGDisplayRotation(displayID),
274 CGDisplayGetDrawingContext(displayID),
275 r0, r1, rg, gamma[1],
276 g0, g1, gg, gamma[1 + gammaCap],
277 b0, b1, bg, gamma[1 + gammaCap * 2]);
278
279// for (uint32_t i = 0; i < gammaCount; ++i)
280// VUserLog(" %8f %8f %8f", gamma[i], gamma[i + gammaCap], gamma[i + gammaCap * 2]);
281
282 free(gamma);
283 }
284
285 return VuoText_make([compositeName UTF8String]);
286}
287
288@implementation NSScreen (VuoAdditions)
292- (NSInteger)deviceId
293{
294 NSDictionary *deviceDescription = [self deviceDescription];
295 return [[deviceDescription objectForKey:@"NSScreenNumber"] integerValue];
296}
297@end
298
303{
304 VuoScreen realizedScreen;
305 bool realized = VuoScreen_realize(screen, &realizedScreen);
306 if (!realized)
307 return NULL;
308 else
309 {
310 VuoScreen_retain(realizedScreen);
311 for (NSScreen *nsscreen in [NSScreen screens])
312 if ([nsscreen deviceId] == realizedScreen.id)
313 {
314 VuoScreen_release(realizedScreen);
315 return nsscreen;
316 }
317 VuoScreen_release(realizedScreen);
318 }
319
320 return NULL;
321}
322
327{
328 CGRect bounds = CGDisplayBounds(display);
329 CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display);
330 int backingScaleFactor = CGDisplayModeGetPixelWidth(mode) / bounds.size.width;
331 int fakeDPI = backingScaleFactor * 72;
332 CGDisplayModeRelease(mode);
333
334 return (VuoScreen){
335 VuoScreenType_MatchId,
336 display,
337 CGDisplayIDToOpenGLDisplayMask(display),
338 VuoScreen_getName(display),
339 true,
340 VuoPoint2d_make(bounds.origin.x, bounds.origin.y),
341 (VuoInteger)bounds.size.width,
342 (VuoInteger)bounds.size.height,
343 fakeDPI,
344 fakeDPI,
345 static_cast<VuoBoolean>(CGDisplayIsInMirrorSet(display))
346 };
347}
348
353{
354 NSScreen *screen = (NSScreen *)vscreen;
355 return VuoScreen_makeFromCGDirectDisplay(screen.deviceId);
356}
357
362{
364
365 // +[NSScreen screens] causes formerly-headless apps to begin bouncing endlessly in the dock,
366 // so use the CoreGraphics functions instead.
367 CGDirectDisplayID displays[256];
368 uint32_t displayCount = 0;
369 CGError e = CGGetOnlineDisplayList(256, displays, &displayCount);
370 if (e != kCGErrorSuccess)
371 {
372 char *errorStr = VuoOsStatus_getText(e);
373 VUserLog("Error: Couldn't get display info: %s", errorStr);
374 free(errorStr);
375 return screens;
376 }
377
378 for (int i = 0; i < displayCount; ++i)
380
381 return screens;
382}
383
388{
389 // +[NSScreen mainScreen] causes formerly-headless apps to begin bouncing endlessly in the dock.
390 // There's no CoreGraphics equivalent (CGMainDisplayID() returns the screen with the menu bar, not the focused screen).
391 VuoApp_init(false);
392
393 return VuoScreen_makeFromNSScreen([NSScreen mainScreen]);
394}
395
402{
403 return VuoScreen_makeFromCGDirectDisplay(CGMainDisplayID());
404}
405
412{
413 CGDirectDisplayID displays[256];
414 uint32_t displayCount = 0;
415 CGError e = CGGetOnlineDisplayList(256, displays, &displayCount);
416 if (e != kCGErrorSuccess)
417 {
418 char *errorStr = VuoOsStatus_getText(e);
419 VUserLog("Error: Couldn't get display info: %s", errorStr);
420 free(errorStr);
421 return VuoScreen_getPrimary();
422 }
423
424 if (displayCount > 1)
425 return VuoScreen_makeFromCGDirectDisplay(displays[1]);
426
427 return VuoScreen_makeFromName(nullptr);
428}
429
433void VuoScreen_reconfiguration(CGDirectDisplayID display, CGDisplayChangeSummaryFlags flags, void *userInfo)
434{
435 // Ignore the "something's about to happen" notification.
436 if (flags == kCGDisplayBeginConfigurationFlag)
437 return;
438
439 // Ignore unchanged displays.
440 if (flags == 0)
441 return;
442
443 vector<string> flagDescriptions;
444 if (flags & kCGDisplayMovedFlag) flagDescriptions.push_back("moved");
445 if (flags & kCGDisplaySetMainFlag) flagDescriptions.push_back("set main");
446 if (flags & kCGDisplaySetModeFlag) flagDescriptions.push_back("set mode");
447 if (flags & kCGDisplayAddFlag) flagDescriptions.push_back("added");
448 if (flags & kCGDisplayRemoveFlag) flagDescriptions.push_back("removed");
449 if (flags & kCGDisplayEnabledFlag) flagDescriptions.push_back("enabled");
450 if (flags & kCGDisplayDisabledFlag) flagDescriptions.push_back("disabled");
451 if (flags & kCGDisplayMirrorFlag) flagDescriptions.push_back("mirrored");
452 if (flags & kCGDisplayUnMirrorFlag) flagDescriptions.push_back("unmirrored");
453 if (flags & kCGDisplayDesktopShapeChangedFlag) flagDescriptions.push_back("desktop shape changed");
454 stringstream flagDescription;
455 for_each(flagDescriptions.begin(), flagDescriptions.end(), [&flagDescription] (const string &s) {
456 if (flagDescription.tellp())
457 flagDescription << ", ";
458 flagDescription << s;
459 });
460
461 VuoList_VuoScreen screensList = VuoScreen_getList();
462 unsigned long screenCount = VuoListGetCount_VuoScreen(screensList);
463 VuoScreen *screens = VuoListGetData_VuoScreen(screensList);
464 VuoLocal(screensList);
465 for (unsigned long i = 0; i < screenCount; ++i)
466 if (screens[i].id == display)
467 VUserLog("Display \"%s\": %s", screens[i].name, flagDescription.str().c_str());
468
469 // If multiple displays are attached, this callback is invoked multiple times.
470 // Coalesce into a single Vuo event.
471 const double notificationDelaySeconds = 0.25;
472 static dispatch_source_t timer = 0;
473 if (timer)
474 // Push the timer back.
475 dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, notificationDelaySeconds*NSEC_PER_SEC), DISPATCH_TIME_FOREVER, notificationDelaySeconds*NSEC_PER_SEC/10);
476 else
477 {
478 timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
479 dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, notificationDelaySeconds*NSEC_PER_SEC), DISPATCH_TIME_FOREVER, notificationDelaySeconds*NSEC_PER_SEC/10);
480 dispatch_source_set_event_handler(timer, ^{
482 dispatch_source_cancel(timer);
483 });
484 dispatch_source_set_cancel_handler(timer, ^{
485 dispatch_release(timer);
486 timer = 0;
487 });
488 dispatch_resume(timer);
489 }
490}
491
499{
500 if (__sync_add_and_fetch(&VuoScreen_useCount, 1) == 1)
501 {
502 // Our process only receives CGDisplay notifications while running in app mode.
503 VuoApp_init(false);
504
505 CGDisplayRegisterReconfigurationCallback(VuoScreen_reconfiguration, nullptr);
506 }
507}
508
516{
517 if (VuoScreen_useCount <= 0)
518 {
519 VUserLog("Error: Unbalanced VuoScreen_use() / _disuse() calls.");
520 return;
521 }
522
523 if (__sync_sub_and_fetch(&VuoScreen_useCount, 1) == 0)
524 CGDisplayRemoveReconfigurationCallback(VuoScreen_reconfiguration, nullptr);
525}
526
536{
537 VuoScreen_callbacks.addTrigger(screens);
538 screens(VuoScreen_getList());
539}
540