react-native-parity-blur 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/ParityBlur.podspec +20 -0
- package/README.md +198 -0
- package/android/build.gradle +57 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/parityblur/AndroidBlurCalibration.kt +82 -0
- package/android/src/main/java/com/parityblur/BlurEngine.kt +132 -0
- package/android/src/main/java/com/parityblur/ParityBlurDebug.kt +24 -0
- package/android/src/main/java/com/parityblur/ParityBlurPackage.kt +17 -0
- package/android/src/main/java/com/parityblur/ParityBlurView.kt +542 -0
- package/android/src/main/java/com/parityblur/ParityBlurViewManager.kt +161 -0
- package/android/src/main/java/com/parityblur/PipelineMath.kt +202 -0
- package/android/src/main/java/com/parityblur/PixelCopySnapshotProvider.kt +108 -0
- package/android/src/main/java/com/parityblur/SoftwareSnapshotProvider.kt +173 -0
- package/android/src/main/java/com/parityblur/WindowBlurContext.kt +116 -0
- package/android/src/test/java/com/parityblur/MiniJson.kt +179 -0
- package/android/src/test/java/com/parityblur/PipelineFixturesTest.kt +222 -0
- package/ios/BlurEngine.swift +106 -0
- package/ios/ColorPipeline.swift +72 -0
- package/ios/MetalPresentationSurface.swift +51 -0
- package/ios/ParityBlurCoreView.swift +382 -0
- package/ios/ParityBlurDebug.swift +18 -0
- package/ios/ParityBlurView.h +14 -0
- package/ios/ParityBlurView.mm +135 -0
- package/ios/PipelineMath.swift +177 -0
- package/ios/SnapshotProvider.swift +128 -0
- package/ios/WindowBlurContext.swift +111 -0
- package/lib/module/BlurView.js +124 -0
- package/lib/module/BlurView.js.map +1 -0
- package/lib/module/ParityBlurViewNativeComponent.ts +82 -0
- package/lib/module/defaults.js +13 -0
- package/lib/module/defaults.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/pipeline/androidCalibration.js +55 -0
- package/lib/module/pipeline/androidCalibration.js.map +1 -0
- package/lib/module/pipeline/captureRect.js +151 -0
- package/lib/module/pipeline/captureRect.js.map +1 -0
- package/lib/module/pipeline/color.js +82 -0
- package/lib/module/pipeline/color.js.map +1 -0
- package/lib/module/pipeline/constants.js +65 -0
- package/lib/module/pipeline/constants.js.map +1 -0
- package/lib/module/pipeline/downsample.js +61 -0
- package/lib/module/pipeline/downsample.js.map +1 -0
- package/lib/module/pipeline/index.js +23 -0
- package/lib/module/pipeline/index.js.map +1 -0
- package/lib/module/pipeline/overlay.js +56 -0
- package/lib/module/pipeline/overlay.js.map +1 -0
- package/lib/module/pipeline/saturation.js +52 -0
- package/lib/module/pipeline/saturation.js.map +1 -0
- package/lib/module/pipeline/types.js +23 -0
- package/lib/module/pipeline/types.js.map +1 -0
- package/lib/module/pipeline/units.js +33 -0
- package/lib/module/pipeline/units.js.map +1 -0
- package/lib/module/presets.js +96 -0
- package/lib/module/presets.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/BlurView.d.ts +16 -0
- package/lib/typescript/src/BlurView.d.ts.map +1 -0
- package/lib/typescript/src/ParityBlurViewNativeComponent.d.ts +63 -0
- package/lib/typescript/src/ParityBlurViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/defaults.d.ts +12 -0
- package/lib/typescript/src/defaults.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/androidCalibration.d.ts +42 -0
- package/lib/typescript/src/pipeline/androidCalibration.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/captureRect.d.ts +84 -0
- package/lib/typescript/src/pipeline/captureRect.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/color.d.ts +19 -0
- package/lib/typescript/src/pipeline/color.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/constants.d.ts +52 -0
- package/lib/typescript/src/pipeline/constants.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/downsample.d.ts +31 -0
- package/lib/typescript/src/pipeline/downsample.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/index.d.ts +20 -0
- package/lib/typescript/src/pipeline/index.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/overlay.d.ts +31 -0
- package/lib/typescript/src/pipeline/overlay.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/saturation.d.ts +38 -0
- package/lib/typescript/src/pipeline/saturation.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/types.d.ts +32 -0
- package/lib/typescript/src/pipeline/types.d.ts.map +1 -0
- package/lib/typescript/src/pipeline/units.d.ts +23 -0
- package/lib/typescript/src/pipeline/units.d.ts.map +1 -0
- package/lib/typescript/src/presets.d.ts +57 -0
- package/lib/typescript/src/presets.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +116 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +167 -0
- package/src/BlurView.tsx +195 -0
- package/src/ParityBlurViewNativeComponent.ts +82 -0
- package/src/defaults.ts +20 -0
- package/src/index.ts +10 -0
- package/src/pipeline/androidCalibration.ts +64 -0
- package/src/pipeline/captureRect.ts +172 -0
- package/src/pipeline/color.ts +66 -0
- package/src/pipeline/constants.ts +65 -0
- package/src/pipeline/downsample.ts +74 -0
- package/src/pipeline/index.ts +20 -0
- package/src/pipeline/overlay.ts +46 -0
- package/src/pipeline/saturation.ts +65 -0
- package/src/pipeline/types.ts +37 -0
- package/src/pipeline/units.ts +32 -0
- package/src/presets.ts +103 -0
- package/src/types.ts +132 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import Metal
|
|
2
|
+
import MetalPerformanceShaders
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
/// The UIKit core of ParityBlurView (plan §15, §20, §36 iOS counterpart = plan §37).
|
|
6
|
+
///
|
|
7
|
+
/// Owned by the Fabric component view (ParityBlurView.mm) as its contentView; children mount
|
|
8
|
+
/// ABOVE it, so they stay sharp (plan §30). Static mode only in this milestone: capture once on
|
|
9
|
+
/// each trigger (attach, layout/size change, pipeline-prop change, refresh()), coalesced per
|
|
10
|
+
/// window; mode='live' renders like static until the shared live coordinator lands (plan §39).
|
|
11
|
+
///
|
|
12
|
+
/// Pipeline per capture (spec §9): PipelineMath geometry (device px, locked rounding) →
|
|
13
|
+
/// LayerRenderSnapshotProvider (model-state, registry exclusion, clipped, downsampled,
|
|
14
|
+
/// sRGB-encoded BGRA8) → upload to .bgra8Unorm texture (gamma-space domain, spec §6) →
|
|
15
|
+
/// MPSImageGaussianBlur(.clamp) → compute post pass (bilinear fractional crop + saturation
|
|
16
|
+
/// matrix + overlay source-over) → CAMetalLayer drawable (no readback).
|
|
17
|
+
@objc(ParityBlurCoreView)
|
|
18
|
+
public final class ParityBlurCoreView: UIView {
|
|
19
|
+
|
|
20
|
+
// MARK: - Props (set by ParityBlurView.mm)
|
|
21
|
+
|
|
22
|
+
@objc public var blurRadius: Double = 0 {
|
|
23
|
+
didSet { if blurRadius != oldValue { requestCapture() } }
|
|
24
|
+
}
|
|
25
|
+
@objc public var mode: NSString = "static" {
|
|
26
|
+
didSet {
|
|
27
|
+
guard mode != oldValue else { return }
|
|
28
|
+
syncLiveRegistration()
|
|
29
|
+
if mode == "static" { requestCapture() } // leaving live: freeze on a fresh static capture
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
@objc public var saturation: Double = 1 {
|
|
33
|
+
didSet { if saturation != oldValue { representOnly() } }
|
|
34
|
+
}
|
|
35
|
+
@objc public var overlayColor: UIColor? {
|
|
36
|
+
didSet { representOnly() }
|
|
37
|
+
}
|
|
38
|
+
@objc public var quality: NSString = "balanced" {
|
|
39
|
+
didSet { if quality != oldValue { requestCapture() } }
|
|
40
|
+
}
|
|
41
|
+
/// 0 = 'auto' sentinel (ParityBlurViewNativeComponent.ts).
|
|
42
|
+
@objc public var downsample: Int = 0 {
|
|
43
|
+
didSet { if downsample != oldValue { requestCapture() } }
|
|
44
|
+
}
|
|
45
|
+
@objc public var maxFps: Int = 30 // stored; live scheduler is M6 scope
|
|
46
|
+
@objc public var fallbackColor: UIColor? {
|
|
47
|
+
didSet { applyFallbackStateIfNeeded() }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@objc public func setCornerRadii(
|
|
51
|
+
topLeft: Double, topRight: Double, bottomRight: Double, bottomLeft: Double
|
|
52
|
+
) {
|
|
53
|
+
presentation.setCornerRadii(
|
|
54
|
+
topLeft: topLeft, topRight: topRight,
|
|
55
|
+
bottomRight: bottomRight, bottomLeft: bottomLeft
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Fabric `refresh()` command (plan §29): coalesced recapture on the next runloop turn.
|
|
60
|
+
@objc public func refresh() {
|
|
61
|
+
requestCapture()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// MARK: - Internals
|
|
65
|
+
|
|
66
|
+
private let presentation = MetalPresentationView()
|
|
67
|
+
private let provider: SnapshotProvider = LayerRenderSnapshotProvider()
|
|
68
|
+
|
|
69
|
+
private var srcTexture: MTLTexture?
|
|
70
|
+
private var dstTexture: MTLTexture?
|
|
71
|
+
/// Post-pass inputs kept for saturation/overlay re-present without recapture (plan §20).
|
|
72
|
+
private var lastCrop: PipelineMath.Rect?
|
|
73
|
+
private var lastSigmaSnapshot: Double = 0
|
|
74
|
+
|
|
75
|
+
private var generation: UInt64 = 0
|
|
76
|
+
private var windowContext: WindowBlurContext?
|
|
77
|
+
private var lastSize = CGSize.zero
|
|
78
|
+
private var observers: [NSObjectProtocol] = []
|
|
79
|
+
|
|
80
|
+
public override init(frame: CGRect) {
|
|
81
|
+
super.init(frame: frame)
|
|
82
|
+
isUserInteractionEnabled = false
|
|
83
|
+
presentation.isUserInteractionEnabled = false
|
|
84
|
+
addSubview(presentation)
|
|
85
|
+
BlurSurfaceRegistry.shared.register(self)
|
|
86
|
+
|
|
87
|
+
observers.append(NotificationCenter.default.addObserver(
|
|
88
|
+
forName: UIAccessibility.reduceTransparencyStatusDidChangeNotification,
|
|
89
|
+
object: nil, queue: .main
|
|
90
|
+
) { [weak self] _ in
|
|
91
|
+
self?.applyFallbackStateIfNeeded()
|
|
92
|
+
// §25: fully release blur machinery when RT turns on — not just skip work. Without this
|
|
93
|
+
// the live view stays registered and the shared CADisplayLink keeps ticking idly.
|
|
94
|
+
self?.syncLiveRegistration()
|
|
95
|
+
self?.requestCapture()
|
|
96
|
+
})
|
|
97
|
+
observers.append(NotificationCenter.default.addObserver(
|
|
98
|
+
forName: UIApplication.didReceiveMemoryWarningNotification,
|
|
99
|
+
object: nil, queue: .main
|
|
100
|
+
) { [weak self] _ in
|
|
101
|
+
// Plan §26: transient CPU snapshot buffers are droppable; textures stay (they present
|
|
102
|
+
// the current static frame). The buffer reallocates on the next capture.
|
|
103
|
+
(self?.provider.buffer)?.release()
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@available(*, unavailable)
|
|
108
|
+
required init?(coder: NSCoder) { fatalError("unavailable") }
|
|
109
|
+
|
|
110
|
+
deinit {
|
|
111
|
+
for o in observers { NotificationCenter.default.removeObserver(o) }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// MARK: - Lifecycle (plan §14.4-equivalent state machine, §20 triggers, §27 laziness)
|
|
115
|
+
|
|
116
|
+
public override func layoutSubviews() {
|
|
117
|
+
super.layoutSubviews()
|
|
118
|
+
presentation.frame = bounds
|
|
119
|
+
sendSubviewToBack(presentation)
|
|
120
|
+
if bounds.size != lastSize {
|
|
121
|
+
lastSize = bounds.size
|
|
122
|
+
requestCapture()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public override func didMoveToWindow() {
|
|
127
|
+
super.didMoveToWindow()
|
|
128
|
+
if let window {
|
|
129
|
+
guard !isFallbackActive, BlurEngine.shared() != nil else {
|
|
130
|
+
applyFallbackStateIfNeeded()
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
let ctx = BlurEngine.shared()!.windowContext(for: window)
|
|
134
|
+
ctx.register(self)
|
|
135
|
+
windowContext = ctx
|
|
136
|
+
requestCapture()
|
|
137
|
+
syncLiveRegistration()
|
|
138
|
+
} else {
|
|
139
|
+
windowContext?.unregister(self)
|
|
140
|
+
windowContext = nil
|
|
141
|
+
ParityBlurDebug.log("instance-release id=\(ObjectIdentifier(self)) mode=\(mode)")
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Milestone 6: live views register with the per-window coordinator (plan §21/§39).
|
|
146
|
+
private func syncLiveRegistration() {
|
|
147
|
+
guard let ctx = windowContext else { return }
|
|
148
|
+
if mode == "live", window != nil, !isFallbackActive {
|
|
149
|
+
ctx.registerLive(self)
|
|
150
|
+
} else {
|
|
151
|
+
ctx.unregisterLive(self)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private var isFallbackActive: Bool {
|
|
156
|
+
UIAccessibility.isReduceTransparencyEnabled
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// Reduce Transparency (plan §25): render fallbackColor, run no blur machinery.
|
|
160
|
+
private func applyFallbackStateIfNeeded() {
|
|
161
|
+
if isFallbackActive {
|
|
162
|
+
presentation.isHidden = true
|
|
163
|
+
backgroundColor = fallbackColor ?? .clear
|
|
164
|
+
} else {
|
|
165
|
+
presentation.isHidden = false
|
|
166
|
+
backgroundColor = .clear
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private var isEligible: Bool {
|
|
171
|
+
window != nil && bounds.width > 0.5 && bounds.height > 0.5 && !isFallbackActive
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private func requestCapture() {
|
|
175
|
+
guard isEligible else { return }
|
|
176
|
+
generation &+= 1
|
|
177
|
+
guard let window else { return }
|
|
178
|
+
let ctx = windowContext ?? BlurEngine.shared().map { engine in
|
|
179
|
+
let c = engine.windowContext(for: window)
|
|
180
|
+
c.register(self)
|
|
181
|
+
windowContext = c
|
|
182
|
+
return c
|
|
183
|
+
}
|
|
184
|
+
ctx?.scheduleCapture(self)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// MARK: - Capture + render (invoked by WindowBlurContext on the main runloop)
|
|
188
|
+
|
|
189
|
+
func performScheduledCapture() {
|
|
190
|
+
guard isEligible, let window, let engine = BlurEngine.shared() else { return }
|
|
191
|
+
let myGeneration = generation
|
|
192
|
+
let target: UIView = window // default target (plan §18); blurTarget override is a later milestone
|
|
193
|
+
|
|
194
|
+
let scale = Double(window.screen.scale)
|
|
195
|
+
let sigmaPx = PipelineMath.sigmaPxFromDp(blurRadius, displayScale: scale)
|
|
196
|
+
|
|
197
|
+
// Geometry in target-local DEVICE PX (spec §3).
|
|
198
|
+
let framePt = convert(bounds, to: target)
|
|
199
|
+
let visible = PipelineMath.Rect(
|
|
200
|
+
x: framePt.minX * scale, y: framePt.minY * scale,
|
|
201
|
+
width: framePt.width * scale, height: framePt.height * scale
|
|
202
|
+
)
|
|
203
|
+
let targetBounds = PipelineMath.Rect(
|
|
204
|
+
x: 0, y: 0,
|
|
205
|
+
width: target.bounds.width * scale, height: target.bounds.height * scale
|
|
206
|
+
)
|
|
207
|
+
let captureRect = PipelineMath.expandCaptureRect(
|
|
208
|
+
visible: visible, targetBounds: targetBounds, sigmaPx: sigmaPx
|
|
209
|
+
)
|
|
210
|
+
guard captureRect.width > 0, captureRect.height > 0 else { return }
|
|
211
|
+
|
|
212
|
+
let d = PipelineMath.resolveDownsample(
|
|
213
|
+
prop: downsample, sigmaPx: sigmaPx,
|
|
214
|
+
captureAreaPx: PipelineMath.rectArea(captureRect), quality: quality as String
|
|
215
|
+
)
|
|
216
|
+
let snapshotRect = PipelineMath.snapshotRectFor(captureRect, downsample: d)
|
|
217
|
+
let crop = PipelineMath.cropRectFor(visible: visible, snapshotRect: snapshotRect, downsample: d)
|
|
218
|
+
guard snapshotRect.width >= 1, snapshotRect.height >= 1 else { return }
|
|
219
|
+
|
|
220
|
+
// Capture (model-state provider; registry exclusion — plan §16 rule 2: exclude ALL
|
|
221
|
+
// registered surfaces, not just self).
|
|
222
|
+
guard provider.capture(
|
|
223
|
+
SnapshotRequest(
|
|
224
|
+
target: target, snapshotRect: snapshotRect,
|
|
225
|
+
downsample: d, displayScale: CGFloat(scale)
|
|
226
|
+
),
|
|
227
|
+
excluding: BlurSurfaceRegistry.shared.all
|
|
228
|
+
) else { return }
|
|
229
|
+
|
|
230
|
+
guard myGeneration == generation else { return } // stale (spec §10)
|
|
231
|
+
|
|
232
|
+
lastCrop = crop
|
|
233
|
+
lastSigmaSnapshot = PipelineMath.sigmaSnapshotFromPx(sigmaPx, downsample: d)
|
|
234
|
+
encodeAndPresent(engine: engine, uploadSnapshot: true)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ------------------------------------------------------------------- live (Milestone 6)
|
|
238
|
+
|
|
239
|
+
private var lastLiveTick: CFTimeInterval = 0
|
|
240
|
+
private var liveInFlight = false
|
|
241
|
+
|
|
242
|
+
/// Visibility heuristic + per-view maxFps throttle + backpressure gate (plan §21, §22, §24).
|
|
243
|
+
func isLiveEligible(now: CFTimeInterval) -> Bool {
|
|
244
|
+
guard mode == "live", !isFallbackActive else { return false }
|
|
245
|
+
guard let window, !isHidden, alpha > 0.02, bounds.width > 0.5, bounds.height > 0.5 else {
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
// Backpressure (plan §22): at most 1 in-flight capture+blur; latest state wins next tick.
|
|
249
|
+
guard !liveInFlight else { return false }
|
|
250
|
+
guard now - lastLiveTick >= 1.0 / Double(max(1, maxFps)) else { return false }
|
|
251
|
+
let frameInWindow = convert(bounds, to: window)
|
|
252
|
+
return frameInWindow.intersects(window.bounds)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/// One live frame: synchronous CPU snapshot + async GPU blur/present. The in-flight flag
|
|
256
|
+
/// clears when the command buffer completes, dropping any ticks that arrive meanwhile
|
|
257
|
+
/// (stale-frame dropping, plan §22).
|
|
258
|
+
func performLiveTick(now: CFTimeInterval) {
|
|
259
|
+
guard let engine = BlurEngine.shared() else { return }
|
|
260
|
+
lastLiveTick = now
|
|
261
|
+
liveInFlight = true
|
|
262
|
+
performScheduledCapture()
|
|
263
|
+
// encodeAndPresent commits asynchronously; approximate completion with the command
|
|
264
|
+
// buffer's completed handler via a lightweight main-queue hop from the engine queue.
|
|
265
|
+
// The capture itself is synchronous, so a conservative clear on the next runloop turn
|
|
266
|
+
// still guarantees at-most-one in-flight GPU pass per view per frame.
|
|
267
|
+
DispatchQueue.main.async { [weak self] in
|
|
268
|
+
self?.liveInFlight = false
|
|
269
|
+
_ = engine // retain engine reference through the frame
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/// Saturation/overlay/radius-mask changes re-present from the cached blurred texture without
|
|
274
|
+
/// recapturing (plan §20). No-op until the first capture exists.
|
|
275
|
+
private func representOnly() {
|
|
276
|
+
guard lastCrop != nil, dstTexture != nil, let engine = BlurEngine.shared() else { return }
|
|
277
|
+
encodeAndPresent(engine: engine, uploadSnapshot: false)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private func encodeAndPresent(engine: BlurEngine, uploadSnapshot: Bool) {
|
|
281
|
+
guard let crop = lastCrop else { return }
|
|
282
|
+
let buffer = provider.buffer
|
|
283
|
+
|
|
284
|
+
if uploadSnapshot {
|
|
285
|
+
let w = buffer.width
|
|
286
|
+
let h = buffer.height
|
|
287
|
+
guard w > 0, h > 0, let data = buffer.data else { return }
|
|
288
|
+
if srcTexture?.width != w || srcTexture?.height != h {
|
|
289
|
+
let desc = MTLTextureDescriptor.texture2DDescriptor(
|
|
290
|
+
pixelFormat: .bgra8Unorm, width: w, height: h, mipmapped: false
|
|
291
|
+
)
|
|
292
|
+
desc.storageMode = .shared
|
|
293
|
+
desc.usage = [.shaderRead, .shaderWrite]
|
|
294
|
+
srcTexture = engine.device.makeTexture(descriptor: desc)
|
|
295
|
+
dstTexture = engine.device.makeTexture(descriptor: desc)
|
|
296
|
+
}
|
|
297
|
+
guard let src = srcTexture else { return }
|
|
298
|
+
src.replace(
|
|
299
|
+
region: MTLRegionMake2D(0, 0, w, h), mipmapLevel: 0,
|
|
300
|
+
withBytes: data, bytesPerRow: buffer.bytesPerRow
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
guard let src = srcTexture, let dst = dstTexture else { return }
|
|
304
|
+
|
|
305
|
+
// Drawable sized to the crop (snapshot px); CAMetalLayer upsamples to bounds (spec §9.12).
|
|
306
|
+
let cropW = max(1, Int(crop.width.rounded()))
|
|
307
|
+
let cropH = max(1, Int(crop.height.rounded()))
|
|
308
|
+
let ml = presentation.metalLayer
|
|
309
|
+
if ml.device == nil {
|
|
310
|
+
ml.device = engine.device
|
|
311
|
+
ml.pixelFormat = .bgra8Unorm
|
|
312
|
+
ml.framebufferOnly = false
|
|
313
|
+
}
|
|
314
|
+
let wanted = CGSize(width: cropW, height: cropH)
|
|
315
|
+
if ml.drawableSize != wanted { ml.drawableSize = wanted }
|
|
316
|
+
|
|
317
|
+
guard let drawable = ml.nextDrawable(),
|
|
318
|
+
let cb = engine.queue.makeCommandBuffer() else { return }
|
|
319
|
+
|
|
320
|
+
// 1. Gaussian blur (spec §9.8). No-blur clamp: sigma <= 0 presents the raw snapshot.
|
|
321
|
+
let sigma = Float(lastSigmaSnapshot)
|
|
322
|
+
if uploadSnapshot {
|
|
323
|
+
if sigma > 0 {
|
|
324
|
+
engine.gaussian(sigma: sigma).encode(commandBuffer: cb, sourceTexture: src, destinationTexture: dst)
|
|
325
|
+
} else if let blit = cb.makeBlitCommandEncoder() {
|
|
326
|
+
blit.copy(from: src, to: dst)
|
|
327
|
+
blit.endEncoding()
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 2. Post pass: fractional crop + saturation + overlay into the drawable (spec §9.9-9.11).
|
|
332
|
+
var overlayRGBA = PipelineMath.RGBA(r: 0, g: 0, b: 0, a: 0)
|
|
333
|
+
if let overlayColor {
|
|
334
|
+
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
|
|
335
|
+
if overlayColor.getRed(&r, green: &g, blue: &b, alpha: &a) {
|
|
336
|
+
overlayRGBA = PipelineMath.RGBA(r: Double(r), g: Double(g), b: Double(b), a: Double(a))
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
guard let pipeline = engine.postProcessPipeline(),
|
|
340
|
+
let sampler = engine.sampler(),
|
|
341
|
+
let compute = cb.makeComputeCommandEncoder() else {
|
|
342
|
+
// Degraded path: blit the integer-aligned crop (blur only). Logged once by the engine.
|
|
343
|
+
if let blit = cb.makeBlitCommandEncoder() {
|
|
344
|
+
let x = min(max(0, Int(crop.x)), dst.width - 1)
|
|
345
|
+
let y = min(max(0, Int(crop.y)), dst.height - 1)
|
|
346
|
+
let w = min(cropW, dst.width - x)
|
|
347
|
+
let h = min(cropH, dst.height - y)
|
|
348
|
+
blit.copy(
|
|
349
|
+
from: dst, sourceSlice: 0, sourceLevel: 0,
|
|
350
|
+
sourceOrigin: MTLOrigin(x: x, y: y, z: 0),
|
|
351
|
+
sourceSize: MTLSize(width: w, height: h, depth: 1),
|
|
352
|
+
to: drawable.texture, destinationSlice: 0, destinationLevel: 0,
|
|
353
|
+
destinationOrigin: MTLOrigin(x: 0, y: 0, z: 0)
|
|
354
|
+
)
|
|
355
|
+
blit.endEncoding()
|
|
356
|
+
}
|
|
357
|
+
cb.present(drawable)
|
|
358
|
+
cb.commit()
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
var uniforms = ColorPipeline.uniforms(
|
|
363
|
+
cropX: crop.x, cropY: crop.y, saturation: saturation, overlay: overlayRGBA
|
|
364
|
+
)
|
|
365
|
+
compute.setComputePipelineState(pipeline)
|
|
366
|
+
compute.setTexture(dst, index: 0)
|
|
367
|
+
compute.setTexture(drawable.texture, index: 1)
|
|
368
|
+
compute.setSamplerState(sampler, index: 0)
|
|
369
|
+
compute.setBytes(&uniforms, length: MemoryLayout<ColorPipeline.Uniforms>.stride, index: 0)
|
|
370
|
+
let tg = MTLSize(width: 8, height: 8, depth: 1)
|
|
371
|
+
let grid = MTLSize(
|
|
372
|
+
width: (cropW + tg.width - 1) / tg.width,
|
|
373
|
+
height: (cropH + tg.height - 1) / tg.height,
|
|
374
|
+
depth: 1
|
|
375
|
+
)
|
|
376
|
+
compute.dispatchThreadgroups(grid, threadsPerThreadgroup: tg)
|
|
377
|
+
compute.endEncoding()
|
|
378
|
+
|
|
379
|
+
cb.present(drawable)
|
|
380
|
+
cb.commit()
|
|
381
|
+
}
|
|
382
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Milestone 7 laziness/lifecycle instrumentation (plan §42.4, §40).
|
|
4
|
+
///
|
|
5
|
+
/// Gated behind `enabled`, which defaults to `false` so a shipped build pays zero cost here.
|
|
6
|
+
/// Flip to `true` only for local hardening runs to prove:
|
|
7
|
+
/// - package import -> no engine init (nothing logs before the first eligible capture)
|
|
8
|
+
/// - first eligible BlurView -> "engine-init" logs exactly once per process
|
|
9
|
+
/// - live registration -> "scheduler-install"/"scheduler-uninstall" bracket the live window
|
|
10
|
+
/// - detach -> "instance-release" logs for every ParityBlurCoreView, live or static
|
|
11
|
+
enum ParityBlurDebug {
|
|
12
|
+
static let enabled = false // debug-only laziness/lifecycle instrumentation (plan §42.4); flip true for local hardening runs
|
|
13
|
+
|
|
14
|
+
@inline(__always)
|
|
15
|
+
static func log(_ event: @autoclosure () -> String) {
|
|
16
|
+
if enabled { NSLog("[ParityBlur.Debug] %@", event()) }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#import <React/RCTViewComponentView.h>
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
|
|
4
|
+
#ifndef ParityBlurViewNativeComponent_h
|
|
5
|
+
#define ParityBlurViewNativeComponent_h
|
|
6
|
+
|
|
7
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
8
|
+
|
|
9
|
+
@interface ParityBlurView : RCTViewComponentView
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
NS_ASSUME_NONNULL_END
|
|
13
|
+
|
|
14
|
+
#endif /* ParityBlurViewNativeComponent_h */
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#import "ParityBlurView.h"
|
|
2
|
+
|
|
3
|
+
#import <React/RCTConversions.h>
|
|
4
|
+
|
|
5
|
+
#import <react/renderer/components/ParityBlurViewSpec/ComponentDescriptors.h>
|
|
6
|
+
#import <react/renderer/components/ParityBlurViewSpec/Props.h>
|
|
7
|
+
#import <react/renderer/components/ParityBlurViewSpec/RCTComponentViewHelpers.h>
|
|
8
|
+
|
|
9
|
+
#import "RCTFabricComponentsPlugins.h"
|
|
10
|
+
|
|
11
|
+
#if __has_include("ParityBlur-Swift.h")
|
|
12
|
+
#import "ParityBlur-Swift.h"
|
|
13
|
+
#else
|
|
14
|
+
#import <ParityBlur/ParityBlur-Swift.h>
|
|
15
|
+
#endif
|
|
16
|
+
|
|
17
|
+
using namespace facebook::react;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Milestone 4: Fabric shell for the iOS static blur backend.
|
|
21
|
+
*
|
|
22
|
+
* All blur work lives in the Swift core (ParityBlurCoreView.swift): lazy BlurEngine,
|
|
23
|
+
* per-window coalesced capture, LayerRender snapshot provider with registry exclusion,
|
|
24
|
+
* MPS gaussian + compute post pass, CAMetalLayer presentation (plan §15, §37;
|
|
25
|
+
* docs/PIPELINE_SPEC.md). This file only:
|
|
26
|
+
* - hosts the core view as contentView (children mount above it, plan §30),
|
|
27
|
+
* - maps Fabric props onto the core's typed properties,
|
|
28
|
+
* - resolves the style border radii for the blur-output clip (plan §31),
|
|
29
|
+
* - forwards the refresh() command (plan §29).
|
|
30
|
+
*/
|
|
31
|
+
@interface ParityBlurView () <RCTParityBlurViewViewProtocol>
|
|
32
|
+
@end
|
|
33
|
+
|
|
34
|
+
/** Resolve one cascaded corner (Point units only; percent is unsupported for the blur clip). */
|
|
35
|
+
static CGFloat PBResolveRadius(const std::optional<ValueUnit> &corner, CGFloat fallback)
|
|
36
|
+
{
|
|
37
|
+
if (corner.has_value() && corner->unit == UnitType::Point) {
|
|
38
|
+
return (CGFloat)corner->value;
|
|
39
|
+
}
|
|
40
|
+
return fallback;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@implementation ParityBlurView {
|
|
44
|
+
ParityBlurCoreView *_core;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
48
|
+
{
|
|
49
|
+
return concreteComponentDescriptorProvider<ParityBlurViewComponentDescriptor>();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
53
|
+
{
|
|
54
|
+
if (self = [super initWithFrame:frame]) {
|
|
55
|
+
static const auto defaultProps = std::make_shared<const ParityBlurViewProps>();
|
|
56
|
+
_props = defaultProps;
|
|
57
|
+
|
|
58
|
+
_core = [[ParityBlurCoreView alloc] initWithFrame:CGRectZero];
|
|
59
|
+
self.contentView = _core;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return self;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// RCTViewComponentView's base -mountChildComponentView:index: inserts every child as a direct
|
|
66
|
+
// subview of `self` at its Fabric child index. The core view occupies subview index 0
|
|
67
|
+
// (contentView), so children shift up one slot to stack above the blur output (plan §30).
|
|
68
|
+
|
|
69
|
+
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
|
|
70
|
+
index:(NSInteger)index
|
|
71
|
+
{
|
|
72
|
+
[self insertSubview:childComponentView atIndex:index + 1];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
|
|
76
|
+
index:(NSInteger)index
|
|
77
|
+
{
|
|
78
|
+
[childComponentView removeFromSuperview];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
82
|
+
{
|
|
83
|
+
const auto &oldViewProps = *std::static_pointer_cast<ParityBlurViewProps const>(_props);
|
|
84
|
+
const auto &newViewProps = *std::static_pointer_cast<ParityBlurViewProps const>(props);
|
|
85
|
+
|
|
86
|
+
if (oldViewProps.blurRadius != newViewProps.blurRadius) {
|
|
87
|
+
_core.blurRadius = newViewProps.blurRadius;
|
|
88
|
+
}
|
|
89
|
+
if (oldViewProps.mode != newViewProps.mode) {
|
|
90
|
+
_core.mode = [NSString stringWithUTF8String:toString(newViewProps.mode).c_str()];
|
|
91
|
+
}
|
|
92
|
+
if (oldViewProps.overlayColor != newViewProps.overlayColor) {
|
|
93
|
+
_core.overlayColor = RCTUIColorFromSharedColor(newViewProps.overlayColor);
|
|
94
|
+
}
|
|
95
|
+
if (oldViewProps.saturation != newViewProps.saturation) {
|
|
96
|
+
_core.saturation = newViewProps.saturation;
|
|
97
|
+
}
|
|
98
|
+
if (oldViewProps.quality != newViewProps.quality) {
|
|
99
|
+
_core.quality = [NSString stringWithUTF8String:toString(newViewProps.quality).c_str()];
|
|
100
|
+
}
|
|
101
|
+
if (oldViewProps.downsample != newViewProps.downsample) {
|
|
102
|
+
_core.downsample = newViewProps.downsample;
|
|
103
|
+
}
|
|
104
|
+
if (oldViewProps.maxFps != newViewProps.maxFps) {
|
|
105
|
+
_core.maxFps = newViewProps.maxFps;
|
|
106
|
+
}
|
|
107
|
+
if (oldViewProps.fallbackColor != newViewProps.fallbackColor) {
|
|
108
|
+
_core.fallbackColor = RCTUIColorFromSharedColor(newViewProps.fallbackColor);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Blur-output rounded clipping (plan §31). Physical corners only, mirroring the Android
|
|
112
|
+
// manager; RTL logical corners and percentage radii are a documented v1 limitation.
|
|
113
|
+
const auto &radii = newViewProps.borderRadii;
|
|
114
|
+
CGFloat uniform = PBResolveRadius(radii.all, 0);
|
|
115
|
+
[_core setCornerRadiiWithTopLeft:PBResolveRadius(radii.topLeft, uniform)
|
|
116
|
+
topRight:PBResolveRadius(radii.topRight, uniform)
|
|
117
|
+
bottomRight:PBResolveRadius(radii.bottomRight, uniform)
|
|
118
|
+
bottomLeft:PBResolveRadius(radii.bottomLeft, uniform)];
|
|
119
|
+
|
|
120
|
+
[super updateProps:props oldProps:oldProps];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#pragma mark - Commands
|
|
124
|
+
|
|
125
|
+
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
|
|
126
|
+
{
|
|
127
|
+
RCTParityBlurViewHandleCommand(self, commandName, args);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
- (void)refresh
|
|
131
|
+
{
|
|
132
|
+
[_core refresh];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@end
|