fix(display): 上次超时的显示器排到最后读取

超时的内核调用返回前占着唯一执行槽,固定顺序读取时排在后面的显示器只会持续得到 busy。上一轮超时的显示器改为最后读取,仍每轮重试。
This commit is contained in:
ysicing authored and 缘生 committed 2026-10-04 21:52:14 +08:00
1 parent dc03cb3b0e
commit a7b894f386
3 files changed
+44 -3

No files matched your search

+2 -1
View File
@@ -631,7 +631,8 @@ that the display applied it. No software dimming or speculative compatibility wr
Blocking native work uses one bounded serial execution slot with a two-second caller deadline.
A timed-out kernel call retains the slot until it returns, so retries cannot accumulate stuck work.
Each display read gets its own cancellation ticket, so one timed-out display does not skip the
others in the same refresh. Cancellation is checked between bus operations; late results cannot repopulate a closed or replaced
others in the same refresh; a display that timed out last time is read last, so while its stuck call
still holds the execution slot, responsive displays are not starved by `.busy`. Cancellation is checked between bus operations; late results cannot repopulate a closed or replaced
view. Protocol, timeout, cancellation and write-confirmation tests use simulated devices. To generate
a repeatable, read-only report on actual hardware (no Set VCP):
@@ -133,7 +133,11 @@ final class DisplayController {
isRefreshing = true
defer { if generation == epoch { isRefreshing = false; cancellation = nil } }
var updated: [UInt32: [DisplayControl: DDCResult]] = [:]
for display in catalog where !display.isBuiltIn {
// 超时的内核调用返回前占着唯一的执行槽,紧随其后的读取只会得到 busy;
// 上一轮超时的显示器排到最后,响应正常的显示器不会被它持续挤掉。
let external = catalog.filter { !$0.isBuiltIn }
let timedOut = { (display: DisplayInfo) in self.readings[display.id]?.values.contains(.timedOut) == true }
for display in external.filter({ !timedOut($0) }) + external.filter(timedOut) {
// 超时会取消本台的令牌;每台独立,避免一台无响应导致后面的显示器跳过读取。
let ticket = DDCCancellation()
cancellation = ticket
@@ -21,13 +21,23 @@ private actor TestDisplayBackend: DisplayDDCBackend {
var gates: [CheckedContinuation<[DisplayControl: DDCResult], Never>] = []
var tickets: [DDCCancellation] = []
var timingOutTargets: Set<UInt32> = []
var readOrder: [UInt32] = []
var slotHeld = false
var timeoutsHoldSlot = false
nonisolated let resets = ResetCounter()
func read(_ target: DisplayTarget, cancellation: DDCCancellation) async -> [DisplayControl: DDCResult] {
reads += 1
tickets.append(cancellation)
readOrder.append(target.id)
// 与 NativeDisplayDDC.execute 一致:超时的调用返回前占着执行槽,紧随其后的请求得到 busy
if slotHeld {
slotHeld = false
return [.brightness: .busy]
}
// 与 NativeDisplayDDC.execute 一致:超时时取消传入的令牌
if timingOutTargets.contains(target.id) {
cancellation.cancel()
slotHeld = timeoutsHoldSlot
return [.brightness: .timedOut]
}
if gateReads { return await withCheckedContinuation { gates.append($0) } }
@@ -39,10 +49,15 @@ private actor TestDisplayBackend: DisplayDDCBackend {
}
nonisolated func resetConnections() { resets.increment() }
func block() { gateReads = true }
func timeOut(_ id: UInt32) { timingOutTargets.insert(id) }
func timeOut(_ id: UInt32, holdingSlot: Bool = false) {
timingOutTargets.insert(id)
timeoutsHoldSlot = holdingSlot
}
func finish(_ value: UInt16) { gates.removeFirst().resume(returning: [.brightness: .value(DDCValue(current: value, maximum: 100))]) }
func setWriteResult(_ value: DDCResult) { writeResult = value }
func count() -> Int { reads }
func order() -> [UInt32] { readOrder }
func releaseSlot() { slotHeld = false }
func writeCount() -> Int { writes }
func wasCancelled(_ index: Int) -> Bool { tickets[index].isCancelled }
}
@@ -80,6 +95,27 @@ struct DisplayControllerTests {
#expect(controller.readings[8]?[.brightness] == .value(DDCValue(current: 75, maximum: 100)))
}
@Test func persistentlyTimedOutDisplayIsReadLastSoOthersGetTheSlot() async throws {
let backend = TestDisplayBackend()
await backend.timeOut(7, holdingSlot: true)
let second = DisplayInfo(target: DisplayTarget(id: 8, identity: "fixture-second"), name: "Second Display",
isBuiltIn: false, isMain: false, summary: "2560×1440 · 60Hz")
let controller = DisplayController(backend: backend, catalog: [display, second])
defer { controller.stop() }
controller.setVisible(true)
try await waitForReads(backend, 2)
try await settle(controller)
// 首轮按目录顺序:7 超时后占着执行槽,8 只得到 busy
#expect(await backend.order() == [7, 8])
#expect(controller.readings[8]?[.brightness] == .busy)
await backend.releaseSlot()
await controller.refresh()
// 下一轮 7 排到最后,8 先拿到执行槽;7 仍会重试,不被永久放弃
#expect(await backend.order() == [7, 8, 8, 7])
#expect(controller.readings[8]?[.brightness] == .value(DDCValue(current: 75, maximum: 100)))
#expect(controller.readings[7]?[.brightness] == .timedOut)
}
@Test func wakeDropsMatchedConnectionsButVisibilityChangesKeepThem() async throws {
let backend = TestDisplayBackend()
let controller = DisplayController(backend: backend, catalog: [display])