Skip to content

Commit 703fad6

Browse files
committed
fix: Add dispatch context & refactor scroll posting
Introduce ScrollDispatchContext to capture event/proxy snapshots, enqueue async posts and guard frames by generation/TTL. Refactor ScrollPoster to use the dispatch context and an unfair lock (remove direct ref tuple), mark/skip synthetic smooth events, and centralize phase/posting logic into new post(...) APIs. Add diagnostics and debug counters, improve lifecycle handling (advance/invalidate generation on stop/reset) and ensure tracking end is posted correctly. Also: ignore /build in .gitignore, add synthetic event marker helpers in ScrollUtils, import os where needed, and call ScrollPoster.stop(.TrackingEnd) on Interceptor.restart. - #868 - #826 - #699 - #687 - #665 - #510 - #512 - #499 - #368 - #597 - #859
1 parent 677d205 commit 703fad6

File tree

6 files changed

+288
-83
lines changed

6 files changed

+288
-83
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ sparkle_private_key.txt
1717

1818
# build
1919
build/*.zip
20+
/build

Mos/ScrollCore/ScrollCore.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,22 @@ class ScrollCore {
4848

4949
// MARK: - 滚动事件处理
5050
let scrollEventCallBack: CGEventTapCallBack = { (proxy, type, event, refcon) in
51+
_ = refcon
52+
// Tap 被系统禁用或重启边界时,强制清理 poster 上下文并失效历史异步帧
53+
if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
54+
ScrollPoster.shared.stop(.TrackingEnd)
55+
return Unmanaged.passUnretained(event)
56+
}
57+
if type != .scrollWheel {
58+
return Unmanaged.passUnretained(event)
59+
}
60+
// 跳过 Mos 自己合成的平滑事件,避免重复进入平滑管线
61+
if ScrollUtils.shared.isSyntheticSmoothEvent(event) {
62+
#if DEBUG
63+
ScrollPoster.shared.recordSkippedSyntheticEvent()
64+
#endif
65+
return Unmanaged.passUnretained(event)
66+
}
5167
// 不处理触控板
5268
// 无法区分黑苹果, 因为黑苹果的触控板驱动直接模拟鼠标输入
5369
// 无法区分 Magic Mouse, 因为其滚动特征与内置的 Trackpad 一致
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//
2+
// ScrollDispatchContext.swift
3+
// Mos
4+
//
5+
// Created by Codex on 2026/3/5.
6+
//
7+
8+
import Cocoa
9+
import os
10+
11+
final class ScrollDispatchContext {
12+
13+
static let shared = ScrollDispatchContext()
14+
15+
struct PostingSnapshot {
16+
let event: CGEvent
17+
let proxy: CGEventTapProxy
18+
let generation: UInt64
19+
let capturedAt: CFTimeInterval
20+
}
21+
22+
private struct SnapshotState {
23+
var eventTemplate: CGEvent?
24+
var proxy: CGEventTapProxy?
25+
var generation: UInt64 = 0
26+
var updatedAt: CFTimeInterval = 0.0
27+
}
28+
29+
private var state = SnapshotState()
30+
private var lock = os_unfair_lock_s()
31+
private let postQueue = DispatchQueue(label: "me.caldis.mos.scrollposter.proxy-post", qos: .userInteractive)
32+
// TTL 仅作为 enqueue 投递时的兜底安全网, 不用于快照创建门控
33+
// 需覆盖最长惯性减速阶段 (通常 1-3s, 极端 ~5s)
34+
private let proxyTTL: CFTimeInterval = 5.0
35+
36+
#if DEBUG
37+
private var postedFrames: UInt64 = 0
38+
private var droppedFramesByGeneration: UInt64 = 0
39+
private var droppedFramesByTTL: UInt64 = 0
40+
private var skippedSyntheticEvents: UInt64 = 0
41+
private var updateSnapshotFailures: UInt64 = 0
42+
#endif
43+
44+
private init() {}
45+
46+
@discardableResult
47+
func capture(event: CGEvent, proxy: CGEventTapProxy) -> Bool {
48+
guard let template = event.copy() else {
49+
#if DEBUG
50+
os_unfair_lock_lock(&lock)
51+
updateSnapshotFailures &+= 1
52+
os_unfair_lock_unlock(&lock)
53+
#endif
54+
return false
55+
}
56+
os_unfair_lock_lock(&lock)
57+
state.eventTemplate = template
58+
state.proxy = proxy
59+
state.updatedAt = CFAbsoluteTimeGetCurrent()
60+
os_unfair_lock_unlock(&lock)
61+
return true
62+
}
63+
64+
func advanceGeneration() {
65+
os_unfair_lock_lock(&lock)
66+
state.generation &+= 1
67+
os_unfair_lock_unlock(&lock)
68+
}
69+
70+
func clearContext() {
71+
os_unfair_lock_lock(&lock)
72+
state.eventTemplate = nil
73+
state.proxy = nil
74+
state.updatedAt = 0.0
75+
os_unfair_lock_unlock(&lock)
76+
}
77+
78+
func invalidateAll() {
79+
os_unfair_lock_lock(&lock)
80+
state.generation &+= 1
81+
state.eventTemplate = nil
82+
state.proxy = nil
83+
state.updatedAt = 0.0
84+
os_unfair_lock_unlock(&lock)
85+
}
86+
87+
func preparePostingSnapshot() -> PostingSnapshot? {
88+
os_unfair_lock_lock(&lock)
89+
guard let proxy = state.proxy,
90+
let eventClone = state.eventTemplate?.copy() else {
91+
os_unfair_lock_unlock(&lock)
92+
return nil
93+
}
94+
let snapshot = PostingSnapshot(event: eventClone, proxy: proxy, generation: state.generation, capturedAt: state.updatedAt)
95+
os_unfair_lock_unlock(&lock)
96+
return snapshot
97+
}
98+
99+
func enqueue(_ snapshot: PostingSnapshot) {
100+
postQueue.async { [self] in
101+
os_unfair_lock_lock(&self.lock)
102+
let now = CFAbsoluteTimeGetCurrent()
103+
let validGeneration = snapshot.generation == self.state.generation
104+
let validTTL = now - snapshot.capturedAt <= self.proxyTTL
105+
if !validGeneration {
106+
#if DEBUG
107+
self.droppedFramesByGeneration &+= 1
108+
#endif
109+
} else if !validTTL {
110+
#if DEBUG
111+
self.droppedFramesByTTL &+= 1
112+
#endif
113+
}
114+
os_unfair_lock_unlock(&self.lock)
115+
guard validGeneration && validTTL else { return }
116+
snapshot.event.tapPostEvent(snapshot.proxy)
117+
#if DEBUG
118+
os_unfair_lock_lock(&self.lock)
119+
self.postedFrames &+= 1
120+
os_unfair_lock_unlock(&self.lock)
121+
#endif
122+
}
123+
}
124+
125+
#if DEBUG
126+
func recordSkippedSyntheticEvent() {
127+
os_unfair_lock_lock(&lock)
128+
skippedSyntheticEvents &+= 1
129+
os_unfair_lock_unlock(&lock)
130+
}
131+
132+
func diagnosticsSnapshot() -> (postedFrames: UInt64, droppedFramesByGeneration: UInt64, droppedFramesByTTL: UInt64, skippedSyntheticEvents: UInt64, updateSnapshotFailures: UInt64) {
133+
os_unfair_lock_lock(&lock)
134+
let snapshot = (
135+
postedFrames: postedFrames,
136+
droppedFramesByGeneration: droppedFramesByGeneration,
137+
droppedFramesByTTL: droppedFramesByTTL,
138+
skippedSyntheticEvents: skippedSyntheticEvents,
139+
updateSnapshotFailures: updateSnapshotFailures
140+
)
141+
os_unfair_lock_unlock(&lock)
142+
return snapshot
143+
}
144+
#endif
145+
}

0 commit comments

Comments
 (0)