fix(display): 多屏且无法读取 EDID 时拒绝 DDC 服务
读不到 proxy 的 EDID 时无法确认其归属,仅在只有一台外接屏时接受,多屏时拒绝,避免亮度、音量写到其他显示器。
This commit is contained in:
1 parent
8ddbead9a8
commit
dc03cb3b0e
4 files changed
+25
-3
No files matched your search
+2
-1
@@ -616,7 +616,8 @@ control calls are not a Store-target compatibility claim; a future sandboxed tar
|
||||
backend. Services are matched by the CoreDisplay registry location, not by a guessed model name;
|
||||
ambiguous/missing matches disable controls. `DCPAVServiceProxy` is not a descendant of its
|
||||
framebuffer, so it is paired by registry order and then rejected if the service's own EDID
|
||||
(vendor, product, serial) disagrees with the CG display; an unreadable EDID keeps the location match.
|
||||
(vendor, product, serial) disagrees with the CG display. An unreadable EDID is accepted only when a
|
||||
single external display is online; with several, unverifiable proxies are rejected.
|
||||
Match results, including misses, are cached per connection on the I/O queue and dropped on
|
||||
screen reconfiguration or wake, so polling does not rescan the registry. Device signatures and
|
||||
controller generations reject stale requests after reconnects. No brightness/volume/contrast values are automatically restored.
|
||||
|
||||
@@ -101,6 +101,12 @@ struct EDIDSignature: Equatable, Sendable {
|
||||
serial = UInt32(bytes[12]) | UInt32(bytes[13]) << 8 | UInt32(bytes[14]) << 16 | UInt32(bytes[15]) << 24
|
||||
}
|
||||
|
||||
/// 读不到 EDID 时无法确认 proxy 归属:只有一台外接屏时不存在错写对象,多屏则拒绝。
|
||||
static func verifies(_ observed: EDIDSignature?, expected: EDIDSignature, externalDisplays: Int) -> Bool {
|
||||
guard let observed else { return externalDisplays == 1 }
|
||||
return observed.matches(expected)
|
||||
}
|
||||
|
||||
/// 序列号为 0 表示 EDID 未提供,此时只比较厂商与产品。
|
||||
func matches(_ other: EDIDSignature) -> Bool {
|
||||
vendor == other.vendor && product == other.product
|
||||
|
||||
@@ -128,6 +128,10 @@ final class NativeDisplayDDC: DisplayDDCBackend, @unchecked Sendable {
|
||||
defer { IOObjectRelease(iterator) }
|
||||
let expected = EDIDSignature(vendor: CGDisplayVendorNumber(target.id), product: CGDisplayModelNumber(target.id),
|
||||
serial: CGDisplaySerialNumber(target.id))
|
||||
var online = [CGDirectDisplayID](repeating: 0, count: 16)
|
||||
var onlineCount: UInt32 = 0
|
||||
guard CGGetOnlineDisplayList(UInt32(online.count), &online, &onlineCount) == .success else { return nil }
|
||||
let externalDisplays = online.prefix(Int(onlineCount)).filter { CGDisplayIsBuiltin($0) == 0 }.count
|
||||
var matchingFramebuffer = false
|
||||
var candidates: [Service] = []
|
||||
for _ in 0..<20_000 {
|
||||
@@ -144,7 +148,8 @@ final class NativeDisplayDDC: DisplayDDCBackend, @unchecked Sendable {
|
||||
} else if matchingFramebuffer && label == "DCPAVServiceProxy" {
|
||||
let kind = IORegistryEntryCreateCFProperty(entry, "Location" as CFString, nil, 0)?.takeRetainedValue() as? String
|
||||
guard kind == "External", let service = api.create(nil, entry)?.takeRetainedValue(),
|
||||
edid(of: service, api: api)?.matches(expected) ?? true else { continue }
|
||||
EDIDSignature.verifies(edid(of: service, api: api), expected: expected,
|
||||
externalDisplays: externalDisplays) else { continue }
|
||||
let provider = IORegistryEntrySearchCFProperty(entry, kIOServicePlane, "EPICProviderClass" as CFString,
|
||||
nil, IOOptionBits(kIORegistryIterateParents | kIORegistryIterateRecursively)) as? String
|
||||
// 部分 M1/M2 HDMI 桥使用另一条路由地址;协议校验和仍使用标准 DDC 地址。
|
||||
@@ -155,7 +160,7 @@ final class NativeDisplayDDC: DisplayDDCBackend, @unchecked Sendable {
|
||||
return candidates[0]
|
||||
}
|
||||
|
||||
/// 读不到 EDID(符号缺失或链路不提供)时返回 nil,由调用方保留位置匹配结果。
|
||||
/// 读不到 EDID(符号缺失或链路不提供)时返回 nil,由 EDIDSignature.verifies 决定是否接受。
|
||||
private static func edid(of service: CFTypeRef, api: Functions) -> EDIDSignature? {
|
||||
guard let copyEDID = api.copyEDID else { return nil }
|
||||
var data: Unmanaged<CFData>?
|
||||
|
||||
@@ -49,6 +49,16 @@ struct DDCProtocolTests {
|
||||
#expect(target.matches(EDIDSignature(vendor: 4268, product: 41400, serial: 0)))
|
||||
}
|
||||
|
||||
@Test func unverifiableProxyIsOnlyAcceptedWithASingleExternalDisplay() {
|
||||
let target = EDIDSignature(vendor: 4268, product: 41400, serial: 810_567_746)
|
||||
let other = EDIDSignature(vendor: 4268, product: 41400, serial: 1)
|
||||
#expect(EDIDSignature.verifies(nil, expected: target, externalDisplays: 1))
|
||||
#expect(!EDIDSignature.verifies(nil, expected: target, externalDisplays: 2))
|
||||
#expect(!EDIDSignature.verifies(nil, expected: target, externalDisplays: 0))
|
||||
#expect(EDIDSignature.verifies(target, expected: target, externalDisplays: 3))
|
||||
#expect(!EDIDSignature.verifies(other, expected: target, externalDisplays: 1))
|
||||
}
|
||||
|
||||
@Test func preservesFullRangeAndRejectsInvalidUserValues() {
|
||||
let value = DDCValue(current: 32768, maximum: 65535)
|
||||
#expect(DDCProtocol.parse(reply(current: 32768, maximum: 65535), control: .brightness) == .value(value))
|
||||
|
||||
Reference in new issue
Block a user