@glassly/bluetooth-sdk 0.1.0-dev.26

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 (548) hide show
  1. package/README.md +563 -0
  2. package/android/build.gradle +450 -0
  3. package/android/gradle.properties.example +9 -0
  4. package/android/lc3Lib/CMakeLists.txt +6 -0
  5. package/android/lc3Lib/build.gradle +212 -0
  6. package/android/lc3Lib/proguard-rules.pro +33 -0
  7. package/android/lc3Lib/src/main/AndroidManifest.xml +2 -0
  8. package/android/lc3Lib/src/main/cpp/CMakeLists.txt +8 -0
  9. package/android/lc3Lib/src/main/cpp/google_opus_stuff/CMakeLists.txt +30 -0
  10. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/CMakeLists.txt +7 -0
  11. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.cc +96 -0
  12. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.h +53 -0
  13. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/Makefile.am +6 -0
  14. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h +26 -0
  15. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h.in +26 -0
  16. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/ogg.h +209 -0
  17. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/os_types.h +158 -0
  18. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus.h +981 -0
  19. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_custom.h +342 -0
  20. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_defines.h +799 -0
  21. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_multistream.h +660 -0
  22. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_projection.h +568 -0
  23. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_types.h +166 -0
  24. package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.cc +268 -0
  25. package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.h +115 -0
  26. package/android/lc3Lib/src/main/cpp/google_opus_stuff/opus_tools/opus_header.h +59 -0
  27. package/android/lc3Lib/src/main/cpp/liblc3/CMakeLists.txt +38 -0
  28. package/android/lc3Lib/src/main/cpp/liblc3/include/lc3.h +309 -0
  29. package/android/lc3Lib/src/main/cpp/liblc3/include/lc3_private.h +162 -0
  30. package/android/lc3Lib/src/main/cpp/liblc3/include/rnnoise.h +114 -0
  31. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/CMakeLists.txt +19 -0
  32. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.c +92 -0
  33. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.h +44 -0
  34. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.c +375 -0
  35. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.h +315 -0
  36. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.c +129 -0
  37. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.h +69 -0
  38. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/common.h +148 -0
  39. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.c +70 -0
  40. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.h +43 -0
  41. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/fastmath.h +158 -0
  42. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/lc3.c +702 -0
  43. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.c +893 -0
  44. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.h +111 -0
  45. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_arm.h +506 -0
  46. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_neon.h +281 -0
  47. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/makefile.mk +35 -0
  48. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.c +452 -0
  49. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.h +57 -0
  50. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct_neon.h +296 -0
  51. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/meson.build +46 -0
  52. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.c +61 -0
  53. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.h +57 -0
  54. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.c +880 -0
  55. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.h +103 -0
  56. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.c +904 -0
  57. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.h +119 -0
  58. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.c +3457 -0
  59. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.h +94 -0
  60. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.c +457 -0
  61. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.h +99 -0
  62. package/android/lc3Lib/src/main/cpp/liblc3/liblc3.cpp +159 -0
  63. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/_kiss_fft_guts.h +182 -0
  64. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/arch.h +261 -0
  65. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.c +279 -0
  66. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.h +59 -0
  67. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/common.h +48 -0
  68. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/compile.sh +3 -0
  69. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/denoise.c +646 -0
  70. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.c +601 -0
  71. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.h +203 -0
  72. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/opus_types.h +159 -0
  73. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.c +526 -0
  74. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.h +149 -0
  75. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.c +178 -0
  76. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.h +69 -0
  77. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.c +11051 -0
  78. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.h +34 -0
  79. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_reader.c +168 -0
  80. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_train.py +66 -0
  81. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/tansig_table.h +45 -0
  82. package/android/lc3Lib/src/main/java/com/glassly/lc3Lib/Lc3Cpp.java +38 -0
  83. package/android/lc3Lib/third_party/CMakeLists.txt +23 -0
  84. package/android/lc3Lib/third_party/CMakeLists_libogg.txt +18 -0
  85. package/android/lc3Lib/third_party/CMakeLists_libopus.txt +18 -0
  86. package/android/lc3Lib/third_party/CMakeLists_opus-tools.txt +23 -0
  87. package/android/lc3Lib/third_party/opus_tools/src/AUTHORS +5 -0
  88. package/android/lc3Lib/third_party/opus_tools/src/CMakeLists.txt +4 -0
  89. package/android/lc3Lib/third_party/opus_tools/src/COPYING +371 -0
  90. package/android/lc3Lib/third_party/opus_tools/src/ChangeLog +3 -0
  91. package/android/lc3Lib/third_party/opus_tools/src/Makefile.am +42 -0
  92. package/android/lc3Lib/third_party/opus_tools/src/Makefile.unix +23 -0
  93. package/android/lc3Lib/third_party/opus_tools/src/NEWS +0 -0
  94. package/android/lc3Lib/third_party/opus_tools/src/README +0 -0
  95. package/android/lc3Lib/third_party/opus_tools/src/autogen.sh +115 -0
  96. package/android/lc3Lib/third_party/opus_tools/src/configure.ac +293 -0
  97. package/android/lc3Lib/third_party/opus_tools/src/src/arch.h +239 -0
  98. package/android/lc3Lib/third_party/opus_tools/src/src/audio-in.c +1046 -0
  99. package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.c +245 -0
  100. package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.h +28 -0
  101. package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.c +320 -0
  102. package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.h +51 -0
  103. package/android/lc3Lib/third_party/opus_tools/src/src/lpc.c +157 -0
  104. package/android/lc3Lib/third_party/opus_tools/src/src/lpc.h +27 -0
  105. package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.c +286 -0
  106. package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.h +59 -0
  107. package/android/lc3Lib/third_party/opus_tools/src/src/opusdec.c +884 -0
  108. package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.c +1021 -0
  109. package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.h +101 -0
  110. package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.c +639 -0
  111. package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.h +51 -0
  112. package/android/lc3Lib/third_party/opus_tools/src/src/os_support.h +167 -0
  113. package/android/lc3Lib/third_party/opus_tools/src/src/resample.c +1137 -0
  114. package/android/lc3Lib/third_party/opus_tools/src/src/speex_resampler.h +344 -0
  115. package/android/lc3Lib/third_party/opus_tools/src/src/stack_alloc.h +115 -0
  116. package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.c +125 -0
  117. package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.h +62 -0
  118. package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.c +223 -0
  119. package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.h +60 -0
  120. package/android/settings.gradle +4 -0
  121. package/android/silero/LICENSE.md +17 -0
  122. package/android/silero/build.gradle +50 -0
  123. package/android/silero/proguard-rules.pro +24 -0
  124. package/android/silero/src/androidTest/assets/hello.wav +0 -0
  125. package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
  126. package/android/silero/src/main/assets/LICENSE +21 -0
  127. package/android/silero/src/main/assets/silero_vad.onnx +0 -0
  128. package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
  129. package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
  130. package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
  131. package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
  132. package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
  133. package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
  134. package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
  135. package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
  136. package/android/src/main/AndroidManifest.xml +42 -0
  137. package/android/src/main/assets/G2Firmware/README.md +11 -0
  138. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +2325 -0
  139. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkAnalytics.kt +216 -0
  140. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkDebug.kt +14 -0
  141. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkDefaults.kt +7 -0
  142. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +1088 -0
  143. package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +953 -0
  144. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +2528 -0
  145. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +360 -0
  146. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +8 -0
  147. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
  148. package/android/src/main/java/com/glassly/bluetoothsdk/LocalNetworkModule.kt +414 -0
  149. package/android/src/main/java/com/glassly/bluetoothsdk/ObservableStore.kt +88 -0
  150. package/android/src/main/java/com/glassly/bluetoothsdk/OtaManifest.kt +191 -0
  151. package/android/src/main/java/com/glassly/bluetoothsdk/ReleaseChangelog.kt +61 -0
  152. package/android/src/main/java/com/glassly/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
  153. package/android/src/main/java/com/glassly/bluetoothsdk/audio/AudioModels.kt +144 -0
  154. package/android/src/main/java/com/glassly/bluetoothsdk/camera/CameraModels.kt +486 -0
  155. package/android/src/main/java/com/glassly/bluetoothsdk/connection/ConnectionModels.kt +78 -0
  156. package/android/src/main/java/com/glassly/bluetoothsdk/controllers/ControllerManager.kt +158 -0
  157. package/android/src/main/java/com/glassly/bluetoothsdk/controllers/R1.kt +937 -0
  158. package/android/src/main/java/com/glassly/bluetoothsdk/debug/BleTraceLogger.kt +165 -0
  159. package/android/src/main/java/com/glassly/bluetoothsdk/events/BluetoothEvents.kt +178 -0
  160. package/android/src/main/java/com/glassly/bluetoothsdk/internal/MapParsing.kt +140 -0
  161. package/android/src/main/java/com/glassly/bluetoothsdk/net/LocalIpv4.kt +57 -0
  162. package/android/src/main/java/com/glassly/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
  163. package/android/src/main/java/com/glassly/bluetoothsdk/otaserver/OtaServerModule.kt +120 -0
  164. package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
  165. package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +622 -0
  166. package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/PhotoReceiverModule.kt +156 -0
  167. package/android/src/main/java/com/glassly/bluetoothsdk/requests/DisplayRequests.kt +56 -0
  168. package/android/src/main/java/com/glassly/bluetoothsdk/services/Foreground.kt +272 -0
  169. package/android/src/main/java/com/glassly/bluetoothsdk/services/PhoneMic.kt +1195 -0
  170. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Ar99.java +2653 -0
  171. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/BesOtaHeartbeatGuard.kt +49 -0
  172. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/BesOtaProgressMapper.kt +27 -0
  173. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G1.kt +3878 -0
  174. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
  175. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +6428 -0
  176. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +49 -0
  177. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +1241 -0
  178. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +270 -0
  179. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +291 -0
  180. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +411 -0
  181. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/GlasslyBle.java +43189 -0
  182. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Mach1.kt +1280 -0
  183. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +10817 -0
  184. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
  185. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
  186. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +2011 -0
  187. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Nimo.kt +2532 -0
  188. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +470 -0
  189. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Simulated.kt +228 -0
  190. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/Ar99OtaManager.java +566 -0
  191. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaByteUtils.java +18 -0
  192. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaCommandConstants.java +35 -0
  193. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaCrc32Util.java +13 -0
  194. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaGattTransport.java +11 -0
  195. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaProtocol.java +92 -0
  196. package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +525 -0
  197. package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +669 -0
  198. package/android/src/main/java/com/glassly/bluetoothsdk/status/RuntimeState.kt +201 -0
  199. package/android/src/main/java/com/glassly/bluetoothsdk/status/WifiHotspotStatus.kt +189 -0
  200. package/android/src/main/java/com/glassly/bluetoothsdk/streaming/StreamModels.kt +518 -0
  201. package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +391 -0
  202. package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +518 -0
  203. package/android/src/main/java/com/glassly/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
  204. package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +248 -0
  205. package/android/src/main/java/com/glassly/bluetoothsdk/types/DeviceModels.kt +96 -0
  206. package/android/src/main/java/com/glassly/bluetoothsdk/utils/AES.java +106 -0
  207. package/android/src/main/java/com/glassly/bluetoothsdk/utils/AudioSessionMonitor.kt +189 -0
  208. package/android/src/main/java/com/glassly/bluetoothsdk/utils/AvifExifStripper.java +573 -0
  209. package/android/src/main/java/com/glassly/bluetoothsdk/utils/BitmapJavaUtils.java +169 -0
  210. package/android/src/main/java/com/glassly/bluetoothsdk/utils/BleJsonCompact.java +496 -0
  211. package/android/src/main/java/com/glassly/bluetoothsdk/utils/BlePhotoUploadService.java +677 -0
  212. package/android/src/main/java/com/glassly/bluetoothsdk/utils/BleWireProtocol.java +108 -0
  213. package/android/src/main/java/com/glassly/bluetoothsdk/utils/Constants.kt +46 -0
  214. package/android/src/main/java/com/glassly/bluetoothsdk/utils/G1Text.kt +436 -0
  215. package/android/src/main/java/com/glassly/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
  216. package/android/src/main/java/com/glassly/bluetoothsdk/utils/IncidentLogBleRelayNaming.java +29 -0
  217. package/android/src/main/java/com/glassly/bluetoothsdk/utils/IncidentLogBleUploadService.java +77 -0
  218. package/android/src/main/java/com/glassly/bluetoothsdk/utils/K900LengthCodec.java +115 -0
  219. package/android/src/main/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtils.java +827 -0
  220. package/android/src/main/java/com/glassly/bluetoothsdk/utils/MessageChunkReassembler.java +228 -0
  221. package/android/src/main/java/com/glassly/bluetoothsdk/utils/MessageChunker.java +325 -0
  222. package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +698 -0
  223. package/android/src/main/java/com/glassly/bluetoothsdk/utils/PhoneAudioMonitor.kt +348 -0
  224. package/android/src/main/java/com/glassly/bluetoothsdk/utils/SmartGlassesConnectionState.java +9 -0
  225. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/Ar99OpusPcmDecoder.java +398 -0
  226. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/ByteUtilAudioPlayer.java +655 -0
  227. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/Lc3Player.java +441 -0
  228. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/PCMAudioPlayer.java +431 -0
  229. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
  230. package/android/src/main/res/values/strings.xml +4 -0
  231. package/android/src/test/java/com/glassly/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
  232. package/android/src/test/java/com/glassly/bluetoothsdk/OtaManifestDowngradeTest.kt +86 -0
  233. package/android/src/test/java/com/glassly/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
  234. package/android/src/test/java/com/glassly/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
  235. package/android/src/test/java/com/glassly/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
  236. package/android/src/test/java/com/glassly/bluetoothsdk/StreamStatusEventTest.kt +29 -0
  237. package/android/src/test/java/com/glassly/bluetoothsdk/camera/PhotoRequestTest.kt +141 -0
  238. package/android/src/test/java/com/glassly/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
  239. package/android/src/test/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
  240. package/android/src/test/java/com/glassly/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
  241. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/Ar99AdvertisementMatcherTest.java +22 -0
  242. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/BesOtaHeartbeatGuardTest.kt +73 -0
  243. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/BesOtaProgressMapperTest.kt +67 -0
  244. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/EvenRleTest.kt +63 -0
  245. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
  246. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +89 -0
  247. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +344 -0
  248. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +200 -0
  249. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocolTest.kt +131 -0
  250. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
  251. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +133 -0
  252. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NimoProtocolTest.kt +333 -0
  253. package/android/src/test/java/com/glassly/bluetoothsdk/utils/AvifExifStripperTest.java +46 -0
  254. package/android/src/test/java/com/glassly/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
  255. package/android/src/test/java/com/glassly/bluetoothsdk/utils/BleJsonCompactTest.java +319 -0
  256. package/android/src/test/java/com/glassly/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +107 -0
  257. package/android/src/test/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
  258. package/android/src/test/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
  259. package/android/src/test/java/com/glassly/bluetoothsdk/utils/PhoneAudioMonitorTest.kt +45 -0
  260. package/android/src/test/resources/avif_with_exif.avif +0 -0
  261. package/app.plugin.js +3 -0
  262. package/build/BluetoothSdk.types.d.ts +1354 -0
  263. package/build/BluetoothSdk.types.d.ts.map +1 -0
  264. package/build/BluetoothSdk.types.js +41 -0
  265. package/build/BluetoothSdk.types.js.map +1 -0
  266. package/build/_internal.d.ts +17 -0
  267. package/build/_internal.d.ts.map +1 -0
  268. package/build/_internal.js +16 -0
  269. package/build/_internal.js.map +1 -0
  270. package/build/_private/BluetoothSdkModule.d.ts +172 -0
  271. package/build/_private/BluetoothSdkModule.d.ts.map +1 -0
  272. package/build/_private/BluetoothSdkModule.js +300 -0
  273. package/build/_private/BluetoothSdkModule.js.map +1 -0
  274. package/build/_private/LocalNetworkModule.d.ts +38 -0
  275. package/build/_private/LocalNetworkModule.d.ts.map +1 -0
  276. package/build/_private/LocalNetworkModule.js +3 -0
  277. package/build/_private/LocalNetworkModule.js.map +1 -0
  278. package/build/_private/cameraRequestPayload.d.ts +4 -0
  279. package/build/_private/cameraRequestPayload.d.ts.map +1 -0
  280. package/build/_private/cameraRequestPayload.js +32 -0
  281. package/build/_private/cameraRequestPayload.js.map +1 -0
  282. package/build/_private/photoRequestPayload.d.ts +6 -0
  283. package/build/_private/photoRequestPayload.d.ts.map +1 -0
  284. package/build/_private/photoRequestPayload.js +86 -0
  285. package/build/_private/photoRequestPayload.js.map +1 -0
  286. package/build/_private/sdkOtaManifest.d.ts +11 -0
  287. package/build/_private/sdkOtaManifest.d.ts.map +1 -0
  288. package/build/_private/sdkOtaManifest.js +15 -0
  289. package/build/_private/sdkOtaManifest.js.map +1 -0
  290. package/build/changelogs.d.ts +7 -0
  291. package/build/changelogs.d.ts.map +1 -0
  292. package/build/changelogs.js +46 -0
  293. package/build/changelogs.js.map +1 -0
  294. package/build/debug.d.ts +3 -0
  295. package/build/debug.d.ts.map +1 -0
  296. package/build/debug.js +8 -0
  297. package/build/debug.js.map +1 -0
  298. package/build/generated/changelogCatalog.d.ts +9 -0
  299. package/build/generated/changelogCatalog.d.ts.map +1 -0
  300. package/build/generated/changelogCatalog.js +12 -0
  301. package/build/generated/changelogCatalog.js.map +1 -0
  302. package/build/generated/releaseMetadata.d.ts +12 -0
  303. package/build/generated/releaseMetadata.d.ts.map +1 -0
  304. package/build/generated/releaseMetadata.js +10 -0
  305. package/build/generated/releaseMetadata.js.map +1 -0
  306. package/build/index.d.ts +6 -0
  307. package/build/index.d.ts.map +1 -0
  308. package/build/index.js +171 -0
  309. package/build/index.js.map +1 -0
  310. package/build/ota-server/OtaServer.types.d.ts +14 -0
  311. package/build/ota-server/OtaServer.types.d.ts.map +1 -0
  312. package/build/ota-server/OtaServer.types.js +2 -0
  313. package/build/ota-server/OtaServer.types.js.map +1 -0
  314. package/build/ota-server/OtaServerModule.d.ts +21 -0
  315. package/build/ota-server/OtaServerModule.d.ts.map +1 -0
  316. package/build/ota-server/OtaServerModule.js +3 -0
  317. package/build/ota-server/OtaServerModule.js.map +1 -0
  318. package/build/ota-server/OtaServerModule.web.d.ts +10 -0
  319. package/build/ota-server/OtaServerModule.web.d.ts.map +1 -0
  320. package/build/ota-server/OtaServerModule.web.js +12 -0
  321. package/build/ota-server/OtaServerModule.web.js.map +1 -0
  322. package/build/ota-server/index.d.ts +3 -0
  323. package/build/ota-server/index.d.ts.map +1 -0
  324. package/build/ota-server/index.js +3 -0
  325. package/build/ota-server/index.js.map +1 -0
  326. package/build/ota-transport/index.d.ts +67 -0
  327. package/build/ota-transport/index.d.ts.map +1 -0
  328. package/build/ota-transport/index.js +58 -0
  329. package/build/ota-transport/index.js.map +1 -0
  330. package/build/photo-receiver/PhotoReceiver.types.d.ts +18 -0
  331. package/build/photo-receiver/PhotoReceiver.types.d.ts.map +1 -0
  332. package/build/photo-receiver/PhotoReceiver.types.js +2 -0
  333. package/build/photo-receiver/PhotoReceiver.types.js.map +1 -0
  334. package/build/photo-receiver/PhotoReceiverModule.d.ts +10 -0
  335. package/build/photo-receiver/PhotoReceiverModule.d.ts.map +1 -0
  336. package/build/photo-receiver/PhotoReceiverModule.js +3 -0
  337. package/build/photo-receiver/PhotoReceiverModule.js.map +1 -0
  338. package/build/photo-receiver/PhotoReceiverModule.web.d.ts +10 -0
  339. package/build/photo-receiver/PhotoReceiverModule.web.d.ts.map +1 -0
  340. package/build/photo-receiver/PhotoReceiverModule.web.js +12 -0
  341. package/build/photo-receiver/PhotoReceiverModule.web.js.map +1 -0
  342. package/build/photo-receiver/index.d.ts +3 -0
  343. package/build/photo-receiver/index.d.ts.map +1 -0
  344. package/build/photo-receiver/index.js +3 -0
  345. package/build/photo-receiver/index.js.map +1 -0
  346. package/build/react/index.d.ts +5 -0
  347. package/build/react/index.d.ts.map +1 -0
  348. package/build/react/index.js +4 -0
  349. package/build/react/index.js.map +1 -0
  350. package/build/react/useBluetooth.d.ts +100 -0
  351. package/build/react/useBluetooth.d.ts.map +1 -0
  352. package/build/react/useBluetooth.js +167 -0
  353. package/build/react/useBluetooth.js.map +1 -0
  354. package/build/react/useBluetoothEvent.d.ts +6 -0
  355. package/build/react/useBluetoothEvent.d.ts.map +1 -0
  356. package/build/react/useBluetoothEvent.js +21 -0
  357. package/build/react/useBluetoothEvent.js.map +1 -0
  358. package/build/react/useBluetoothScan.d.ts +22 -0
  359. package/build/react/useBluetoothScan.d.ts.map +1 -0
  360. package/build/react/useBluetoothScan.js +135 -0
  361. package/build/react/useBluetoothScan.js.map +1 -0
  362. package/build/react/useBluetoothStatus.d.ts +16 -0
  363. package/build/react/useBluetoothStatus.d.ts.map +1 -0
  364. package/build/react/useBluetoothStatus.js +137 -0
  365. package/build/react/useBluetoothStatus.js.map +1 -0
  366. package/build/react/useGlassesConnection.d.ts +29 -0
  367. package/build/react/useGlassesConnection.d.ts.map +1 -0
  368. package/build/react/useGlassesConnection.js +153 -0
  369. package/build/react/useGlassesConnection.js.map +1 -0
  370. package/build/types/index.d.ts +3 -0
  371. package/build/types/index.d.ts.map +1 -0
  372. package/build/types/index.js +2 -0
  373. package/build/types/index.js.map +1 -0
  374. package/expo-module.config.json +14 -0
  375. package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
  376. package/ios/BluetoothSdkModule.swift +1034 -0
  377. package/ios/GlasslyBluetoothSDK.podspec +106 -0
  378. package/ios/LocalIPv4.swift +150 -0
  379. package/ios/LocalOtaServer.swift +309 -0
  380. package/ios/LocalPhotoUploadServer.swift +754 -0
  381. package/ios/OtaServerModule.swift +147 -0
  382. package/ios/Packages/CoreObjC/CoreObjC.xcodeproj/project.pbxproj +213 -0
  383. package/ios/Packages/CoreObjC/PcmConverter.h +22 -0
  384. package/ios/Packages/CoreObjC/PcmConverter.m +266 -0
  385. package/ios/Packages/CoreObjC/attdet.c +92 -0
  386. package/ios/Packages/CoreObjC/attdet.h +44 -0
  387. package/ios/Packages/CoreObjC/bits.c +375 -0
  388. package/ios/Packages/CoreObjC/bits.h +315 -0
  389. package/ios/Packages/CoreObjC/bwdet.c +129 -0
  390. package/ios/Packages/CoreObjC/bwdet.h +69 -0
  391. package/ios/Packages/CoreObjC/common.h +151 -0
  392. package/ios/Packages/CoreObjC/energy.c +70 -0
  393. package/ios/Packages/CoreObjC/energy.h +43 -0
  394. package/ios/Packages/CoreObjC/fastmath.h +158 -0
  395. package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
  396. package/ios/Packages/CoreObjC/lc3.c +704 -0
  397. package/ios/Packages/CoreObjC/lc3.h +313 -0
  398. package/ios/Packages/CoreObjC/lc3_cpp.h +283 -0
  399. package/ios/Packages/CoreObjC/lc3_private.h +163 -0
  400. package/ios/Packages/CoreObjC/ltpf.c +905 -0
  401. package/ios/Packages/CoreObjC/ltpf.h +111 -0
  402. package/ios/Packages/CoreObjC/ltpf_arm.h +506 -0
  403. package/ios/Packages/CoreObjC/ltpf_neon.h +281 -0
  404. package/ios/Packages/CoreObjC/makefile.mk +35 -0
  405. package/ios/Packages/CoreObjC/mdct.c +469 -0
  406. package/ios/Packages/CoreObjC/mdct.h +57 -0
  407. package/ios/Packages/CoreObjC/mdct_neon.h +296 -0
  408. package/ios/Packages/CoreObjC/meson.build +61 -0
  409. package/ios/Packages/CoreObjC/plc.c +61 -0
  410. package/ios/Packages/CoreObjC/plc.h +57 -0
  411. package/ios/Packages/CoreObjC/rnnoise.h +114 -0
  412. package/ios/Packages/CoreObjC/sns.c +880 -0
  413. package/ios/Packages/CoreObjC/sns.h +103 -0
  414. package/ios/Packages/CoreObjC/spec.c +907 -0
  415. package/ios/Packages/CoreObjC/spec.h +119 -0
  416. package/ios/Packages/CoreObjC/tables.c +3457 -0
  417. package/ios/Packages/CoreObjC/tables.h +94 -0
  418. package/ios/Packages/CoreObjC/tns.c +457 -0
  419. package/ios/Packages/CoreObjC/tns.h +99 -0
  420. package/ios/Packages/SherpaOnnx/Model/joiner.onnx +0 -0
  421. package/ios/Packages/SherpaOnnx/Model/tokens.txt +502 -0
  422. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +2383 -0
  423. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +48 -0
  424. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4689 -0
  425. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +2036 -0
  426. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
  427. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4689 -0
  428. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +2036 -0
  429. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
  430. package/ios/Packages/libbz2/module.modulemap +5 -0
  431. package/ios/Packages/libbz2/shim.h +1 -0
  432. package/ios/PhotoReceiverModule.swift +129 -0
  433. package/ios/Source/BluetoothSDK.swift +2303 -0
  434. package/ios/Source/BluetoothSDKDebug.swift +12 -0
  435. package/ios/Source/BluetoothSdkDefaults.swift +29 -0
  436. package/ios/Source/Bridge.swift +708 -0
  437. package/ios/Source/Bridging-Header.h +16 -0
  438. package/ios/Source/DeviceManager.swift +2109 -0
  439. package/ios/Source/DeviceStore.swift +336 -0
  440. package/ios/Source/GeneratedChangelogCatalog.swift +7 -0
  441. package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
  442. package/ios/Source/ObservableStore.swift +118 -0
  443. package/ios/Source/OtaManifest.swift +211 -0
  444. package/ios/Source/PrivacyInfo.xcprivacy +23 -0
  445. package/ios/Source/ReleaseChangelog.swift +65 -0
  446. package/ios/Source/Resources/G2Firmware/README.md +10 -0
  447. package/ios/Source/audio/AudioModels.swift +178 -0
  448. package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
  449. package/ios/Source/camera/CameraModels.swift +866 -0
  450. package/ios/Source/connection/ScanSession.swift +27 -0
  451. package/ios/Source/controllers/ControllerManager.swift +184 -0
  452. package/ios/Source/controllers/R1.swift +829 -0
  453. package/ios/Source/errors/BluetoothSdkError.swift +19 -0
  454. package/ios/Source/events/BluetoothEvents.swift +342 -0
  455. package/ios/Source/internal/BleTraceLogger.swift +192 -0
  456. package/ios/Source/internal/BluetoothAvailability.swift +78 -0
  457. package/ios/Source/internal/BluetoothSdkAnalytics.swift +199 -0
  458. package/ios/Source/internal/ValueParsing.swift +99 -0
  459. package/ios/Source/requests/DisplayRequests.swift +82 -0
  460. package/ios/Source/services/PhoneMic.swift +662 -0
  461. package/ios/Source/sgcs/Ar99.swift +2451 -0
  462. package/ios/Source/sgcs/Frame.swift +649 -0
  463. package/ios/Source/sgcs/G1.swift +2554 -0
  464. package/ios/Source/sgcs/G2.swift +6887 -0
  465. package/ios/Source/sgcs/G2CfwClassification.swift +43 -0
  466. package/ios/Source/sgcs/G2CfwRenderer.swift +1279 -0
  467. package/ios/Source/sgcs/G2CfwTextureCache.swift +233 -0
  468. package/ios/Source/sgcs/G2FlashProtocol.swift +312 -0
  469. package/ios/Source/sgcs/G2Flasher.swift +773 -0
  470. package/ios/Source/sgcs/Mach1.swift +541 -0
  471. package/ios/Source/sgcs/MentraLive.swift +7011 -0
  472. package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
  473. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
  474. package/ios/Source/sgcs/MentraNex.swift +3221 -0
  475. package/ios/Source/sgcs/Nimo.swift +1886 -0
  476. package/ios/Source/sgcs/SGCManager.swift +608 -0
  477. package/ios/Source/sgcs/Simulated.swift +274 -0
  478. package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
  479. package/ios/Source/sgcs/glassly_ble.pb.swift +4354 -0
  480. package/ios/Source/speakerid/SpeakerIdEngine.swift +573 -0
  481. package/ios/Source/status/DeviceStatus.swift +981 -0
  482. package/ios/Source/status/RuntimeState.swift +359 -0
  483. package/ios/Source/status/WifiHotspotStatus.swift +345 -0
  484. package/ios/Source/streaming/StreamModels.swift +693 -0
  485. package/ios/Source/stt/STTTools.swift +191 -0
  486. package/ios/Source/stt/SherpaOnnxTranscriber.swift +466 -0
  487. package/ios/Source/tts/TTSTools.swift +171 -0
  488. package/ios/Source/types/DeviceModels.swift +177 -0
  489. package/ios/Source/utils/AudioSessionMonitor.swift +283 -0
  490. package/ios/Source/utils/BinaryCodec.swift +61 -0
  491. package/ios/Source/utils/BleJsonCompact.swift +405 -0
  492. package/ios/Source/utils/BleWireProtocol.swift +92 -0
  493. package/ios/Source/utils/Constants.swift +78 -0
  494. package/ios/Source/utils/Enums.swift +95 -0
  495. package/ios/Source/utils/G1Text.swift +2096 -0
  496. package/ios/Source/utils/JSCExperiment.swift +241 -0
  497. package/ios/Source/utils/MemoryMonitor.swift +44 -0
  498. package/ios/Source/utils/MessageChunkReassembler.swift +190 -0
  499. package/ios/Source/utils/MessageChunker.swift +294 -0
  500. package/ios/Source/utils/Models.swift +135 -0
  501. package/ios/Source/utils/PhoneAudioMonitor.swift +230 -0
  502. package/ios/Source/utils/TarBz2Extractor.swift +257 -0
  503. package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +78 -0
  504. package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +372 -0
  505. package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +162 -0
  506. package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +182 -0
  507. package/ios/Tests/MentraLivePairingAdvertisementTests.swift +160 -0
  508. package/ios/Tests/ReleaseChangelogTests.swift +13 -0
  509. package/package.json +129 -0
  510. package/plugin/build/index.d.ts +10 -0
  511. package/plugin/build/index.js +12 -0
  512. package/plugin/build/withAndroid.d.ts +3 -0
  513. package/plugin/build/withAndroid.js +190 -0
  514. package/plugin/build/withIos.d.ts +5 -0
  515. package/plugin/build/withIos.js +108 -0
  516. package/scripts/inject-ios-sdk-version.mjs +65 -0
  517. package/scripts/public-ota-api.test.mjs +37 -0
  518. package/scripts/verify-release-package.mjs +45 -0
  519. package/scripts/write-release-metadata.mjs +152 -0
  520. package/scripts/write-release-metadata.test.mjs +37 -0
  521. package/src/BluetoothSdk.types.ts +1623 -0
  522. package/src/_internal.ts +16 -0
  523. package/src/_private/BluetoothSdkModule.ts +684 -0
  524. package/src/_private/LocalNetworkModule.ts +50 -0
  525. package/src/_private/cameraRequestPayload.ts +34 -0
  526. package/src/_private/photoRequestPayload.ts +90 -0
  527. package/src/_private/sdkOtaManifest.ts +18 -0
  528. package/src/changelogs.ts +46 -0
  529. package/src/debug.ts +9 -0
  530. package/src/generated/changelogCatalog.ts +13 -0
  531. package/src/generated/releaseMetadata.ts +20 -0
  532. package/src/index.ts +321 -0
  533. package/src/ota-server/OtaServer.types.ts +10 -0
  534. package/src/ota-server/OtaServerModule.ts +23 -0
  535. package/src/ota-server/OtaServerModule.web.ts +17 -0
  536. package/src/ota-server/index.ts +2 -0
  537. package/src/ota-transport/index.ts +116 -0
  538. package/src/photo-receiver/PhotoReceiver.types.ts +20 -0
  539. package/src/photo-receiver/PhotoReceiverModule.ts +11 -0
  540. package/src/photo-receiver/PhotoReceiverModule.web.ts +17 -0
  541. package/src/photo-receiver/index.ts +2 -0
  542. package/src/react/index.ts +21 -0
  543. package/src/react/useBluetooth.ts +309 -0
  544. package/src/react/useBluetoothEvent.ts +35 -0
  545. package/src/react/useBluetoothScan.ts +173 -0
  546. package/src/react/useBluetoothStatus.ts +180 -0
  547. package/src/react/useGlassesConnection.ts +202 -0
  548. package/src/types/index.ts +12 -0
@@ -0,0 +1,1279 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ /// Serial background encoder for Glassly CFW's direct-framebuffer mode.
5
+ /// Actor isolation keeps frames ordered without blocking G2's MainActor BLE/UI work.
6
+ ///
7
+ /// Placement mirrors the stock compositor, which blits its 576×288 canvas into
8
+ /// the 640×480 panel 1:1 at a per-lens offset: `y` from the user's height level
9
+ /// and `x` from the depth level, in OPPOSITE directions per eye (that is the
10
+ /// convergence control). The outer margin is never drawn on — it exists so the
11
+ /// window can move, and the optics are only clean where the window sits.
12
+ actor G2CfwRenderer {
13
+ static let frameWidth = 640
14
+ static let frameHeight = 480
15
+
16
+ /// Level → pixel tables from firmware 2.2.6.10 (`DAT_0046c738`, `DAT_0046c730/34`).
17
+ static let heightOffsets: [Int32] = [192, 176, 160, 144, 128, 112, 96, 80, 64, 48, 32, 16, 0]
18
+ static let depthOffsetsLeft: [Int32] = [40, 32, 24]
19
+ static let depthOffsetsRight: [Int32] = [24, 32, 40]
20
+
21
+ /// Where each lens draws the canvas. Mode 3's box header quantizes x/w to
22
+ /// multiples of 4 and y/h to multiples of 2.
23
+ struct LensBoxes: Equatable {
24
+ let left: CGPoint
25
+ let right: CGPoint
26
+ let width: Int32
27
+ let height: Int32
28
+ }
29
+
30
+ static func lensBoxes(
31
+ canvasWidth: Int32, canvasHeight: Int32, heightLevel: Int, depthLevel: Int
32
+ ) -> LensBoxes {
33
+ let h = min(max(heightLevel, 0), heightOffsets.count - 1)
34
+ let d = min(max(depthLevel, 0), depthOffsetsLeft.count - 1)
35
+ let width = min(max(canvasWidth, 4), Int32(frameWidth)) & ~3
36
+ let height = min(max(canvasHeight, 2), Int32(frameHeight)) & ~1
37
+ func place(_ x: Int32, _ y: Int32) -> CGPoint {
38
+ CGPoint(
39
+ x: CGFloat(min(max(x, 0), Int32(frameWidth) - width) & ~3),
40
+ y: CGFloat(min(max(y, 0), Int32(frameHeight) - height) & ~1)
41
+ )
42
+ }
43
+ return LensBoxes(
44
+ left: place(depthOffsetsLeft[d], heightOffsets[h]),
45
+ right: place(depthOffsetsRight[d], heightOffsets[h]),
46
+ width: width, height: height
47
+ )
48
+ }
49
+
50
+ /// Line pitch for `font: "mono"` text — the contract apps size their grids by.
51
+ static func monoLineHeight(fontSize: CGFloat) -> CGFloat {
52
+ (fontSize * 1.2).rounded()
53
+ }
54
+
55
+ // MARK: - Grid-exact mono glyphs (texture-cache path)
56
+
57
+ /// Texture-cache state. Modes 12/13/14 draw at absolute panel coordinates
58
+ /// (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.
62
+ private let textureCache = G2CfwTextureCache()
63
+ private var glyphRasterMemo: [String: Data] = [:]
64
+
65
+ /// Fixed cell metrics of the phone's monospaced face at `fontSize`.
66
+ static func monoCellMetrics(fontSize: Int32) -> (cellWidth: Int, lineHeight: Int) {
67
+ let size = CGFloat(fontSize > 0 ? fontSize : 20)
68
+ let font = UIFont.monospacedSystemFont(ofSize: size, weight: .regular)
69
+ let advance = ("0" as NSString).size(withAttributes: [.font: font]).width
70
+ return (max(1, Int(advance.rounded())), max(1, Int(monoLineHeight(fontSize: size))))
71
+ }
72
+
73
+ /// One full-cell glyph raster (cellWidth × lineHeight, 8-bit gray, white
74
+ /// ink on black). This single function feeds BOTH the frame raster (grid
75
+ /// blits) and the mode-12 uploads, so an on-glasses cached draw reproduces
76
+ /// the phone raster byte-for-byte.
77
+ static func monoGlyphRaster(
78
+ _ scalar: Unicode.Scalar, fontSize: Int32, cellWidth: Int, lineHeight: Int
79
+ ) -> Data? {
80
+ let size = CGFloat(fontSize > 0 ? fontSize : 20)
81
+ let font = UIFont.monospacedSystemFont(ofSize: size, weight: .regular)
82
+ var gray = Data(count: cellWidth * lineHeight)
83
+ let ok = gray.withUnsafeMutableBytes { buffer -> Bool in
84
+ guard let context = CGContext(
85
+ data: buffer.baseAddress, width: cellWidth, height: lineHeight,
86
+ bitsPerComponent: 8, bytesPerRow: cellWidth,
87
+ space: CGColorSpaceCreateDeviceGray(),
88
+ bitmapInfo: CGImageAlphaInfo.none.rawValue
89
+ ) else { return false }
90
+ context.setFillColor(gray: 0, alpha: 1)
91
+ context.fill(CGRect(x: 0, y: 0, width: CGFloat(cellWidth), height: CGFloat(lineHeight)))
92
+ // Flip to UIKit's top-left origin so NSString.draw lands top-down
93
+ // like the frame raster.
94
+ context.translateBy(x: 0, y: CGFloat(lineHeight))
95
+ context.scaleBy(x: 1, y: -1)
96
+ UIGraphicsPushContext(context)
97
+ let yOffset = max(0, (CGFloat(lineHeight) - font.lineHeight) / 2)
98
+ (String(scalar) as NSString).draw(
99
+ at: CGPoint(x: 0, y: yOffset),
100
+ withAttributes: [.font: font, .foregroundColor: UIColor.white]
101
+ )
102
+ UIGraphicsPopContext()
103
+ return true
104
+ }
105
+ return ok ? gray : nil
106
+ }
107
+
108
+ private func memoizedGlyphRaster(
109
+ _ scalar: Unicode.Scalar, fontSize: Int32, cellWidth: Int, lineHeight: Int
110
+ ) -> Data? {
111
+ let key = "\(fontSize)|\(scalar.value)"
112
+ if let cached = glyphRasterMemo[key] { return cached }
113
+ guard let raster = Self.monoGlyphRaster(
114
+ scalar, fontSize: fontSize, cellWidth: cellWidth, lineHeight: lineHeight
115
+ ) else { return nil }
116
+ glyphRasterMemo[key] = raster
117
+ return raster
118
+ }
119
+
120
+ /// Rasterize + encode a scene as a stereo mode-3 frame at `boxes`.
121
+ func encode(_ frame: SceneFrame, boxes: LensBoxes, fid: UInt16) -> Data? {
122
+ guard let pixels = Self.rasterize(frame, width: Int(boxes.width), height: Int(boxes.height))
123
+ else { return nil }
124
+ return Self.stereoFrame(pixels: pixels, boxes: boxes, fid: fid)
125
+ }
126
+
127
+ // MARK: - Incremental updates (dirty rects / rect copy / builtin text)
128
+
129
+ /// A canvas-space rectangle aligned to mode 3's box quantization
130
+ /// (x/w multiples of 4, y/h multiples of 2).
131
+ struct CanvasRect: Equatable {
132
+ let x: Int
133
+ let y: Int
134
+ let w: Int
135
+ let h: Int
136
+ }
137
+
138
+ /// Full-width vertical band copy inside the canvas: rows
139
+ /// [srcY, srcY+height) become rows [dstY, dstY+height).
140
+ struct RowCopy: Equatable {
141
+ let srcY: Int
142
+ let dstY: Int
143
+ let height: Int
144
+ }
145
+
146
+ /// What an incremental frame needs: a full re-send, or a scroll copy plus
147
+ /// the aligned rects whose pixels still differ after it.
148
+ struct DeltaPlan: Equatable {
149
+ let full: Bool
150
+ let copy: RowCopy?
151
+ let rects: [CanvasRect]
152
+
153
+ static let noop = DeltaPlan(full: false, copy: nil, rects: [])
154
+ static let fullFrame = DeltaPlan(full: true, copy: nil, rects: [])
155
+ var isNoop: Bool { !full && copy == nil && rects.isEmpty }
156
+ }
157
+
158
+ /// One firmware-drawn (mode 15) text line at absolute panel coordinates.
159
+ struct TextDraw: Equatable {
160
+ let x: Int
161
+ let y: Int
162
+ let text: String
163
+ }
164
+
165
+ /// The result of one incremental encode: the wire payload (nil when the
166
+ /// frame is visually identical to the previous one), mode-12 texture
167
+ /// uploads that must be queued AHEAD of it, the raster backing the
168
+ /// on-glass shadow after this payload applies, and whether the payload
169
+ /// re-sends the whole canvas.
170
+ struct SceneUpdate {
171
+ let payload: Data?
172
+ let uploads: [Data]
173
+ let raster: Data
174
+ let usedFullFrame: Bool
175
+ }
176
+
177
+ /// One mono text element's glyph grid, produced by the grid-exact
178
+ /// rasterizer: cell (0,0) sits at (originX, originY) in canvas coords, and
179
+ /// only the `fullCols`×`fullRows` cells wholly inside the element box (and
180
+ /// canvas) are expressible as cached draws — clipped edge cells stay baked.
181
+ struct MonoGrid {
182
+ let z: Int
183
+ let fontSize: Int32
184
+ let originX: Int
185
+ let originY: Int
186
+ let cellWidth: Int
187
+ let lineHeight: Int
188
+ let fullCols: Int
189
+ let fullRows: Int
190
+ /// Unicode scalar per cell; rows/cols beyond the text are spaces.
191
+ let rows: [[Unicode.Scalar]]
192
+
193
+ func scalar(row: Int, col: Int) -> Unicode.Scalar {
194
+ guard row < rows.count else { return " " }
195
+ let line = rows[row]
196
+ return col < line.count ? line[col] : " "
197
+ }
198
+
199
+ /// The canvas region whose every pixel belongs to a full cell.
200
+ var expressibleRegion: CanvasRect {
201
+ CanvasRect(x: originX, y: originY, w: fullCols * cellWidth, h: fullRows * lineHeight)
202
+ }
203
+
204
+ func covers(_ rect: CanvasRect) -> Bool {
205
+ let region = expressibleRegion
206
+ return rect.x >= region.x && rect.y >= region.y
207
+ && rect.x + rect.w <= region.x + region.w
208
+ && rect.y + rect.h <= region.y + region.h
209
+ }
210
+ }
211
+
212
+ /// Caps for the delta planner. maxDirtyRects bounds fids the caller must
213
+ /// pre-allocate (one per rect, plus full frame, plus a move blank).
214
+ static let maxDirtyRects = 6
215
+ static let requiredFidCount = maxDirtyRects + 2
216
+ /// Cap on mode-14 runs per update (mode-8's count byte is the hard bound).
217
+ static let maxGlyphRuns = 180
218
+ /// Line pitch and default draw options of the glasses' builtin 20 px font
219
+ /// chain (mode 15): lineHeight 27 / baseline 22 in firmware 2.2.6.10.
220
+ /// 0x1F = top color 15 (white) + bit 4 (source color 0 transparent).
221
+ static let builtinLineHeight = 27
222
+ static let builtinTextOptions: UInt8 = 0x1F
223
+ private static let dirtyRowMergeGap = 8
224
+ private static let fullFrameCoveragePercent = 60
225
+ private static let minScrollBandRows = 16
226
+ private static let scrollSavingsPercent = 30
227
+
228
+ /// Rasterize + plan + encode one incremental scene update.
229
+ ///
230
+ /// `previousRaster` is the canvas raster matching the on-glass shadow; nil
231
+ /// forces a full frame. `forcedRects` are treated as changed even when the
232
+ /// rasters agree (used to erase firmware-drawn text the raster never
233
+ /// contained). `textDraws` are appended after the pixel segments so glyphs
234
+ /// land on top of freshly pushed background. `blankBoxes` prepends a solid
235
+ /// black frame at a previous window position (window move) in the same
236
+ /// atomic bundle. `omitBuiltinText` skips `font == "builtin"` text during
237
+ /// rasterization — the firmware draws those lines itself.
238
+ func encodeUpdate(
239
+ _ frame: SceneFrame, boxes: LensBoxes, previousRaster: Data?,
240
+ forcedRects: [CanvasRect], fids: [UInt16], textDraws: [TextDraw],
241
+ allowRectCopy: Bool, blankBoxes: LensBoxes?, omitBuiltinText: Bool,
242
+ textureGlyphsEnabled: Bool = false, textureImagesEnabled: Bool = false,
243
+ cacheGeneration: Int = 0
244
+ ) -> SceneUpdate? {
245
+ guard fids.count >= Self.requiredFidCount else { return nil }
246
+ let width = Int(boxes.width)
247
+ let height = Int(boxes.height)
248
+ // Texture draws (modes 13/14) land at absolute panel coordinates with
249
+ // no per-lens variant — only a lens-equal window may use them.
250
+ let lensEqual = boxes.left == boxes.right
251
+ let useTexGlyphs = textureGlyphsEnabled && lensEqual
252
+ let useTexImages = textureImagesEnabled && lensEqual
253
+ // A full cache resets between frames (never mid-frame: draws already
254
+ // planned this frame reference the old offsets). Bumping past the
255
+ // caller's generation keeps the caller's own bumps monotonic.
256
+ if useTexGlyphs || useTexImages, textureCache.generation != cacheGeneration || textureCache.exhausted {
257
+ textureCache.reset(generation: cacheGeneration)
258
+ }
259
+
260
+ var grids: [MonoGrid] = []
261
+ let raster: Data
262
+ if useTexGlyphs {
263
+ guard let result = rasterizeGridExact(
264
+ frame, width: width, height: height, omitBuiltinText: omitBuiltinText
265
+ ) else { return nil }
266
+ raster = Self.quantizeToPanelLevels(result.raster)
267
+ grids = result.grids
268
+ } else {
269
+ guard let flat = Self.rasterize(
270
+ frame, width: width, height: height, omitBuiltinText: omitBuiltinText
271
+ ) else { return nil }
272
+ raster = Self.quantizeToPanelLevels(flat)
273
+ }
274
+
275
+ var nextFid = 0
276
+ func takeFid() -> UInt16 {
277
+ defer { nextFid += 1 }
278
+ return fids[nextFid]
279
+ }
280
+
281
+ var segments: [Data] = []
282
+ if let blankBoxes, let blank = Self.solidStereoFrame(color: 0, boxes: blankBoxes, fid: takeFid()) {
283
+ segments.append(blank)
284
+ }
285
+
286
+ let plan: DeltaPlan
287
+ if let previousRaster, previousRaster.count == raster.count, blankBoxes == nil {
288
+ plan = Self.diffPlan(
289
+ previous: previousRaster, next: raster, width: width, height: height,
290
+ forced: forcedRects, allowCopy: allowRectCopy
291
+ )
292
+ } else {
293
+ plan = .fullFrame
294
+ }
295
+
296
+ if plan.isNoop {
297
+ let uploads = textureCache.takeUploadPayloads()
298
+ return SceneUpdate(payload: nil, uploads: uploads, raster: raster, usedFullFrame: false)
299
+ }
300
+
301
+ if plan.full {
302
+ guard let full = Self.stereoFrame(pixels: raster, boxes: boxes, fid: takeFid())
303
+ else { return nil }
304
+ segments.append(full)
305
+ } else {
306
+ if let copy = plan.copy {
307
+ segments.append(Self.rectCopyPayload(copy: copy, width: width, boxes: boxes))
308
+ }
309
+ var deltaRects = plan.rects
310
+ var drawSegments: [Data] = []
311
+ if useTexGlyphs || useTexImages {
312
+ // Reserve one fid for the bundle-overflow fallback below.
313
+ let fidBudget = Self.requiredFidCount - nextFid - 1
314
+ let expressed = expressTextureRects(
315
+ plan.rects, grids: useTexGlyphs ? grids : [], frame: frame,
316
+ boxes: boxes, raster: raster, width: width, height: height,
317
+ allowImages: useTexImages, fidBudget: fidBudget
318
+ )
319
+ deltaRects = expressed.deltas
320
+ drawSegments = expressed.draws
321
+ }
322
+ for rect in deltaRects {
323
+ guard let delta = Self.boxDeltaFrame(
324
+ raster: raster, canvasWidth: width, rect: rect, boxes: boxes, fid: takeFid()
325
+ ) else { return nil }
326
+ segments.append(delta)
327
+ }
328
+ segments.append(contentsOf: drawSegments)
329
+ }
330
+
331
+ for draw in textDraws {
332
+ if let text = Self.builtinStringPayload(
333
+ x: draw.x, y: draw.y, options: Self.builtinTextOptions, text: draw.text
334
+ ) {
335
+ segments.append(text)
336
+ }
337
+ }
338
+
339
+ let uploads = textureCache.takeUploadPayloads()
340
+ if segments.count == 1 {
341
+ return SceneUpdate(payload: segments[0], uploads: uploads, raster: raster, usedFullFrame: plan.full)
342
+ }
343
+ if let bundled = Self.bundle(segments) {
344
+ return SceneUpdate(payload: bundled, uploads: uploads, raster: raster, usedFullFrame: plan.full)
345
+ }
346
+ // A segment overflowed the bundle's 64 KiB cap (a near-incompressible
347
+ // full canvas). Fall back to one bare full frame with any builtin text
348
+ // baked into the bitmap so nothing is lost. The uploads still go out —
349
+ // residency was already recorded in the model.
350
+ guard let flat = Self.rasterize(frame, width: width, height: height, omitBuiltinText: false),
351
+ let full = Self.stereoFrame(pixels: flat, boxes: boxes, fid: takeFid())
352
+ else { return nil }
353
+ return SceneUpdate(payload: full, uploads: uploads, raster: flat, usedFullFrame: true)
354
+ }
355
+
356
+ /// Blit `font: "mono"` elements as opaque glyph grids: the element's box
357
+ /// is cleared to black and every non-space cell blits the SAME full-cell
358
+ /// raster mode-14 draws replay, so an on-glasses cached draw reproduces
359
+ /// the raster exactly. (Opaque cells are also what makes cached draws
360
+ /// self-erasing.) Mono grids paint after all other elements — an exotic
361
+ /// frame stacking a non-mono element ON TOP of a mono grid would z-order
362
+ /// differently here, which no current miniapp does.
363
+ private func rasterizeGridExact(
364
+ _ frame: SceneFrame, width: Int, height: Int, omitBuiltinText: Bool
365
+ ) -> (raster: Data, grids: [MonoGrid])? {
366
+ guard var raster = Self.rasterize(
367
+ frame, width: width, height: height, omitBuiltinText: omitBuiltinText, omitMonoText: true
368
+ ) else { return nil }
369
+
370
+ var grids: [MonoGrid] = []
371
+ for (z, element) in frame.elements.enumerated()
372
+ where element.type == "text" && element.font == "mono"
373
+ {
374
+ let bx = Int(element.x)
375
+ let by = Int(element.y)
376
+ let clipX0 = max(0, bx)
377
+ let clipY0 = max(0, by)
378
+ let clipX1 = min(width, bx + Int(element.w))
379
+ let clipY1 = min(height, by + Int(element.h))
380
+ guard clipX0 < clipX1, clipY0 < clipY1 else { continue }
381
+ let metrics = Self.monoCellMetrics(fontSize: element.fontSize)
382
+ let originX = bx + 2
383
+ let originY = by + 2
384
+ raster.withUnsafeMutableBytes { buf in
385
+ let base = buf.bindMemory(to: UInt8.self).baseAddress!
386
+ for y in clipY0 ..< clipY1 {
387
+ memset(base + y * width + clipX0, 0, clipX1 - clipX0)
388
+ }
389
+ }
390
+ let lines: [[Unicode.Scalar]] = (element.text ?? "")
391
+ .split(separator: "\n", omittingEmptySubsequences: false)
392
+ .map { Array($0.unicodeScalars) }
393
+ let blitCols = max(0, (clipX1 - originX + metrics.cellWidth - 1) / metrics.cellWidth)
394
+ let blitRows = max(0, (clipY1 - originY + metrics.lineHeight - 1) / metrics.lineHeight)
395
+ for row in 0 ..< min(blitRows, lines.count) {
396
+ let line = lines[row]
397
+ for col in 0 ..< min(blitCols, line.count) where line[col].value != 32 {
398
+ guard let glyph = memoizedGlyphRaster(
399
+ line[col], fontSize: element.fontSize,
400
+ cellWidth: metrics.cellWidth, lineHeight: metrics.lineHeight
401
+ ) else { continue }
402
+ Self.blitCell(
403
+ glyph, into: &raster, rasterWidth: width,
404
+ atX: originX + col * metrics.cellWidth, atY: originY + row * metrics.lineHeight,
405
+ cellWidth: metrics.cellWidth, cellHeight: metrics.lineHeight,
406
+ clipX0: clipX0, clipY0: clipY0, clipX1: clipX1, clipY1: clipY1
407
+ )
408
+ }
409
+ }
410
+ grids.append(MonoGrid(
411
+ z: z, fontSize: element.fontSize, originX: originX, originY: originY,
412
+ cellWidth: metrics.cellWidth, lineHeight: metrics.lineHeight,
413
+ fullCols: max(0, (clipX1 - originX) / metrics.cellWidth),
414
+ fullRows: max(0, (clipY1 - originY) / metrics.lineHeight),
415
+ rows: lines
416
+ ))
417
+ }
418
+ return (raster, grids)
419
+ }
420
+
421
+ private static func blitCell(
422
+ _ glyph: Data, into raster: inout Data, rasterWidth: Int,
423
+ atX: Int, atY: Int, cellWidth: Int, cellHeight: Int,
424
+ clipX0: Int, clipY0: Int, clipX1: Int, clipY1: Int
425
+ ) {
426
+ let x0 = max(atX, clipX0)
427
+ let y0 = max(atY, clipY0)
428
+ let x1 = min(atX + cellWidth, clipX1)
429
+ let y1 = min(atY + cellHeight, clipY1)
430
+ guard x0 < x1, y0 < y1 else { return }
431
+ raster.withUnsafeMutableBytes { dstRaw in
432
+ glyph.withUnsafeBytes { srcRaw in
433
+ let dst = dstRaw.bindMemory(to: UInt8.self).baseAddress!
434
+ let src = srcRaw.bindMemory(to: UInt8.self).baseAddress!
435
+ for y in y0 ..< y1 {
436
+ memcpy(
437
+ dst + y * rasterWidth + x0,
438
+ src + (y - atY) * cellWidth + (x0 - atX),
439
+ x1 - x0
440
+ )
441
+ }
442
+ }
443
+ }
444
+ }
445
+
446
+ /// Express-or-bake: each dirty rect either becomes cached draws (mode-14
447
+ /// glyph runs from the topmost covering mono grid, or one mode-13 draw of
448
+ /// an isolated repeated image) and drops its bitmap delta, or stays a
449
+ /// plain delta untouched. All-or-nothing per rect keeps the shadow model
450
+ /// trivially exact — no partial punch-out. Non-ASCII cells (mode 14 is
451
+ /// 32..126 only) become small residual bitmap deltas within an otherwise
452
+ /// expressed rect.
453
+ private func expressTextureRects(
454
+ _ rects: [CanvasRect], grids: [MonoGrid], frame: SceneFrame,
455
+ boxes: LensBoxes, raster: Data, width: Int, height: Int,
456
+ allowImages: Bool, fidBudget: Int
457
+ ) -> (deltas: [CanvasRect], draws: [Data]) {
458
+ struct ElementBox {
459
+ let z: Int
460
+ let x0: Int
461
+ let y0: Int
462
+ let x1: Int
463
+ let y1: Int
464
+ func intersects(_ r: CanvasRect) -> Bool {
465
+ r.x < x1 && r.x + r.w > x0 && r.y < y1 && r.y + r.h > y0
466
+ }
467
+ }
468
+ let elementBoxes: [ElementBox] = frame.elements.enumerated().map { z, el in
469
+ ElementBox(
470
+ z: z, x0: max(0, Int(el.x)), y0: max(0, Int(el.y)),
471
+ x1: min(width, Int(el.x) + Int(el.w)), y1: min(height, Int(el.y) + Int(el.h))
472
+ )
473
+ }
474
+
475
+ // Isolated image candidates: nothing else overlaps their box, so the
476
+ // frame raster inside the box IS the standalone render and an opaque
477
+ // full-box cached draw reproduces it. Sightings are recorded every
478
+ // frame; only content seen before enters the cache (streaming content
479
+ // like a live minimap never repeats a hash).
480
+ struct ImageCandidate {
481
+ let box: ElementBox
482
+ let key: String
483
+ let seen: Bool
484
+ }
485
+ var imageCandidates: [Int: ImageCandidate] = [:]
486
+ if allowImages {
487
+ for (z, el) in frame.elements.enumerated() where el.type == "image" {
488
+ let box = elementBoxes[z]
489
+ let w = box.x1 - box.x0
490
+ let h = box.y1 - box.y0
491
+ guard w > 0, h > 0, w <= 255, h <= 255, !el.contentHash.isEmpty else { continue }
492
+ let isolated = !elementBoxes.contains { other in
493
+ other.z != z && other.x0 < box.x1 && other.x1 > box.x0
494
+ && other.y0 < box.y1 && other.y1 > box.y0
495
+ }
496
+ guard isolated else { continue }
497
+ let key = "\(el.contentHash)@\(w)x\(h)"
498
+ imageCandidates[z] = ImageCandidate(
499
+ box: box, key: key, seen: textureCache.noteImageSighting(key)
500
+ )
501
+ }
502
+ }
503
+
504
+ var deltas: [CanvasRect] = []
505
+ var imageDraws: [Int: Data] = [:]
506
+ var runPayloads: [Data] = []
507
+ var runCount = 0
508
+
509
+ rectLoop: for (index, rect) in rects.enumerated() {
510
+ let remaining = rects.count - index - 1
511
+
512
+ // Glyph express: topmost mono grid fully covering the rect, with
513
+ // nothing stacked above it in that region.
514
+ if let grid = grids.filter({ $0.covers(rect) }).max(by: { $0.z < $1.z }),
515
+ !elementBoxes.contains(where: { $0.z > grid.z && $0.intersects(rect) }),
516
+ let font = textureCache.font(
517
+ fontSize: grid.fontSize, cellWidth: grid.cellWidth, lineHeight: grid.lineHeight
518
+ )
519
+ {
520
+ let r0 = (rect.y - grid.originY) / grid.lineHeight
521
+ let r1 = (rect.y + rect.h - 1 - grid.originY) / grid.lineHeight
522
+ let c0 = (rect.x - grid.originX) / grid.cellWidth
523
+ let c1 = (rect.x + rect.w - 1 - grid.originX) / grid.cellWidth
524
+ var newRuns: [(x: Int, y: Int, chars: [UInt8])] = []
525
+ var residuals: [CanvasRect] = []
526
+ var expressible = true
527
+ for row in r0 ... r1 {
528
+ let rowY = grid.originY + row * grid.lineHeight
529
+ var runChars: [UInt8] = []
530
+ var runStartCol = c0
531
+ var residualStart: Int?
532
+ func flushRun(endCol: Int) {
533
+ guard !runChars.isEmpty else { return }
534
+ newRuns.append((
535
+ x: grid.originX + runStartCol * grid.cellWidth, y: rowY, chars: runChars
536
+ ))
537
+ runChars = []
538
+ _ = endCol
539
+ }
540
+ func flushResidual(endCol: Int) {
541
+ guard let start = residualStart else { return }
542
+ let x0 = max(0, (grid.originX + start * grid.cellWidth) & ~3)
543
+ let x1 = min(width, (grid.originX + endCol * grid.cellWidth + 3) & ~3)
544
+ let y0 = max(0, rowY & ~1)
545
+ let y1 = min(height, (rowY + grid.lineHeight + 1) & ~1)
546
+ residuals.append(CanvasRect(x: x0, y: y0, w: x1 - x0, h: y1 - y0))
547
+ residualStart = nil
548
+ }
549
+ for col in c0 ... c1 {
550
+ let scalar = grid.scalar(row: row, col: col)
551
+ if scalar.value >= 32, scalar.value <= 126,
552
+ textureCache.ensureGlyph(font: font, char: UInt8(scalar.value), rasterize: {
553
+ self.memoizedGlyphRaster(
554
+ scalar, fontSize: grid.fontSize,
555
+ cellWidth: grid.cellWidth, lineHeight: grid.lineHeight
556
+ )
557
+ })
558
+ {
559
+ flushResidual(endCol: col)
560
+ if runChars.isEmpty { runStartCol = col }
561
+ runChars.append(UInt8(scalar.value))
562
+ if runChars.count == 255 { flushRun(endCol: col + 1) }
563
+ } else {
564
+ flushRun(endCol: col)
565
+ if residualStart == nil { residualStart = col }
566
+ }
567
+ }
568
+ flushRun(endCol: c1 + 1)
569
+ flushResidual(endCol: c1 + 1)
570
+ if runCount + newRuns.count > Self.maxGlyphRuns { expressible = false; break }
571
+ }
572
+ // Residual deltas + every remaining rect must still fit the
573
+ // fid budget; otherwise keep this rect as one plain delta.
574
+ if expressible, deltas.count + residuals.count + remaining + 1 <= fidBudget,
575
+ residuals.count <= 3
576
+ {
577
+ var payloads: [Data] = []
578
+ for run in newRuns {
579
+ guard let payload = G2CfwTextureCache.stringRunPayload(
580
+ fontTableOffset: font.tableOffset,
581
+ x: Int(boxes.left.x) + run.x, y: Int(boxes.left.y) + run.y,
582
+ options: G2CfwTextureCache.opaqueDrawOptions, chars: run.chars
583
+ ) else { payloads = []; break }
584
+ payloads.append(payload)
585
+ }
586
+ if !payloads.isEmpty || newRuns.isEmpty {
587
+ runPayloads.append(contentsOf: payloads)
588
+ runCount += payloads.count
589
+ deltas.append(contentsOf: residuals)
590
+ continue rectLoop
591
+ }
592
+ }
593
+ }
594
+
595
+ // Image express: the rect sits wholly inside an isolated, repeated
596
+ // image's box — one opaque full-box draw covers it (and any other
597
+ // rect in the same box).
598
+ if allowImages,
599
+ let (z, candidate) = imageCandidates.first(where: { _, c in
600
+ c.seen && rect.x >= c.box.x0 && rect.y >= c.box.y0
601
+ && rect.x + rect.w <= c.box.x1 && rect.y + rect.h <= c.box.y1
602
+ })
603
+ {
604
+ if imageDraws[z] != nil { continue rectLoop }
605
+ let w = candidate.box.x1 - candidate.box.x0
606
+ let h = candidate.box.y1 - candidate.box.y0
607
+ var gray = Data(capacity: w * h)
608
+ raster.withUnsafeBytes { raw in
609
+ let src = raw.bindMemory(to: UInt8.self)
610
+ for y in candidate.box.y0 ..< candidate.box.y1 {
611
+ gray.append(contentsOf: UnsafeBufferPointer(
612
+ start: src.baseAddress! + y * width + candidate.box.x0, count: w
613
+ ))
614
+ }
615
+ }
616
+ if let offset = textureCache.ensureImage(hash: candidate.key, gray: gray, width: w, height: h),
617
+ let draw = G2CfwTextureCache.imageDrawPayload(
618
+ offset: offset,
619
+ x: Int(boxes.left.x) + candidate.box.x0, y: Int(boxes.left.y) + candidate.box.y0,
620
+ options: G2CfwTextureCache.opaqueDrawOptions
621
+ )
622
+ {
623
+ imageDraws[z] = draw
624
+ continue rectLoop
625
+ }
626
+ }
627
+
628
+ deltas.append(rect)
629
+ }
630
+
631
+ let draws = imageDraws.sorted { $0.key < $1.key }.map(\.value) + runPayloads
632
+ return (deltas, draws)
633
+ }
634
+
635
+ /// Diff two same-size canvas rasters into a scroll copy + aligned dirty
636
+ /// rects, or a full-frame decision when the change is too large to be
637
+ /// worth splitting.
638
+ static func diffPlan(
639
+ previous: Data, next: Data, width: Int, height: Int,
640
+ forced: [CanvasRect], allowCopy: Bool
641
+ ) -> DeltaPlan {
642
+ guard previous.count == width * height, next.count == width * height,
643
+ width > 0, height > 0
644
+ else { return .fullFrame }
645
+
646
+ return previous.withUnsafeBytes { prevRaw in
647
+ next.withUnsafeBytes { nextRaw in
648
+ let prev = prevRaw.bindMemory(to: UInt8.self)
649
+ let new = nextRaw.bindMemory(to: UInt8.self)
650
+
651
+ var changed = [Bool](repeating: false, count: height)
652
+ var anyChanged = false
653
+ for y in 0 ..< height {
654
+ let row = y * width
655
+ if memcmp(prev.baseAddress! + row, new.baseAddress! + row, width) != 0 {
656
+ changed[y] = true
657
+ anyChanged = true
658
+ }
659
+ }
660
+ if !anyChanged, forced.isEmpty { return .noop }
661
+
662
+ // Scroll detection: a vertical band of new rows that byte-match
663
+ // previous rows shifted by dy. Row hashes score candidate
664
+ // shifts; the winning band is byte-verified before use.
665
+ var copy: RowCopy?
666
+ var predicted: [UInt8]?
667
+ if allowCopy, anyChanged {
668
+ var prevHash = [UInt64](repeating: 0, count: height)
669
+ var newHash = [UInt64](repeating: 0, count: height)
670
+ for y in 0 ..< height {
671
+ prevHash[y] = rowHash(prev, offset: y * width, count: width)
672
+ newHash[y] = rowHash(new, offset: y * width, count: width)
673
+ }
674
+ let changedCount = changed.lazy.filter { $0 }.count
675
+ var bestDy = 0
676
+ var bestScore = 0
677
+ let maxShift = height - minScrollBandRows
678
+ if maxShift > 0 {
679
+ for dy in -maxShift ... maxShift where dy != 0 {
680
+ var score = 0
681
+ let lo = max(0, -dy)
682
+ let hi = min(height, height - dy)
683
+ for y in lo ..< hi where changed[y] && newHash[y] == prevHash[y + dy] {
684
+ score += 1
685
+ }
686
+ if score > bestScore {
687
+ bestScore = score
688
+ bestDy = dy
689
+ }
690
+ }
691
+ }
692
+ if bestDy != 0, bestScore >= minScrollBandRows,
693
+ bestScore * 100 >= changedCount * scrollSavingsPercent
694
+ {
695
+ // Longest contiguous hash-matching run, then byte-verify.
696
+ var runStart = -1
697
+ var bandStart = 0
698
+ var bandLen = 0
699
+ let lo = max(0, -bestDy)
700
+ let hi = min(height, height - bestDy)
701
+ for y in lo ... hi {
702
+ let matches = y < hi && newHash[y] == prevHash[y + bestDy]
703
+ if matches {
704
+ if runStart < 0 { runStart = y }
705
+ } else if runStart >= 0 {
706
+ if y - runStart > bandLen {
707
+ bandStart = runStart
708
+ bandLen = y - runStart
709
+ }
710
+ runStart = -1
711
+ }
712
+ }
713
+ if bandLen >= minScrollBandRows {
714
+ var verified = true
715
+ for y in bandStart ..< bandStart + bandLen where verified {
716
+ verified = memcmp(
717
+ new.baseAddress! + y * width,
718
+ prev.baseAddress! + (y + bestDy) * width, width
719
+ ) == 0
720
+ }
721
+ if verified {
722
+ copy = RowCopy(srcY: bandStart + bestDy, dstY: bandStart, height: bandLen)
723
+ var scratch = [UInt8](repeating: 0, count: width * height)
724
+ scratch.withUnsafeMutableBufferPointer { buf in
725
+ memcpy(buf.baseAddress!, prev.baseAddress!, width * height)
726
+ memmove(
727
+ buf.baseAddress! + bandStart * width,
728
+ prev.baseAddress! + (bandStart + bestDy) * width,
729
+ bandLen * width
730
+ )
731
+ }
732
+ predicted = scratch
733
+ for y in bandStart ..< bandStart + bandLen {
734
+ changed[y] = false
735
+ }
736
+ anyChanged = changed.contains(true)
737
+ }
738
+ }
739
+ }
740
+ }
741
+
742
+ // Per-row dirty column ranges against the (possibly copied) base.
743
+ var colLo = [Int](repeating: Int.max, count: height)
744
+ var colHi = [Int](repeating: -1, count: height)
745
+ func base(_ y: Int, _ x: Int) -> UInt8 {
746
+ if let predicted { return predicted[y * width + x] }
747
+ return prev[y * width + x]
748
+ }
749
+ for y in 0 ..< height where changed[y] {
750
+ var lo = 0
751
+ while lo < width, base(y, lo) == new[y * width + lo] { lo += 1 }
752
+ var hi = width - 1
753
+ while hi > lo, base(y, hi) == new[y * width + hi] { hi -= 1 }
754
+ colLo[y] = lo
755
+ colHi[y] = hi
756
+ }
757
+ var dirtyRows = changed
758
+ for rect in forced {
759
+ let y0 = max(0, rect.y)
760
+ let y1 = min(height, rect.y + rect.h)
761
+ guard y0 < y1, rect.w > 0 else { continue }
762
+ for y in y0 ..< y1 {
763
+ dirtyRows[y] = true
764
+ colLo[y] = min(colLo[y], max(0, rect.x))
765
+ colHi[y] = max(colHi[y], min(width - 1, rect.x + rect.w - 1))
766
+ }
767
+ }
768
+ guard dirtyRows.contains(true) else {
769
+ if let copy { return DeltaPlan(full: false, copy: copy, rects: []) }
770
+ return .noop
771
+ }
772
+
773
+ // Group dirty rows into bands (small gaps merge), then align
774
+ // each band to the mode-3 quantization grid.
775
+ var rects: [CanvasRect] = []
776
+ var y = 0
777
+ while y < height {
778
+ guard dirtyRows[y] else {
779
+ y += 1
780
+ continue
781
+ }
782
+ var end = y
783
+ var gap = 0
784
+ var scan = y
785
+ while scan < height, gap <= dirtyRowMergeGap {
786
+ if dirtyRows[scan] {
787
+ end = scan
788
+ gap = 0
789
+ } else {
790
+ gap += 1
791
+ }
792
+ scan += 1
793
+ }
794
+ var lo = Int.max
795
+ var hi = -1
796
+ for row in y ... end where dirtyRows[row] {
797
+ lo = min(lo, colLo[row])
798
+ hi = max(hi, colHi[row])
799
+ }
800
+ let x0 = max(0, lo & ~3)
801
+ let x1 = min(width, (hi + 4) & ~3)
802
+ let ry0 = max(0, y & ~1)
803
+ let ry1 = min(height, (end + 2) & ~1)
804
+ rects.append(CanvasRect(x: x0, y: ry0, w: x1 - x0, h: ry1 - ry0))
805
+ y = end + 1
806
+ }
807
+ if rects.count > maxDirtyRects {
808
+ var x0 = Int.max
809
+ var y0 = Int.max
810
+ var x1 = 0
811
+ var y1 = 0
812
+ for rect in rects {
813
+ x0 = min(x0, rect.x)
814
+ y0 = min(y0, rect.y)
815
+ x1 = max(x1, rect.x + rect.w)
816
+ y1 = max(y1, rect.y + rect.h)
817
+ }
818
+ rects = [CanvasRect(x: x0, y: y0, w: x1 - x0, h: y1 - y0)]
819
+ }
820
+ let coverage = rects.reduce(0) { $0 + $1.w * $1.h }
821
+ if copy == nil, coverage * 100 >= width * height * fullFrameCoveragePercent {
822
+ return .fullFrame
823
+ }
824
+ return DeltaPlan(full: false, copy: copy, rects: rects)
825
+ }
826
+ }
827
+ }
828
+
829
+ private static func rowHash(_ pixels: UnsafeBufferPointer<UInt8>, offset: Int, count: Int) -> UInt64 {
830
+ // FNV-1a 64 — collision-safe enough for scoring; bands are byte-verified.
831
+ var hash: UInt64 = 0xCBF2_9CE4_8422_2325
832
+ for i in offset ..< offset + count {
833
+ hash = (hash ^ UInt64(pixels[i])) &* 0x0000_0100_0000_01B3
834
+ }
835
+ return hash
836
+ }
837
+
838
+ /// Stereo mode-3 delta of one canvas sub-rect, drawn at each lens's window
839
+ /// origin plus the rect offset. Rect must be grid-aligned and in bounds.
840
+ static func boxDeltaFrame(
841
+ raster: Data, canvasWidth: Int, rect: CanvasRect, boxes: LensBoxes, fid: UInt16
842
+ ) -> Data? {
843
+ guard rect.w > 0, rect.h > 0, rect.x % 4 == 0, rect.w % 4 == 0,
844
+ rect.y % 2 == 0, rect.h % 2 == 0,
845
+ rect.x + rect.w <= canvasWidth,
846
+ (rect.y + rect.h) * canvasWidth <= raster.count
847
+ else { return nil }
848
+ var pixels = Data(capacity: rect.w * rect.h)
849
+ raster.withUnsafeBytes { raw in
850
+ let src = raw.bindMemory(to: UInt8.self)
851
+ for y in rect.y ..< rect.y + rect.h {
852
+ pixels.append(contentsOf: UnsafeBufferPointer(
853
+ start: src.baseAddress! + y * canvasWidth + rect.x, count: rect.w
854
+ ))
855
+ }
856
+ }
857
+ let rle = rleEncode(pack4Bpp(pixels))
858
+ return stereoRleFrame(
859
+ rle: rle,
860
+ lx: Int(boxes.left.x) + rect.x, ly: Int(boxes.left.y) + rect.y,
861
+ rx: Int(boxes.right.x) + rect.x, ry: Int(boxes.right.y) + rect.y,
862
+ w: Int32(rect.w), h: Int32(rect.h), fid: fid
863
+ )
864
+ }
865
+
866
+ /// Mode-9 rect copy with the lenses-differ flag: a full-width canvas band
867
+ /// moves srcY→dstY inside each lens's window.
868
+ /// `[0x89][L src LTWH dst LTWH][R src LTWH dst LTWH]`, u16 LE each.
869
+ static func rectCopyPayload(copy: RowCopy, width: Int, boxes: LensBoxes) -> Data {
870
+ var payload = Data([0x89])
871
+ func appendRect(_ x: Int, _ y: Int) {
872
+ for value in [x, y, width, copy.height] {
873
+ payload.append(UInt8(value & 0xFF))
874
+ payload.append(UInt8((value >> 8) & 0xFF))
875
+ }
876
+ }
877
+ appendRect(Int(boxes.left.x), Int(boxes.left.y) + copy.srcY)
878
+ appendRect(Int(boxes.left.x), Int(boxes.left.y) + copy.dstY)
879
+ appendRect(Int(boxes.right.x), Int(boxes.right.y) + copy.srcY)
880
+ appendRect(Int(boxes.right.x), Int(boxes.right.y) + copy.dstY)
881
+ return payload
882
+ }
883
+
884
+ /// Mode-15 builtin-font string draw:
885
+ /// `[15][x:u16][y:u16][options:u8][strlen:u8][UTF-8]`. Bytes 1–31 are
886
+ /// firmware x-adjust controls, so control characters are stripped here;
887
+ /// the text is truncated to 255 UTF-8 bytes at a codepoint boundary.
888
+ static func builtinStringPayload(x: Int, y: Int, options: UInt8, text: String) -> Data? {
889
+ guard x >= 0, x <= 0xFFFF, y >= 0, y <= 0xFFFF else { return nil }
890
+ let sanitized = String(text.unicodeScalars.filter { $0.value >= 32 })
891
+ var bytes = Array(sanitized.utf8)
892
+ if bytes.count > 255 {
893
+ var cut = 255
894
+ while cut > 0, bytes[cut] & 0xC0 == 0x80 { cut -= 1 }
895
+ bytes = Array(bytes[..<cut])
896
+ }
897
+ guard !bytes.isEmpty else { return nil }
898
+ var payload = Data([15])
899
+ payload.append(UInt8(x & 0xFF))
900
+ payload.append(UInt8((x >> 8) & 0xFF))
901
+ payload.append(UInt8(y & 0xFF))
902
+ payload.append(UInt8((y >> 8) & 0xFF))
903
+ payload.append(options)
904
+ payload.append(UInt8(bytes.count))
905
+ payload.append(contentsOf: bytes)
906
+ return payload
907
+ }
908
+
909
+ /// The firmware-drawn text lines of a frame, at absolute panel
910
+ /// coordinates. Mode 15 has no per-lens variant, so callers must place the
911
+ /// window at lens-equal offsets (depth level 1) whenever these are used.
912
+ /// The +2 inset matches the rasterizer's text padding.
913
+ static func builtinTextDraws(_ frame: SceneFrame, boxes: LensBoxes) -> [TextDraw] {
914
+ var draws: [TextDraw] = []
915
+ for element in frame.elements where element.type == "text" && element.font == "builtin" {
916
+ guard let text = element.text, !text.isEmpty else { continue }
917
+ let baseX = Int(boxes.left.x) + Int(element.x) + 2
918
+ let baseY = Int(boxes.left.y) + Int(element.y) + 2
919
+ for (index, line) in text.split(separator: "\n", omittingEmptySubsequences: false).enumerated() {
920
+ let y = baseY + index * builtinLineHeight
921
+ guard y >= 0, y + builtinLineHeight <= frameHeight, !line.isEmpty else { continue }
922
+ draws.append(TextDraw(x: baseX, y: y, text: String(line)))
923
+ }
924
+ }
925
+ return draws
926
+ }
927
+
928
+ /// Conservative canvas-space bounds of firmware-drawn text, used to force
929
+ /// background re-pushes that erase glyphs the phone raster never
930
+ /// contained. Width is overestimated (the phone has no advance tables).
931
+ static func builtinTextBounds(
932
+ _ draws: [TextDraw], boxes: LensBoxes, canvasWidth: Int, canvasHeight: Int
933
+ ) -> [CanvasRect] {
934
+ draws.compactMap { draw in
935
+ let x = max(0, draw.x - Int(boxes.left.x) - 4)
936
+ let y = max(0, draw.y - Int(boxes.left.y) - 2)
937
+ guard x < canvasWidth, y < canvasHeight else { return nil }
938
+ let width = min(canvasWidth - x, draw.text.count * 13 + 30)
939
+ let height = min(canvasHeight - y, builtinLineHeight + 4)
940
+ return CanvasRect(x: x, y: y, w: width, h: height)
941
+ }
942
+ }
943
+
944
+ /// Rasterize a scene into a width×height 8-bit grayscale buffer (row-major,
945
+ /// one byte per pixel), or nil when an image element fails to decode.
946
+ /// `omitBuiltinText` skips `font == "builtin"` text elements — the CFW
947
+ /// draws those with mode 15, so the bitmap must not also contain them.
948
+ /// `omitMonoText` skips `font == "mono"` text elements — the grid-exact
949
+ /// texture path blits those from the same glyph rasters it uploads.
950
+ static func rasterize(
951
+ _ frame: SceneFrame, width: Int, height: Int, omitBuiltinText: Bool = false,
952
+ omitMonoText: Bool = false
953
+ ) -> Data? {
954
+ var decodedImages: [Int: UIImage] = [:]
955
+ for (index, element) in frame.elements.enumerated() where element.type == "image" {
956
+ guard let encoded = element.data,
957
+ let data = Data(base64Encoded: encoded),
958
+ let image = UIImage(data: data)
959
+ else { return nil }
960
+ decodedImages[index] = image
961
+ }
962
+
963
+ let format = UIGraphicsImageRendererFormat()
964
+ format.scale = 1
965
+ format.opaque = true
966
+ let renderer = UIGraphicsImageRenderer(
967
+ size: CGSize(width: CGFloat(width), height: CGFloat(height)),
968
+ format: format
969
+ )
970
+ let image = renderer.image { rendererContext in
971
+ let context = rendererContext.cgContext
972
+ context.setFillColor(UIColor.black.cgColor)
973
+ context.fill(CGRect(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height)))
974
+
975
+ context.saveGState()
976
+ for (index, element) in frame.elements.enumerated() {
977
+ // Mode 15 draws only the glyphs, so an omitted builtin-text
978
+ // element's border must still come from the raster.
979
+ let skipBuiltinInk = omitBuiltinText && element.type == "text"
980
+ && element.font == "builtin"
981
+ if skipBuiltinInk, element.border <= 0 {
982
+ continue
983
+ }
984
+ if omitMonoText, element.type == "text", element.font == "mono" {
985
+ continue
986
+ }
987
+ let rect = CGRect(
988
+ x: CGFloat(element.x), y: CGFloat(element.y),
989
+ width: CGFloat(element.w), height: CGFloat(element.h)
990
+ )
991
+ context.saveGState()
992
+ context.clip(to: rect)
993
+
994
+ switch element.type {
995
+ case "text" where skipBuiltinInk:
996
+ break
997
+
998
+ case "text":
999
+ let paragraph = NSMutableParagraphStyle()
1000
+ let font: UIFont
1001
+ if element.font == "mono" {
1002
+ // Grid text: fixed pitch, fixed line height, no re-wrap —
1003
+ // the app laid out cols × rows against these metrics.
1004
+ let size = CGFloat(element.fontSize > 0 ? element.fontSize : 20)
1005
+ font = UIFont.monospacedSystemFont(ofSize: size, weight: .regular)
1006
+ paragraph.lineBreakMode = .byClipping
1007
+ paragraph.minimumLineHeight = Self.monoLineHeight(fontSize: size)
1008
+ paragraph.maximumLineHeight = paragraph.minimumLineHeight
1009
+ } else {
1010
+ font = UIFont.systemFont(ofSize: 20)
1011
+ paragraph.lineBreakMode = .byWordWrapping
1012
+ }
1013
+ let attributes: [NSAttributedString.Key: Any] = [
1014
+ .font: font,
1015
+ .foregroundColor: UIColor.white,
1016
+ .paragraphStyle: paragraph,
1017
+ ]
1018
+ ((element.text ?? "") as NSString).draw(
1019
+ in: rect.insetBy(dx: 2, dy: 2), withAttributes: attributes
1020
+ )
1021
+
1022
+ case "image":
1023
+ if let source = decodedImages[index] {
1024
+ let sourceSize = source.size
1025
+ let imageScale = min(
1026
+ rect.width / max(sourceSize.width, 1),
1027
+ rect.height / max(sourceSize.height, 1)
1028
+ )
1029
+ let target = CGSize(
1030
+ width: sourceSize.width * imageScale,
1031
+ height: sourceSize.height * imageScale
1032
+ )
1033
+ source.draw(
1034
+ in: CGRect(
1035
+ x: rect.midX - target.width / 2,
1036
+ y: rect.midY - target.height / 2,
1037
+ width: target.width, height: target.height
1038
+ )
1039
+ )
1040
+ }
1041
+
1042
+ case "rect":
1043
+ break
1044
+
1045
+ default:
1046
+ break
1047
+ }
1048
+
1049
+ let logicalBorder = max(element.type == "rect" ? Int32(1) : 0, element.border)
1050
+ if logicalBorder > 0 {
1051
+ context.setStrokeColor(UIColor.white.cgColor)
1052
+ context.setLineWidth(CGFloat(logicalBorder))
1053
+ let inset = CGFloat(logicalBorder) / 2
1054
+ let borderRect = rect.insetBy(dx: inset, dy: inset)
1055
+ let radius = min(
1056
+ CGFloat(max(0, element.radius)),
1057
+ min(borderRect.width, borderRect.height) / 2
1058
+ )
1059
+ context.addPath(
1060
+ UIBezierPath(roundedRect: borderRect, cornerRadius: radius).cgPath
1061
+ )
1062
+ context.strokePath()
1063
+ }
1064
+ context.restoreGState()
1065
+ }
1066
+ context.restoreGState()
1067
+ }
1068
+
1069
+ guard let cgImage = image.cgImage else { return nil }
1070
+ var grayscale = Data(count: width * height)
1071
+ let rendered = grayscale.withUnsafeMutableBytes { buffer -> Bool in
1072
+ guard let context = CGContext(
1073
+ data: buffer.baseAddress,
1074
+ width: width,
1075
+ height: height,
1076
+ bitsPerComponent: 8,
1077
+ bytesPerRow: width,
1078
+ space: CGColorSpaceCreateDeviceGray(),
1079
+ bitmapInfo: CGImageAlphaInfo.none.rawValue
1080
+ ) else { return false }
1081
+ context.setFillColor(gray: 0, alpha: 1)
1082
+ context.fill(CGRect(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height)))
1083
+ context.draw(cgImage, in: CGRect(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height)))
1084
+ return true
1085
+ }
1086
+ return rendered ? grayscale : nil
1087
+ }
1088
+
1089
+ /// Mode 3 with the lenses-differ bit: one zlib(RLE(4bpp)) payload of the
1090
+ /// box pixels, drawn by each lens at its own box.
1091
+ /// `[0x83][lx/4][ly/2][w/4][h/2][rx/4][ry/2][w/4][h/2][fid lo][fid hi][zlib]`
1092
+ static func stereoFrame(pixels: Data, boxes: LensBoxes, fid: UInt16) -> Data? {
1093
+ guard pixels.count == Int(boxes.width) * Int(boxes.height) else { return nil }
1094
+ let rle = rleEncode(pack4Bpp(pixels))
1095
+ return stereoFrame(rle: rle, boxes: boxes, fid: fid)
1096
+ }
1097
+
1098
+ /// Solid-color mode-3 frame at `boxes` — blanks a previous window position.
1099
+ static func solidStereoFrame(color: UInt8, boxes: LensBoxes, fid: UInt16) -> Data? {
1100
+ let rle = rleEncodeSolid(color: color, pixelCount: Int(boxes.width) * Int(boxes.height))
1101
+ return stereoFrame(rle: rle, boxes: boxes, fid: fid)
1102
+ }
1103
+
1104
+ private static func stereoFrame(rle: Data, boxes: LensBoxes, fid: UInt16) -> Data? {
1105
+ stereoRleFrame(
1106
+ rle: rle,
1107
+ lx: Int(boxes.left.x), ly: Int(boxes.left.y),
1108
+ rx: Int(boxes.right.x), ry: Int(boxes.right.y),
1109
+ w: boxes.width, h: boxes.height, fid: fid
1110
+ )
1111
+ }
1112
+
1113
+ private static func stereoRleFrame(
1114
+ rle: Data, lx: Int, ly: Int, rx: Int, ry: Int, w: Int32, h: Int32, fid: UInt16
1115
+ ) -> Data? {
1116
+ guard let compressed = BinaryCodec.zlibRFC1950(rle) else { return nil }
1117
+ var payload = Data([0x83])
1118
+ for (x, y) in [(lx, ly), (rx, ry)] {
1119
+ payload.append(UInt8(x / 4))
1120
+ payload.append(UInt8(y / 2))
1121
+ payload.append(UInt8(w / 4))
1122
+ payload.append(UInt8(h / 2))
1123
+ }
1124
+ payload.append(UInt8(fid & 0xFF))
1125
+ payload.append(UInt8(fid >> 8))
1126
+ payload.append(compressed)
1127
+ return payload
1128
+ }
1129
+
1130
+ /// Mode 8: apply every segment to the shadow, then present once.
1131
+ /// `[8][count][len lo][len hi][segment]...`
1132
+ static func bundle(_ segments: [Data]) -> Data? {
1133
+ guard !segments.isEmpty, segments.count <= 255 else { return nil }
1134
+ for segment in segments where segment.isEmpty || segment.count > 0xFFFF {
1135
+ return nil
1136
+ }
1137
+ var payload = Data([8, UInt8(segments.count)])
1138
+ for segment in segments {
1139
+ payload.append(UInt8(segment.count & 0xFF))
1140
+ payload.append(UInt8(segment.count >> 8))
1141
+ payload.append(segment)
1142
+ }
1143
+ return payload
1144
+ }
1145
+
1146
+ /// Mode-6 full-panel seed with the frame's own pixels composited at the
1147
+ /// window position on black — seeds the shadow AND paints the frame in ONE
1148
+ /// payload, so a resync shows content immediately instead of flashing a
1149
+ /// black panel (the faceclaw reference client's resyncs are mode-6 content
1150
+ /// frames and never flash). Mode 6 has no per-lens variant, so this
1151
+ /// returns nil unless the window is lens-equal — callers fall back to the
1152
+ /// black seed there.
1153
+ static func seededFramePayload(pixels: Data, boxes: LensBoxes) -> Data? {
1154
+ guard boxes.left == boxes.right,
1155
+ pixels.count == Int(boxes.width) * Int(boxes.height)
1156
+ else { return nil }
1157
+ var panel = Data(count: frameWidth * frameHeight)
1158
+ let x0 = Int(boxes.left.x)
1159
+ let y0 = Int(boxes.left.y)
1160
+ let w = Int(boxes.width)
1161
+ let h = Int(boxes.height)
1162
+ panel.withUnsafeMutableBytes { dstRaw in
1163
+ pixels.withUnsafeBytes { srcRaw in
1164
+ let dst = dstRaw.bindMemory(to: UInt8.self).baseAddress!
1165
+ let src = srcRaw.bindMemory(to: UInt8.self).baseAddress!
1166
+ for row in 0 ..< h {
1167
+ memcpy(dst + (y0 + row) * frameWidth + x0, src + row * w, w)
1168
+ }
1169
+ }
1170
+ }
1171
+ return encodePixels(panel)
1172
+ }
1173
+
1174
+ /// Mode 6 full-panel black: seeds the shadow mode-3 frames composite onto.
1175
+ static func blackPayload() -> Data? {
1176
+ let pixelCount = frameWidth * frameHeight
1177
+ let rle = rleEncodeSolid(color: 0, pixelCount: pixelCount)
1178
+ guard let compressed = BinaryCodec.zlibRFC1950(rle) else { return nil }
1179
+ var payload = Data([6])
1180
+ payload.append(compressed)
1181
+ return payload
1182
+ }
1183
+
1184
+ static func encodePixels(_ grayscale: Data) -> Data? {
1185
+ guard grayscale.count == frameWidth * frameHeight else { return nil }
1186
+ let packed = pack4Bpp(grayscale)
1187
+ let rle = rleEncode(packed)
1188
+ guard let compressed = BinaryCodec.zlibRFC1950(rle) else { return nil }
1189
+ var payload = Data([6])
1190
+ payload.append(compressed)
1191
+ return payload
1192
+ }
1193
+
1194
+ /// 8-bit gray → 4-bit panel level, round-to-nearest (matches the reference
1195
+ /// client's GRAY_TO_NIBBLE). 0 stays 0 so the shell's transparent color key
1196
+ /// survives; plain truncation instead darkened the upper half of every
1197
+ /// 16-value bucket by a full visible step on the 16-level panel.
1198
+ static func grayNibble(_ value: UInt8) -> UInt8 {
1199
+ value == 0 ? 0 : UInt8(min(15, (Int(value) + 8) >> 4))
1200
+ }
1201
+
1202
+ /// Snap an 8bpp raster to the 16 panel levels (`grayNibble(v) * 16`) before
1203
+ /// it enters the diff/shadow pipeline. The reference client diffs packed
1204
+ /// 4bpp bytes — exactly what the panel shows; diffing raw 8bpp instead
1205
+ /// marked rows dirty for sub-quantization AA noise the panel can never
1206
+ /// display (spurious rects, inflated coverage, and a scroll-copy detector
1207
+ /// that almost never matched). `grayNibble` round-trips `level * 16`
1208
+ /// exactly, so packing a quantized raster is lossless.
1209
+ static func quantizeToPanelLevels(_ raster: Data) -> Data {
1210
+ var out = raster
1211
+ out.withUnsafeMutableBytes { buffer in
1212
+ guard let base = buffer.bindMemory(to: UInt8.self).baseAddress else { return }
1213
+ for index in 0 ..< buffer.count {
1214
+ base[index] = grayNibble(base[index]) &* 16
1215
+ }
1216
+ }
1217
+ return out
1218
+ }
1219
+
1220
+ static func pack4Bpp(_ grayscale: Data) -> Data {
1221
+ let source = [UInt8](grayscale)
1222
+ var packed = Data(count: (source.count + 1) / 2)
1223
+ for index in packed.indices {
1224
+ let left = grayNibble(source[index * 2])
1225
+ let rightIndex = index * 2 + 1
1226
+ let right = rightIndex < source.count ? grayNibble(source[rightIndex]) : 0
1227
+ packed[index] = (left << 4) | right
1228
+ }
1229
+ return packed
1230
+ }
1231
+
1232
+ static func rleEncode(_ packed: Data) -> Data {
1233
+ let bytes = [UInt8](packed)
1234
+ let pixelCount = bytes.count * 2
1235
+ var output = Data()
1236
+ output.reserveCapacity(pixelCount / 4)
1237
+
1238
+ func nibble(_ index: Int) -> UInt8 {
1239
+ index.isMultiple(of: 2) ? bytes[index / 2] >> 4 : bytes[index / 2] & 0x0F
1240
+ }
1241
+
1242
+ var index = 0
1243
+ while index < pixelCount {
1244
+ let value = nibble(index)
1245
+ var end = index + 1
1246
+ while end < pixelCount, nibble(end) == value {
1247
+ end += 1
1248
+ }
1249
+ appendRun(color: value, count: end - index, to: &output)
1250
+ index = end
1251
+ }
1252
+ return output
1253
+ }
1254
+
1255
+ private static func rleEncodeSolid(color: UInt8, pixelCount: Int) -> Data {
1256
+ var output = Data()
1257
+ appendRun(color: color & 0x0F, count: pixelCount, to: &output)
1258
+ return output
1259
+ }
1260
+
1261
+ private static func appendRun(color: UInt8, count: Int, to output: inout Data) {
1262
+ var remaining = count
1263
+ while remaining > 0 {
1264
+ let chunk = min(remaining, 0xFFFF)
1265
+ if chunk <= 15 {
1266
+ output.append(UInt8(chunk << 4) | color)
1267
+ } else if chunk <= 255 {
1268
+ output.append(color)
1269
+ output.append(UInt8(chunk))
1270
+ } else {
1271
+ output.append(color)
1272
+ output.append(0)
1273
+ output.append(UInt8(chunk & 0xFF))
1274
+ output.append(UInt8((chunk >> 8) & 0xFF))
1275
+ }
1276
+ remaining -= chunk
1277
+ }
1278
+ }
1279
+ }