@glassly/bluetooth-sdk 0.1.0-dev.99 → 0.2.8-dev.445

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 (155) hide show
  1. package/README.md +47 -6
  2. package/android/build.gradle +7 -7
  3. package/android/lc3Lib/build.gradle +2 -2
  4. package/android/src/main/assets/G2Firmware/README.md +27 -2
  5. package/android/src/main/assets/G2Firmware/{g2-stock-2.2.9.22.evenota.bin → g2-glassly-cfw-2.3.0.24.evenota.bin} +0 -0
  6. package/android/src/main/assets/G2Firmware/{g2-glassly-cfw-2.2.9.22.evenota.bin → g2-stock-2.3.0.24.evenota.bin} +0 -0
  7. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +38 -0
  8. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +50 -14
  9. package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +18 -0
  10. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +139 -10
  11. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +28 -1
  12. package/android/src/main/java/com/glassly/bluetoothsdk/DisplayPacer.kt +199 -0
  13. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +10 -1
  14. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +4 -4
  15. package/android/src/main/java/com/glassly/bluetoothsdk/GlassesNotifications.kt +155 -0
  16. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +1065 -255
  17. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2BleLink.kt +119 -0
  18. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +29 -28
  19. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +138 -33
  20. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwScene.kt +673 -0
  21. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +235 -162
  22. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransport.kt +402 -0
  23. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnostics.kt +84 -0
  24. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +3 -3
  25. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +9 -9
  26. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Keyboard.kt +689 -0
  27. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardBridge.kt +452 -0
  28. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardHid.kt +208 -0
  29. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFile.kt +181 -0
  30. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2RingBattery.kt +20 -0
  31. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Sensors.kt +67 -0
  32. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2TouchInput.kt +39 -0
  33. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +5 -5
  34. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +14 -14
  35. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/{GlasslyBle.java → NexProto.java} +6462 -7618
  36. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIds.kt +58 -0
  37. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +77 -2
  38. package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +1 -1
  39. package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +55 -0
  40. package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +1 -1
  41. package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -1
  42. package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +1 -1
  43. package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +24 -149
  44. package/android/src/test/java/com/glassly/bluetoothsdk/DisplayPacerTest.kt +277 -0
  45. package/android/src/test/java/com/glassly/bluetoothsdk/G2BleLinkTest.kt +70 -0
  46. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +45 -56
  47. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +78 -5
  48. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwSceneTest.kt +489 -0
  49. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +100 -43
  50. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransportTest.kt +252 -0
  51. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnosticsTest.kt +64 -0
  52. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardTest.kt +271 -0
  53. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFileTest.kt +204 -0
  54. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2RingBatteryTest.kt +56 -0
  55. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2SensorsTest.kt +56 -0
  56. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2TouchInputTest.kt +60 -0
  57. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NexProtoDescriptorTest.kt +32 -0
  58. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIdsTest.kt +58 -0
  59. package/build/BluetoothSdk.types.d.ts +209 -20
  60. package/build/BluetoothSdk.types.d.ts.map +1 -1
  61. package/build/BluetoothSdk.types.js.map +1 -1
  62. package/build/_internal.d.ts +1 -1
  63. package/build/_internal.d.ts.map +1 -1
  64. package/build/_internal.js.map +1 -1
  65. package/build/_private/BluetoothSdkModule.d.ts +30 -1
  66. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  67. package/build/_private/BluetoothSdkModule.js.map +1 -1
  68. package/build/generated/changelogCatalog.d.ts +28 -1
  69. package/build/generated/changelogCatalog.d.ts.map +1 -1
  70. package/build/generated/changelogCatalog.js +37 -1
  71. package/build/generated/changelogCatalog.js.map +1 -1
  72. package/build/generated/releaseMetadata.js +4 -4
  73. package/build/generated/releaseMetadata.js.map +1 -1
  74. package/build/index.d.ts +1 -1
  75. package/build/index.d.ts.map +1 -1
  76. package/build/index.js +14 -0
  77. package/build/index.js.map +1 -1
  78. package/ios/BluetoothSdkModule.swift +102 -13
  79. package/ios/GlasslyBluetoothSDK.podspec +1 -1
  80. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +8 -3
  81. package/ios/Source/BluetoothSDK.swift +53 -0
  82. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  83. package/ios/Source/Bridge.swift +13 -0
  84. package/ios/Source/DeviceManager.swift +151 -33
  85. package/ios/Source/DeviceStore.swift +28 -14
  86. package/ios/Source/DisplayPacer.swift +180 -0
  87. package/ios/Source/GeneratedChangelogCatalog.swift +10 -1
  88. package/ios/Source/GeneratedReleaseMetadata.swift +4 -4
  89. package/ios/Source/GlassesNotifications.swift +181 -0
  90. package/ios/Source/ObservableStore.swift +14 -2
  91. package/ios/Source/Resources/G2Firmware/README.md +27 -2
  92. package/ios/Source/Resources/G2Firmware/{g2-stock-2.2.9.22.evenota.bin → g2-glassly-cfw-2.3.0.24.evenota.bin} +0 -0
  93. package/ios/Source/Resources/G2Firmware/{g2-glassly-cfw-2.2.9.22.evenota.bin → g2-stock-2.3.0.24.evenota.bin} +0 -0
  94. package/ios/Source/controllers/R1.swift +0 -1
  95. package/ios/Source/internal/BluetoothAvailability.swift +31 -0
  96. package/ios/Source/sgcs/G2.swift +1420 -495
  97. package/ios/Source/sgcs/G2AncsRelay.swift +268 -0
  98. package/ios/Source/sgcs/G2BleLink.swift +102 -0
  99. package/ios/Source/sgcs/G2CfwClassification.swift +45 -27
  100. package/ios/Source/sgcs/G2CfwRenderer.swift +166 -33
  101. package/ios/Source/sgcs/G2CfwScene.swift +779 -0
  102. package/ios/Source/sgcs/G2CfwTextureCache.swift +244 -117
  103. package/ios/Source/sgcs/G2CfwTransport.swift +485 -0
  104. package/ios/Source/sgcs/G2DisplayDiagnostics.swift +154 -0
  105. package/ios/Source/sgcs/G2FlashProtocol.swift +2 -2
  106. package/ios/Source/sgcs/G2Flasher.swift +9 -9
  107. package/ios/Source/sgcs/G2Keyboard.swift +780 -0
  108. package/ios/Source/sgcs/G2KeyboardBridge.swift +438 -0
  109. package/ios/Source/sgcs/G2KeyboardHid.swift +290 -0
  110. package/ios/Source/sgcs/G2NotificationTuning.swift +24 -0
  111. package/ios/Source/sgcs/G2RingBattery.swift +22 -0
  112. package/ios/Source/sgcs/G2Sensors.swift +75 -0
  113. package/ios/Source/sgcs/G2TouchInput.swift +51 -0
  114. package/ios/Source/sgcs/G2WritePacing.swift +18 -0
  115. package/ios/Source/sgcs/MentraNex.swift +44 -44
  116. package/ios/Source/sgcs/SGCManager.swift +120 -13
  117. package/ios/Source/sgcs/{glassly_ble.pb.swift → nex_ble.pb.swift} +405 -465
  118. package/ios/Source/speakerid/SpeakerIdEngine.swift +2 -2
  119. package/ios/Source/status/DeviceStatus.swift +27 -0
  120. package/ios/Source/stt/SherpaOnnxTranscriber.swift +15 -14
  121. package/ios/Source/utils/Enums.swift +0 -1
  122. package/ios/Source/utils/G1Text.swift +0 -1
  123. package/ios/Source/utils/G2NotifyFile.swift +20 -10
  124. package/ios/Source/utils/JSCExperiment.swift +6 -6
  125. package/ios/Source/utils/Models.swift +0 -1
  126. package/ios/Tests/BluetoothSDKTests/BluetoothAvailabilityTests.swift +29 -0
  127. package/ios/Tests/BluetoothSDKTests/DisplayPacerTests.swift +255 -0
  128. package/ios/Tests/BluetoothSDKTests/G2AncsRelayTests.swift +186 -0
  129. package/ios/Tests/BluetoothSDKTests/G2BleLinkTests.swift +52 -0
  130. package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +46 -51
  131. package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +164 -5
  132. package/ios/Tests/BluetoothSDKTests/G2CfwSceneTests.swift +497 -0
  133. package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +108 -44
  134. package/ios/Tests/BluetoothSDKTests/G2CfwTransportTests.swift +259 -0
  135. package/ios/Tests/BluetoothSDKTests/G2DisplayDiagnosticsTests.swift +89 -0
  136. package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +3 -2
  137. package/ios/Tests/BluetoothSDKTests/G2ImageAckTests.swift +104 -0
  138. package/ios/Tests/BluetoothSDKTests/G2KeyboardTests.swift +274 -0
  139. package/ios/Tests/BluetoothSDKTests/G2NotifyFileTests.swift +21 -5
  140. package/ios/Tests/BluetoothSDKTests/G2RingBatteryTests.swift +56 -0
  141. package/ios/Tests/BluetoothSDKTests/G2SensorsTests.swift +56 -0
  142. package/ios/Tests/BluetoothSDKTests/G2TouchInputTests.swift +71 -0
  143. package/ios/Tests/BluetoothSDKTests/G2WritePacingTests.swift +35 -0
  144. package/ios/Tests/BluetoothSDKTests/GlassesNotificationTests.swift +95 -0
  145. package/package.json +1 -1
  146. package/plugin/build/withAndroid.js +2 -2
  147. package/plugin/build/withIos.js +1 -1
  148. package/scripts/gen-nex-proto.sh +12 -0
  149. package/scripts/inject-ios-sdk-version.mjs +1 -1
  150. package/src/BluetoothSdk.types.ts +214 -19
  151. package/src/_internal.ts +1 -1
  152. package/src/_private/BluetoothSdkModule.ts +40 -0
  153. package/src/generated/changelogCatalog.ts +37 -1
  154. package/src/generated/releaseMetadata.ts +4 -4
  155. package/src/index.ts +24 -0
@@ -27,6 +27,11 @@ actor G2CfwRenderer {
27
27
  let height: Int32
28
28
  }
29
29
 
30
+ /// The global setting is 1...3; the firmware placement tables are zero-based.
31
+ static func depthLevel(forSetting depth: Int) -> Int {
32
+ min(max(depth, 1), 3) - 1
33
+ }
34
+
30
35
  static func lensBoxes(
31
36
  canvasWidth: Int32, canvasHeight: Int32, heightLevel: Int, depthLevel: Int
32
37
  ) -> LensBoxes {
@@ -54,13 +59,138 @@ actor G2CfwRenderer {
54
59
 
55
60
  // MARK: - Grid-exact mono glyphs (texture-cache path)
56
61
 
57
- /// Texture-cache state. Modes 12/13/14 draw at absolute panel coordinates
62
+ /// Asset-store mirror. Modes 19/20 draw at absolute panel coordinates
58
63
  /// (no per-lens variant), so the caller only enables this path on frames
59
- /// whose window is placed lens-equal. The cache model resets whenever the
60
- /// caller's generation changes (lease cycled / residency unknown); the
61
- /// glyph raster memo survives resets — rasters don't change.
64
+ /// whose window is placed lens-equal. The mirror resets whenever the
65
+ /// caller's generation changes (lease cycled / cache reset); the glyph
66
+ /// raster memo survives resets — rasters don't change.
62
67
  private let textureCache = G2CfwTextureCache()
63
68
  private var glyphRasterMemo: [String: Data] = [:]
69
+ /// Retained object cache (modes 37-41); shares the asset mirror with the
70
+ /// raster path. Two lens mirrors: while every frame is flat only the left
71
+ /// one is encoded and its SHOW goes to both lenses; the first frame with
72
+ /// global or element depth copies it into the right mirror (`stereoSplit`) and from
73
+ /// then on each lens gets its own SHOW whenever the two encodings differ.
74
+ private let scenes = [G2CfwScene(), G2CfwScene()]
75
+ private var stereoSplit = false
76
+
77
+ /// One frame of the object cache: the SHOWs to send, in order, each with
78
+ /// its lens targets, after the asset `uploads`.
79
+ struct StereoEncoded {
80
+ struct Show: Equatable {
81
+ let payload: Data
82
+ let lenses: UInt8
83
+ }
84
+ let shows: [Show]
85
+ let uploads: [Data]
86
+ /// False when the frame is the one already on screen (nothing new to send).
87
+ let changed: Bool
88
+ let puts: Int
89
+ let hits: Int
90
+ let tweens: Int
91
+ }
92
+
93
+ /// The panel went blank while the cache stayed: hidden objects that were
94
+ /// mid-animation are re-snapped on their next show.
95
+ func sceneHidden() {
96
+ for scene in scenes {
97
+ scene.hidden()
98
+ }
99
+ }
100
+
101
+ /// The RESET reply named the store each lens could allocate; predict against the smaller one.
102
+ func setStoreCapacity(bytes: Int) {
103
+ textureCache.storeCapacity = max(G2CfwTextureCache.granule, min(G2CfwTextureCache.storeSize, bytes))
104
+ }
105
+
106
+ /// A lens answered MISSING: these ids (objects or assets) are redefined on the next frame.
107
+ func forget(ids: [UInt32]) {
108
+ for scene in scenes {
109
+ scene.forget(objectIds: ids)
110
+ }
111
+ textureCache.forget(assetIds: ids)
112
+ }
113
+
114
+ /// Upload payloads that were dropped or failed: their assets may not be resident.
115
+ func forgetUploads(_ payloads: [Data]) {
116
+ textureCache.forget(assetIds: payloads.flatMap(G2CfwTextureCache.assetIds(inUploadPayload:)))
117
+ }
118
+
119
+ /// Encode `frame` as SHOWs of the object cache (plus the asset PUT
120
+ /// messages that must precede them): one SHOW for both lenses while the
121
+ /// lens mirrors agree, else one per lens (element `depth` shifts the two
122
+ /// encodings apart). `invalidate` means residency is unknown (a send
123
+ /// failed): every object the frame shows is redefined, and the mirrors
124
+ /// re-converge. Nil when the frame does not fit the object table or the
125
+ /// asset store.
126
+ func encodeVector(
127
+ _ frame: SceneFrame, boxes: LensBoxes, cacheGeneration: Int, invalidate: Bool, inlineText: Bool,
128
+ tag: Int? = nil
129
+ ) -> StereoEncoded? {
130
+ if textureCache.generation != cacheGeneration {
131
+ textureCache.reset(generation: cacheGeneration)
132
+ }
133
+ if scenes[0].generation != textureCache.generation {
134
+ for scene in scenes {
135
+ scene.reset(generation: textureCache.generation)
136
+ }
137
+ stereoSplit = false
138
+ } else if invalidate {
139
+ for scene in scenes {
140
+ scene.invalidate()
141
+ }
142
+ stereoSplit = false
143
+ }
144
+ // Both mirrors rasterize the same element pixels: do it once per frame.
145
+ var rasters: [String: Data?] = [:]
146
+ let rasterize: (SceneElement, Int, Int) -> Data? = { element, w, h in
147
+ let key = "\(element.id)|\(element.contentHash)|\(w)x\(h)"
148
+ if let hit = rasters[key] { return hit }
149
+ let pixels = Self.rasterize(
150
+ SceneFrame(
151
+ appId: frame.appId, epoch: frame.epoch, replay: false,
152
+ elements: [element.atOrigin()], removed: [],
153
+ canvasWidth: Int32(w), canvasHeight: Int32(h)
154
+ ),
155
+ width: w, height: h
156
+ )
157
+ rasters[key] = pixels
158
+ return pixels
159
+ }
160
+ let mono = G2CfwScene.MonoFace(metrics: Self.monoCellMetrics) {
161
+ Self.monoGlyphRaster($0, fontSize: $1, cellWidth: $2, lineHeight: $3)
162
+ }
163
+ let ox = Int(boxes.left.x)
164
+ let oy = Int(boxes.left.y)
165
+ if !stereoSplit, boxes.left != boxes.right || frame.elements.contains(where: { $0.depth != 0 }) {
166
+ scenes[1].adopt(scenes[0])
167
+ stereoSplit = true
168
+ }
169
+ guard let left = scenes[0].encode(frame, originX: ox, originY: oy, inlineText: inlineText, mono: mono,
170
+ tag: tag, parallax: stereoSplit ? 1 : 0, cache: textureCache, rasterize: rasterize)
171
+ else { return nil }
172
+ guard stereoSplit else {
173
+ return StereoEncoded(shows: [.init(payload: left.payload, lenses: G2CfwTransport.lensBoth)],
174
+ uploads: left.uploads, changed: left.changed,
175
+ puts: left.puts, hits: left.hits, tweens: left.tweens)
176
+ }
177
+ guard let right = scenes[1].encode(frame, originX: Int(boxes.right.x), originY: Int(boxes.right.y), inlineText: inlineText, mono: mono,
178
+ tag: tag, parallax: -1, cache: textureCache, rasterize: rasterize)
179
+ else { return nil }
180
+ var shows: [StereoEncoded.Show] = []
181
+ if left.payload == right.payload {
182
+ shows.append(.init(payload: left.payload, lenses: G2CfwTransport.lensBoth))
183
+ } else {
184
+ // A lens whose frame did not change keeps quiet, unless neither changed
185
+ // (then both SHOWs go, so a caller that always sends stays correct).
186
+ let both = left.changed == right.changed
187
+ if both || left.changed { shows.append(.init(payload: left.payload, lenses: G2CfwTransport.lensLeft)) }
188
+ if both || right.changed { shows.append(.init(payload: right.payload, lenses: G2CfwTransport.lensRight)) }
189
+ }
190
+ return StereoEncoded(shows: shows, uploads: left.uploads + right.uploads, changed: left.changed || right.changed,
191
+ puts: left.puts + right.puts, hits: left.hits + right.hits, tweens: left.tweens + right.tweens)
192
+ }
193
+
64
194
 
65
195
  /// Fixed cell metrics of the phone's monospaced face at `fontSize`.
66
196
  static func monoCellMetrics(fontSize: Int32) -> (cellWidth: Int, lineHeight: Int) {
@@ -259,13 +389,17 @@ actor G2CfwRenderer {
259
389
  let lensEqual = boxes.left == boxes.right
260
390
  let useTexGlyphs = textureGlyphsEnabled && lensEqual
261
391
  let useTexImages = textureImagesEnabled && lensEqual
262
- // A full cache resets between frames (never mid-frame: draws already
263
- // planned this frame reference the old offsets). Bumping past the
264
- // caller's generation keeps the caller's own bumps monotonic.
265
- if useTexGlyphs || useTexImages, textureCache.generation != cacheGeneration || textureCache.exhausted {
392
+ // The asset mirror follows the caller's generation (a lost or reset
393
+ // on-glass cache); the glasses evict by themselves otherwise.
394
+ if useTexGlyphs || useTexImages, textureCache.generation != cacheGeneration {
266
395
  textureCache.reset(generation: cacheGeneration)
396
+ for scene in scenes {
397
+ scene.reset(generation: cacheGeneration)
398
+ }
399
+ stereoSplit = false
267
400
  }
268
401
 
402
+
269
403
  var grids: [MonoGrid] = []
270
404
  let raster: Data
271
405
  if useTexGlyphs {
@@ -528,7 +662,13 @@ actor G2CfwRenderer {
528
662
  if let grid = grids.filter({ $0.covers(rect) }).max(by: { $0.z < $1.z }),
529
663
  !elementBoxes.contains(where: { $0.z > grid.z && $0.intersects(rect) }),
530
664
  let font = textureCache.font(
531
- fontSize: grid.fontSize, cellWidth: grid.cellWidth, lineHeight: grid.lineHeight
665
+ fontSize: grid.fontSize, cellWidth: grid.cellWidth, lineHeight: grid.lineHeight,
666
+ rasterize: { code in
667
+ self.memoizedGlyphRaster(
668
+ Unicode.Scalar(UInt32(code))!, fontSize: grid.fontSize,
669
+ cellWidth: grid.cellWidth, lineHeight: grid.lineHeight
670
+ )
671
+ }
532
672
  )
533
673
  {
534
674
  let r0 = (rect.y - grid.originY) / grid.lineHeight
@@ -562,14 +702,7 @@ actor G2CfwRenderer {
562
702
  }
563
703
  for col in c0 ... c1 {
564
704
  let scalar = grid.scalar(row: row, col: col)
565
- if scalar.value >= 32, scalar.value <= 126,
566
- textureCache.ensureGlyph(font: font, char: UInt8(scalar.value), rasterize: {
567
- self.memoizedGlyphRaster(
568
- scalar, fontSize: grid.fontSize,
569
- cellWidth: grid.cellWidth, lineHeight: grid.lineHeight
570
- )
571
- })
572
- {
705
+ if scalar.value >= 32, scalar.value <= 126, font.glyphs.contains(UInt8(scalar.value)) {
573
706
  flushResidual(endCol: col)
574
707
  if runChars.isEmpty { runStartCol = col }
575
708
  runChars.append(UInt8(scalar.value))
@@ -591,7 +724,7 @@ actor G2CfwRenderer {
591
724
  var payloads: [Data] = []
592
725
  for run in newRuns {
593
726
  guard let payload = G2CfwTextureCache.stringRunPayload(
594
- fontTableOffset: font.tableOffset,
727
+ fontId: font.id,
595
728
  x: Int(boxes.left.x) + run.x, y: Int(boxes.left.y) + run.y,
596
729
  options: G2CfwTextureCache.opaqueDrawOptions, chars: run.chars
597
730
  ) else { payloads = []; break }
@@ -627,9 +760,9 @@ actor G2CfwRenderer {
627
760
  ))
628
761
  }
629
762
  }
630
- if let offset = textureCache.ensureImage(hash: candidate.key, gray: gray, width: w, height: h),
763
+ if let asset = textureCache.ensureImage(hash: candidate.key, gray: gray, width: w, height: h),
631
764
  let draw = G2CfwTextureCache.imageDrawPayload(
632
- offset: offset,
765
+ assetId: asset,
633
766
  x: Int(boxes.left.x) + candidate.box.x0, y: Int(boxes.left.y) + candidate.box.y0,
634
767
  options: G2CfwTextureCache.opaqueDrawOptions
635
768
  )
@@ -1046,12 +1179,15 @@ actor G2CfwRenderer {
1046
1179
  width: sourceSize.width * imageScale,
1047
1180
  height: sourceSize.height * imageScale
1048
1181
  )
1182
+ // The ink level scales the pixels, as the texture ramp does on vector firmware.
1183
+ let alpha = element.color < 0 ? 1 : CGFloat(max(0, min(15, element.color))) / 15
1049
1184
  source.draw(
1050
1185
  in: CGRect(
1051
1186
  x: rect.midX - target.width / 2,
1052
1187
  y: rect.midY - target.height / 2,
1053
1188
  width: target.width, height: target.height
1054
- )
1189
+ ),
1190
+ blendMode: .normal, alpha: alpha
1055
1191
  )
1056
1192
  }
1057
1193
 
@@ -1102,9 +1238,9 @@ actor G2CfwRenderer {
1102
1238
  return rendered ? grayscale : nil
1103
1239
  }
1104
1240
 
1105
- /// Mode 3 with the lenses-differ bit: one zlib(RLE(4bpp)) payload of the
1106
- /// box pixels, drawn by each lens at its own box.
1107
- /// `[0x83][lx/4][ly/2][w/4][h/2][rx/4][ry/2][w/4][h/2][fid lo][fid hi][zlib]`
1241
+ /// Mode 3 with the lenses-differ bit: one RLE(4bpp) payload of the
1242
+ /// window drawn at each lens's box (the SID-0xf0 transport compresses).
1243
+ /// `[0x83][lx/4][ly/2][w/4][h/2][rx/4][ry/2][w/4][h/2][fid lo][fid hi][rle]`
1108
1244
  static func stereoFrame(pixels: Data, boxes: LensBoxes, fid: UInt16) -> Data? {
1109
1245
  guard pixels.count == Int(boxes.width) * Int(boxes.height) else { return nil }
1110
1246
  let rle = rleEncode(pack4Bpp(pixels))
@@ -1129,7 +1265,7 @@ actor G2CfwRenderer {
1129
1265
  private static func stereoRleFrame(
1130
1266
  rle: Data, lx: Int, ly: Int, rx: Int, ry: Int, w: Int32, h: Int32, fid: UInt16
1131
1267
  ) -> Data? {
1132
- guard let compressed = BinaryCodec.zlibRFC1950(rle) else { return nil }
1268
+ guard !rle.isEmpty else { return nil }
1133
1269
  var payload = Data([0x83])
1134
1270
  for (x, y) in [(lx, ly), (rx, ry)] {
1135
1271
  payload.append(UInt8(x / 4))
@@ -1139,7 +1275,8 @@ actor G2CfwRenderer {
1139
1275
  }
1140
1276
  payload.append(UInt8(fid & 0xFF))
1141
1277
  payload.append(UInt8(fid >> 8))
1142
- payload.append(compressed)
1278
+ // Plain RLE: the SID-0xf0 transport deflates the whole message.
1279
+ payload.append(rle)
1143
1280
  return payload
1144
1281
  }
1145
1282
 
@@ -1190,20 +1327,16 @@ actor G2CfwRenderer {
1190
1327
  /// Mode 6 full-panel black: seeds the shadow mode-3 frames composite onto.
1191
1328
  static func blackPayload() -> Data? {
1192
1329
  let pixelCount = frameWidth * frameHeight
1193
- let rle = rleEncodeSolid(color: 0, pixelCount: pixelCount)
1194
- guard let compressed = BinaryCodec.zlibRFC1950(rle) else { return nil }
1195
1330
  var payload = Data([6])
1196
- payload.append(compressed)
1331
+ payload.append(rleEncodeSolid(color: 0, pixelCount: pixelCount))
1197
1332
  return payload
1198
1333
  }
1199
1334
 
1335
+ /// `[6][rle]`: plain RLE, the SID-0xf0 transport deflates the whole message.
1200
1336
  static func encodePixels(_ grayscale: Data) -> Data? {
1201
1337
  guard grayscale.count == frameWidth * frameHeight else { return nil }
1202
- let packed = pack4Bpp(grayscale)
1203
- let rle = rleEncode(packed)
1204
- guard let compressed = BinaryCodec.zlibRFC1950(rle) else { return nil }
1205
1338
  var payload = Data([6])
1206
- payload.append(compressed)
1339
+ payload.append(rleEncode(pack4Bpp(grayscale)))
1207
1340
  return payload
1208
1341
  }
1209
1342