-- v2: winemac.drv: Support to generate EDID from display parameters from Core Graphics APIs. winemac.drv: Support to get EDID from IODisplayConnect. winemac.drv: Support to get EDID from DCPAVServiceProxy.
From: Jactry Zeng jzeng@codeweavers.com
IOAVServiceCopyEDID() is a private API of macOS; ideally, maybe we should avoid using it. But this is the most straightforward way to get the EDID of an external monitor on Apple Silicon. --- dlls/winemac.drv/cocoa_display.m | 89 ++++++++++++++++++++++++++++++-- dlls/winemac.drv/display.c | 8 +-- dlls/winemac.drv/macdrv_cocoa.h | 4 +- 3 files changed, 94 insertions(+), 7 deletions(-)
diff --git a/dlls/winemac.drv/cocoa_display.m b/dlls/winemac.drv/cocoa_display.m index 372d89381f1..d398832766b 100644 --- a/dlls/winemac.drv/cocoa_display.m +++ b/dlls/winemac.drv/cocoa_display.m @@ -24,6 +24,7 @@ #ifdef HAVE_MTLDEVICE_REGISTRYID #import <Metal/Metal.h> #endif +#include <dlfcn.h> #include "macdrv_cocoa.h"
#pragma GCC diagnostic ignored "-Wdeclaration-after-statement" @@ -581,6 +582,78 @@ void macdrv_free_adapters(struct macdrv_adapter* adapters) free(adapters); }
+static unsigned int get_edid_from_dcpav_service_proxy(uint32_t vendor_number, uint32_t model_number, + uint32_t serial_number, unsigned char **prop) +{ + typedef CFTypeRef IOAVServiceRef; + static IOAVServiceRef (*pIOAVServiceCreateWithService)(CFAllocatorRef, io_service_t); + static IOReturn (*pIOAVServiceCopyEDID)(IOAVServiceRef, CFDataRef*); + static dispatch_once_t once; + io_iterator_t iterator; + kern_return_t result; + io_service_t service; + int length = 0; + + *prop = NULL; + dispatch_once(&once, ^{ + void *handle = dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", RTLD_LAZY | RTLD_LOCAL); + if (handle) + { + pIOAVServiceCreateWithService = dlsym(handle, "IOAVServiceCreateWithService"); + pIOAVServiceCopyEDID = dlsym(handle, "IOAVServiceCopyEDID"); + } + }); + + if (!pIOAVServiceCreateWithService || !pIOAVServiceCopyEDID) + return length; + + result = IOServiceGetMatchingServices(0, IOServiceMatching("DCPAVServiceProxy"), &iterator); + if (result != KERN_SUCCESS) + return length; + + while((service = IOIteratorNext(iterator))) + { + uint32_t vendor_number_edid, model_number_edid, serial_number_edid; + const unsigned char *edid_ptr; + IOAVServiceRef avservice; + CFDataRef edid = NULL; + IOReturn edid_result; + + avservice = pIOAVServiceCreateWithService(kCFAllocatorDefault, service); + IOObjectRelease(service); + if (!avservice) + continue; + + edid_result = pIOAVServiceCopyEDID(avservice, &edid); + CFRelease(avservice); + if (edid_result != kIOReturnSuccess || !edid || CFDataGetLength(edid) < 13) + continue; + + edid_ptr = CFDataGetBytePtr(edid); + vendor_number_edid = (uint16_t)(edid_ptr[9] | (edid_ptr[8] << 8)); + model_number_edid = *((uint16_t *)&edid_ptr[10]); + serial_number_edid = *((uint32_t *)&edid_ptr[12]); + if (vendor_number == vendor_number_edid && + model_number == model_number_edid && + serial_number == serial_number_edid) + { + length = CFDataGetLength(edid); + if (length && (*prop = malloc(length))) + memcpy(*prop, edid_ptr, length); + else + length = 0; + } + + CFRelease(edid); + + if (length) + break; + } + + IOObjectRelease(iterator); + return length; +} + /*********************************************************************** * macdrv_get_monitors * @@ -597,7 +670,7 @@ int macdrv_get_monitors(CGDirectDisplayID adapter_id, struct macdrv_monitor** ne struct macdrv_monitor* monitors = NULL; struct macdrv_monitor* realloc_monitors; CGDirectDisplayID display_ids[16]; - uint32_t display_id_count; + uint32_t display_id_count, vendor_number, model_number, serial_number; NSArray<NSScreen *> *screens = [NSScreen screens]; NSRect primary_frame; int primary_index = 0; @@ -654,6 +727,13 @@ int macdrv_get_monitors(CGDirectDisplayID adapter_id, struct macdrv_monitor** ne monitors[monitor_count].id = display_ids[i]; monitors[monitor_count].rc_monitor = convert_display_rect(screen.frame, primary_frame); monitors[monitor_count].rc_work = convert_display_rect(screen.visibleFrame, primary_frame); + + vendor_number = CGDisplayVendorNumber(monitors[monitor_count].id); + model_number = CGDisplayModelNumber(monitors[monitor_count].id); + serial_number = CGDisplaySerialNumber(monitors[monitor_count].id); + monitors[monitor_count].edid_len = get_edid_from_dcpav_service_proxy(vendor_number, model_number, + serial_number, &monitors[monitor_count].edid); + monitor_count++; break; } @@ -674,7 +754,7 @@ int macdrv_get_monitors(CGDirectDisplayID adapter_id, struct macdrv_monitor** ne ret = 0; done: if (ret) - macdrv_free_monitors(monitors); + macdrv_free_monitors(monitors, capacity); return ret; } } @@ -684,8 +764,11 @@ int macdrv_get_monitors(CGDirectDisplayID adapter_id, struct macdrv_monitor** ne * * Frees an monitor list allocated from macdrv_get_monitors() */ -void macdrv_free_monitors(struct macdrv_monitor* monitors) +void macdrv_free_monitors(struct macdrv_monitor* monitors, int monitor_count) { + while (monitor_count--) + if (monitors[monitor_count].edid) + free(monitors[monitor_count].edid); if (monitors) free(monitors); } diff --git a/dlls/winemac.drv/display.c b/dlls/winemac.drv/display.c index 8b012379b2e..2a5c55093bd 100644 --- a/dlls/winemac.drv/display.c +++ b/dlls/winemac.drv/display.c @@ -213,7 +213,7 @@ static void init_original_display_mode(void) HKEY mac_driver_hkey, parent_hkey; DWORD disposition; struct macdrv_monitor *monitors = NULL; - int num_monitors, i; + int num_monitors = 0, i;
if (inited_original_display_mode) return; @@ -252,7 +252,7 @@ done: success = TRUE;
fail: - macdrv_free_monitors(monitors); + macdrv_free_monitors(monitors, num_monitors); NtClose(parent_hkey); if (!success && parent_hkey) reg_delete_tree(mac_driver_hkey, initial_mode_keyW, sizeof(initial_mode_keyW)); @@ -1093,6 +1093,8 @@ UINT macdrv_UpdateDisplayDevices(const struct gdi_device_manager *device_manager { .rc_monitor = rect_from_cgrect(monitor->rc_monitor), .rc_work = rect_from_cgrect(monitor->rc_work), + .edid_len = monitor->edid_len, + .edid = monitor->edid, }; device_manager->add_monitor( &gdi_monitor, param );
@@ -1104,7 +1106,7 @@ UINT macdrv_UpdateDisplayDevices(const struct gdi_device_manager *device_manager if (!(modes = display_get_modes(adapter->id, &mode_count))) break; device_manager->add_modes( ¤t_mode, mode_count, modes, param ); free(modes); - macdrv_free_monitors(monitors); + macdrv_free_monitors(monitors, monitor_count); }
macdrv_free_adapters(adapters); diff --git a/dlls/winemac.drv/macdrv_cocoa.h b/dlls/winemac.drv/macdrv_cocoa.h index 954acb0fc67..82ef110dce5 100644 --- a/dlls/winemac.drv/macdrv_cocoa.h +++ b/dlls/winemac.drv/macdrv_cocoa.h @@ -253,6 +253,8 @@ static inline CGPoint cgpoint_win_from_mac(CGPoint point) CGRect rc_monitor; /* as RcWork in MONITORINFO struct after conversion by rect_from_cgrect */ CGRect rc_work; + unsigned char *edid; + uint32_t edid_len; };
extern int macdrv_set_display_mode(CGDirectDisplayID id, CGDisplayModeRef display_mode); @@ -261,7 +263,7 @@ static inline CGPoint cgpoint_win_from_mac(CGPoint point) extern int macdrv_get_adapters(uint64_t gpu_id, struct macdrv_adapter** adapters, int* count); extern void macdrv_free_adapters(struct macdrv_adapter* adapters); extern int macdrv_get_monitors(CGDirectDisplayID adapter_id, struct macdrv_monitor** monitors, int* count); -extern void macdrv_free_monitors(struct macdrv_monitor* monitors); +extern void macdrv_free_monitors(struct macdrv_monitor* monitors, int monitor_count);
/* event */
From: Jactry Zeng jzeng@codeweavers.com
IOAVServiceCopyEDID() isn't available on Intel Mac, let's try to get EDID from IODisplayConnect. --- dlls/winemac.drv/cocoa_display.m | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+)
diff --git a/dlls/winemac.drv/cocoa_display.m b/dlls/winemac.drv/cocoa_display.m index d398832766b..bde791b3e10 100644 --- a/dlls/winemac.drv/cocoa_display.m +++ b/dlls/winemac.drv/cocoa_display.m @@ -21,6 +21,7 @@ #include "config.h"
#import <AppKit/AppKit.h> +#import <IOKit/graphics/IOGraphicsLib.h> #ifdef HAVE_MTLDEVICE_REGISTRYID #import <Metal/Metal.h> #endif @@ -654,6 +655,60 @@ static unsigned int get_edid_from_dcpav_service_proxy(uint32_t vendor_number, ui return length; }
+static unsigned int get_edid_from_io_display_edid(uint32_t vendor_number, uint32_t model_number, + uint32_t serial_number, unsigned char **prop) +{ + io_iterator_t iterator; + kern_return_t result; + io_service_t service; + int length = 0; + + *prop = NULL; + result = IOServiceGetMatchingServices(0, IOServiceMatching("IODisplayConnect"), &iterator); + if (result != KERN_SUCCESS) + return length; + + while((service = IOIteratorNext(iterator))) + { + uint32_t vendor_number_edid, model_number_edid, serial_number_edid; + const unsigned char *edid_ptr; + CFDictionaryRef display_dict; + CFDataRef edid; + + display_dict = IOCreateDisplayInfoDictionary(service, 0); + if (display_dict) + { + edid = CFDictionaryGetValue(display_dict, CFSTR(kIODisplayEDIDKey)); + if (edid && (CFDataGetLength(edid) >= 13)) + { + edid_ptr = CFDataGetBytePtr(edid); + vendor_number_edid = (uint16_t)(edid_ptr[9] | (edid_ptr[8] << 8)); + model_number_edid = *((uint16_t *)&edid_ptr[10]); + serial_number_edid = *((uint32_t *)&edid_ptr[12]); + if (vendor_number == vendor_number_edid && + model_number == model_number_edid && + /* CGDisplaySerialNumber() isn't reliable on Intel machines; it returns 0 sometimes. */ + (!serial_number || serial_number == serial_number_edid)) + { + length = CFDataGetLength(edid); + if (length && (*prop = malloc(length))) + memcpy(*prop, edid_ptr, length); + else + length = 0; + } + } + CFRelease(display_dict); + } + + IOObjectRelease(service); + if (length) + break; + } + + IOObjectRelease(iterator); + return length; +} + /*********************************************************************** * macdrv_get_monitors * @@ -733,6 +788,9 @@ int macdrv_get_monitors(CGDirectDisplayID adapter_id, struct macdrv_monitor** ne serial_number = CGDisplaySerialNumber(monitors[monitor_count].id); monitors[monitor_count].edid_len = get_edid_from_dcpav_service_proxy(vendor_number, model_number, serial_number, &monitors[monitor_count].edid); + if (!monitors[monitor_count].edid_len) + monitors[monitor_count].edid_len = get_edid_from_io_display_edid(vendor_number, model_number, + serial_number, &monitors[monitor_count].edid);
monitor_count++; break;
From: Jactry Zeng jzeng@codeweavers.com
Neither IOAVServiceCopyEDID() nor "IODisplayEDID" is available for Apple Silicon MacBook's built-in screen, let's try to generate EDID data based on display parameters from Core Graphics APIs. This isn't perfect since some parameters are missing, but at least it satisfies some Unreal games. --- dlls/winemac.drv/cocoa_display.m | 198 +++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+)
diff --git a/dlls/winemac.drv/cocoa_display.m b/dlls/winemac.drv/cocoa_display.m index bde791b3e10..e50fc3423dd 100644 --- a/dlls/winemac.drv/cocoa_display.m +++ b/dlls/winemac.drv/cocoa_display.m @@ -709,6 +709,201 @@ static unsigned int get_edid_from_io_display_edid(uint32_t vendor_number, uint32 return length; }
+static uint16_t get_manufacturer_from_vendor(uint32_t vendor) +{ + uint16_t manufacturer = 0; + + manufacturer |= ((vendor >> 10) & 0x1f) << 10; + manufacturer |= ((vendor >> 5) & 0x1f) << 5; + manufacturer |= (vendor & 0x1f); + manufacturer = (manufacturer >> 8) | (manufacturer << 8); + return manufacturer; +} + +static void fill_detailed_timing_desc(uint8_t *data, int horizontal, int vertical, + double refresh, size_t mwidth, size_t mheight) +{ + int h_front_porch = 8, h_sync_pulse = 32, h_back_porch = 40; + int v_front_porch = 6, v_sync_pulse = 8, v_back_porch = 40; + int h_blanking, v_blanking, h_total, v_total, pixel_clock; + + h_blanking = h_front_porch + h_sync_pulse + h_back_porch; + v_blanking = v_front_porch + v_sync_pulse + v_back_porch; + h_total = horizontal + h_blanking; + v_total = vertical + v_blanking; + + if (refresh <= 0.0) refresh = 60.0; + pixel_clock = (int)round(h_total * v_total * refresh / 10000.0); + + memset(data, 0, 18); + data[0] = pixel_clock & 0xff; + data[1] = (pixel_clock >> 8) & 0xff; + data[2] = horizontal; + data[3] = h_total - horizontal; + data[4] = (((h_total - horizontal) >> 8) & 0xf) | (((horizontal >> 8) & 0xf) << 4); + data[5] = vertical; + data[6] = v_total - vertical; + data[7] = (((v_total - vertical) >> 8) & 0xf) | (((vertical >> 8) & 0xf) << 4); + data[8] = h_front_porch; + data[9] = h_sync_pulse; + data[10] = ((v_front_porch & 0xf) << 4) | (v_sync_pulse & 0xf); + data[11] = (((h_front_porch >> 8) & 3) << 6) + | (((h_sync_pulse >> 8) & 3) << 4) + | (((v_front_porch >> 4) & 3) << 2) + | ((v_sync_pulse >> 4) & 3); + data[12] = mwidth; + data[13] = mheight; + data[14] = (((mwidth >> 8) & 0xf) << 4) | ((mheight >> 8) & 0xf); + data[17] = 0x1e; +} + +static unsigned int generate_edid(CGDirectDisplayID display_id, uint32_t vendor_number, uint32_t model_number, + uint32_t serial_number, unsigned char **prop) +{ + struct display_param { + size_t width; + size_t height; + double refresh; + } best_params[3]; + double mwidth, mheight, refresh; + CGDisplayModeRef mode = NULL; + size_t width = 0, height = 0; + uint8_t *edid, sum, *p; + int i, j, timings = 0; + CGSize screen_size; + CFArrayRef modes; + + edid = malloc(128); + if (!edid) + return 0; + + screen_size = CGDisplayScreenSize(display_id); + mwidth = screen_size.width; + mheight = screen_size.height; + + *(uint64_t *)&edid[0] = 0x00ffffffffffff00; + + *(uint16_t *)&edid[8] = get_manufacturer_from_vendor(vendor_number); + *(uint16_t *)&edid[10] = (uint16_t)model_number; + *(uint32_t *)&edid[12] = serial_number; + edid[16] = 52; /* weeks */ + edid[17] = 30; /* year */ + edid[18] = 1; /* version */ + edid[19] = 4; /* revision */ + edid[20] = 0xb5; /* video input parameters: 10 bits DisplayPort */ + edid[21] = (uint8_t)(mwidth / 10.0); /* horizontal screen size in cm */ + edid[22] = (uint8_t)(mheight / 10.0); /* vertical screen size in cm */ + edid[23] = 0x78; /* gamma: 2.2 */ + edid[24] = 0x06; /* supported features: RGB 4:4:4, sRGB, native pixel format and refresh rate */ + + /* color characteristics: sRGB */ + edid[25] = 0xee; + edid[26] = 0x91; + edid[27] = 0xa3; + edid[28] = 0x54; + edid[29] = 0x4c; + edid[30] = 0x99; + edid[31] = 0x26; + edid[32] = 0x0f; + edid[33] = 0x50; + edid[34] = 0x54; + + for (i = 0; i < 16; ++i) edid[38 + i] = 1; + + /* detailed timing descriptors */ + p = edid + 54; + modes = CGDisplayCopyAllDisplayModes(display_id, NULL); + if (modes) + i = CFArrayGetCount(modes); + else + i = 0; + for (i--; i >= 0; i--) + { + CGDisplayModeRef candidate_mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(modes, i); + int flags = CGDisplayModeGetIOFlags(candidate_mode); + if (flags & kDisplayModeNativeFlag || flags & kDisplayModeDefaultFlag) + { + width = CGDisplayModeGetPixelWidth(candidate_mode); + height = CGDisplayModeGetPixelHeight(candidate_mode); + refresh = CGDisplayModeGetRefreshRate(candidate_mode); + if (!timings) + { + best_params[0].width = width; + best_params[0].height = height; + best_params[0].refresh = refresh; + timings++; + } + else + { + for (j = 0; j < timings; j++) + { + if (best_params[j].width < width || + (best_params[j].width == width && best_params[j].refresh < refresh)) + { + struct display_param swap_display_param = best_params[j]; + + best_params[j].width = width; + best_params[j].height = height; + best_params[j].refresh = refresh; + width = swap_display_param.width; + height = swap_display_param.height; + refresh = swap_display_param.refresh; + } + } + if (timings != 3) + { + best_params[timings].width = width; + best_params[timings].height = height; + best_params[timings].refresh = refresh; + timings++; + } + } + } + } + if (modes) + CFRelease(modes); + if (!timings) + { + mode = CGDisplayCopyDisplayMode(display_id); + width = CGDisplayModeGetPixelWidth(mode); + height = CGDisplayModeGetPixelHeight(mode); + refresh = CGDisplayModeGetRefreshRate(mode); + fill_detailed_timing_desc(p, width, height, refresh, (size_t)mwidth, (size_t)mheight); + timings++; + CGDisplayModeRelease(mode); + } + else + { + for (i = 0; i < timings; i++) + { + fill_detailed_timing_desc(p, best_params[i].width, best_params[i].height, + best_params[i].refresh, (size_t)mwidth, (size_t)mheight); + p += 18; + p[3] = 0x10; + } + } + while (timings != 3) + { + p += 18; + p[3] = 0x10; + timings++; + } + + /* display product name */ + p[3] = 0xfc; + strcpy((char *)p + 5, "Wine Monitor"); + p[17] = 0x0a; + + /* checksum */ + sum = 0; + for (i = 0; i < 127; ++i) + sum += edid[i]; + edid[127] = 256 - sum; + + *prop = edid; + return 128; +} + /*********************************************************************** * macdrv_get_monitors * @@ -791,6 +986,9 @@ int macdrv_get_monitors(CGDirectDisplayID adapter_id, struct macdrv_monitor** ne if (!monitors[monitor_count].edid_len) monitors[monitor_count].edid_len = get_edid_from_io_display_edid(vendor_number, model_number, serial_number, &monitors[monitor_count].edid); + if (!monitors[monitor_count].edid_len) + monitors[monitor_count].edid_len = generate_edid(monitors[monitor_count].id, vendor_number, + model_number, serial_number, &monitors[monitor_count].edid);
monitor_count++; break;
On Mon Nov 17 13:37:36 2025 +0000, Brendan Shanks wrote:
Overall looks good, I tested the 3 code paths.
Hi Brendan,
Thanks for the review! I pushed a v2 with changes according to your comment.
v2: - Adjustments for the recent deletion of macdrv_get_displays(). - Call IOObjectRelease(service)/CFRelease(avservice) right after IOAVServiceCreateWithService()/IOAVServiceCopyEDID(). - Check CFDataGetLength(edid) before access edid_ptr[n]. - Initialize i when modes is NULL in generate_edid(). - Simply EDID buffer allocation in generate_edid().
Brendan Shanks (@bshanks) commented about dlls/winemac.drv/cocoa_display.m:
- {
uint32_t vendor_number_edid, model_number_edid, serial_number_edid;const unsigned char *edid_ptr;IOAVServiceRef avservice;CFDataRef edid = NULL;IOReturn edid_result;avservice = pIOAVServiceCreateWithService(kCFAllocatorDefault, service);IOObjectRelease(service);if (!avservice)continue;edid_result = pIOAVServiceCopyEDID(avservice, &edid);CFRelease(avservice);if (edid_result != kIOReturnSuccess || !edid || CFDataGetLength(edid) < 13)continue;
A too-short `edid` would leak here
Brendan Shanks (@bshanks) commented about dlls/winemac.drv/cocoa_display.m:
monitors[monitor_count].id = display_ids[i]; monitors[monitor_count].rc_monitor = convert_display_rect(screen.frame, primary_frame); monitors[monitor_count].rc_work = convert_display_rect(screen.visibleFrame, primary_frame);
vendor_number = CGDisplayVendorNumber(monitors[monitor_count].id);model_number = CGDisplayModelNumber(monitors[monitor_count].id);serial_number = CGDisplaySerialNumber(monitors[monitor_count].id);monitors[monitor_count].edid_len = get_edid_from_dcpav_service_proxy(vendor_number, model_number,
It's up to you, but maybe the `get_edid_*` and `generate_edid` functions could return a CFData, and then `macdrv_get_monitors` would do the malloc?