@glassly/bluetooth-sdk 0.1.0-dev.99 → 0.2.8-dev.445
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -6
- package/android/build.gradle +7 -7
- package/android/lc3Lib/build.gradle +2 -2
- package/android/src/main/assets/G2Firmware/README.md +27 -2
- package/android/src/main/assets/G2Firmware/{g2-stock-2.2.9.22.evenota.bin → g2-glassly-cfw-2.3.0.24.evenota.bin} +0 -0
- package/android/src/main/assets/G2Firmware/{g2-glassly-cfw-2.2.9.22.evenota.bin → g2-stock-2.3.0.24.evenota.bin} +0 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +38 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +50 -14
- package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +18 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +139 -10
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +28 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/DisplayPacer.kt +199 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +10 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +4 -4
- package/android/src/main/java/com/glassly/bluetoothsdk/GlassesNotifications.kt +155 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +1065 -255
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2BleLink.kt +119 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +29 -28
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +138 -33
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwScene.kt +673 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +235 -162
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransport.kt +402 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnostics.kt +84 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +3 -3
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +9 -9
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Keyboard.kt +689 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardBridge.kt +452 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardHid.kt +208 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFile.kt +181 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2RingBattery.kt +20 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Sensors.kt +67 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2TouchInput.kt +39 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +5 -5
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +14 -14
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/{GlasslyBle.java → NexProto.java} +6462 -7618
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIds.kt +58 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +77 -2
- package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +55 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +24 -149
- package/android/src/test/java/com/glassly/bluetoothsdk/DisplayPacerTest.kt +277 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/G2BleLinkTest.kt +70 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +45 -56
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +78 -5
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwSceneTest.kt +489 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +100 -43
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransportTest.kt +252 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnosticsTest.kt +64 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardTest.kt +271 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFileTest.kt +204 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2RingBatteryTest.kt +56 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2SensorsTest.kt +56 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2TouchInputTest.kt +60 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NexProtoDescriptorTest.kt +32 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIdsTest.kt +58 -0
- package/build/BluetoothSdk.types.d.ts +209 -20
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +1 -1
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +30 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +28 -1
- package/build/generated/changelogCatalog.d.ts.map +1 -1
- package/build/generated/changelogCatalog.js +37 -1
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +4 -4
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +14 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +102 -13
- package/ios/GlasslyBluetoothSDK.podspec +1 -1
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +8 -3
- package/ios/Source/BluetoothSDK.swift +53 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +13 -0
- package/ios/Source/DeviceManager.swift +151 -33
- package/ios/Source/DeviceStore.swift +28 -14
- package/ios/Source/DisplayPacer.swift +180 -0
- package/ios/Source/GeneratedChangelogCatalog.swift +10 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +4 -4
- package/ios/Source/GlassesNotifications.swift +181 -0
- package/ios/Source/ObservableStore.swift +14 -2
- package/ios/Source/Resources/G2Firmware/README.md +27 -2
- package/ios/Source/Resources/G2Firmware/{g2-stock-2.2.9.22.evenota.bin → g2-glassly-cfw-2.3.0.24.evenota.bin} +0 -0
- package/ios/Source/Resources/G2Firmware/{g2-glassly-cfw-2.2.9.22.evenota.bin → g2-stock-2.3.0.24.evenota.bin} +0 -0
- package/ios/Source/controllers/R1.swift +0 -1
- package/ios/Source/internal/BluetoothAvailability.swift +31 -0
- package/ios/Source/sgcs/G2.swift +1420 -495
- package/ios/Source/sgcs/G2AncsRelay.swift +268 -0
- package/ios/Source/sgcs/G2BleLink.swift +102 -0
- package/ios/Source/sgcs/G2CfwClassification.swift +45 -27
- package/ios/Source/sgcs/G2CfwRenderer.swift +166 -33
- package/ios/Source/sgcs/G2CfwScene.swift +779 -0
- package/ios/Source/sgcs/G2CfwTextureCache.swift +244 -117
- package/ios/Source/sgcs/G2CfwTransport.swift +485 -0
- package/ios/Source/sgcs/G2DisplayDiagnostics.swift +154 -0
- package/ios/Source/sgcs/G2FlashProtocol.swift +2 -2
- package/ios/Source/sgcs/G2Flasher.swift +9 -9
- package/ios/Source/sgcs/G2Keyboard.swift +780 -0
- package/ios/Source/sgcs/G2KeyboardBridge.swift +438 -0
- package/ios/Source/sgcs/G2KeyboardHid.swift +290 -0
- package/ios/Source/sgcs/G2NotificationTuning.swift +24 -0
- package/ios/Source/sgcs/G2RingBattery.swift +22 -0
- package/ios/Source/sgcs/G2Sensors.swift +75 -0
- package/ios/Source/sgcs/G2TouchInput.swift +51 -0
- package/ios/Source/sgcs/G2WritePacing.swift +18 -0
- package/ios/Source/sgcs/MentraNex.swift +44 -44
- package/ios/Source/sgcs/SGCManager.swift +120 -13
- package/ios/Source/sgcs/{glassly_ble.pb.swift → nex_ble.pb.swift} +405 -465
- package/ios/Source/speakerid/SpeakerIdEngine.swift +2 -2
- package/ios/Source/status/DeviceStatus.swift +27 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +15 -14
- package/ios/Source/utils/Enums.swift +0 -1
- package/ios/Source/utils/G1Text.swift +0 -1
- package/ios/Source/utils/G2NotifyFile.swift +20 -10
- package/ios/Source/utils/JSCExperiment.swift +6 -6
- package/ios/Source/utils/Models.swift +0 -1
- package/ios/Tests/BluetoothSDKTests/BluetoothAvailabilityTests.swift +29 -0
- package/ios/Tests/BluetoothSDKTests/DisplayPacerTests.swift +255 -0
- package/ios/Tests/BluetoothSDKTests/G2AncsRelayTests.swift +186 -0
- package/ios/Tests/BluetoothSDKTests/G2BleLinkTests.swift +52 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +46 -51
- package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +164 -5
- package/ios/Tests/BluetoothSDKTests/G2CfwSceneTests.swift +497 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +108 -44
- package/ios/Tests/BluetoothSDKTests/G2CfwTransportTests.swift +259 -0
- package/ios/Tests/BluetoothSDKTests/G2DisplayDiagnosticsTests.swift +89 -0
- package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +3 -2
- package/ios/Tests/BluetoothSDKTests/G2ImageAckTests.swift +104 -0
- package/ios/Tests/BluetoothSDKTests/G2KeyboardTests.swift +274 -0
- package/ios/Tests/BluetoothSDKTests/G2NotifyFileTests.swift +21 -5
- package/ios/Tests/BluetoothSDKTests/G2RingBatteryTests.swift +56 -0
- package/ios/Tests/BluetoothSDKTests/G2SensorsTests.swift +56 -0
- package/ios/Tests/BluetoothSDKTests/G2TouchInputTests.swift +71 -0
- package/ios/Tests/BluetoothSDKTests/G2WritePacingTests.swift +35 -0
- package/ios/Tests/BluetoothSDKTests/GlassesNotificationTests.swift +95 -0
- package/package.json +1 -1
- package/plugin/build/withAndroid.js +2 -2
- package/plugin/build/withIos.js +1 -1
- package/scripts/gen-nex-proto.sh +12 -0
- package/scripts/inject-ios-sdk-version.mjs +1 -1
- package/src/BluetoothSdk.types.ts +214 -19
- package/src/_internal.ts +1 -1
- package/src/_private/BluetoothSdkModule.ts +40 -0
- package/src/generated/changelogCatalog.ts +37 -1
- package/src/generated/releaseMetadata.ts +4 -4
- package/src/index.ts +24 -0
|
@@ -1,59 +1,83 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
|
-
/// Phone-side
|
|
4
|
-
///
|
|
3
|
+
/// Phone-side mirror of the Glassly CFW's asset store (GLASSLYCFW/38): the
|
|
4
|
+
/// images, cached fonts and strings that scene objects reference by 32-bit id.
|
|
5
5
|
///
|
|
6
|
-
/// The
|
|
7
|
-
///
|
|
8
|
-
///
|
|
9
|
-
///
|
|
10
|
-
///
|
|
6
|
+
/// The glasses hold assets in a 256 KiB store behind their own allocator and
|
|
7
|
+
/// evict the least recently used unreferenced ones when room is needed, so the
|
|
8
|
+
/// wire never carries a storage offset: every asset has an id (a hash of what
|
|
9
|
+
/// it is) and a version (a hash of its bytes), and a PUT_ASSET entry uploads
|
|
10
|
+
/// it. This mirror remembers which ids it has queued for upload and predicts
|
|
11
|
+
/// the store's occupancy; when the prediction overflows it forgets what the
|
|
12
|
+
/// glasses are most likely to have evicted. A wrong prediction is harmless:
|
|
13
|
+
/// the firmware answers a SHOW that references an absent asset with MISSING,
|
|
14
|
+
/// and `forget(assetIds:)` makes the next frame re-upload exactly those.
|
|
15
|
+
///
|
|
16
|
+
/// The model is valid for one cache generation: the owner bumps a generation
|
|
17
|
+
/// counter whenever the on-glass cache is lost (lease end, reset, failed send)
|
|
18
|
+
/// and the model re-learns from scratch.
|
|
11
19
|
///
|
|
12
20
|
/// Cached image format: `[width:u8][height:u8][4bpp RLE]` where the RLE covers
|
|
13
21
|
/// exactly width*height pixels with NO row-pad nibbles (unlike modes 3/6).
|
|
14
|
-
///
|
|
15
|
-
/// characters 32..127
|
|
16
|
-
///
|
|
17
|
-
/// allocations start past them.
|
|
22
|
+
/// A font asset is a 96-entry little-endian u32 table of glyph image offsets
|
|
23
|
+
/// (characters 32..127, relative to the asset; 0 = absent) followed by the
|
|
24
|
+
/// glyph images, uploaded whole.
|
|
18
25
|
final class G2CfwTextureCache {
|
|
19
|
-
static let
|
|
26
|
+
static let storeSize = 256 * 1024
|
|
27
|
+
static let granule = 16
|
|
28
|
+
static let maxAssets = 256
|
|
20
29
|
static let fontTableChars = 96 // ASCII 32..127
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
static let fontTableEntryBytes = 4
|
|
31
|
+
/// A PUT message is one transport message; keep them a few round trips
|
|
32
|
+
/// long rather than one 64 KiB stream that acks late.
|
|
33
|
+
static let maxUploadPayloadBytes = 8192
|
|
23
34
|
/// 0x0F: identity ramp (top color 15), opaque — a full-cell glyph or a
|
|
24
35
|
/// full-box image drawn opaque self-erases whatever was under it.
|
|
25
36
|
static let opaqueDrawOptions: UInt8 = 0x0F
|
|
37
|
+
static let kindImage: UInt8 = 0
|
|
38
|
+
static let kindFont: UInt8 = 1
|
|
39
|
+
static let kindString: UInt8 = 2
|
|
26
40
|
|
|
27
41
|
final class FontEntry {
|
|
28
|
-
let
|
|
42
|
+
let id: UInt32
|
|
29
43
|
let cellWidth: Int
|
|
30
44
|
let lineHeight: Int
|
|
31
|
-
///
|
|
32
|
-
|
|
45
|
+
/// Wire codes (32..127) whose glyphs the uploaded font carries.
|
|
46
|
+
let glyphs: Set<UInt8>
|
|
33
47
|
|
|
34
|
-
init(
|
|
35
|
-
self.
|
|
48
|
+
init(id: UInt32, cellWidth: Int, lineHeight: Int, glyphs: Set<UInt8>) {
|
|
49
|
+
self.id = id
|
|
36
50
|
self.cellWidth = cellWidth
|
|
37
51
|
self.lineHeight = lineHeight
|
|
52
|
+
self.glyphs = glyphs
|
|
38
53
|
}
|
|
39
54
|
}
|
|
40
55
|
|
|
56
|
+
private struct Asset {
|
|
57
|
+
let id: UInt32
|
|
58
|
+
let version: UInt32
|
|
59
|
+
let bytes: Int // granule-rounded store footprint
|
|
60
|
+
var lastUse: Int
|
|
61
|
+
}
|
|
62
|
+
|
|
41
63
|
private(set) var generation = -1
|
|
42
|
-
|
|
43
|
-
|
|
64
|
+
/// Bytes the glasses could allocate for the store (the RESET reply says; 256 KiB when unknown).
|
|
65
|
+
var storeCapacity = G2CfwTextureCache.storeSize
|
|
66
|
+
private var fonts: [String: FontEntry] = [:]
|
|
67
|
+
/// Asset key → the resident (or queued) asset.
|
|
68
|
+
private var assets: [String: Asset] = [:]
|
|
69
|
+
private var byId: [UInt32: String] = [:]
|
|
44
70
|
/// Content hashes seen in earlier frames. Streaming content (a minimap
|
|
45
71
|
/// re-rendered every frame) never repeats a hash, so it never enters the
|
|
46
72
|
/// cache; static icons promote on their second sighting.
|
|
47
73
|
private var seenImageHashes: [String] = []
|
|
48
|
-
///
|
|
49
|
-
private var
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
///
|
|
54
|
-
///
|
|
55
|
-
/// for the rest of the lease. The renderer checks this before each frame
|
|
56
|
-
/// and starts a fresh generation.
|
|
74
|
+
/// PUT_ASSET entries not yet flushed into payloads.
|
|
75
|
+
private var pendingEntries: [Data] = []
|
|
76
|
+
private var useClock = 0
|
|
77
|
+
/// Predicted store occupancy of everything this mirror believes resident.
|
|
78
|
+
private(set) var predictedBytes = 0
|
|
79
|
+
/// The prediction overflowed the store and the mirror dropped its oldest
|
|
80
|
+
/// assets; advisory for the renderer.
|
|
57
81
|
private(set) var exhausted = false
|
|
58
82
|
|
|
59
83
|
/// Forget everything and adopt `generation` (the on-glass cache was freed
|
|
@@ -61,105 +85,202 @@ final class G2CfwTextureCache {
|
|
|
61
85
|
func reset(generation: Int) {
|
|
62
86
|
self.generation = generation
|
|
63
87
|
fonts.removeAll()
|
|
64
|
-
|
|
88
|
+
assets.removeAll()
|
|
89
|
+
byId.removeAll()
|
|
65
90
|
seenImageHashes.removeAll()
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
pendingEntries.removeAll()
|
|
92
|
+
predictedBytes = 0
|
|
93
|
+
useClock = 0
|
|
68
94
|
exhausted = false
|
|
69
95
|
}
|
|
70
96
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
97
|
+
/// The glasses reported these asset ids absent: upload them again on next use.
|
|
98
|
+
func forget(assetIds: [UInt32]) {
|
|
99
|
+
for id in assetIds {
|
|
100
|
+
guard let key = byId.removeValue(forKey: id), let asset = assets.removeValue(forKey: key) else { continue }
|
|
101
|
+
predictedBytes -= asset.bytes
|
|
102
|
+
fonts = fonts.filter { $0.value.id != id }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// True when `id` names an asset this mirror believes resident.
|
|
107
|
+
func holds(assetId id: UInt32) -> Bool { byId[id] != nil }
|
|
108
|
+
|
|
109
|
+
/// Nonzero 31-bit FNV-1a of a key: asset ids and content versions.
|
|
110
|
+
static func hash31(_ s: String) -> UInt32 {
|
|
111
|
+
hash31(Data(s.utf8))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
static func hash31(_ bytes: Data) -> UInt32 {
|
|
115
|
+
var h: UInt32 = 0x811C_9DC5
|
|
116
|
+
for byte in bytes {
|
|
117
|
+
h ^= UInt32(byte)
|
|
118
|
+
h = h &* 0x0100_0193
|
|
119
|
+
}
|
|
120
|
+
h &= 0x7FFF_FFFF
|
|
121
|
+
return h == 0 ? 1 : h
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private static func footprint(_ length: Int) -> Int {
|
|
125
|
+
(length + granule - 1) / granule * granule
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Register `data` under `key` as an asset of `kind`, queueing its upload.
|
|
129
|
+
/// Nil when it can never fit the store or its id is taken by another key.
|
|
130
|
+
private func ensureAsset(key: String, kind: UInt8, data: Data) -> UInt32? {
|
|
131
|
+
useClock += 1
|
|
132
|
+
if var asset = assets[key] {
|
|
133
|
+
asset.lastUse = useClock
|
|
134
|
+
assets[key] = asset
|
|
135
|
+
return asset.id
|
|
136
|
+
}
|
|
137
|
+
guard !data.isEmpty, data.count <= storeCapacity else { return nil }
|
|
138
|
+
let id = Self.hash31(key)
|
|
139
|
+
if let other = byId[id], other != key {
|
|
74
140
|
return nil
|
|
75
141
|
}
|
|
76
|
-
|
|
77
|
-
|
|
142
|
+
let footprint = Self.footprint(data.count)
|
|
143
|
+
makeRoom(for: footprint)
|
|
144
|
+
let asset = Asset(id: id, version: Self.hash31(data), bytes: footprint, lastUse: useClock)
|
|
145
|
+
assets[key] = asset
|
|
146
|
+
byId[id] = key
|
|
147
|
+
predictedBytes += footprint
|
|
148
|
+
pendingEntries.append(contentsOf: Self.putAssetEntries(id: id, version: asset.version, kind: kind, data: data))
|
|
149
|
+
return id
|
|
78
150
|
}
|
|
79
151
|
|
|
80
|
-
///
|
|
81
|
-
///
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
152
|
+
/// Drop the mirror's least recently used assets until `footprint` fits the
|
|
153
|
+
/// predicted store: the glasses evict the same way, so believing an asset
|
|
154
|
+
/// gone is safer than believing it resident.
|
|
155
|
+
private func makeRoom(for footprint: Int) {
|
|
156
|
+
while predictedBytes + footprint > storeCapacity || assets.count >= Self.maxAssets {
|
|
157
|
+
guard let victim = assets.min(by: { $0.value.lastUse < $1.value.lastUse }) else { break }
|
|
158
|
+
exhausted = true
|
|
159
|
+
assets.removeValue(forKey: victim.key)
|
|
160
|
+
byId.removeValue(forKey: victim.value.id)
|
|
161
|
+
fonts = fonts.filter { $0.value.id != victim.value.id }
|
|
162
|
+
predictedBytes -= victim.value.bytes
|
|
163
|
+
}
|
|
88
164
|
}
|
|
89
165
|
|
|
90
|
-
///
|
|
91
|
-
///
|
|
92
|
-
/// 8-bit grayscale raster (cellWidth × lineHeight)
|
|
93
|
-
///
|
|
94
|
-
func
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
166
|
+
/// The font asset for `fontSize`, rasterizing every glyph the variant needs
|
|
167
|
+
/// and uploading the whole font on first use. `rasterize` supplies the
|
|
168
|
+
/// full-cell 8-bit grayscale raster (cellWidth × lineHeight) for a wire
|
|
169
|
+
/// code; a code it cannot raster is left absent from the font.
|
|
170
|
+
func font(
|
|
171
|
+
fontSize: Int32, cellWidth: Int, lineHeight: Int, variant: String = "ascii",
|
|
172
|
+
codes: [UInt8] = Array(32 ... 126), rasterize: (UInt8) -> Data?
|
|
173
|
+
) -> FontEntry? {
|
|
174
|
+
let key = "font:\(fontSize):\(cellWidth)x\(lineHeight):\(variant)"
|
|
175
|
+
if let existing = fonts[key], assets[key] != nil {
|
|
176
|
+
useClock += 1
|
|
177
|
+
assets[key]?.lastUse = useClock
|
|
178
|
+
return existing
|
|
179
|
+
}
|
|
180
|
+
var table = Data(count: Self.fontTableChars * Self.fontTableEntryBytes)
|
|
181
|
+
var body = Data()
|
|
182
|
+
var glyphs = Set<UInt8>()
|
|
183
|
+
for code in codes where code >= 32 && code <= 127 {
|
|
184
|
+
guard let gray = rasterize(code), gray.count == cellWidth * lineHeight else { continue }
|
|
185
|
+
let offset = table.count + body.count
|
|
186
|
+
let index = (Int(code) - 32) * Self.fontTableEntryBytes
|
|
187
|
+
table[index] = UInt8(offset & 0xFF)
|
|
188
|
+
table[index + 1] = UInt8((offset >> 8) & 0xFF)
|
|
189
|
+
table[index + 2] = UInt8((offset >> 16) & 0xFF)
|
|
190
|
+
table[index + 3] = UInt8((offset >> 24) & 0xFF)
|
|
191
|
+
body.append(UInt8(cellWidth)); body.append(UInt8(lineHeight))
|
|
192
|
+
body.append(Self.rleEncodePixels(gray))
|
|
193
|
+
glyphs.insert(code)
|
|
194
|
+
}
|
|
195
|
+
guard !glyphs.isEmpty, let id = ensureAsset(key: key, kind: Self.kindFont, data: table + body) else { return nil }
|
|
196
|
+
let entry = FontEntry(id: id, cellWidth: cellWidth, lineHeight: lineHeight, glyphs: glyphs)
|
|
197
|
+
fonts[key] = entry
|
|
198
|
+
return entry
|
|
109
199
|
}
|
|
110
200
|
|
|
111
201
|
/// Record a sighting of an image content hash; true when it was already
|
|
112
202
|
/// seen in an earlier frame (worth caching).
|
|
113
203
|
func noteImageSighting(_ hash: String) -> Bool {
|
|
114
|
-
if
|
|
204
|
+
if assets[hash] != nil { return true }
|
|
115
205
|
if seenImageHashes.contains(hash) { return true }
|
|
116
206
|
seenImageHashes.append(hash)
|
|
117
207
|
if seenImageHashes.count > 32 { seenImageHashes.removeFirst() }
|
|
118
208
|
return false
|
|
119
209
|
}
|
|
120
210
|
|
|
121
|
-
/// The
|
|
211
|
+
/// The asset id of an image, uploading it on first residency.
|
|
122
212
|
/// `gray` is the width×height 8-bit grayscale raster.
|
|
123
|
-
func ensureImage(hash: String, gray: Data, width: Int, height: Int) ->
|
|
124
|
-
if let
|
|
125
|
-
guard width > 0, width <= 255, height > 0, height <= 255,
|
|
126
|
-
gray.count == width * height
|
|
127
|
-
else { return nil }
|
|
213
|
+
func ensureImage(hash: String, gray: Data, width: Int, height: Int) -> UInt32? {
|
|
214
|
+
if let asset = assets[hash] { useClock += 1; assets[hash]?.lastUse = useClock; return asset.id }
|
|
215
|
+
guard width > 0, width <= 255, height > 0, height <= 255, gray.count == width * height else { return nil }
|
|
128
216
|
var image = Data([UInt8(width), UInt8(height)])
|
|
129
217
|
image.append(Self.rleEncodePixels(gray))
|
|
130
|
-
|
|
131
|
-
images[hash] = offset
|
|
132
|
-
pendingUploads.append((offset: offset, data: image))
|
|
133
|
-
return offset
|
|
218
|
+
return ensureAsset(key: hash, kind: Self.kindImage, data: image)
|
|
134
219
|
}
|
|
135
220
|
|
|
136
|
-
///
|
|
137
|
-
|
|
138
|
-
|
|
221
|
+
/// The asset id of a string (1..128 bytes of UTF-8, no NUL), uploading it on first use.
|
|
222
|
+
func ensureBytes(key: String, bytes: [UInt8]) -> UInt32? {
|
|
223
|
+
guard !bytes.isEmpty, bytes.count <= 128, !bytes.contains(0) else { return nil }
|
|
224
|
+
return ensureAsset(key: key, kind: Self.kindString, data: Data(bytes))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// Drain the pending PUT_ASSET entries into `[37][epoch 0][request 0][entries]`
|
|
228
|
+
/// payloads, each at most `maxUploadPayloadBytes` (the link stamps the epoch
|
|
229
|
+
/// and request). An asset larger than the budget is split into chunks.
|
|
139
230
|
func takeUploadPayloads() -> [Data] {
|
|
140
|
-
guard !
|
|
231
|
+
guard !pendingEntries.isEmpty else { return [] }
|
|
141
232
|
var payloads: [Data] = []
|
|
142
|
-
var current = Data([
|
|
143
|
-
for entry in
|
|
144
|
-
|
|
145
|
-
record.append(UInt8(entry.offset & 0xFF))
|
|
146
|
-
record.append(UInt8((entry.offset >> 8) & 0xFF))
|
|
147
|
-
record.append(UInt8(entry.data.count & 0xFF))
|
|
148
|
-
record.append(UInt8((entry.data.count >> 8) & 0xFF))
|
|
149
|
-
record.append(entry.data)
|
|
150
|
-
if current.count > 1, current.count + record.count > Self.maxUploadPayloadBytes {
|
|
233
|
+
var current = Data([37, 0, 0, 0, 0])
|
|
234
|
+
for entry in pendingEntries {
|
|
235
|
+
if current.count > 5, current.count + entry.count > Self.maxUploadPayloadBytes {
|
|
151
236
|
payloads.append(current)
|
|
152
|
-
current = Data([
|
|
237
|
+
current = Data([37, 0, 0, 0, 0])
|
|
153
238
|
}
|
|
154
|
-
current.append(
|
|
239
|
+
current.append(entry)
|
|
155
240
|
}
|
|
156
|
-
if current.count >
|
|
157
|
-
|
|
241
|
+
if current.count > 5 { payloads.append(current) }
|
|
242
|
+
pendingEntries.removeAll()
|
|
158
243
|
return payloads
|
|
159
244
|
}
|
|
160
245
|
|
|
246
|
+
/// Asset ids a `takeUploadPayloads` payload puts, in order.
|
|
247
|
+
static func assetIds(inUploadPayload payload: Data) -> [UInt32] {
|
|
248
|
+
let b = [UInt8](payload)
|
|
249
|
+
guard b.count > 5, b[0] == 37 else { return [] }
|
|
250
|
+
var ids: [UInt32] = []
|
|
251
|
+
var i = 5
|
|
252
|
+
while i + 20 <= b.count, b[i] == 0 {
|
|
253
|
+
let id = UInt32(b[i + 1]) | UInt32(b[i + 2]) << 8 | UInt32(b[i + 3]) << 16 | UInt32(b[i + 4]) << 24
|
|
254
|
+
if ids.last != id { ids.append(id) }
|
|
255
|
+
i += 20 + (Int(b[i + 18]) | Int(b[i + 19]) << 8)
|
|
256
|
+
}
|
|
257
|
+
return ids
|
|
258
|
+
}
|
|
259
|
+
|
|
161
260
|
// MARK: - Wire encoders
|
|
162
261
|
|
|
262
|
+
/// PUT_ASSET entries for one asset: `[0][id:u32][version:u32][kind][total:u32][offset:u32][len:u16][data]`
|
|
263
|
+
/// per chunk, chunks in order.
|
|
264
|
+
static func putAssetEntries(id: UInt32, version: UInt32, kind: UInt8, data: Data) -> [Data] {
|
|
265
|
+
var out: [Data] = []
|
|
266
|
+
var offset = 0
|
|
267
|
+
let chunk = maxUploadPayloadBytes - 32
|
|
268
|
+
repeat {
|
|
269
|
+
let len = min(chunk, data.count - offset)
|
|
270
|
+
var entry = Data([0])
|
|
271
|
+
appendU32(Int(id), to: &entry)
|
|
272
|
+
appendU32(Int(version), to: &entry)
|
|
273
|
+
entry.append(kind)
|
|
274
|
+
appendU32(data.count, to: &entry)
|
|
275
|
+
appendU32(offset, to: &entry)
|
|
276
|
+
entry.append(UInt8(len & 0xFF)); entry.append(UInt8(len >> 8))
|
|
277
|
+
entry.append(data.subdata(in: (data.startIndex + offset) ..< (data.startIndex + offset + len)))
|
|
278
|
+
out.append(entry)
|
|
279
|
+
offset += len
|
|
280
|
+
} while offset < data.count
|
|
281
|
+
return out
|
|
282
|
+
}
|
|
283
|
+
|
|
163
284
|
/// RLE over a flat 8-bit grayscale pixel sequence (quantized to 4bpp),
|
|
164
285
|
/// covering exactly the pixel count — no row padding, per the cached-image
|
|
165
286
|
/// contract. Token forms match modes 3/6.
|
|
@@ -195,36 +316,42 @@ final class G2CfwTextureCache {
|
|
|
195
316
|
return output
|
|
196
317
|
}
|
|
197
318
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
319
|
+
static func appendU32(_ value: Int, to payload: inout Data) {
|
|
320
|
+
payload.append(UInt8(value & 0xFF))
|
|
321
|
+
payload.append(UInt8((value >> 8) & 0xFF))
|
|
322
|
+
payload.append(UInt8((value >> 16) & 0xFF))
|
|
323
|
+
payload.append(UInt8((value >> 24) & 0xFF))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
static func appendI16(_ value: Int, to payload: inout Data) {
|
|
327
|
+
let bits = UInt16(bitPattern: Int16(clamping: value))
|
|
328
|
+
payload.append(UInt8(bits & 0xFF))
|
|
329
|
+
payload.append(UInt8(bits >> 8))
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/// Mode-19 image draw: `[19][asset id:u32][x:i16][y:i16][options]`.
|
|
333
|
+
static func imageDrawPayload(assetId: UInt32, x: Int, y: Int, options: UInt8) -> Data? {
|
|
334
|
+
guard assetId != 0, x >= -32768, x <= 32767, y >= -32768, y <= 32767 else { return nil }
|
|
335
|
+
var payload = Data([19])
|
|
336
|
+
appendU32(Int(assetId), to: &payload)
|
|
337
|
+
appendI16(x, to: &payload)
|
|
338
|
+
appendI16(y, to: &payload)
|
|
207
339
|
payload.append(options)
|
|
208
340
|
return payload
|
|
209
341
|
}
|
|
210
342
|
|
|
211
|
-
/// Mode-
|
|
212
|
-
///
|
|
213
|
-
///
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
) -> Data? {
|
|
218
|
-
guard !chars.isEmpty, chars.count <= 255,
|
|
219
|
-
fontTableOffset >= 0, fontTableOffset <= cacheSize - fontTableChars * 2,
|
|
220
|
-
x >= 0, x <= 0xFFFF, y >= 0, y <= 0xFFFF,
|
|
343
|
+
/// Mode-20 glyph run: `[20][font asset id:u32][x:i16][y:i16][options][strlen:u8][chars]`.
|
|
344
|
+
/// Every char must be 32..126 and present in the font (the firmware
|
|
345
|
+
/// validates each referenced glyph and rejects the whole draw otherwise).
|
|
346
|
+
static func stringRunPayload(fontId: UInt32, x: Int, y: Int, options: UInt8, chars: [UInt8]) -> Data? {
|
|
347
|
+
guard !chars.isEmpty, chars.count <= 255, fontId != 0,
|
|
348
|
+
x >= -32768, x <= 32767, y >= -32768, y <= 32767,
|
|
221
349
|
chars.allSatisfy({ $0 >= 32 && $0 <= 126 })
|
|
222
350
|
else { return nil }
|
|
223
|
-
var payload = Data([
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
351
|
+
var payload = Data([20])
|
|
352
|
+
appendU32(Int(fontId), to: &payload)
|
|
353
|
+
appendI16(x, to: &payload)
|
|
354
|
+
appendI16(y, to: &payload)
|
|
228
355
|
payload.append(options)
|
|
229
356
|
payload.append(UInt8(chars.count))
|
|
230
357
|
payload.append(contentsOf: chars)
|