@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.
- package/README.md +563 -0
- package/android/build.gradle +450 -0
- package/android/gradle.properties.example +9 -0
- package/android/lc3Lib/CMakeLists.txt +6 -0
- package/android/lc3Lib/build.gradle +212 -0
- package/android/lc3Lib/proguard-rules.pro +33 -0
- package/android/lc3Lib/src/main/AndroidManifest.xml +2 -0
- package/android/lc3Lib/src/main/cpp/CMakeLists.txt +8 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/CMakeLists.txt +30 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/CMakeLists.txt +7 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.cc +96 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.h +53 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/Makefile.am +6 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h +26 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h.in +26 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/ogg.h +209 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/os_types.h +158 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus.h +981 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_custom.h +342 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_defines.h +799 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_multistream.h +660 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_projection.h +568 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_types.h +166 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.cc +268 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.h +115 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/opus_tools/opus_header.h +59 -0
- package/android/lc3Lib/src/main/cpp/liblc3/CMakeLists.txt +38 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/lc3.h +309 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/lc3_private.h +162 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/rnnoise.h +114 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/CMakeLists.txt +19 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.c +92 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.h +44 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.c +375 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.h +315 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.c +129 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.h +69 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/common.h +148 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.c +70 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.h +43 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/fastmath.h +158 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/lc3.c +702 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.c +893 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.h +111 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_arm.h +506 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_neon.h +281 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/makefile.mk +35 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.c +452 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.h +57 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct_neon.h +296 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/meson.build +46 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.c +61 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.h +57 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.c +880 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.h +103 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.c +904 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.h +119 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.c +3457 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.h +94 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.c +457 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.h +99 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3.cpp +159 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/_kiss_fft_guts.h +182 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/arch.h +261 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.c +279 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.h +59 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/common.h +48 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/compile.sh +3 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/denoise.c +646 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.c +601 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.h +203 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/opus_types.h +159 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.c +526 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.h +149 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.c +178 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.h +69 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.c +11051 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.h +34 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_reader.c +168 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_train.py +66 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/tansig_table.h +45 -0
- package/android/lc3Lib/src/main/java/com/glassly/lc3Lib/Lc3Cpp.java +38 -0
- package/android/lc3Lib/third_party/CMakeLists.txt +23 -0
- package/android/lc3Lib/third_party/CMakeLists_libogg.txt +18 -0
- package/android/lc3Lib/third_party/CMakeLists_libopus.txt +18 -0
- package/android/lc3Lib/third_party/CMakeLists_opus-tools.txt +23 -0
- package/android/lc3Lib/third_party/opus_tools/src/AUTHORS +5 -0
- package/android/lc3Lib/third_party/opus_tools/src/CMakeLists.txt +4 -0
- package/android/lc3Lib/third_party/opus_tools/src/COPYING +371 -0
- package/android/lc3Lib/third_party/opus_tools/src/ChangeLog +3 -0
- package/android/lc3Lib/third_party/opus_tools/src/Makefile.am +42 -0
- package/android/lc3Lib/third_party/opus_tools/src/Makefile.unix +23 -0
- package/android/lc3Lib/third_party/opus_tools/src/NEWS +0 -0
- package/android/lc3Lib/third_party/opus_tools/src/README +0 -0
- package/android/lc3Lib/third_party/opus_tools/src/autogen.sh +115 -0
- package/android/lc3Lib/third_party/opus_tools/src/configure.ac +293 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/arch.h +239 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/audio-in.c +1046 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.c +245 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.h +28 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.c +320 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.h +51 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/lpc.c +157 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/lpc.h +27 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.c +286 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.h +59 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusdec.c +884 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.c +1021 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.h +101 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.c +639 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.h +51 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/os_support.h +167 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/resample.c +1137 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/speex_resampler.h +344 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/stack_alloc.h +115 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.c +125 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.h +62 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.c +223 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.h +60 -0
- package/android/settings.gradle +4 -0
- package/android/silero/LICENSE.md +17 -0
- package/android/silero/build.gradle +50 -0
- package/android/silero/proguard-rules.pro +24 -0
- package/android/silero/src/androidTest/assets/hello.wav +0 -0
- package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
- package/android/silero/src/main/assets/LICENSE +21 -0
- package/android/silero/src/main/assets/silero_vad.onnx +0 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
- package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
- package/android/src/main/AndroidManifest.xml +42 -0
- package/android/src/main/assets/G2Firmware/README.md +11 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +2325 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkAnalytics.kt +216 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkDebug.kt +14 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkDefaults.kt +7 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +1088 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +953 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +2528 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +360 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +8 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/LocalNetworkModule.kt +414 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/ObservableStore.kt +88 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/OtaManifest.kt +191 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/ReleaseChangelog.kt +61 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/audio/AudioModels.kt +144 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/camera/CameraModels.kt +486 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/connection/ConnectionModels.kt +78 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/controllers/ControllerManager.kt +158 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/controllers/R1.kt +937 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/debug/BleTraceLogger.kt +165 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/events/BluetoothEvents.kt +178 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/internal/MapParsing.kt +140 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/net/LocalIpv4.kt +57 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/otaserver/OtaServerModule.kt +120 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +622 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/PhotoReceiverModule.kt +156 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/requests/DisplayRequests.kt +56 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/services/Foreground.kt +272 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/services/PhoneMic.kt +1195 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Ar99.java +2653 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/BesOtaHeartbeatGuard.kt +49 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/BesOtaProgressMapper.kt +27 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G1.kt +3878 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +6428 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +49 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +1241 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +270 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +291 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +411 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/GlasslyBle.java +43189 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Mach1.kt +1280 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +10817 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +2011 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Nimo.kt +2532 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +470 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Simulated.kt +228 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/Ar99OtaManager.java +566 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaByteUtils.java +18 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaCommandConstants.java +35 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaCrc32Util.java +13 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaGattTransport.java +11 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaProtocol.java +92 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +525 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +669 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/status/RuntimeState.kt +201 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/status/WifiHotspotStatus.kt +189 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/streaming/StreamModels.kt +518 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +391 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +518 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +248 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/types/DeviceModels.kt +96 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/AES.java +106 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/AudioSessionMonitor.kt +189 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/AvifExifStripper.java +573 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/BitmapJavaUtils.java +169 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/BleJsonCompact.java +496 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/BlePhotoUploadService.java +677 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/BleWireProtocol.java +108 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/Constants.kt +46 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/G1Text.kt +436 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/IncidentLogBleRelayNaming.java +29 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/IncidentLogBleUploadService.java +77 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/K900LengthCodec.java +115 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtils.java +827 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/MessageChunkReassembler.java +228 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/MessageChunker.java +325 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +698 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/PhoneAudioMonitor.kt +348 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/SmartGlassesConnectionState.java +9 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/Ar99OpusPcmDecoder.java +398 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/ByteUtilAudioPlayer.java +655 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/Lc3Player.java +441 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/PCMAudioPlayer.java +431 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
- package/android/src/main/res/values/strings.xml +4 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/OtaManifestDowngradeTest.kt +86 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/StreamStatusEventTest.kt +29 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/camera/PhotoRequestTest.kt +141 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/Ar99AdvertisementMatcherTest.java +22 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/BesOtaHeartbeatGuardTest.kt +73 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/BesOtaProgressMapperTest.kt +67 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/EvenRleTest.kt +63 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +89 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +344 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +200 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocolTest.kt +131 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +133 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NimoProtocolTest.kt +333 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/AvifExifStripperTest.java +46 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/BleJsonCompactTest.java +319 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +107 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/PhoneAudioMonitorTest.kt +45 -0
- package/android/src/test/resources/avif_with_exif.avif +0 -0
- package/app.plugin.js +3 -0
- package/build/BluetoothSdk.types.d.ts +1354 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -0
- package/build/BluetoothSdk.types.js +41 -0
- package/build/BluetoothSdk.types.js.map +1 -0
- package/build/_internal.d.ts +17 -0
- package/build/_internal.d.ts.map +1 -0
- package/build/_internal.js +16 -0
- package/build/_internal.js.map +1 -0
- package/build/_private/BluetoothSdkModule.d.ts +172 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -0
- package/build/_private/BluetoothSdkModule.js +300 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -0
- package/build/_private/LocalNetworkModule.d.ts +38 -0
- package/build/_private/LocalNetworkModule.d.ts.map +1 -0
- package/build/_private/LocalNetworkModule.js +3 -0
- package/build/_private/LocalNetworkModule.js.map +1 -0
- package/build/_private/cameraRequestPayload.d.ts +4 -0
- package/build/_private/cameraRequestPayload.d.ts.map +1 -0
- package/build/_private/cameraRequestPayload.js +32 -0
- package/build/_private/cameraRequestPayload.js.map +1 -0
- package/build/_private/photoRequestPayload.d.ts +6 -0
- package/build/_private/photoRequestPayload.d.ts.map +1 -0
- package/build/_private/photoRequestPayload.js +86 -0
- package/build/_private/photoRequestPayload.js.map +1 -0
- package/build/_private/sdkOtaManifest.d.ts +11 -0
- package/build/_private/sdkOtaManifest.d.ts.map +1 -0
- package/build/_private/sdkOtaManifest.js +15 -0
- package/build/_private/sdkOtaManifest.js.map +1 -0
- package/build/changelogs.d.ts +7 -0
- package/build/changelogs.d.ts.map +1 -0
- package/build/changelogs.js +46 -0
- package/build/changelogs.js.map +1 -0
- package/build/debug.d.ts +3 -0
- package/build/debug.d.ts.map +1 -0
- package/build/debug.js +8 -0
- package/build/debug.js.map +1 -0
- package/build/generated/changelogCatalog.d.ts +9 -0
- package/build/generated/changelogCatalog.d.ts.map +1 -0
- package/build/generated/changelogCatalog.js +12 -0
- package/build/generated/changelogCatalog.js.map +1 -0
- package/build/generated/releaseMetadata.d.ts +12 -0
- package/build/generated/releaseMetadata.d.ts.map +1 -0
- package/build/generated/releaseMetadata.js +10 -0
- package/build/generated/releaseMetadata.js.map +1 -0
- package/build/index.d.ts +6 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +171 -0
- package/build/index.js.map +1 -0
- package/build/ota-server/OtaServer.types.d.ts +14 -0
- package/build/ota-server/OtaServer.types.d.ts.map +1 -0
- package/build/ota-server/OtaServer.types.js +2 -0
- package/build/ota-server/OtaServer.types.js.map +1 -0
- package/build/ota-server/OtaServerModule.d.ts +21 -0
- package/build/ota-server/OtaServerModule.d.ts.map +1 -0
- package/build/ota-server/OtaServerModule.js +3 -0
- package/build/ota-server/OtaServerModule.js.map +1 -0
- package/build/ota-server/OtaServerModule.web.d.ts +10 -0
- package/build/ota-server/OtaServerModule.web.d.ts.map +1 -0
- package/build/ota-server/OtaServerModule.web.js +12 -0
- package/build/ota-server/OtaServerModule.web.js.map +1 -0
- package/build/ota-server/index.d.ts +3 -0
- package/build/ota-server/index.d.ts.map +1 -0
- package/build/ota-server/index.js +3 -0
- package/build/ota-server/index.js.map +1 -0
- package/build/ota-transport/index.d.ts +67 -0
- package/build/ota-transport/index.d.ts.map +1 -0
- package/build/ota-transport/index.js +58 -0
- package/build/ota-transport/index.js.map +1 -0
- package/build/photo-receiver/PhotoReceiver.types.d.ts +18 -0
- package/build/photo-receiver/PhotoReceiver.types.d.ts.map +1 -0
- package/build/photo-receiver/PhotoReceiver.types.js +2 -0
- package/build/photo-receiver/PhotoReceiver.types.js.map +1 -0
- package/build/photo-receiver/PhotoReceiverModule.d.ts +10 -0
- package/build/photo-receiver/PhotoReceiverModule.d.ts.map +1 -0
- package/build/photo-receiver/PhotoReceiverModule.js +3 -0
- package/build/photo-receiver/PhotoReceiverModule.js.map +1 -0
- package/build/photo-receiver/PhotoReceiverModule.web.d.ts +10 -0
- package/build/photo-receiver/PhotoReceiverModule.web.d.ts.map +1 -0
- package/build/photo-receiver/PhotoReceiverModule.web.js +12 -0
- package/build/photo-receiver/PhotoReceiverModule.web.js.map +1 -0
- package/build/photo-receiver/index.d.ts +3 -0
- package/build/photo-receiver/index.d.ts.map +1 -0
- package/build/photo-receiver/index.js +3 -0
- package/build/photo-receiver/index.js.map +1 -0
- package/build/react/index.d.ts +5 -0
- package/build/react/index.d.ts.map +1 -0
- package/build/react/index.js +4 -0
- package/build/react/index.js.map +1 -0
- package/build/react/useBluetooth.d.ts +100 -0
- package/build/react/useBluetooth.d.ts.map +1 -0
- package/build/react/useBluetooth.js +167 -0
- package/build/react/useBluetooth.js.map +1 -0
- package/build/react/useBluetoothEvent.d.ts +6 -0
- package/build/react/useBluetoothEvent.d.ts.map +1 -0
- package/build/react/useBluetoothEvent.js +21 -0
- package/build/react/useBluetoothEvent.js.map +1 -0
- package/build/react/useBluetoothScan.d.ts +22 -0
- package/build/react/useBluetoothScan.d.ts.map +1 -0
- package/build/react/useBluetoothScan.js +135 -0
- package/build/react/useBluetoothScan.js.map +1 -0
- package/build/react/useBluetoothStatus.d.ts +16 -0
- package/build/react/useBluetoothStatus.d.ts.map +1 -0
- package/build/react/useBluetoothStatus.js +137 -0
- package/build/react/useBluetoothStatus.js.map +1 -0
- package/build/react/useGlassesConnection.d.ts +29 -0
- package/build/react/useGlassesConnection.d.ts.map +1 -0
- package/build/react/useGlassesConnection.js +153 -0
- package/build/react/useGlassesConnection.js.map +1 -0
- package/build/types/index.d.ts +3 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/index.js +2 -0
- package/build/types/index.js.map +1 -0
- package/expo-module.config.json +14 -0
- package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
- package/ios/BluetoothSdkModule.swift +1034 -0
- package/ios/GlasslyBluetoothSDK.podspec +106 -0
- package/ios/LocalIPv4.swift +150 -0
- package/ios/LocalOtaServer.swift +309 -0
- package/ios/LocalPhotoUploadServer.swift +754 -0
- package/ios/OtaServerModule.swift +147 -0
- package/ios/Packages/CoreObjC/CoreObjC.xcodeproj/project.pbxproj +213 -0
- package/ios/Packages/CoreObjC/PcmConverter.h +22 -0
- package/ios/Packages/CoreObjC/PcmConverter.m +266 -0
- package/ios/Packages/CoreObjC/attdet.c +92 -0
- package/ios/Packages/CoreObjC/attdet.h +44 -0
- package/ios/Packages/CoreObjC/bits.c +375 -0
- package/ios/Packages/CoreObjC/bits.h +315 -0
- package/ios/Packages/CoreObjC/bwdet.c +129 -0
- package/ios/Packages/CoreObjC/bwdet.h +69 -0
- package/ios/Packages/CoreObjC/common.h +151 -0
- package/ios/Packages/CoreObjC/energy.c +70 -0
- package/ios/Packages/CoreObjC/energy.h +43 -0
- package/ios/Packages/CoreObjC/fastmath.h +158 -0
- package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
- package/ios/Packages/CoreObjC/lc3.c +704 -0
- package/ios/Packages/CoreObjC/lc3.h +313 -0
- package/ios/Packages/CoreObjC/lc3_cpp.h +283 -0
- package/ios/Packages/CoreObjC/lc3_private.h +163 -0
- package/ios/Packages/CoreObjC/ltpf.c +905 -0
- package/ios/Packages/CoreObjC/ltpf.h +111 -0
- package/ios/Packages/CoreObjC/ltpf_arm.h +506 -0
- package/ios/Packages/CoreObjC/ltpf_neon.h +281 -0
- package/ios/Packages/CoreObjC/makefile.mk +35 -0
- package/ios/Packages/CoreObjC/mdct.c +469 -0
- package/ios/Packages/CoreObjC/mdct.h +57 -0
- package/ios/Packages/CoreObjC/mdct_neon.h +296 -0
- package/ios/Packages/CoreObjC/meson.build +61 -0
- package/ios/Packages/CoreObjC/plc.c +61 -0
- package/ios/Packages/CoreObjC/plc.h +57 -0
- package/ios/Packages/CoreObjC/rnnoise.h +114 -0
- package/ios/Packages/CoreObjC/sns.c +880 -0
- package/ios/Packages/CoreObjC/sns.h +103 -0
- package/ios/Packages/CoreObjC/spec.c +907 -0
- package/ios/Packages/CoreObjC/spec.h +119 -0
- package/ios/Packages/CoreObjC/tables.c +3457 -0
- package/ios/Packages/CoreObjC/tables.h +94 -0
- package/ios/Packages/CoreObjC/tns.c +457 -0
- package/ios/Packages/CoreObjC/tns.h +99 -0
- package/ios/Packages/SherpaOnnx/Model/joiner.onnx +0 -0
- package/ios/Packages/SherpaOnnx/Model/tokens.txt +502 -0
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +2383 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +48 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4689 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +2036 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4689 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +2036 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
- package/ios/Packages/libbz2/module.modulemap +5 -0
- package/ios/Packages/libbz2/shim.h +1 -0
- package/ios/PhotoReceiverModule.swift +129 -0
- package/ios/Source/BluetoothSDK.swift +2303 -0
- package/ios/Source/BluetoothSDKDebug.swift +12 -0
- package/ios/Source/BluetoothSdkDefaults.swift +29 -0
- package/ios/Source/Bridge.swift +708 -0
- package/ios/Source/Bridging-Header.h +16 -0
- package/ios/Source/DeviceManager.swift +2109 -0
- package/ios/Source/DeviceStore.swift +336 -0
- package/ios/Source/GeneratedChangelogCatalog.swift +7 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
- package/ios/Source/ObservableStore.swift +118 -0
- package/ios/Source/OtaManifest.swift +211 -0
- package/ios/Source/PrivacyInfo.xcprivacy +23 -0
- package/ios/Source/ReleaseChangelog.swift +65 -0
- package/ios/Source/Resources/G2Firmware/README.md +10 -0
- package/ios/Source/audio/AudioModels.swift +178 -0
- package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
- package/ios/Source/camera/CameraModels.swift +866 -0
- package/ios/Source/connection/ScanSession.swift +27 -0
- package/ios/Source/controllers/ControllerManager.swift +184 -0
- package/ios/Source/controllers/R1.swift +829 -0
- package/ios/Source/errors/BluetoothSdkError.swift +19 -0
- package/ios/Source/events/BluetoothEvents.swift +342 -0
- package/ios/Source/internal/BleTraceLogger.swift +192 -0
- package/ios/Source/internal/BluetoothAvailability.swift +78 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +199 -0
- package/ios/Source/internal/ValueParsing.swift +99 -0
- package/ios/Source/requests/DisplayRequests.swift +82 -0
- package/ios/Source/services/PhoneMic.swift +662 -0
- package/ios/Source/sgcs/Ar99.swift +2451 -0
- package/ios/Source/sgcs/Frame.swift +649 -0
- package/ios/Source/sgcs/G1.swift +2554 -0
- package/ios/Source/sgcs/G2.swift +6887 -0
- package/ios/Source/sgcs/G2CfwClassification.swift +43 -0
- package/ios/Source/sgcs/G2CfwRenderer.swift +1279 -0
- package/ios/Source/sgcs/G2CfwTextureCache.swift +233 -0
- package/ios/Source/sgcs/G2FlashProtocol.swift +312 -0
- package/ios/Source/sgcs/G2Flasher.swift +773 -0
- package/ios/Source/sgcs/Mach1.swift +541 -0
- package/ios/Source/sgcs/MentraLive.swift +7011 -0
- package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
- package/ios/Source/sgcs/MentraNex.swift +3221 -0
- package/ios/Source/sgcs/Nimo.swift +1886 -0
- package/ios/Source/sgcs/SGCManager.swift +608 -0
- package/ios/Source/sgcs/Simulated.swift +274 -0
- package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
- package/ios/Source/sgcs/glassly_ble.pb.swift +4354 -0
- package/ios/Source/speakerid/SpeakerIdEngine.swift +573 -0
- package/ios/Source/status/DeviceStatus.swift +981 -0
- package/ios/Source/status/RuntimeState.swift +359 -0
- package/ios/Source/status/WifiHotspotStatus.swift +345 -0
- package/ios/Source/streaming/StreamModels.swift +693 -0
- package/ios/Source/stt/STTTools.swift +191 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +466 -0
- package/ios/Source/tts/TTSTools.swift +171 -0
- package/ios/Source/types/DeviceModels.swift +177 -0
- package/ios/Source/utils/AudioSessionMonitor.swift +283 -0
- package/ios/Source/utils/BinaryCodec.swift +61 -0
- package/ios/Source/utils/BleJsonCompact.swift +405 -0
- package/ios/Source/utils/BleWireProtocol.swift +92 -0
- package/ios/Source/utils/Constants.swift +78 -0
- package/ios/Source/utils/Enums.swift +95 -0
- package/ios/Source/utils/G1Text.swift +2096 -0
- package/ios/Source/utils/JSCExperiment.swift +241 -0
- package/ios/Source/utils/MemoryMonitor.swift +44 -0
- package/ios/Source/utils/MessageChunkReassembler.swift +190 -0
- package/ios/Source/utils/MessageChunker.swift +294 -0
- package/ios/Source/utils/Models.swift +135 -0
- package/ios/Source/utils/PhoneAudioMonitor.swift +230 -0
- package/ios/Source/utils/TarBz2Extractor.swift +257 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +78 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +372 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +162 -0
- package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +182 -0
- package/ios/Tests/MentraLivePairingAdvertisementTests.swift +160 -0
- package/ios/Tests/ReleaseChangelogTests.swift +13 -0
- package/package.json +129 -0
- package/plugin/build/index.d.ts +10 -0
- package/plugin/build/index.js +12 -0
- package/plugin/build/withAndroid.d.ts +3 -0
- package/plugin/build/withAndroid.js +190 -0
- package/plugin/build/withIos.d.ts +5 -0
- package/plugin/build/withIos.js +108 -0
- package/scripts/inject-ios-sdk-version.mjs +65 -0
- package/scripts/public-ota-api.test.mjs +37 -0
- package/scripts/verify-release-package.mjs +45 -0
- package/scripts/write-release-metadata.mjs +152 -0
- package/scripts/write-release-metadata.test.mjs +37 -0
- package/src/BluetoothSdk.types.ts +1623 -0
- package/src/_internal.ts +16 -0
- package/src/_private/BluetoothSdkModule.ts +684 -0
- package/src/_private/LocalNetworkModule.ts +50 -0
- package/src/_private/cameraRequestPayload.ts +34 -0
- package/src/_private/photoRequestPayload.ts +90 -0
- package/src/_private/sdkOtaManifest.ts +18 -0
- package/src/changelogs.ts +46 -0
- package/src/debug.ts +9 -0
- package/src/generated/changelogCatalog.ts +13 -0
- package/src/generated/releaseMetadata.ts +20 -0
- package/src/index.ts +321 -0
- package/src/ota-server/OtaServer.types.ts +10 -0
- package/src/ota-server/OtaServerModule.ts +23 -0
- package/src/ota-server/OtaServerModule.web.ts +17 -0
- package/src/ota-server/index.ts +2 -0
- package/src/ota-transport/index.ts +116 -0
- package/src/photo-receiver/PhotoReceiver.types.ts +20 -0
- package/src/photo-receiver/PhotoReceiverModule.ts +11 -0
- package/src/photo-receiver/PhotoReceiverModule.web.ts +17 -0
- package/src/photo-receiver/index.ts +2 -0
- package/src/react/index.ts +21 -0
- package/src/react/useBluetooth.ts +309 -0
- package/src/react/useBluetoothEvent.ts +35 -0
- package/src/react/useBluetoothScan.ts +173 -0
- package/src/react/useBluetoothStatus.ts +180 -0
- package/src/react/useGlassesConnection.ts +202 -0
- 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
|
+
}
|