@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,1241 @@
|
|
|
1
|
+
package com.glassly.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.BitmapFactory
|
|
5
|
+
import android.graphics.Canvas
|
|
6
|
+
import android.graphics.Color
|
|
7
|
+
import android.graphics.Paint
|
|
8
|
+
import android.graphics.Rect
|
|
9
|
+
import android.graphics.RectF
|
|
10
|
+
import android.graphics.Typeface
|
|
11
|
+
import android.text.Layout
|
|
12
|
+
import android.text.StaticLayout
|
|
13
|
+
import android.text.TextPaint
|
|
14
|
+
import android.util.Base64
|
|
15
|
+
import java.io.ByteArrayOutputStream
|
|
16
|
+
import java.util.zip.DeflaterOutputStream
|
|
17
|
+
import kotlin.math.roundToInt
|
|
18
|
+
|
|
19
|
+
/** Phone-side full-scene rasterizer for Glassly CFW's direct-framebuffer protocol. */
|
|
20
|
+
internal object G2CfwRenderer {
|
|
21
|
+
const val FRAME_WIDTH = 640
|
|
22
|
+
const val FRAME_HEIGHT = 480
|
|
23
|
+
|
|
24
|
+
private val heightOffsets = intArrayOf(192, 176, 160, 144, 128, 112, 96, 80, 64, 48, 32, 16, 0)
|
|
25
|
+
private val depthOffsetsLeft = intArrayOf(40, 32, 24)
|
|
26
|
+
private val depthOffsetsRight = intArrayOf(24, 32, 40)
|
|
27
|
+
|
|
28
|
+
data class LensBoxes(
|
|
29
|
+
val leftX: Int,
|
|
30
|
+
val leftY: Int,
|
|
31
|
+
val rightX: Int,
|
|
32
|
+
val rightY: Int,
|
|
33
|
+
val width: Int,
|
|
34
|
+
val height: Int,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
fun lensBoxes(
|
|
38
|
+
canvasWidth: Int,
|
|
39
|
+
canvasHeight: Int,
|
|
40
|
+
heightLevel: Int,
|
|
41
|
+
depthLevel: Int,
|
|
42
|
+
): LensBoxes {
|
|
43
|
+
val heightIndex = heightLevel.coerceIn(heightOffsets.indices)
|
|
44
|
+
val depthIndex = depthLevel.coerceIn(depthOffsetsLeft.indices)
|
|
45
|
+
val width = canvasWidth.coerceIn(4, FRAME_WIDTH) and 3.inv()
|
|
46
|
+
val height = canvasHeight.coerceIn(2, FRAME_HEIGHT) and 1.inv()
|
|
47
|
+
|
|
48
|
+
fun x(value: Int) = value.coerceIn(0, FRAME_WIDTH - width) and 3.inv()
|
|
49
|
+
fun y(value: Int) = value.coerceIn(0, FRAME_HEIGHT - height) and 1.inv()
|
|
50
|
+
return LensBoxes(
|
|
51
|
+
leftX = x(depthOffsetsLeft[depthIndex]),
|
|
52
|
+
leftY = y(heightOffsets[heightIndex]),
|
|
53
|
+
rightX = x(depthOffsetsRight[depthIndex]),
|
|
54
|
+
rightY = y(heightOffsets[heightIndex]),
|
|
55
|
+
width = width,
|
|
56
|
+
height = height,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fun monoLineHeight(fontSize: Float): Float = (fontSize * 1.2f).roundToInt().toFloat()
|
|
61
|
+
|
|
62
|
+
fun encode(frame: SceneFrame, boxes: LensBoxes, frameId: Int): ByteArray? {
|
|
63
|
+
val pixels = rasterize(frame, boxes.width, boxes.height) ?: return null
|
|
64
|
+
return stereoFrame(pixels, boxes, frameId)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Grid-exact mono glyphs (texture-cache path) ─────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Texture-cache state. Modes 12/13/14 draw at absolute panel coordinates
|
|
71
|
+
* (no per-lens variant), so the caller only enables this path on frames
|
|
72
|
+
* whose window is placed lens-equal. The cache model resets whenever the
|
|
73
|
+
* caller's generation changes (lease cycled / residency unknown); the
|
|
74
|
+
* glyph raster memo survives resets — rasters don't change.
|
|
75
|
+
*/
|
|
76
|
+
private val textureCache = G2CfwTextureCache()
|
|
77
|
+
private val glyphRasterMemo = mutableMapOf<String, ByteArray>()
|
|
78
|
+
|
|
79
|
+
private fun monoPaint(fontSize: Float): Paint =
|
|
80
|
+
Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
81
|
+
color = Color.WHITE
|
|
82
|
+
textSize = fontSize
|
|
83
|
+
typeface = Typeface.MONOSPACE
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Fixed cell metrics of the phone's monospaced face at `fontSize`: (cellWidth, lineHeight). */
|
|
87
|
+
fun monoCellMetrics(fontSize: Int): Pair<Int, Int> {
|
|
88
|
+
val size = (if (fontSize > 0) fontSize else 20).toFloat()
|
|
89
|
+
val advance = monoPaint(size).measureText("0")
|
|
90
|
+
return maxOf(1, advance.roundToInt()) to maxOf(1, monoLineHeight(size).toInt())
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* One full-cell glyph raster (cellWidth × lineHeight, 8-bit gray, white
|
|
95
|
+
* ink on black). This single function feeds BOTH the frame raster (grid
|
|
96
|
+
* blits) and the mode-12 uploads, so an on-glasses cached draw reproduces
|
|
97
|
+
* the phone raster byte-for-byte. Baseline placement matches the flat
|
|
98
|
+
* rasterizer's mono path (-ascent from the cell top).
|
|
99
|
+
*/
|
|
100
|
+
fun monoGlyphRaster(codePoint: Int, fontSize: Int, cellWidth: Int, lineHeight: Int): ByteArray? {
|
|
101
|
+
val size = (if (fontSize > 0) fontSize else 20).toFloat()
|
|
102
|
+
val paint = monoPaint(size)
|
|
103
|
+
val bitmap = Bitmap.createBitmap(cellWidth, lineHeight, Bitmap.Config.ARGB_8888)
|
|
104
|
+
try {
|
|
105
|
+
val canvas = Canvas(bitmap)
|
|
106
|
+
canvas.drawColor(Color.BLACK)
|
|
107
|
+
canvas.drawText(String(Character.toChars(codePoint)), 0f, -paint.fontMetrics.ascent, paint)
|
|
108
|
+
val colors = IntArray(cellWidth * lineHeight)
|
|
109
|
+
bitmap.getPixels(colors, 0, cellWidth, 0, 0, cellWidth, lineHeight)
|
|
110
|
+
return ByteArray(colors.size) { index ->
|
|
111
|
+
val color = colors[index]
|
|
112
|
+
((Color.red(color) * 299 + Color.green(color) * 587 + Color.blue(color) * 114) / 1000).toByte()
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
bitmap.recycle()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private fun memoizedGlyphRaster(codePoint: Int, fontSize: Int, cellWidth: Int, lineHeight: Int): ByteArray? {
|
|
120
|
+
val key = "$fontSize|$codePoint"
|
|
121
|
+
glyphRasterMemo[key]?.let { return it }
|
|
122
|
+
val raster = monoGlyphRaster(codePoint, fontSize, cellWidth, lineHeight) ?: return null
|
|
123
|
+
glyphRasterMemo[key] = raster
|
|
124
|
+
return raster
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** A line's Unicode code points (mode 14 addresses cells per code point). */
|
|
128
|
+
private fun codePoints(line: String): IntArray {
|
|
129
|
+
val out = mutableListOf<Int>()
|
|
130
|
+
var i = 0
|
|
131
|
+
while (i < line.length) {
|
|
132
|
+
val cp = line.codePointAt(i)
|
|
133
|
+
out.add(cp)
|
|
134
|
+
i += Character.charCount(cp)
|
|
135
|
+
}
|
|
136
|
+
return out.toIntArray()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* One mono text element's glyph grid, produced by the grid-exact
|
|
141
|
+
* rasterizer: cell (0,0) sits at (originX, originY) in canvas coords, and
|
|
142
|
+
* only the `fullCols`×`fullRows` cells wholly inside the element box (and
|
|
143
|
+
* canvas) are expressible as cached draws — clipped edge cells stay baked.
|
|
144
|
+
*/
|
|
145
|
+
data class MonoGrid(
|
|
146
|
+
val z: Int,
|
|
147
|
+
val fontSize: Int,
|
|
148
|
+
val originX: Int,
|
|
149
|
+
val originY: Int,
|
|
150
|
+
val cellWidth: Int,
|
|
151
|
+
val lineHeight: Int,
|
|
152
|
+
val fullCols: Int,
|
|
153
|
+
val fullRows: Int,
|
|
154
|
+
/** Code point per cell; rows/cols beyond the text are spaces. */
|
|
155
|
+
val rows: List<IntArray>,
|
|
156
|
+
) {
|
|
157
|
+
fun scalar(row: Int, col: Int): Int {
|
|
158
|
+
if (row >= rows.size) return 32
|
|
159
|
+
val line = rows[row]
|
|
160
|
+
return if (col < line.size) line[col] else 32
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The canvas region whose every pixel belongs to a full cell. */
|
|
164
|
+
val expressibleRegion: CanvasRect
|
|
165
|
+
get() = CanvasRect(originX, originY, fullCols * cellWidth, fullRows * lineHeight)
|
|
166
|
+
|
|
167
|
+
fun covers(rect: CanvasRect): Boolean {
|
|
168
|
+
val region = expressibleRegion
|
|
169
|
+
return rect.x >= region.x && rect.y >= region.y &&
|
|
170
|
+
rect.x + rect.w <= region.x + region.w &&
|
|
171
|
+
rect.y + rect.h <= region.y + region.h
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Incremental updates (dirty rects / rect copy / builtin text) ────────
|
|
176
|
+
|
|
177
|
+
/** Canvas-space rect aligned to mode 3's quantization (x/w %4, y/h %2). */
|
|
178
|
+
data class CanvasRect(val x: Int, val y: Int, val w: Int, val h: Int)
|
|
179
|
+
|
|
180
|
+
/** Full-width vertical band copy: rows [srcY, srcY+height) → [dstY, ...). */
|
|
181
|
+
data class RowCopy(val srcY: Int, val dstY: Int, val height: Int)
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* What an incremental frame needs: a full re-send, or a scroll copy plus
|
|
185
|
+
* the aligned rects whose pixels still differ after it.
|
|
186
|
+
*/
|
|
187
|
+
data class DeltaPlan(val full: Boolean, val copy: RowCopy?, val rects: List<CanvasRect>) {
|
|
188
|
+
val isNoop: Boolean get() = !full && copy == null && rects.isEmpty()
|
|
189
|
+
|
|
190
|
+
companion object {
|
|
191
|
+
val NOOP = DeltaPlan(full = false, copy = null, rects = emptyList())
|
|
192
|
+
val FULL_FRAME = DeltaPlan(full = true, copy = null, rects = emptyList())
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** One firmware-drawn (mode 15) text line at absolute panel coordinates. */
|
|
197
|
+
data class TextDraw(val x: Int, val y: Int, val text: String)
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* One incremental encode: the wire payload (null when the frame is
|
|
201
|
+
* visually identical), mode-12 texture uploads that must be queued AHEAD
|
|
202
|
+
* of it, the raster backing the on-glass shadow after this payload
|
|
203
|
+
* applies, and whether the payload re-sends the whole canvas.
|
|
204
|
+
*/
|
|
205
|
+
class SceneUpdate(
|
|
206
|
+
val payload: ByteArray?,
|
|
207
|
+
val uploads: List<ByteArray>,
|
|
208
|
+
val raster: ByteArray,
|
|
209
|
+
val usedFullFrame: Boolean,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
const val MAX_DIRTY_RECTS = 6
|
|
213
|
+
const val REQUIRED_FID_COUNT = MAX_DIRTY_RECTS + 2
|
|
214
|
+
|
|
215
|
+
/** Cap on mode-14 runs per update (mode-8's count byte is the hard bound). */
|
|
216
|
+
const val MAX_GLYPH_RUNS = 180
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Line pitch and default draw options of the glasses' builtin 20 px font
|
|
220
|
+
* chain (mode 15): lineHeight 27 / baseline 22 in firmware 2.2.6.10.
|
|
221
|
+
* 0x1F = top color 15 (white) + bit 4 (source color 0 transparent).
|
|
222
|
+
*/
|
|
223
|
+
const val BUILTIN_LINE_HEIGHT = 27
|
|
224
|
+
const val BUILTIN_TEXT_OPTIONS = 0x1F
|
|
225
|
+
private const val DIRTY_ROW_MERGE_GAP = 8
|
|
226
|
+
private const val FULL_FRAME_COVERAGE_PERCENT = 60
|
|
227
|
+
private const val MIN_SCROLL_BAND_ROWS = 16
|
|
228
|
+
private const val SCROLL_SAVINGS_PERCENT = 30
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Rasterize + plan + encode one incremental scene update. Mirrors the iOS
|
|
232
|
+
* renderer: see G2CfwRenderer.swift `encodeUpdate` for the contract.
|
|
233
|
+
*/
|
|
234
|
+
fun encodeUpdate(
|
|
235
|
+
frame: SceneFrame,
|
|
236
|
+
boxes: LensBoxes,
|
|
237
|
+
previousRaster: ByteArray?,
|
|
238
|
+
forcedRects: List<CanvasRect>,
|
|
239
|
+
fids: List<Int>,
|
|
240
|
+
textDraws: List<TextDraw>,
|
|
241
|
+
allowRectCopy: Boolean,
|
|
242
|
+
blankBoxes: LensBoxes?,
|
|
243
|
+
omitBuiltinText: Boolean,
|
|
244
|
+
textureGlyphsEnabled: Boolean = false,
|
|
245
|
+
textureImagesEnabled: Boolean = false,
|
|
246
|
+
cacheGeneration: Int = 0,
|
|
247
|
+
): SceneUpdate? {
|
|
248
|
+
if (fids.size < REQUIRED_FID_COUNT) return null
|
|
249
|
+
val width = boxes.width
|
|
250
|
+
val height = boxes.height
|
|
251
|
+
// Texture draws (modes 13/14) land at absolute panel coordinates with
|
|
252
|
+
// no per-lens variant — only a lens-equal window may use them.
|
|
253
|
+
val lensEqual = boxes.leftX == boxes.rightX && boxes.leftY == boxes.rightY
|
|
254
|
+
val useTexGlyphs = textureGlyphsEnabled && lensEqual
|
|
255
|
+
val useTexImages = textureImagesEnabled && lensEqual
|
|
256
|
+
// A full cache resets between frames (never mid-frame: draws already
|
|
257
|
+
// planned this frame reference the old offsets). Bumping past the
|
|
258
|
+
// caller's generation keeps the caller's own bumps monotonic.
|
|
259
|
+
if ((useTexGlyphs || useTexImages) && (textureCache.generation != cacheGeneration || textureCache.exhausted)) {
|
|
260
|
+
textureCache.reset(cacheGeneration)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
var grids: List<MonoGrid> = emptyList()
|
|
264
|
+
val raster: ByteArray
|
|
265
|
+
if (useTexGlyphs) {
|
|
266
|
+
val result = rasterizeGridExact(frame, width, height, omitBuiltinText) ?: return null
|
|
267
|
+
raster = quantizeToPanelLevels(result.first)
|
|
268
|
+
grids = result.second
|
|
269
|
+
} else {
|
|
270
|
+
raster = quantizeToPanelLevels(rasterize(frame, width, height, omitBuiltinText) ?: return null)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
var nextFid = 0
|
|
274
|
+
fun takeFid(): Int = fids[nextFid++]
|
|
275
|
+
|
|
276
|
+
val segments = mutableListOf<ByteArray>()
|
|
277
|
+
if (blankBoxes != null) {
|
|
278
|
+
solidStereoFrame(0, blankBoxes, takeFid())?.let { segments.add(it) }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
val plan =
|
|
282
|
+
if (previousRaster != null && previousRaster.size == raster.size && blankBoxes == null) {
|
|
283
|
+
diffPlan(previousRaster, raster, width, height, forcedRects, allowRectCopy)
|
|
284
|
+
} else {
|
|
285
|
+
DeltaPlan.FULL_FRAME
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (plan.isNoop) {
|
|
289
|
+
return SceneUpdate(
|
|
290
|
+
payload = null, uploads = textureCache.takeUploadPayloads(),
|
|
291
|
+
raster = raster, usedFullFrame = false,
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (plan.full) {
|
|
296
|
+
val full = stereoFrame(raster, boxes, takeFid()) ?: return null
|
|
297
|
+
segments.add(full)
|
|
298
|
+
} else {
|
|
299
|
+
plan.copy?.let { segments.add(rectCopyPayload(it, width, boxes)) }
|
|
300
|
+
var deltaRects = plan.rects
|
|
301
|
+
var drawSegments: List<ByteArray> = emptyList()
|
|
302
|
+
if (useTexGlyphs || useTexImages) {
|
|
303
|
+
// Reserve one fid for the bundle-overflow fallback below.
|
|
304
|
+
val fidBudget = REQUIRED_FID_COUNT - nextFid - 1
|
|
305
|
+
val expressed =
|
|
306
|
+
expressTextureRects(
|
|
307
|
+
plan.rects,
|
|
308
|
+
grids = if (useTexGlyphs) grids else emptyList(),
|
|
309
|
+
frame = frame, boxes = boxes, raster = raster,
|
|
310
|
+
width = width, height = height,
|
|
311
|
+
allowImages = useTexImages, fidBudget = fidBudget,
|
|
312
|
+
)
|
|
313
|
+
deltaRects = expressed.first
|
|
314
|
+
drawSegments = expressed.second
|
|
315
|
+
}
|
|
316
|
+
for (rect in deltaRects) {
|
|
317
|
+
val delta = boxDeltaFrame(raster, width, rect, boxes, takeFid()) ?: return null
|
|
318
|
+
segments.add(delta)
|
|
319
|
+
}
|
|
320
|
+
segments.addAll(drawSegments)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
for (draw in textDraws) {
|
|
324
|
+
builtinStringPayload(draw.x, draw.y, BUILTIN_TEXT_OPTIONS, draw.text)?.let { segments.add(it) }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
val uploads = textureCache.takeUploadPayloads()
|
|
328
|
+
if (segments.size == 1) return SceneUpdate(segments[0], uploads, raster, plan.full)
|
|
329
|
+
bundle(segments)?.let { return SceneUpdate(it, uploads, raster, plan.full) }
|
|
330
|
+
// A segment overflowed the bundle's 64 KiB cap (a near-incompressible
|
|
331
|
+
// full canvas). Fall back to one bare full frame with any builtin text
|
|
332
|
+
// baked into the bitmap so nothing is lost. The uploads still go out —
|
|
333
|
+
// residency was already recorded in the model.
|
|
334
|
+
val flat = rasterize(frame, width, height, omitBuiltinText = false) ?: return null
|
|
335
|
+
val full = stereoFrame(flat, boxes, takeFid()) ?: return null
|
|
336
|
+
return SceneUpdate(full, uploads, flat, usedFullFrame = true)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Blit `font: "mono"` elements as opaque glyph grids: the element's box
|
|
341
|
+
* is cleared to black and every non-space cell blits the SAME full-cell
|
|
342
|
+
* raster mode-14 draws replay, so an on-glasses cached draw reproduces
|
|
343
|
+
* the raster exactly. (Opaque cells are also what makes cached draws
|
|
344
|
+
* self-erasing.) Mono grids paint after all other elements — an exotic
|
|
345
|
+
* frame stacking a non-mono element ON TOP of a mono grid would z-order
|
|
346
|
+
* differently here, which no current miniapp does.
|
|
347
|
+
*/
|
|
348
|
+
private fun rasterizeGridExact(
|
|
349
|
+
frame: SceneFrame,
|
|
350
|
+
width: Int,
|
|
351
|
+
height: Int,
|
|
352
|
+
omitBuiltinText: Boolean,
|
|
353
|
+
): Pair<ByteArray, List<MonoGrid>>? {
|
|
354
|
+
val raster =
|
|
355
|
+
rasterize(frame, width, height, omitBuiltinText, omitMonoText = true) ?: return null
|
|
356
|
+
|
|
357
|
+
val grids = mutableListOf<MonoGrid>()
|
|
358
|
+
for ((z, element) in frame.elements.withIndex()) {
|
|
359
|
+
if (element.type != "text" || element.font != "mono") continue
|
|
360
|
+
val bx = element.x
|
|
361
|
+
val by = element.y
|
|
362
|
+
val clipX0 = maxOf(0, bx)
|
|
363
|
+
val clipY0 = maxOf(0, by)
|
|
364
|
+
val clipX1 = minOf(width, bx + element.w)
|
|
365
|
+
val clipY1 = minOf(height, by + element.h)
|
|
366
|
+
if (clipX0 >= clipX1 || clipY0 >= clipY1) continue
|
|
367
|
+
val (cellWidth, lineHeight) = monoCellMetrics(element.fontSize)
|
|
368
|
+
val originX = bx + 2
|
|
369
|
+
val originY = by + 2
|
|
370
|
+
for (y in clipY0 until clipY1) {
|
|
371
|
+
raster.fill(0, y * width + clipX0, y * width + clipX1)
|
|
372
|
+
}
|
|
373
|
+
val lines = (element.text ?: "").split('\n').map { codePoints(it) }
|
|
374
|
+
val blitCols = maxOf(0, (clipX1 - originX + cellWidth - 1) / cellWidth)
|
|
375
|
+
val blitRows = maxOf(0, (clipY1 - originY + lineHeight - 1) / lineHeight)
|
|
376
|
+
for (row in 0 until minOf(blitRows, lines.size)) {
|
|
377
|
+
val line = lines[row]
|
|
378
|
+
for (col in 0 until minOf(blitCols, line.size)) {
|
|
379
|
+
if (line[col] == 32) continue
|
|
380
|
+
val glyph =
|
|
381
|
+
memoizedGlyphRaster(line[col], element.fontSize, cellWidth, lineHeight)
|
|
382
|
+
?: continue
|
|
383
|
+
blitCell(
|
|
384
|
+
glyph, raster, width,
|
|
385
|
+
atX = originX + col * cellWidth, atY = originY + row * lineHeight,
|
|
386
|
+
cellWidth = cellWidth, cellHeight = lineHeight,
|
|
387
|
+
clipX0 = clipX0, clipY0 = clipY0, clipX1 = clipX1, clipY1 = clipY1,
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
grids.add(
|
|
392
|
+
MonoGrid(
|
|
393
|
+
z = z, fontSize = element.fontSize, originX = originX, originY = originY,
|
|
394
|
+
cellWidth = cellWidth, lineHeight = lineHeight,
|
|
395
|
+
fullCols = maxOf(0, (clipX1 - originX) / cellWidth),
|
|
396
|
+
fullRows = maxOf(0, (clipY1 - originY) / lineHeight),
|
|
397
|
+
rows = lines,
|
|
398
|
+
)
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
return raster to grids
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private fun blitCell(
|
|
405
|
+
glyph: ByteArray,
|
|
406
|
+
raster: ByteArray,
|
|
407
|
+
rasterWidth: Int,
|
|
408
|
+
atX: Int,
|
|
409
|
+
atY: Int,
|
|
410
|
+
cellWidth: Int,
|
|
411
|
+
cellHeight: Int,
|
|
412
|
+
clipX0: Int,
|
|
413
|
+
clipY0: Int,
|
|
414
|
+
clipX1: Int,
|
|
415
|
+
clipY1: Int,
|
|
416
|
+
) {
|
|
417
|
+
val x0 = maxOf(atX, clipX0)
|
|
418
|
+
val y0 = maxOf(atY, clipY0)
|
|
419
|
+
val x1 = minOf(atX + cellWidth, clipX1)
|
|
420
|
+
val y1 = minOf(atY + cellHeight, clipY1)
|
|
421
|
+
if (x0 >= x1 || y0 >= y1) return
|
|
422
|
+
for (y in y0 until y1) {
|
|
423
|
+
System.arraycopy(
|
|
424
|
+
glyph, (y - atY) * cellWidth + (x0 - atX),
|
|
425
|
+
raster, y * rasterWidth + x0,
|
|
426
|
+
x1 - x0,
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Express-or-bake: each dirty rect either becomes cached draws (mode-14
|
|
433
|
+
* glyph runs from the topmost covering mono grid, or one mode-13 draw of
|
|
434
|
+
* an isolated repeated image) and drops its bitmap delta, or stays a
|
|
435
|
+
* plain delta untouched. All-or-nothing per rect keeps the shadow model
|
|
436
|
+
* trivially exact — no partial punch-out. Non-ASCII cells (mode 14 is
|
|
437
|
+
* 32..126 only) become small residual bitmap deltas within an otherwise
|
|
438
|
+
* expressed rect.
|
|
439
|
+
*/
|
|
440
|
+
private fun expressTextureRects(
|
|
441
|
+
rects: List<CanvasRect>,
|
|
442
|
+
grids: List<MonoGrid>,
|
|
443
|
+
frame: SceneFrame,
|
|
444
|
+
boxes: LensBoxes,
|
|
445
|
+
raster: ByteArray,
|
|
446
|
+
width: Int,
|
|
447
|
+
height: Int,
|
|
448
|
+
allowImages: Boolean,
|
|
449
|
+
fidBudget: Int,
|
|
450
|
+
): Pair<List<CanvasRect>, List<ByteArray>> {
|
|
451
|
+
class ElementBox(val z: Int, val x0: Int, val y0: Int, val x1: Int, val y1: Int) {
|
|
452
|
+
fun intersects(r: CanvasRect): Boolean =
|
|
453
|
+
r.x < x1 && r.x + r.w > x0 && r.y < y1 && r.y + r.h > y0
|
|
454
|
+
}
|
|
455
|
+
val elementBoxes =
|
|
456
|
+
frame.elements.mapIndexed { z, el ->
|
|
457
|
+
ElementBox(
|
|
458
|
+
z = z, x0 = maxOf(0, el.x), y0 = maxOf(0, el.y),
|
|
459
|
+
x1 = minOf(width, el.x + el.w), y1 = minOf(height, el.y + el.h),
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Isolated image candidates: nothing else overlaps their box, so the
|
|
464
|
+
// frame raster inside the box IS the standalone render and an opaque
|
|
465
|
+
// full-box cached draw reproduces it. Sightings are recorded every
|
|
466
|
+
// frame; only content seen before enters the cache (streaming content
|
|
467
|
+
// like a live minimap never repeats a hash).
|
|
468
|
+
class ImageCandidate(val box: ElementBox, val key: String, val seen: Boolean)
|
|
469
|
+
val imageCandidates = mutableMapOf<Int, ImageCandidate>()
|
|
470
|
+
if (allowImages) {
|
|
471
|
+
for ((z, el) in frame.elements.withIndex()) {
|
|
472
|
+
if (el.type != "image") continue
|
|
473
|
+
val box = elementBoxes[z]
|
|
474
|
+
val w = box.x1 - box.x0
|
|
475
|
+
val h = box.y1 - box.y0
|
|
476
|
+
if (w <= 0 || h <= 0 || w > 255 || h > 255 || el.contentHash.isEmpty()) continue
|
|
477
|
+
val isolated =
|
|
478
|
+
elementBoxes.none { other ->
|
|
479
|
+
other.z != z && other.x0 < box.x1 && other.x1 > box.x0 &&
|
|
480
|
+
other.y0 < box.y1 && other.y1 > box.y0
|
|
481
|
+
}
|
|
482
|
+
if (!isolated) continue
|
|
483
|
+
val key = "${el.contentHash}@${w}x$h"
|
|
484
|
+
imageCandidates[z] = ImageCandidate(box, key, textureCache.noteImageSighting(key))
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
val deltas = mutableListOf<CanvasRect>()
|
|
489
|
+
val imageDraws = sortedMapOf<Int, ByteArray>()
|
|
490
|
+
val runPayloads = mutableListOf<ByteArray>()
|
|
491
|
+
var runCount = 0
|
|
492
|
+
|
|
493
|
+
rectLoop@ for ((index, rect) in rects.withIndex()) {
|
|
494
|
+
val remaining = rects.size - index - 1
|
|
495
|
+
|
|
496
|
+
// Glyph express: topmost mono grid fully covering the rect, with
|
|
497
|
+
// nothing stacked above it in that region.
|
|
498
|
+
val grid = grids.filter { it.covers(rect) }.maxByOrNull { it.z }
|
|
499
|
+
if (grid != null &&
|
|
500
|
+
elementBoxes.none { it.z > grid.z && it.intersects(rect) }
|
|
501
|
+
) {
|
|
502
|
+
val font = textureCache.font(grid.fontSize, grid.cellWidth, grid.lineHeight)
|
|
503
|
+
if (font != null) {
|
|
504
|
+
val r0 = (rect.y - grid.originY) / grid.lineHeight
|
|
505
|
+
val r1 = (rect.y + rect.h - 1 - grid.originY) / grid.lineHeight
|
|
506
|
+
val c0 = (rect.x - grid.originX) / grid.cellWidth
|
|
507
|
+
val c1 = (rect.x + rect.w - 1 - grid.originX) / grid.cellWidth
|
|
508
|
+
val newRuns = mutableListOf<Triple<Int, Int, ByteArray>>() // x, y, chars
|
|
509
|
+
val residuals = mutableListOf<CanvasRect>()
|
|
510
|
+
var expressible = true
|
|
511
|
+
rowLoop@ for (row in r0..r1) {
|
|
512
|
+
val rowY = grid.originY + row * grid.lineHeight
|
|
513
|
+
var runChars = mutableListOf<Byte>()
|
|
514
|
+
var runStartCol = c0
|
|
515
|
+
var residualStart: Int? = null
|
|
516
|
+
fun flushRun() {
|
|
517
|
+
if (runChars.isEmpty()) return
|
|
518
|
+
newRuns.add(
|
|
519
|
+
Triple(
|
|
520
|
+
grid.originX + runStartCol * grid.cellWidth, rowY,
|
|
521
|
+
runChars.toByteArray(),
|
|
522
|
+
)
|
|
523
|
+
)
|
|
524
|
+
runChars = mutableListOf()
|
|
525
|
+
}
|
|
526
|
+
fun flushResidual(endCol: Int) {
|
|
527
|
+
val start = residualStart ?: return
|
|
528
|
+
val x0 = maxOf(0, (grid.originX + start * grid.cellWidth) and 3.inv())
|
|
529
|
+
val x1 = minOf(width, (grid.originX + endCol * grid.cellWidth + 3) and 3.inv())
|
|
530
|
+
val y0 = maxOf(0, rowY and 1.inv())
|
|
531
|
+
val y1 = minOf(height, (rowY + grid.lineHeight + 1) and 1.inv())
|
|
532
|
+
residuals.add(CanvasRect(x0, y0, x1 - x0, y1 - y0))
|
|
533
|
+
residualStart = null
|
|
534
|
+
}
|
|
535
|
+
for (col in c0..c1) {
|
|
536
|
+
val scalar = grid.scalar(row, col)
|
|
537
|
+
val resident =
|
|
538
|
+
scalar in 32..126 &&
|
|
539
|
+
textureCache.ensureGlyph(font, scalar) {
|
|
540
|
+
memoizedGlyphRaster(
|
|
541
|
+
scalar, grid.fontSize, grid.cellWidth, grid.lineHeight
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
if (resident) {
|
|
545
|
+
flushResidual(col)
|
|
546
|
+
if (runChars.isEmpty()) runStartCol = col
|
|
547
|
+
runChars.add(scalar.toByte())
|
|
548
|
+
if (runChars.size == 255) flushRun()
|
|
549
|
+
} else {
|
|
550
|
+
flushRun()
|
|
551
|
+
if (residualStart == null) residualStart = col
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
flushRun()
|
|
555
|
+
flushResidual(c1 + 1)
|
|
556
|
+
if (runCount + newRuns.size > MAX_GLYPH_RUNS) {
|
|
557
|
+
expressible = false
|
|
558
|
+
break@rowLoop
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// Residual deltas + every remaining rect must still fit the
|
|
562
|
+
// fid budget; otherwise keep this rect as one plain delta.
|
|
563
|
+
if (expressible &&
|
|
564
|
+
deltas.size + residuals.size + remaining + 1 <= fidBudget &&
|
|
565
|
+
residuals.size <= 3
|
|
566
|
+
) {
|
|
567
|
+
var payloads = mutableListOf<ByteArray>()
|
|
568
|
+
for ((runX, runY, chars) in newRuns) {
|
|
569
|
+
val payload =
|
|
570
|
+
G2CfwTextureCache.stringRunPayload(
|
|
571
|
+
fontTableOffset = font.tableOffset,
|
|
572
|
+
x = boxes.leftX + runX, y = boxes.leftY + runY,
|
|
573
|
+
options = G2CfwTextureCache.OPAQUE_DRAW_OPTIONS, chars = chars,
|
|
574
|
+
)
|
|
575
|
+
if (payload == null) {
|
|
576
|
+
payloads = mutableListOf()
|
|
577
|
+
break
|
|
578
|
+
}
|
|
579
|
+
payloads.add(payload)
|
|
580
|
+
}
|
|
581
|
+
if (payloads.isNotEmpty() || newRuns.isEmpty()) {
|
|
582
|
+
runPayloads.addAll(payloads)
|
|
583
|
+
runCount += payloads.size
|
|
584
|
+
deltas.addAll(residuals)
|
|
585
|
+
continue@rectLoop
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Image express: the rect sits wholly inside an isolated, repeated
|
|
592
|
+
// image's box — one opaque full-box draw covers it (and any other
|
|
593
|
+
// rect in the same box).
|
|
594
|
+
if (allowImages) {
|
|
595
|
+
val entry =
|
|
596
|
+
imageCandidates.entries.firstOrNull { (_, c) ->
|
|
597
|
+
c.seen && rect.x >= c.box.x0 && rect.y >= c.box.y0 &&
|
|
598
|
+
rect.x + rect.w <= c.box.x1 && rect.y + rect.h <= c.box.y1
|
|
599
|
+
}
|
|
600
|
+
if (entry != null) {
|
|
601
|
+
val (z, candidate) = entry
|
|
602
|
+
if (imageDraws.containsKey(z)) continue@rectLoop
|
|
603
|
+
val w = candidate.box.x1 - candidate.box.x0
|
|
604
|
+
val h = candidate.box.y1 - candidate.box.y0
|
|
605
|
+
val gray = ByteArray(w * h)
|
|
606
|
+
for (y in candidate.box.y0 until candidate.box.y1) {
|
|
607
|
+
System.arraycopy(
|
|
608
|
+
raster, y * width + candidate.box.x0,
|
|
609
|
+
gray, (y - candidate.box.y0) * w, w,
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
val offset = textureCache.ensureImage(candidate.key, gray, w, h)
|
|
613
|
+
val draw =
|
|
614
|
+
offset?.let {
|
|
615
|
+
G2CfwTextureCache.imageDrawPayload(
|
|
616
|
+
offset = it,
|
|
617
|
+
x = boxes.leftX + candidate.box.x0,
|
|
618
|
+
y = boxes.leftY + candidate.box.y0,
|
|
619
|
+
options = G2CfwTextureCache.OPAQUE_DRAW_OPTIONS,
|
|
620
|
+
)
|
|
621
|
+
}
|
|
622
|
+
if (draw != null) {
|
|
623
|
+
imageDraws[z] = draw
|
|
624
|
+
continue@rectLoop
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
deltas.add(rect)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return deltas to (imageDraws.values.toList() + runPayloads)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Diff two same-size canvas rasters into a scroll copy + aligned dirty
|
|
637
|
+
* rects, or a full-frame decision when the change is too large.
|
|
638
|
+
*/
|
|
639
|
+
fun diffPlan(
|
|
640
|
+
previous: ByteArray,
|
|
641
|
+
next: ByteArray,
|
|
642
|
+
width: Int,
|
|
643
|
+
height: Int,
|
|
644
|
+
forced: List<CanvasRect>,
|
|
645
|
+
allowCopy: Boolean,
|
|
646
|
+
): DeltaPlan {
|
|
647
|
+
if (previous.size != width * height || next.size != width * height || width <= 0 || height <= 0) {
|
|
648
|
+
return DeltaPlan.FULL_FRAME
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
fun rowsEqual(a: ByteArray, aRow: Int, b: ByteArray, bRow: Int): Boolean {
|
|
652
|
+
val aOff = aRow * width
|
|
653
|
+
val bOff = bRow * width
|
|
654
|
+
for (x in 0 until width) {
|
|
655
|
+
if (a[aOff + x] != b[bOff + x]) return false
|
|
656
|
+
}
|
|
657
|
+
return true
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
val changed = BooleanArray(height) { y -> !rowsEqual(previous, y, next, y) }
|
|
661
|
+
var anyChanged = changed.any { it }
|
|
662
|
+
if (!anyChanged && forced.isEmpty()) return DeltaPlan.NOOP
|
|
663
|
+
|
|
664
|
+
// Scroll detection: a vertical band of new rows that byte-match
|
|
665
|
+
// previous rows shifted by dy. Row hashes score candidate shifts;
|
|
666
|
+
// the winning band is byte-verified before use.
|
|
667
|
+
var copy: RowCopy? = null
|
|
668
|
+
var predicted: ByteArray? = null
|
|
669
|
+
if (allowCopy && anyChanged) {
|
|
670
|
+
val prevHash = LongArray(height) { y -> rowHash(previous, y * width, width) }
|
|
671
|
+
val newHash = LongArray(height) { y -> rowHash(next, y * width, width) }
|
|
672
|
+
val changedCount = changed.count { it }
|
|
673
|
+
var bestDy = 0
|
|
674
|
+
var bestScore = 0
|
|
675
|
+
val maxShift = height - MIN_SCROLL_BAND_ROWS
|
|
676
|
+
if (maxShift > 0) {
|
|
677
|
+
for (dy in -maxShift..maxShift) {
|
|
678
|
+
if (dy == 0) continue
|
|
679
|
+
var score = 0
|
|
680
|
+
for (y in maxOf(0, -dy) until minOf(height, height - dy)) {
|
|
681
|
+
if (changed[y] && newHash[y] == prevHash[y + dy]) score += 1
|
|
682
|
+
}
|
|
683
|
+
if (score > bestScore) {
|
|
684
|
+
bestScore = score
|
|
685
|
+
bestDy = dy
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (bestDy != 0 && bestScore >= MIN_SCROLL_BAND_ROWS &&
|
|
690
|
+
bestScore * 100 >= changedCount * SCROLL_SAVINGS_PERCENT
|
|
691
|
+
) {
|
|
692
|
+
var runStart = -1
|
|
693
|
+
var bandStart = 0
|
|
694
|
+
var bandLen = 0
|
|
695
|
+
val lo = maxOf(0, -bestDy)
|
|
696
|
+
val hi = minOf(height, height - bestDy)
|
|
697
|
+
for (y in lo..hi) {
|
|
698
|
+
val matches = y < hi && newHash[y] == prevHash[y + bestDy]
|
|
699
|
+
if (matches) {
|
|
700
|
+
if (runStart < 0) runStart = y
|
|
701
|
+
} else if (runStart >= 0) {
|
|
702
|
+
if (y - runStart > bandLen) {
|
|
703
|
+
bandStart = runStart
|
|
704
|
+
bandLen = y - runStart
|
|
705
|
+
}
|
|
706
|
+
runStart = -1
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (bandLen >= MIN_SCROLL_BAND_ROWS) {
|
|
710
|
+
var verified = true
|
|
711
|
+
for (y in bandStart until bandStart + bandLen) {
|
|
712
|
+
if (!rowsEqual(next, y, previous, y + bestDy)) {
|
|
713
|
+
verified = false
|
|
714
|
+
break
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (verified) {
|
|
718
|
+
copy = RowCopy(srcY = bandStart + bestDy, dstY = bandStart, height = bandLen)
|
|
719
|
+
predicted = previous.copyOf().also {
|
|
720
|
+
System.arraycopy(
|
|
721
|
+
previous, (bandStart + bestDy) * width, it, bandStart * width, bandLen * width
|
|
722
|
+
)
|
|
723
|
+
}
|
|
724
|
+
for (y in bandStart until bandStart + bandLen) changed[y] = false
|
|
725
|
+
anyChanged = changed.any { it }
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Per-row dirty column ranges against the (possibly copied) base.
|
|
732
|
+
val base = predicted ?: previous
|
|
733
|
+
val colLo = IntArray(height) { Int.MAX_VALUE }
|
|
734
|
+
val colHi = IntArray(height) { -1 }
|
|
735
|
+
for (y in 0 until height) {
|
|
736
|
+
if (!changed[y]) continue
|
|
737
|
+
var lo = 0
|
|
738
|
+
while (lo < width && base[y * width + lo] == next[y * width + lo]) lo += 1
|
|
739
|
+
var hi = width - 1
|
|
740
|
+
while (hi > lo && base[y * width + hi] == next[y * width + hi]) hi -= 1
|
|
741
|
+
colLo[y] = lo
|
|
742
|
+
colHi[y] = hi
|
|
743
|
+
}
|
|
744
|
+
val dirtyRows = changed.copyOf()
|
|
745
|
+
for (rect in forced) {
|
|
746
|
+
val y0 = maxOf(0, rect.y)
|
|
747
|
+
val y1 = minOf(height, rect.y + rect.h)
|
|
748
|
+
if (y0 >= y1 || rect.w <= 0) continue
|
|
749
|
+
for (y in y0 until y1) {
|
|
750
|
+
dirtyRows[y] = true
|
|
751
|
+
colLo[y] = minOf(colLo[y], maxOf(0, rect.x))
|
|
752
|
+
colHi[y] = maxOf(colHi[y], minOf(width - 1, rect.x + rect.w - 1))
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (!dirtyRows.any { it }) {
|
|
756
|
+
return if (copy != null) DeltaPlan(full = false, copy = copy, rects = emptyList()) else DeltaPlan.NOOP
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Group dirty rows into bands (small gaps merge), aligned to the grid.
|
|
760
|
+
var rects = mutableListOf<CanvasRect>()
|
|
761
|
+
var y = 0
|
|
762
|
+
while (y < height) {
|
|
763
|
+
if (!dirtyRows[y]) {
|
|
764
|
+
y += 1
|
|
765
|
+
continue
|
|
766
|
+
}
|
|
767
|
+
var end = y
|
|
768
|
+
var gap = 0
|
|
769
|
+
var scan = y
|
|
770
|
+
while (scan < height && gap <= DIRTY_ROW_MERGE_GAP) {
|
|
771
|
+
if (dirtyRows[scan]) {
|
|
772
|
+
end = scan
|
|
773
|
+
gap = 0
|
|
774
|
+
} else {
|
|
775
|
+
gap += 1
|
|
776
|
+
}
|
|
777
|
+
scan += 1
|
|
778
|
+
}
|
|
779
|
+
var lo = Int.MAX_VALUE
|
|
780
|
+
var hi = -1
|
|
781
|
+
for (row in y..end) {
|
|
782
|
+
if (!dirtyRows[row]) continue
|
|
783
|
+
lo = minOf(lo, colLo[row])
|
|
784
|
+
hi = maxOf(hi, colHi[row])
|
|
785
|
+
}
|
|
786
|
+
val x0 = maxOf(0, lo and 3.inv())
|
|
787
|
+
val x1 = minOf(width, (hi + 4) and 3.inv())
|
|
788
|
+
val ry0 = maxOf(0, y and 1.inv())
|
|
789
|
+
val ry1 = minOf(height, (end + 2) and 1.inv())
|
|
790
|
+
rects.add(CanvasRect(x0, ry0, x1 - x0, ry1 - ry0))
|
|
791
|
+
y = end + 1
|
|
792
|
+
}
|
|
793
|
+
if (rects.size > MAX_DIRTY_RECTS) {
|
|
794
|
+
val x0 = rects.minOf { it.x }
|
|
795
|
+
val y0 = rects.minOf { it.y }
|
|
796
|
+
val x1 = rects.maxOf { it.x + it.w }
|
|
797
|
+
val y1 = rects.maxOf { it.y + it.h }
|
|
798
|
+
rects = mutableListOf(CanvasRect(x0, y0, x1 - x0, y1 - y0))
|
|
799
|
+
}
|
|
800
|
+
val coverage = rects.sumOf { it.w * it.h }
|
|
801
|
+
if (copy == null && coverage * 100 >= width * height * FULL_FRAME_COVERAGE_PERCENT) {
|
|
802
|
+
return DeltaPlan.FULL_FRAME
|
|
803
|
+
}
|
|
804
|
+
return DeltaPlan(full = false, copy = copy, rects = rects)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
private fun rowHash(pixels: ByteArray, offset: Int, count: Int): Long {
|
|
808
|
+
// FNV-1a 64 — collision-safe enough for scoring; bands are byte-verified.
|
|
809
|
+
var hash = -0x340d631b7bdddcdbL
|
|
810
|
+
for (i in offset until offset + count) {
|
|
811
|
+
hash = (hash xor (pixels[i].toLong() and 0xFF)) * 0x100000001b3L
|
|
812
|
+
}
|
|
813
|
+
return hash
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Stereo mode-3 delta of one canvas sub-rect, drawn at each lens's window
|
|
818
|
+
* origin plus the rect offset. Rect must be grid-aligned and in bounds.
|
|
819
|
+
*/
|
|
820
|
+
fun boxDeltaFrame(
|
|
821
|
+
raster: ByteArray,
|
|
822
|
+
canvasWidth: Int,
|
|
823
|
+
rect: CanvasRect,
|
|
824
|
+
boxes: LensBoxes,
|
|
825
|
+
frameId: Int,
|
|
826
|
+
): ByteArray? {
|
|
827
|
+
if (rect.w <= 0 || rect.h <= 0 || rect.x % 4 != 0 || rect.w % 4 != 0 ||
|
|
828
|
+
rect.y % 2 != 0 || rect.h % 2 != 0 || rect.x + rect.w > canvasWidth ||
|
|
829
|
+
(rect.y + rect.h) * canvasWidth > raster.size
|
|
830
|
+
) {
|
|
831
|
+
return null
|
|
832
|
+
}
|
|
833
|
+
val pixels = ByteArray(rect.w * rect.h)
|
|
834
|
+
for (y in 0 until rect.h) {
|
|
835
|
+
System.arraycopy(raster, (rect.y + y) * canvasWidth + rect.x, pixels, y * rect.w, rect.w)
|
|
836
|
+
}
|
|
837
|
+
return stereoRleFrame(
|
|
838
|
+
rleEncode(pack4Bpp(pixels)),
|
|
839
|
+
lx = boxes.leftX + rect.x, ly = boxes.leftY + rect.y,
|
|
840
|
+
rx = boxes.rightX + rect.x, ry = boxes.rightY + rect.y,
|
|
841
|
+
w = rect.w, h = rect.h, frameId = frameId,
|
|
842
|
+
)
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Mode-9 rect copy with the lenses-differ flag: a full-width canvas band
|
|
847
|
+
* moves srcY→dstY inside each lens's window.
|
|
848
|
+
* `[0x89][L src LTWH dst LTWH][R src LTWH dst LTWH]`, u16 LE each.
|
|
849
|
+
*/
|
|
850
|
+
fun rectCopyPayload(copy: RowCopy, width: Int, boxes: LensBoxes): ByteArray {
|
|
851
|
+
val output = ByteArrayOutputStream()
|
|
852
|
+
output.write(0x89)
|
|
853
|
+
fun writeRect(x: Int, y: Int) {
|
|
854
|
+
for (value in intArrayOf(x, y, width, copy.height)) {
|
|
855
|
+
output.write(value and 0xFF)
|
|
856
|
+
output.write((value ushr 8) and 0xFF)
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
writeRect(boxes.leftX, boxes.leftY + copy.srcY)
|
|
860
|
+
writeRect(boxes.leftX, boxes.leftY + copy.dstY)
|
|
861
|
+
writeRect(boxes.rightX, boxes.rightY + copy.srcY)
|
|
862
|
+
writeRect(boxes.rightX, boxes.rightY + copy.dstY)
|
|
863
|
+
return output.toByteArray()
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Mode-15 builtin-font string draw:
|
|
868
|
+
* `[15][x:u16][y:u16][options:u8][strlen:u8][UTF-8]`. Bytes 1–31 are
|
|
869
|
+
* firmware x-adjust controls, so control characters are stripped here;
|
|
870
|
+
* the text is truncated to 255 UTF-8 bytes at a codepoint boundary.
|
|
871
|
+
*/
|
|
872
|
+
fun builtinStringPayload(x: Int, y: Int, options: Int, text: String): ByteArray? {
|
|
873
|
+
if (x !in 0..0xFFFF || y !in 0..0xFFFF) return null
|
|
874
|
+
val sanitized = text.filter { it.code >= 32 }
|
|
875
|
+
var bytes = sanitized.toByteArray(Charsets.UTF_8)
|
|
876
|
+
if (bytes.size > 255) {
|
|
877
|
+
var cut = 255
|
|
878
|
+
while (cut > 0 && (bytes[cut].toInt() and 0xC0) == 0x80) cut -= 1
|
|
879
|
+
bytes = bytes.copyOfRange(0, cut)
|
|
880
|
+
}
|
|
881
|
+
if (bytes.isEmpty()) return null
|
|
882
|
+
val output = ByteArrayOutputStream()
|
|
883
|
+
output.write(15)
|
|
884
|
+
output.write(x and 0xFF)
|
|
885
|
+
output.write((x ushr 8) and 0xFF)
|
|
886
|
+
output.write(y and 0xFF)
|
|
887
|
+
output.write((y ushr 8) and 0xFF)
|
|
888
|
+
output.write(options and 0xFF)
|
|
889
|
+
output.write(bytes.size)
|
|
890
|
+
output.write(bytes)
|
|
891
|
+
return output.toByteArray()
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* The firmware-drawn text lines of a frame, at absolute panel
|
|
896
|
+
* coordinates. Mode 15 has no per-lens variant, so callers must place the
|
|
897
|
+
* window at lens-equal offsets (depth level 1) whenever these are used.
|
|
898
|
+
* The +2 inset matches the rasterizer's text padding.
|
|
899
|
+
*/
|
|
900
|
+
fun builtinTextDraws(frame: SceneFrame, boxes: LensBoxes): List<TextDraw> {
|
|
901
|
+
val draws = mutableListOf<TextDraw>()
|
|
902
|
+
for (element in frame.elements) {
|
|
903
|
+
if (element.type != "text" || element.font != "builtin") continue
|
|
904
|
+
val text = element.text ?: continue
|
|
905
|
+
if (text.isEmpty()) continue
|
|
906
|
+
val baseX = boxes.leftX + element.x + 2
|
|
907
|
+
val baseY = boxes.leftY + element.y + 2
|
|
908
|
+
text.split('\n').forEachIndexed { index, line ->
|
|
909
|
+
val y = baseY + index * BUILTIN_LINE_HEIGHT
|
|
910
|
+
if (y >= 0 && y + BUILTIN_LINE_HEIGHT <= FRAME_HEIGHT && line.isNotEmpty()) {
|
|
911
|
+
draws.add(TextDraw(baseX, y, line))
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return draws
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Conservative canvas-space bounds of firmware-drawn text, used to force
|
|
920
|
+
* background re-pushes that erase glyphs the phone raster never
|
|
921
|
+
* contained. Width is overestimated (the phone has no advance tables).
|
|
922
|
+
*/
|
|
923
|
+
fun builtinTextBounds(
|
|
924
|
+
draws: List<TextDraw>,
|
|
925
|
+
boxes: LensBoxes,
|
|
926
|
+
canvasWidth: Int,
|
|
927
|
+
canvasHeight: Int,
|
|
928
|
+
): List<CanvasRect> =
|
|
929
|
+
draws.mapNotNull { draw ->
|
|
930
|
+
val x = maxOf(0, draw.x - boxes.leftX - 4)
|
|
931
|
+
val y = maxOf(0, draw.y - boxes.leftY - 2)
|
|
932
|
+
if (x >= canvasWidth || y >= canvasHeight) return@mapNotNull null
|
|
933
|
+
CanvasRect(
|
|
934
|
+
x = x,
|
|
935
|
+
y = y,
|
|
936
|
+
w = minOf(canvasWidth - x, draw.text.length * 13 + 30),
|
|
937
|
+
h = minOf(canvasHeight - y, BUILTIN_LINE_HEIGHT + 4),
|
|
938
|
+
)
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* `omitBuiltinText` skips `font == "builtin"` text elements — the CFW
|
|
943
|
+
* draws those with mode 15, so the bitmap must not also contain them.
|
|
944
|
+
* `omitMonoText` skips `font == "mono"` text elements — the grid-exact
|
|
945
|
+
* texture path blits those from the same glyph rasters it uploads.
|
|
946
|
+
*/
|
|
947
|
+
fun rasterize(
|
|
948
|
+
frame: SceneFrame,
|
|
949
|
+
width: Int,
|
|
950
|
+
height: Int,
|
|
951
|
+
omitBuiltinText: Boolean = false,
|
|
952
|
+
omitMonoText: Boolean = false,
|
|
953
|
+
): ByteArray? {
|
|
954
|
+
if (width <= 0 || height <= 0) return null
|
|
955
|
+
val decodedImages = mutableMapOf<Int, Bitmap>()
|
|
956
|
+
try {
|
|
957
|
+
for ((index, element) in frame.elements.withIndex()) {
|
|
958
|
+
if (element.type != "image") continue
|
|
959
|
+
val encoded = element.data ?: return null
|
|
960
|
+
val data =
|
|
961
|
+
try {
|
|
962
|
+
Base64.decode(encoded, Base64.DEFAULT)
|
|
963
|
+
} catch (_: IllegalArgumentException) {
|
|
964
|
+
return null
|
|
965
|
+
}
|
|
966
|
+
decodedImages[index] = BitmapFactory.decodeByteArray(data, 0, data.size) ?: return null
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
970
|
+
val canvas = Canvas(bitmap)
|
|
971
|
+
canvas.drawColor(Color.BLACK)
|
|
972
|
+
for ((index, element) in frame.elements.withIndex()) {
|
|
973
|
+
// Mode 15 draws only the glyphs, so an omitted builtin-text
|
|
974
|
+
// element's border must still come from the raster.
|
|
975
|
+
val skipBuiltinInk = omitBuiltinText && element.type == "text" && element.font == "builtin"
|
|
976
|
+
if (skipBuiltinInk && element.border <= 0) continue
|
|
977
|
+
if (omitMonoText && element.type == "text" && element.font == "mono") continue
|
|
978
|
+
val left = element.x.toFloat()
|
|
979
|
+
val top = element.y.toFloat()
|
|
980
|
+
val rect = RectF(left, top, left + element.w, top + element.h)
|
|
981
|
+
val save = canvas.save()
|
|
982
|
+
canvas.clipRect(rect)
|
|
983
|
+
when (element.type) {
|
|
984
|
+
"text" -> if (!skipBuiltinInk) drawText(canvas, element, rect)
|
|
985
|
+
"image" -> decodedImages[index]?.let { drawImage(canvas, it, rect) }
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
val border = maxOf(if (element.type == "rect") 1 else 0, element.border)
|
|
989
|
+
if (border > 0) {
|
|
990
|
+
val paint =
|
|
991
|
+
Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
992
|
+
color = Color.WHITE
|
|
993
|
+
style = Paint.Style.STROKE
|
|
994
|
+
strokeWidth = border.toFloat()
|
|
995
|
+
}
|
|
996
|
+
val inset = border / 2f
|
|
997
|
+
val borderRect = RectF(rect).apply { inset(inset, inset) }
|
|
998
|
+
val radius =
|
|
999
|
+
element.radius
|
|
1000
|
+
.coerceAtLeast(0)
|
|
1001
|
+
.toFloat()
|
|
1002
|
+
.coerceAtMost(minOf(borderRect.width(), borderRect.height()) / 2f)
|
|
1003
|
+
canvas.drawRoundRect(borderRect, radius, radius, paint)
|
|
1004
|
+
}
|
|
1005
|
+
canvas.restoreToCount(save)
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
val colors = IntArray(width * height)
|
|
1009
|
+
bitmap.getPixels(colors, 0, width, 0, 0, width, height)
|
|
1010
|
+
bitmap.recycle()
|
|
1011
|
+
return ByteArray(colors.size) { index ->
|
|
1012
|
+
val color = colors[index]
|
|
1013
|
+
((Color.red(color) * 299 + Color.green(color) * 587 + Color.blue(color) * 114) / 1000).toByte()
|
|
1014
|
+
}
|
|
1015
|
+
} finally {
|
|
1016
|
+
decodedImages.values.forEach { it.recycle() }
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
private fun drawText(canvas: Canvas, element: SceneElement, rect: RectF) {
|
|
1021
|
+
val text = element.text.orEmpty()
|
|
1022
|
+
if (text.isEmpty()) return
|
|
1023
|
+
val availableWidth = (rect.width() - 4f).toInt().coerceAtLeast(1)
|
|
1024
|
+
val save = canvas.save()
|
|
1025
|
+
canvas.translate(rect.left + 2f, rect.top + 2f)
|
|
1026
|
+
if (element.font == "mono") {
|
|
1027
|
+
val size = (if (element.fontSize > 0) element.fontSize else 20).toFloat()
|
|
1028
|
+
val paint =
|
|
1029
|
+
Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
1030
|
+
color = Color.WHITE
|
|
1031
|
+
textSize = size
|
|
1032
|
+
typeface = Typeface.MONOSPACE
|
|
1033
|
+
}
|
|
1034
|
+
val baseline = -paint.fontMetrics.ascent
|
|
1035
|
+
val pitch = monoLineHeight(size)
|
|
1036
|
+
text.split('\n').forEachIndexed { index, line ->
|
|
1037
|
+
canvas.drawText(line, 0f, baseline + index * pitch, paint)
|
|
1038
|
+
}
|
|
1039
|
+
} else {
|
|
1040
|
+
val paint =
|
|
1041
|
+
TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
1042
|
+
color = Color.WHITE
|
|
1043
|
+
textSize = 20f
|
|
1044
|
+
typeface = Typeface.DEFAULT
|
|
1045
|
+
}
|
|
1046
|
+
StaticLayout.Builder.obtain(text, 0, text.length, paint, availableWidth)
|
|
1047
|
+
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
|
|
1048
|
+
.setIncludePad(false)
|
|
1049
|
+
.build()
|
|
1050
|
+
.draw(canvas)
|
|
1051
|
+
}
|
|
1052
|
+
canvas.restoreToCount(save)
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
private fun drawImage(canvas: Canvas, bitmap: Bitmap, rect: RectF) {
|
|
1056
|
+
val scale = minOf(rect.width() / bitmap.width.coerceAtLeast(1), rect.height() / bitmap.height.coerceAtLeast(1))
|
|
1057
|
+
val targetWidth = bitmap.width * scale
|
|
1058
|
+
val targetHeight = bitmap.height * scale
|
|
1059
|
+
canvas.drawBitmap(
|
|
1060
|
+
bitmap,
|
|
1061
|
+
Rect(0, 0, bitmap.width, bitmap.height),
|
|
1062
|
+
RectF(
|
|
1063
|
+
rect.centerX() - targetWidth / 2f,
|
|
1064
|
+
rect.centerY() - targetHeight / 2f,
|
|
1065
|
+
rect.centerX() + targetWidth / 2f,
|
|
1066
|
+
rect.centerY() + targetHeight / 2f,
|
|
1067
|
+
),
|
|
1068
|
+
Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG),
|
|
1069
|
+
)
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
fun stereoFrame(pixels: ByteArray, boxes: LensBoxes, frameId: Int): ByteArray? {
|
|
1073
|
+
if (pixels.size != boxes.width * boxes.height) return null
|
|
1074
|
+
return stereoRleFrame(rleEncode(pack4Bpp(pixels)), boxes, frameId)
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
fun solidStereoFrame(color: Int, boxes: LensBoxes, frameId: Int): ByteArray? =
|
|
1078
|
+
stereoRleFrame(rleEncodeSolid(color and 0x0F, boxes.width * boxes.height), boxes, frameId)
|
|
1079
|
+
|
|
1080
|
+
private fun stereoRleFrame(rle: ByteArray, boxes: LensBoxes, frameId: Int): ByteArray? =
|
|
1081
|
+
stereoRleFrame(rle, boxes.leftX, boxes.leftY, boxes.rightX, boxes.rightY, boxes.width, boxes.height, frameId)
|
|
1082
|
+
|
|
1083
|
+
private fun stereoRleFrame(
|
|
1084
|
+
rle: ByteArray,
|
|
1085
|
+
lx: Int,
|
|
1086
|
+
ly: Int,
|
|
1087
|
+
rx: Int,
|
|
1088
|
+
ry: Int,
|
|
1089
|
+
w: Int,
|
|
1090
|
+
h: Int,
|
|
1091
|
+
frameId: Int,
|
|
1092
|
+
): ByteArray? {
|
|
1093
|
+
val compressed = zlib(rle) ?: return null
|
|
1094
|
+
return byteArrayOf(
|
|
1095
|
+
0x83.toByte(),
|
|
1096
|
+
(lx / 4).toByte(),
|
|
1097
|
+
(ly / 2).toByte(),
|
|
1098
|
+
(w / 4).toByte(),
|
|
1099
|
+
(h / 2).toByte(),
|
|
1100
|
+
(rx / 4).toByte(),
|
|
1101
|
+
(ry / 2).toByte(),
|
|
1102
|
+
(w / 4).toByte(),
|
|
1103
|
+
(h / 2).toByte(),
|
|
1104
|
+
(frameId and 0xFF).toByte(),
|
|
1105
|
+
((frameId ushr 8) and 0xFF).toByte(),
|
|
1106
|
+
) + compressed
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
fun bundle(segments: List<ByteArray>): ByteArray? {
|
|
1110
|
+
if (segments.isEmpty() || segments.size > 255 || segments.any { it.isEmpty() || it.size > 0xFFFF }) {
|
|
1111
|
+
return null
|
|
1112
|
+
}
|
|
1113
|
+
val output = ByteArrayOutputStream()
|
|
1114
|
+
output.write(8)
|
|
1115
|
+
output.write(segments.size)
|
|
1116
|
+
for (segment in segments) {
|
|
1117
|
+
output.write(segment.size and 0xFF)
|
|
1118
|
+
output.write((segment.size ushr 8) and 0xFF)
|
|
1119
|
+
output.write(segment)
|
|
1120
|
+
}
|
|
1121
|
+
return output.toByteArray()
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Mode-6 full-panel seed with the frame's own pixels composited at the
|
|
1126
|
+
* window position on black — seeds the shadow AND paints the frame in ONE
|
|
1127
|
+
* payload, so a resync shows content immediately instead of flashing a
|
|
1128
|
+
* black panel (the faceclaw reference client's resyncs are mode-6 content
|
|
1129
|
+
* frames and never flash). Mode 6 has no per-lens variant, so this
|
|
1130
|
+
* returns null unless the window is lens-equal — callers fall back to the
|
|
1131
|
+
* black seed there.
|
|
1132
|
+
*/
|
|
1133
|
+
fun seededFramePayload(pixels: ByteArray, boxes: LensBoxes): ByteArray? {
|
|
1134
|
+
if (boxes.leftX != boxes.rightX || boxes.leftY != boxes.rightY) return null
|
|
1135
|
+
if (pixels.size != boxes.width * boxes.height) return null
|
|
1136
|
+
val panel = ByteArray(FRAME_WIDTH * FRAME_HEIGHT)
|
|
1137
|
+
for (row in 0 until boxes.height) {
|
|
1138
|
+
System.arraycopy(
|
|
1139
|
+
pixels,
|
|
1140
|
+
row * boxes.width,
|
|
1141
|
+
panel,
|
|
1142
|
+
(boxes.leftY + row) * FRAME_WIDTH + boxes.leftX,
|
|
1143
|
+
boxes.width,
|
|
1144
|
+
)
|
|
1145
|
+
}
|
|
1146
|
+
return encodePixels(panel)
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
fun blackPayload(): ByteArray? {
|
|
1150
|
+
val compressed = zlib(rleEncodeSolid(0, FRAME_WIDTH * FRAME_HEIGHT)) ?: return null
|
|
1151
|
+
return byteArrayOf(6) + compressed
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
fun encodePixels(grayscale: ByteArray): ByteArray? {
|
|
1155
|
+
if (grayscale.size != FRAME_WIDTH * FRAME_HEIGHT) return null
|
|
1156
|
+
val compressed = zlib(rleEncode(pack4Bpp(grayscale))) ?: return null
|
|
1157
|
+
return byteArrayOf(6) + compressed
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Snap an 8bpp raster to the 16 panel levels (`grayNibble(v) * 16`) before
|
|
1162
|
+
* it enters the diff/shadow pipeline. The reference client diffs packed
|
|
1163
|
+
* 4bpp bytes — exactly what the panel shows; diffing raw 8bpp instead
|
|
1164
|
+
* marked rows dirty for sub-quantization AA noise the panel can never
|
|
1165
|
+
* display (spurious rects, inflated coverage, and a scroll-copy detector
|
|
1166
|
+
* that almost never matched). `grayNibble` round-trips `level * 16`
|
|
1167
|
+
* exactly, so packing a quantized raster is lossless.
|
|
1168
|
+
*/
|
|
1169
|
+
fun quantizeToPanelLevels(raster: ByteArray): ByteArray =
|
|
1170
|
+
ByteArray(raster.size) { index -> (grayNibble(raster[index].toInt()) * 16).toByte() }
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* 8-bit gray → 4-bit panel level, round-to-nearest (matches the reference
|
|
1174
|
+
* client's GRAY_TO_NIBBLE). 0 stays 0 so the shell's transparent color key
|
|
1175
|
+
* survives; plain truncation instead darkened the upper half of every
|
|
1176
|
+
* 16-value bucket by a full visible step on the 16-level panel.
|
|
1177
|
+
*/
|
|
1178
|
+
fun grayNibble(value: Int): Int {
|
|
1179
|
+
val v = value and 0xFF
|
|
1180
|
+
return if (v == 0) 0 else minOf(15, (v + 8) ushr 4)
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
fun pack4Bpp(grayscale: ByteArray): ByteArray =
|
|
1184
|
+
ByteArray((grayscale.size + 1) / 2) { index ->
|
|
1185
|
+
val left = grayNibble(grayscale[index * 2].toInt())
|
|
1186
|
+
val rightIndex = index * 2 + 1
|
|
1187
|
+
val right = if (rightIndex < grayscale.size) grayNibble(grayscale[rightIndex].toInt()) else 0
|
|
1188
|
+
((left shl 4) or right).toByte()
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
fun rleEncode(packed: ByteArray): ByteArray {
|
|
1192
|
+
val pixelCount = packed.size * 2
|
|
1193
|
+
fun nibble(index: Int): Int =
|
|
1194
|
+
if (index % 2 == 0) (packed[index / 2].toInt() ushr 4) and 0x0F
|
|
1195
|
+
else packed[index / 2].toInt() and 0x0F
|
|
1196
|
+
|
|
1197
|
+
val output = ByteArrayOutputStream()
|
|
1198
|
+
var index = 0
|
|
1199
|
+
while (index < pixelCount) {
|
|
1200
|
+
val color = nibble(index)
|
|
1201
|
+
var end = index + 1
|
|
1202
|
+
while (end < pixelCount && nibble(end) == color) end += 1
|
|
1203
|
+
appendRun(output, color, end - index)
|
|
1204
|
+
index = end
|
|
1205
|
+
}
|
|
1206
|
+
return output.toByteArray()
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
private fun rleEncodeSolid(color: Int, pixelCount: Int): ByteArray =
|
|
1210
|
+
ByteArrayOutputStream().also { appendRun(it, color, pixelCount) }.toByteArray()
|
|
1211
|
+
|
|
1212
|
+
private fun appendRun(output: ByteArrayOutputStream, color: Int, count: Int) {
|
|
1213
|
+
var remaining = count
|
|
1214
|
+
while (remaining > 0) {
|
|
1215
|
+
val chunk = minOf(remaining, 0xFFFF)
|
|
1216
|
+
when {
|
|
1217
|
+
chunk <= 15 -> output.write((chunk shl 4) or color)
|
|
1218
|
+
chunk <= 255 -> {
|
|
1219
|
+
output.write(color)
|
|
1220
|
+
output.write(chunk)
|
|
1221
|
+
}
|
|
1222
|
+
else -> {
|
|
1223
|
+
output.write(color)
|
|
1224
|
+
output.write(0)
|
|
1225
|
+
output.write(chunk and 0xFF)
|
|
1226
|
+
output.write((chunk ushr 8) and 0xFF)
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
remaining -= chunk
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
private fun zlib(data: ByteArray): ByteArray? =
|
|
1234
|
+
try {
|
|
1235
|
+
ByteArrayOutputStream().also { output ->
|
|
1236
|
+
DeflaterOutputStream(output).use { it.write(data) }
|
|
1237
|
+
}.toByteArray()
|
|
1238
|
+
} catch (_: Exception) {
|
|
1239
|
+
null
|
|
1240
|
+
}
|
|
1241
|
+
}
|