1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
|
// The one GPU owner for panel transitions and persistent scene effects.
// CoreText remains the renderer: while either exists PardesView draws its
// complete ordinary frame into this retained bitmap, then Core Image runs the
// standalone shaders/crt.ci.metal kernels over it. With neither, the view
// bypasses this object entirely.
import CoreGraphics
import CoreImage
import Darwin
import Metal
final class ScenePostprocessor {
enum CompositeOutcome {
case presented
case retry
case unavailable
}
private let colorSpace: CGColorSpace
private let sceneKernel: CIKernel
private let panelClearKernel: CIKernel
private let panelKernel: CIKernel
private let context: CIContext
private var raster: CGContext?
private var previousRaster: CGContext?
private var rasterWidth = 0
private var rasterHeight = 0
private var rasterScale: CGFloat = 0
init?() {
let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)
?? CGColorSpaceCreateDeviceRGB()
guard let source = ScenePostprocessor.kernelSource(),
let device = MTLCreateSystemDefaultDevice(),
let made = try? CIKernel.kernels(withMetalString: source),
let sceneKernel = made.first(where: { $0.name == "pardesScene" }),
let panelClearKernel = made.first(where: { $0.name == "pardesPanelClear" }),
let panelKernel = made.first(where: { $0.name == "pardesPanel" })
else { return nil }
self.colorSpace = colorSpace
self.sceneKernel = sceneKernel
self.panelClearKernel = panelClearKernel
self.panelKernel = panelKernel
self.context = CIContext(mtlDevice: device, options: [
.workingColorSpace: colorSpace,
.outputColorSpace: colorSpace,
.cacheIntermediates: false,
])
}
private static func kernelSource() -> String? {
var candidates: [URL] = []
if let resources = Bundle.main.resourceURL {
candidates.append(resources.appendingPathComponent("crt.ci.metal"))
}
// An unbundled e2e executable has no Contents/Resources. Its build
// puts the asset next to the binary, and this explicit candidate keeps
// that dev loop independent of Bundle's treatment of command tools.
if let executable = Bundle.main.executableURL {
candidates.append(executable.deletingLastPathComponent()
.appendingPathComponent("crt.ci.metal"))
}
for url in candidates {
if let source = try? String(contentsOf: url, encoding: .utf8) {
return source
}
}
return nil
}
/// A body-font-sized, top-left-origin CGContext matching the view's own
/// coordinate system. It is retained across frames; only a resize or a
/// backing-scale move reallocates the pixel storage.
func renderContext(size: CGSize, scale: CGFloat) -> CGContext? {
let scale = max(1, scale)
let width = max(1, Int((size.width * scale).rounded(.up)))
let height = max(1, Int((size.height * scale).rounded(.up)))
if raster == nil || width != rasterWidth || height != rasterHeight || scale != rasterScale {
let info = CGBitmapInfo.byteOrder32Big.rawValue
| CGImageAlphaInfo.premultipliedLast.rawValue
guard let next = CGContext(
data: nil, width: width, height: height,
bitsPerComponent: 8, bytesPerRow: width * 4,
space: colorSpace, bitmapInfo: info)
else { return nil }
next.translateBy(x: 0, y: CGFloat(height))
next.scaleBy(x: scale, y: -scale)
raster = next
// The old and new snapshots must have identical extents. Lazily
// rebuild the old owner only if an effect actually needs it.
previousRaster = nil
rasterWidth = width
rasterHeight = height
rasterScale = scale
}
return raster
}
/// A second complete CoreText raster for the frozen old grid. It is
/// intentionally a peer of `raster`, not a collection of per-pane image
/// pointers: deleted PDF/image panes have already released their core
/// bytes, while PardesView's frozen CGImages remain safe to draw here.
func previousRenderContext(size: CGSize, scale: CGFloat) -> CGContext? {
guard renderContext(size: size, scale: scale) != nil else { return nil }
if previousRaster == nil {
let info = CGBitmapInfo.byteOrder32Big.rawValue
| CGImageAlphaInfo.premultipliedLast.rawValue
guard let next = CGContext(
data: nil, width: rasterWidth, height: rasterHeight,
bitsPerComponent: 8, bytesPerRow: rasterWidth * 4,
space: colorSpace, bitmapInfo: info)
else { return nil }
next.translateBy(x: 0, y: CGFloat(rasterHeight))
next.scaleBy(x: rasterScale, y: -rasterScale)
previousRaster = next
}
return previousRaster
}
/// Run the one scene kernel and copy its result into the flipped NSView
/// context. Allocation/device failures may be retried briefly. A kernel
/// rejecting its fixed argument layout is permanent for this executable,
/// so the caller can turn the advertised effects back off instead of
/// silently spinning a display clock forever.
func composite(
scene: pardes_scene_s,
tracks: UnsafeBufferPointer<pardes_panel_track_s>,
changedCells: UnsafePointer<UInt8>?,
changedAttachmentRects: [CGRect],
gridSize: (cols: Int, rows: Int),
cellSize: CGSize,
ground: UInt32?,
size: CGSize,
into destination: CGContext
) -> CompositeOutcome {
guard let raster, let sourceImage = raster.makeImage() else { return .retry }
let extent = CGRect(x: 0, y: 0, width: rasterWidth, height: rasterHeight)
let current = CIImage(cgImage: sourceImage, options: [.colorSpace: colorSpace])
let old: CIImage
if let previousImage = previousRaster?.makeImage() {
old = CIImage(cgImage: previousImage, options: [.colorSpace: colorSpace])
} else {
old = current
}
let mask = changeMask(
changedCells, attachmentRects: changedAttachmentRects,
gridSize: gridSize, cellSize: cellSize) ??
CIImage(color: CIColor(red: 0, green: 0, blue: 0, alpha: 1))
.cropped(to: CGRect(x: 0, y: 0, width: 1, height: 1))
var filtered = current
// Geometric targets are prepared before any presented panel is drawn.
// Horizontal slide/zoom use the theme ground; a vertical opener uses
// the exact old frame inside its fixed clip. Data effects and closing
// tombstones leave the canonical current frame untouched underneath.
let clear = ground.map { encoded in
CIColor(
red: CGFloat((encoded >> 16) & 0xFF) / 255,
green: CGFloat((encoded >> 8) & 0xFF) / 255,
blue: CGFloat(encoded & 0xFF) / 255,
alpha: 1)
} ?? CIColor(red: 0, green: 0, blue: 0, alpha: 0)
for track in tracks {
let content = track.phase == UInt8(PARDES_PANEL_CLOSING) ? track.from : track.to
guard let next = panelClearKernel.apply(
extent: extent,
roiCallback: { _, _ in extent },
arguments: [
filtered, old,
pixelBox(content, cellSize: cellSize), clear,
Float(track.effect), Float(track.phase),
])
else { return .unavailable }
filtered = next
}
// libpardes orders moving, opening, then closing tombstones. Each pass
// samples immutable old/new rasters and composes over the prior pass,
// so a deleted pane can safely outlive its core Pane allocation.
let pixelCell = CIVector(
x: cellSize.width * rasterScale,
y: cellSize.height * rasterScale)
for track in tracks {
let content = track.phase == UInt8(PARDES_PANEL_CLOSING) ? track.from : track.to
guard let next = panelKernel.apply(
extent: extent,
roiCallback: { _, _ in extent },
arguments: [
current, old, mask, filtered,
pixelBox(content, cellSize: cellSize),
pixelBox(track.from, cellSize: cellSize),
pixelBox(track.to, cellSize: cellSize),
Float(track.effect), Float(track.phase), Float(track.frame),
CIVector(
x: CGFloat(track.serial & 0xFFFF),
y: CGFloat(track.serial >> 16)),
pixelCell,
CIVector(
x: 0,
y: extent.height - CGFloat(gridSize.rows) * pixelCell.y),
])
else { return .unavailable }
filtered = next
}
if scene.flags != 0 {
let crt: Float = scene.flags & UInt32(PARDES_SCENE_CRT) != 0 ? 1 : 0
let ripple: Float = scene.flags & UInt32(PARDES_SCENE_RIPPLE) != 0 ? 1 : 0
let glitch: Float = scene.flags & UInt32(PARDES_SCENE_GLITCH) != 0 ? 1 : 0
// SDL samples the scene with CLAMP_TO_EDGE. A finite Core Image
// instead supplies transparent black outside its extent, which
// would make ripple/glitch tear dark seams at the window edge.
// Clamp the input only; the kernel still emits true black before
// sampling when a destination lies outside the CRT tube.
let sceneInput = filtered.clampedToExtent()
guard let next = sceneKernel.apply(
extent: extent,
roiCallback: { _, _ in extent },
arguments: [
sceneInput, scene.time_seconds, Float(scene.frame),
crt, ripple, glitch,
CIVector(x: extent.width, y: extent.height),
CIVector(x: extent.minX, y: extent.minY),
])
else { return .unavailable }
filtered = next
}
guard let presented = context.createCGImage(filtered, from: extent) else { return .retry }
destination.clear(CGRect(origin: .zero, size: size))
destination.saveGState()
destination.translateBy(x: 0, y: size.height)
destination.scaleBy(x: 1, y: -1)
destination.interpolationQuality = .none
destination.draw(presented, in: CGRect(origin: .zero, size: size))
destination.restoreGState()
return .presented
}
/// Byte-per-cell mask from the core. Zig expands true to 255 so the image
/// is a literal normalized 0/1 texture; sampling cell centres below keeps
/// adjacent values from interpolating at pane/cell boundaries.
private func changeMask(
_ cells: UnsafePointer<UInt8>?,
attachmentRects: [CGRect],
gridSize: (cols: Int, rows: Int),
cellSize: CGSize
) -> CIImage? {
let count = gridSize.cols * gridSize.rows
guard count > 0, let cells, cellSize.width > 0, cellSize.height > 0 else { return nil }
var bytes = Array(UnsafeBufferPointer(start: cells, count: count))
for rect in attachmentRects where !rect.isNull && !rect.isEmpty {
let x0 = max(0, Int((rect.minX / cellSize.width).rounded(.down)))
let x1 = min(gridSize.cols, Int((rect.maxX / cellSize.width).rounded(.up)))
let y0 = max(0, Int((rect.minY / cellSize.height).rounded(.down)))
let y1 = min(gridSize.rows, Int((rect.maxY / cellSize.height).rounded(.up)))
guard x0 < x1, y0 < y1 else { continue }
for row in y0..<y1 {
for col in x0..<x1 { bytes[row * gridSize.cols + col] = 255 }
}
}
return bytes.withUnsafeBufferPointer { buffer in
guard let base = buffer.baseAddress,
let data = CFDataCreate(nil, base, count),
let provider = CGDataProvider(data: data),
let gray = CGColorSpace(name: CGColorSpace.linearGray),
let image = CGImage(
width: gridSize.cols, height: gridSize.rows,
bitsPerComponent: 8, bitsPerPixel: 8,
bytesPerRow: gridSize.cols, space: gray,
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue),
provider: provider, decode: nil,
shouldInterpolate: false, intent: .defaultIntent)
else { return nil }
return CIImage(cgImage: image, options: [.colorSpace: gray])
}
}
/// Convert the core's top-left logical-cell box to Core Image's
/// bottom-left backing-pixel rectangle.
private func pixelBox(_ box: pardes_panel_box_s, cellSize: CGSize) -> CIVector {
let cellW = cellSize.width * rasterScale
let cellH = cellSize.height * rasterScale
let x = CGFloat(box.x) * cellW
let w = CGFloat(box.w) * cellW
let h = CGFloat(box.h) * cellH
let y = CGFloat(rasterHeight) - (CGFloat(box.y) + CGFloat(box.h)) * cellH
return CIVector(x: x, y: y, z: w, w: h)
}
/// The scene kernel is a destination-to-source mapping. Pointer input must
/// apply that same mapping before the grid and its panel tracks see a cell,
/// otherwise a visible word under ripple/glitch is not the word Look gets.
static func sourcePoint(
_ point: CGPoint,
size: CGSize,
scene: pardes_scene_s
) -> CGPoint? {
guard size.width > 0, size.height > 0 else { return nil }
if scene.flags == 0 { return point }
var uvX = Float(point.x / size.width)
var uvY = 1 - Float(point.y / size.height) // Core Image is bottom-up
var ccX = uvX * 2 - 1
var ccY = uvY * 2 - 1
let crt: Float = scene.flags & UInt32(PARDES_SCENE_CRT) != 0 ? 1 : 0
let radius2 = ccX * ccX + ccY * ccY
let barrel = 1 + 0.016 * radius2 * crt
ccX *= barrel
ccY *= barrel
uvX = ccX * 0.5 + 0.5
uvY = ccY * 0.5 + 0.5
// Only CRT has a black tube. Ripple/glitch without CRT sample through
// the texture's clamp-to-edge policy and remain clickable there.
if crt > 0.5 && min(min(uvX, uvY), min(1 - uvX, 1 - uvY)) <= 0 { return nil }
let time = scene.time_seconds
if scene.flags & UInt32(PARDES_SCENE_RIPPLE) != 0 {
let centeredX = uvX - 0.5
let centeredY = uvY - 0.5
let radius = sqrtf(centeredX * centeredX + centeredY * centeredY)
let envelope = 1 - smoothstep(0.04, 0.72, radius)
let wave = sinf(radius * 42 - time * 5.2)
let travel = wave * envelope * 0.0032 / max(radius, 0.00001)
uvX += centeredX * travel
uvY += centeredY * travel
}
if scene.flags & UInt32(PARDES_SCENE_GLITCH) != 0 {
let band = floorf(uvY * 96)
let seed = noise(band, floorf(time * 13))
let tear = smoothstep(0.955, 1, seed)
uvX += (noise(seed, band) - 0.5) * tear * 0.020
}
uvX = min(max(uvX, 0), max(0, 1 - 1 / Float(size.width)))
uvY = min(max(uvY, 0), max(0, 1 - 1 / Float(size.height)))
return CGPoint(
x: CGFloat(uvX) * size.width,
y: CGFloat(1 - uvY) * size.height)
}
private static func smoothstep(_ edge0: Float, _ edge1: Float, _ x: Float) -> Float {
let t = min(max((x - edge0) / (edge1 - edge0), 0), 1)
return t * t * (3 - 2 * t)
}
/// Scalar spelling of pardesNoise in crt.ci.metal. Float arithmetic is
/// deliberate: the shader's band decision must not move at a Double round.
private static func noise(_ x: Float, _ y: Float) -> Float {
var qx = fract(x * 0.1031)
var qy = fract(y * 0.1031)
var qz = fract(x * 0.1031)
let d = qx * (qy + 33.33) + qy * (qz + 33.33) + qz * (qx + 33.33)
qx += d
qy += d
qz += d
return fract((qx + qy) * qz)
}
private static func fract(_ x: Float) -> Float { x - floorf(x) }
}
|