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.
Files changed (109) hide show
  1. package/LICENSE +20 -0
  2. package/ParityBlur.podspec +20 -0
  3. package/README.md +198 -0
  4. package/android/build.gradle +57 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/parityblur/AndroidBlurCalibration.kt +82 -0
  7. package/android/src/main/java/com/parityblur/BlurEngine.kt +132 -0
  8. package/android/src/main/java/com/parityblur/ParityBlurDebug.kt +24 -0
  9. package/android/src/main/java/com/parityblur/ParityBlurPackage.kt +17 -0
  10. package/android/src/main/java/com/parityblur/ParityBlurView.kt +542 -0
  11. package/android/src/main/java/com/parityblur/ParityBlurViewManager.kt +161 -0
  12. package/android/src/main/java/com/parityblur/PipelineMath.kt +202 -0
  13. package/android/src/main/java/com/parityblur/PixelCopySnapshotProvider.kt +108 -0
  14. package/android/src/main/java/com/parityblur/SoftwareSnapshotProvider.kt +173 -0
  15. package/android/src/main/java/com/parityblur/WindowBlurContext.kt +116 -0
  16. package/android/src/test/java/com/parityblur/MiniJson.kt +179 -0
  17. package/android/src/test/java/com/parityblur/PipelineFixturesTest.kt +222 -0
  18. package/ios/BlurEngine.swift +106 -0
  19. package/ios/ColorPipeline.swift +72 -0
  20. package/ios/MetalPresentationSurface.swift +51 -0
  21. package/ios/ParityBlurCoreView.swift +382 -0
  22. package/ios/ParityBlurDebug.swift +18 -0
  23. package/ios/ParityBlurView.h +14 -0
  24. package/ios/ParityBlurView.mm +135 -0
  25. package/ios/PipelineMath.swift +177 -0
  26. package/ios/SnapshotProvider.swift +128 -0
  27. package/ios/WindowBlurContext.swift +111 -0
  28. package/lib/module/BlurView.js +124 -0
  29. package/lib/module/BlurView.js.map +1 -0
  30. package/lib/module/ParityBlurViewNativeComponent.ts +82 -0
  31. package/lib/module/defaults.js +13 -0
  32. package/lib/module/defaults.js.map +1 -0
  33. package/lib/module/index.js +5 -0
  34. package/lib/module/index.js.map +1 -0
  35. package/lib/module/package.json +1 -0
  36. package/lib/module/pipeline/androidCalibration.js +55 -0
  37. package/lib/module/pipeline/androidCalibration.js.map +1 -0
  38. package/lib/module/pipeline/captureRect.js +151 -0
  39. package/lib/module/pipeline/captureRect.js.map +1 -0
  40. package/lib/module/pipeline/color.js +82 -0
  41. package/lib/module/pipeline/color.js.map +1 -0
  42. package/lib/module/pipeline/constants.js +65 -0
  43. package/lib/module/pipeline/constants.js.map +1 -0
  44. package/lib/module/pipeline/downsample.js +61 -0
  45. package/lib/module/pipeline/downsample.js.map +1 -0
  46. package/lib/module/pipeline/index.js +23 -0
  47. package/lib/module/pipeline/index.js.map +1 -0
  48. package/lib/module/pipeline/overlay.js +56 -0
  49. package/lib/module/pipeline/overlay.js.map +1 -0
  50. package/lib/module/pipeline/saturation.js +52 -0
  51. package/lib/module/pipeline/saturation.js.map +1 -0
  52. package/lib/module/pipeline/types.js +23 -0
  53. package/lib/module/pipeline/types.js.map +1 -0
  54. package/lib/module/pipeline/units.js +33 -0
  55. package/lib/module/pipeline/units.js.map +1 -0
  56. package/lib/module/presets.js +96 -0
  57. package/lib/module/presets.js.map +1 -0
  58. package/lib/module/types.js +4 -0
  59. package/lib/module/types.js.map +1 -0
  60. package/lib/typescript/package.json +1 -0
  61. package/lib/typescript/src/BlurView.d.ts +16 -0
  62. package/lib/typescript/src/BlurView.d.ts.map +1 -0
  63. package/lib/typescript/src/ParityBlurViewNativeComponent.d.ts +63 -0
  64. package/lib/typescript/src/ParityBlurViewNativeComponent.d.ts.map +1 -0
  65. package/lib/typescript/src/defaults.d.ts +12 -0
  66. package/lib/typescript/src/defaults.d.ts.map +1 -0
  67. package/lib/typescript/src/index.d.ts +5 -0
  68. package/lib/typescript/src/index.d.ts.map +1 -0
  69. package/lib/typescript/src/pipeline/androidCalibration.d.ts +42 -0
  70. package/lib/typescript/src/pipeline/androidCalibration.d.ts.map +1 -0
  71. package/lib/typescript/src/pipeline/captureRect.d.ts +84 -0
  72. package/lib/typescript/src/pipeline/captureRect.d.ts.map +1 -0
  73. package/lib/typescript/src/pipeline/color.d.ts +19 -0
  74. package/lib/typescript/src/pipeline/color.d.ts.map +1 -0
  75. package/lib/typescript/src/pipeline/constants.d.ts +52 -0
  76. package/lib/typescript/src/pipeline/constants.d.ts.map +1 -0
  77. package/lib/typescript/src/pipeline/downsample.d.ts +31 -0
  78. package/lib/typescript/src/pipeline/downsample.d.ts.map +1 -0
  79. package/lib/typescript/src/pipeline/index.d.ts +20 -0
  80. package/lib/typescript/src/pipeline/index.d.ts.map +1 -0
  81. package/lib/typescript/src/pipeline/overlay.d.ts +31 -0
  82. package/lib/typescript/src/pipeline/overlay.d.ts.map +1 -0
  83. package/lib/typescript/src/pipeline/saturation.d.ts +38 -0
  84. package/lib/typescript/src/pipeline/saturation.d.ts.map +1 -0
  85. package/lib/typescript/src/pipeline/types.d.ts +32 -0
  86. package/lib/typescript/src/pipeline/types.d.ts.map +1 -0
  87. package/lib/typescript/src/pipeline/units.d.ts +23 -0
  88. package/lib/typescript/src/pipeline/units.d.ts.map +1 -0
  89. package/lib/typescript/src/presets.d.ts +57 -0
  90. package/lib/typescript/src/presets.d.ts.map +1 -0
  91. package/lib/typescript/src/types.d.ts +116 -0
  92. package/lib/typescript/src/types.d.ts.map +1 -0
  93. package/package.json +167 -0
  94. package/src/BlurView.tsx +195 -0
  95. package/src/ParityBlurViewNativeComponent.ts +82 -0
  96. package/src/defaults.ts +20 -0
  97. package/src/index.ts +10 -0
  98. package/src/pipeline/androidCalibration.ts +64 -0
  99. package/src/pipeline/captureRect.ts +172 -0
  100. package/src/pipeline/color.ts +66 -0
  101. package/src/pipeline/constants.ts +65 -0
  102. package/src/pipeline/downsample.ts +74 -0
  103. package/src/pipeline/index.ts +20 -0
  104. package/src/pipeline/overlay.ts +46 -0
  105. package/src/pipeline/saturation.ts +65 -0
  106. package/src/pipeline/types.ts +37 -0
  107. package/src/pipeline/units.ts +32 -0
  108. package/src/presets.ts +103 -0
  109. 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