@geastack/apple 0.2.8
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/LICENSE +202 -0
- package/README.md +54 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.js +3076 -0
- package/generated/AVFoundation.d.ts +112 -0
- package/generated/ActivityKit.d.ts +7 -0
- package/generated/AppKit.d.ts +268 -0
- package/generated/CoreGraphics.d.ts +18 -0
- package/generated/CoreLocation.d.ts +28 -0
- package/generated/CoreMedia.d.ts +8 -0
- package/generated/Dispatch.d.ts +2 -0
- package/generated/Foundation.d.ts +13 -0
- package/generated/MapKit.d.ts +28 -0
- package/generated/Metal.d.ts +91 -0
- package/generated/MetalKit.d.ts +22 -0
- package/generated/Photos.d.ts +5 -0
- package/generated/QuartzCore.d.ts +12 -0
- package/generated/SwiftData.d.ts +11 -0
- package/generated/UIKit.d.ts +187 -0
- package/package.json +123 -0
- package/runtime/AVFoundation.js +136 -0
- package/runtime/ActivityKit.js +18 -0
- package/runtime/AppKit.js +313 -0
- package/runtime/CoreGraphics.js +15 -0
- package/runtime/CoreLocation.js +30 -0
- package/runtime/CoreMedia.js +7 -0
- package/runtime/Dispatch.js +11 -0
- package/runtime/Foundation.js +21 -0
- package/runtime/MapKit.js +22 -0
- package/runtime/Metal.js +126 -0
- package/runtime/MetalKit.js +15 -0
- package/runtime/Photos.js +9 -0
- package/runtime/QuartzCore.js +9 -0
- package/runtime/SwiftData.js +29 -0
- package/runtime/UIKit.js +242 -0
- package/runtime/empty.js +1 -0
- package/targets/ios/Info.plist.in +25 -0
- package/targets/ios/README.md +46 -0
- package/targets/ios/build-ios.sh +395 -0
- package/targets/ios/detect-development-team.mjs +109 -0
- package/targets/ios/generate-resident-entry.mjs +67 -0
- package/targets/ios/generate-resident-registry.mjs +162 -0
- package/targets/ios/generate-xcode-project.mjs +520 -0
- package/targets/ios/main/canvas_view.h +7 -0
- package/targets/ios/main/canvas_view.mm +94 -0
- package/targets/ios/main/font_registry.h +17 -0
- package/targets/ios/main/font_registry.mm +129 -0
- package/targets/ios/main/image_bridge.h +13 -0
- package/targets/ios/main/image_bridge.mm +53 -0
- package/targets/ios/main/ios_apps.c +28 -0
- package/targets/ios/main/ios_camera.mm +446 -0
- package/targets/ios/main/ios_display.mm +344 -0
- package/targets/ios/main/ios_main.mm +811 -0
- package/targets/ios/main/ios_memory.cpp +55 -0
- package/targets/ios/main/ios_renderer.h +20 -0
- package/targets/ios/main/ios_renderer.mm +27 -0
- package/targets/ios/main/ios_root_background.cpp +20 -0
- package/targets/ios/main/ios_root_background.h +11 -0
- package/targets/ios/main/ios_sensors.mm +244 -0
- package/targets/ios/main/ios_timers.mm +53 -0
- package/targets/ios/main/renderer/ios_renderer_internal.h +65 -0
- package/targets/ios/main/renderer/ios_renderer_support.mm +111 -0
- package/targets/ios/main/renderer/native_button.mm +179 -0
- package/targets/ios/main/renderer/native_label.mm +161 -0
- package/targets/ios/main/renderer/native_scroll_container.mm +279 -0
- package/targets/ios/main/renderer/view_reconciler.mm +274 -0
- package/targets/macos/Info.plist.in +26 -0
- package/targets/macos/README.md +131 -0
- package/targets/macos/build-child-lifecycle.sh +119 -0
- package/targets/macos/build-macos.sh +2156 -0
- package/targets/macos/check-module-graph-freshness.mjs +82 -0
- package/targets/macos/compiler-input-fingerprint.mjs +32 -0
- package/targets/macos/finalize-apple-native-output.mjs +77 -0
- package/targets/macos/generate-resident-entry.mjs +80 -0
- package/targets/macos/generate-resident-registry.mjs +141 -0
- package/targets/macos/heavy-build-lock.sh +176 -0
- package/targets/macos/main/canvas_view.h +9 -0
- package/targets/macos/main/canvas_view.mm +176 -0
- package/targets/macos/main/color_convert.h +18 -0
- package/targets/macos/main/color_convert.mm +39 -0
- package/targets/macos/main/font_registry.h +35 -0
- package/targets/macos/main/font_registry.mm +350 -0
- package/targets/macos/main/image_bridge.h +16 -0
- package/targets/macos/main/image_bridge.mm +52 -0
- package/targets/macos/main/macos_app_platform.mm +95 -0
- package/targets/macos/main/macos_apps.c +31 -0
- package/targets/macos/main/macos_device_control.mm +56 -0
- package/targets/macos/main/macos_display.mm +484 -0
- package/targets/macos/main/macos_host_device_control.mm +95 -0
- package/targets/macos/main/macos_main.mm +839 -0
- package/targets/macos/main/macos_memory.cpp +31 -0
- package/targets/macos/main/macos_native_shell.h +31 -0
- package/targets/macos/main/macos_native_shell.mm +396 -0
- package/targets/macos/main/macos_network.mm +253 -0
- package/targets/macos/main/macos_renderer.h +39 -0
- package/targets/macos/main/macos_renderer.mm +1969 -0
- package/targets/macos/main/macos_sensors.cpp +148 -0
- package/targets/macos/main/macos_smoke_app.cpp +65 -0
- package/targets/macos/main/macos_timers.mm +52 -0
- package/targets/macos/main/press_bridge.h +21 -0
- package/targets/macos/main/press_bridge.mm +72 -0
- package/targets/macos/main/thermalright_hid_display.h +14 -0
- package/targets/macos/main/thermalright_hid_display.mm +995 -0
- package/targets/macos/native-artifact-provenance.mjs +421 -0
- package/targets/macos/order-compile-edges.mjs +64 -0
- package/targets/macos/preflight-hard-muted-artifact.mjs +93 -0
- package/targets/macos/prune-generated-modules.mjs +147 -0
- package/targets/macos/sync-resources.mjs +153 -0
- package/targets/shared/analyze-audio-capture.mjs +131 -0
- package/targets/shared/apple_audio.mm +1194 -0
- package/targets/shared/apple_audio_selftest.mm +602 -0
- package/targets/shared/apple_audio_testing.h +34 -0
- package/targets/shared/build-audio-selftest.sh +50 -0
|
@@ -0,0 +1,995 @@
|
|
|
1
|
+
#import <CoreGraphics/CoreGraphics.h>
|
|
2
|
+
#import <Foundation/Foundation.h>
|
|
3
|
+
#import <IOKit/IOCFPlugIn.h>
|
|
4
|
+
#import <IOKit/IOKitLib.h>
|
|
5
|
+
#import <IOKit/hid/IOHIDLib.h>
|
|
6
|
+
#import <IOKit/usb/IOUSBLib.h>
|
|
7
|
+
#import <ImageIO/ImageIO.h>
|
|
8
|
+
|
|
9
|
+
#include "pixel.h"
|
|
10
|
+
#include "thermalright_hid_display.h"
|
|
11
|
+
|
|
12
|
+
#include <algorithm>
|
|
13
|
+
#include <chrono>
|
|
14
|
+
#include <condition_variable>
|
|
15
|
+
#include <cstdint>
|
|
16
|
+
#include <cstdlib>
|
|
17
|
+
#include <cstring>
|
|
18
|
+
#include <mutex>
|
|
19
|
+
#include <string>
|
|
20
|
+
#include <thread>
|
|
21
|
+
#include <vector>
|
|
22
|
+
|
|
23
|
+
#ifndef GEA_EMBEDDED_DISPLAY_WIDTH
|
|
24
|
+
#define GEA_EMBEDDED_DISPLAY_WIDTH 1280
|
|
25
|
+
#endif
|
|
26
|
+
#ifndef GEA_EMBEDDED_DISPLAY_HEIGHT
|
|
27
|
+
#define GEA_EMBEDDED_DISPLAY_HEIGHT 480
|
|
28
|
+
#endif
|
|
29
|
+
#ifndef GEA_MACOS_THERMALRIGHT_PRODUCT_ID
|
|
30
|
+
#define GEA_MACOS_THERMALRIGHT_PRODUCT_ID 0
|
|
31
|
+
#endif
|
|
32
|
+
|
|
33
|
+
namespace {
|
|
34
|
+
|
|
35
|
+
constexpr int kVendorId = 0x0416;
|
|
36
|
+
constexpr int kDefaultWidth = GEA_EMBEDDED_DISPLAY_WIDTH;
|
|
37
|
+
constexpr int kDefaultHeight = GEA_EMBEDDED_DISPLAY_HEIGHT;
|
|
38
|
+
constexpr int kReportBytes = 512;
|
|
39
|
+
constexpr std::uint8_t kMagic[4] = {0xDA, 0xDB, 0xDC, 0xDD};
|
|
40
|
+
constexpr int kLyChunkBytes = 512;
|
|
41
|
+
constexpr int kLyChunkHeaderBytes = 16;
|
|
42
|
+
constexpr int kLyChunkPayloadBytes = 496;
|
|
43
|
+
constexpr int kLyBurstBytes = 4096;
|
|
44
|
+
|
|
45
|
+
enum class Transport {
|
|
46
|
+
Hid,
|
|
47
|
+
LyBulk,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
struct DeviceProfile {
|
|
51
|
+
int productId;
|
|
52
|
+
int width;
|
|
53
|
+
int height;
|
|
54
|
+
const char *name;
|
|
55
|
+
Transport transport;
|
|
56
|
+
int rotationDegrees;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
constexpr DeviceProfile kDeviceProfiles[] = {
|
|
60
|
+
{0x5302, 1280, 480, "1280x480 HID", Transport::Hid, 0},
|
|
61
|
+
{0x5408, 1920, 462, "1920x462 LY bulk", Transport::LyBulk, 180},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const DeviceProfile *profileForProduct(int productId) {
|
|
65
|
+
for (const auto &profile : kDeviceProfiles) {
|
|
66
|
+
if (profile.productId == productId)
|
|
67
|
+
return &profile;
|
|
68
|
+
}
|
|
69
|
+
return nullptr;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const DeviceProfile *profileForSize(int width, int height) {
|
|
73
|
+
for (const auto &profile : kDeviceProfiles) {
|
|
74
|
+
if (profile.width == width && profile.height == height)
|
|
75
|
+
return &profile;
|
|
76
|
+
}
|
|
77
|
+
return nullptr;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
bool truthy(const char *value) {
|
|
81
|
+
if (!value || !*value)
|
|
82
|
+
return false;
|
|
83
|
+
if (std::strcmp(value, "0") == 0)
|
|
84
|
+
return false;
|
|
85
|
+
if (std::strcmp(value, "false") == 0)
|
|
86
|
+
return false;
|
|
87
|
+
if (std::strcmp(value, "FALSE") == 0)
|
|
88
|
+
return false;
|
|
89
|
+
if (std::strcmp(value, "off") == 0)
|
|
90
|
+
return false;
|
|
91
|
+
if (std::strcmp(value, "OFF") == 0)
|
|
92
|
+
return false;
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
bool selectedByEnv() {
|
|
97
|
+
if (truthy(std::getenv("GEA_MACOS_THERMALRIGHT_DISPLAY")))
|
|
98
|
+
return true;
|
|
99
|
+
const char *target = std::getenv("GEA_MACOS_DISPLAY_TARGET");
|
|
100
|
+
if (!target)
|
|
101
|
+
return false;
|
|
102
|
+
return std::strcmp(target, "thermalright") == 0 ||
|
|
103
|
+
std::strcmp(target, "thermalright-hid") == 0 ||
|
|
104
|
+
std::strcmp(target, "thermalright-usb") == 0 ||
|
|
105
|
+
std::strcmp(target, "thermalright-wide") == 0 ||
|
|
106
|
+
std::strcmp(target, "thermalright-hid-wide") == 0 ||
|
|
107
|
+
std::strcmp(target, "thermalright-usb-wide") == 0 ||
|
|
108
|
+
std::strcmp(target, "thermalright-1280x480") == 0 ||
|
|
109
|
+
std::strcmp(target, "thermalright-hid-1280x480") == 0 ||
|
|
110
|
+
std::strcmp(target, "thermalright-1920x462") == 0 ||
|
|
111
|
+
std::strcmp(target, "thermalright-ly-1920x462") == 0 ||
|
|
112
|
+
std::strcmp(target, "thermalright-1920x480") == 0 ||
|
|
113
|
+
std::strcmp(target, "thermalright-hid-1920x480") == 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
int envInt(const char *name, int fallback) {
|
|
117
|
+
const char *value = std::getenv(name);
|
|
118
|
+
if (!value || !*value)
|
|
119
|
+
return fallback;
|
|
120
|
+
const int parsed = std::atoi(value);
|
|
121
|
+
return parsed > 0 ? parsed : fallback;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
int envIntBase0(const char *name, int fallback) {
|
|
125
|
+
const char *value = std::getenv(name);
|
|
126
|
+
if (!value || !*value)
|
|
127
|
+
return fallback;
|
|
128
|
+
char *end = nullptr;
|
|
129
|
+
const long parsed = std::strtol(value, &end, 0);
|
|
130
|
+
return end && *end == '\0' && parsed > 0 ? static_cast<int>(parsed)
|
|
131
|
+
: fallback;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
double envDouble(const char *name, double fallback) {
|
|
135
|
+
const char *value = std::getenv(name);
|
|
136
|
+
if (!value || !*value)
|
|
137
|
+
return fallback;
|
|
138
|
+
char *end = nullptr;
|
|
139
|
+
const double parsed = std::strtod(value, &end);
|
|
140
|
+
return end && *end == '\0' && parsed > 0.0 ? parsed : fallback;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
void writeLe16(std::uint8_t *dst, std::uint16_t value) {
|
|
144
|
+
dst[0] = static_cast<std::uint8_t>(value & 0xFF);
|
|
145
|
+
dst[1] = static_cast<std::uint8_t>((value >> 8) & 0xFF);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
void writeLe32(std::uint8_t *dst, std::uint32_t value) {
|
|
149
|
+
dst[0] = static_cast<std::uint8_t>(value & 0xFF);
|
|
150
|
+
dst[1] = static_cast<std::uint8_t>((value >> 8) & 0xFF);
|
|
151
|
+
dst[2] = static_cast<std::uint8_t>((value >> 16) & 0xFF);
|
|
152
|
+
dst[3] = static_cast<std::uint8_t>((value >> 24) & 0xFF);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
std::size_t alignReport(std::size_t value) {
|
|
156
|
+
return (value + static_cast<std::size_t>(kReportBytes - 1)) &
|
|
157
|
+
~static_cast<std::size_t>(kReportBytes - 1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
void addInt(CFMutableDictionaryRef dict, const void *key, int value) {
|
|
161
|
+
CFNumberRef number =
|
|
162
|
+
CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &value);
|
|
163
|
+
if (!number)
|
|
164
|
+
return;
|
|
165
|
+
CFDictionarySetValue(dict, key, number);
|
|
166
|
+
CFRelease(number);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
int deviceInt(IOHIDDeviceRef device, CFStringRef key, int fallback = 0) {
|
|
170
|
+
if (!device)
|
|
171
|
+
return fallback;
|
|
172
|
+
CFTypeRef value = IOHIDDeviceGetProperty(device, key);
|
|
173
|
+
if (!value || CFGetTypeID(value) != CFNumberGetTypeID())
|
|
174
|
+
return fallback;
|
|
175
|
+
int out = fallback;
|
|
176
|
+
CFNumberGetValue(static_cast<CFNumberRef>(value), kCFNumberIntType, &out);
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
int serviceInt(io_service_t service, CFStringRef key, int fallback = 0) {
|
|
181
|
+
CFTypeRef value =
|
|
182
|
+
IORegistryEntryCreateCFProperty(service, key, kCFAllocatorDefault, 0);
|
|
183
|
+
if (!value)
|
|
184
|
+
return fallback;
|
|
185
|
+
int out = fallback;
|
|
186
|
+
if (CFGetTypeID(value) == CFNumberGetTypeID()) {
|
|
187
|
+
CFNumberGetValue(static_cast<CFNumberRef>(value), kCFNumberIntType, &out);
|
|
188
|
+
}
|
|
189
|
+
CFRelease(value);
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
class ThermalrightHidDisplay {
|
|
194
|
+
public:
|
|
195
|
+
ThermalrightHidDisplay()
|
|
196
|
+
: width_(envInt("GEA_MACOS_THERMALRIGHT_WIDTH", kDefaultWidth)),
|
|
197
|
+
height_(envInt("GEA_MACOS_THERMALRIGHT_HEIGHT", kDefaultHeight)),
|
|
198
|
+
preferredProductId_(envIntBase0("GEA_MACOS_THERMALRIGHT_PRODUCT_ID",
|
|
199
|
+
GEA_MACOS_THERMALRIGHT_PRODUCT_ID)),
|
|
200
|
+
fps_(std::clamp(envInt("GEA_MACOS_THERMALRIGHT_FPS", 30), 1, 60)),
|
|
201
|
+
jpegQuality_(std::clamp(
|
|
202
|
+
envDouble("GEA_MACOS_THERMALRIGHT_JPEG_QUALITY", 0.45), 0.1, 1.0)) {
|
|
203
|
+
if (preferredProductId_ <= 0) {
|
|
204
|
+
if (const DeviceProfile *profile = profileForSize(width_, height_)) {
|
|
205
|
+
preferredProductId_ = profile->productId;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
~ThermalrightHidDisplay() {
|
|
211
|
+
stopWorker();
|
|
212
|
+
close();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
int width() const { return width_; }
|
|
216
|
+
int height() const { return height_; }
|
|
217
|
+
int fps() const { return fps_; }
|
|
218
|
+
|
|
219
|
+
bool submitRgb565(const std::uint16_t *pixels, int width, int height) {
|
|
220
|
+
if (!pixels || width <= 0 || height <= 0)
|
|
221
|
+
return false;
|
|
222
|
+
startWorker();
|
|
223
|
+
const std::size_t pixelCount =
|
|
224
|
+
static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
|
225
|
+
{
|
|
226
|
+
std::lock_guard<std::mutex> lock(frameMutex_);
|
|
227
|
+
pendingPixels_.assign(pixels, pixels + pixelCount);
|
|
228
|
+
pendingWidth_ = width;
|
|
229
|
+
pendingHeight_ = height;
|
|
230
|
+
pendingGeneration_++;
|
|
231
|
+
}
|
|
232
|
+
frameCv_.notify_one();
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
void close() {
|
|
237
|
+
if (device_) {
|
|
238
|
+
IOHIDDeviceClose(device_, kIOHIDOptionsTypeNone);
|
|
239
|
+
CFRelease(device_);
|
|
240
|
+
device_ = nullptr;
|
|
241
|
+
}
|
|
242
|
+
if (manager_) {
|
|
243
|
+
IOHIDManagerClose(manager_, kIOHIDOptionsTypeNone);
|
|
244
|
+
CFRelease(manager_);
|
|
245
|
+
manager_ = nullptr;
|
|
246
|
+
}
|
|
247
|
+
if (usbInterface_) {
|
|
248
|
+
(*usbInterface_)->USBInterfaceClose(usbInterface_);
|
|
249
|
+
(*usbInterface_)->Release(usbInterface_);
|
|
250
|
+
usbInterface_ = nullptr;
|
|
251
|
+
}
|
|
252
|
+
usbInPipe_ = 0;
|
|
253
|
+
usbOutPipe_ = 0;
|
|
254
|
+
activeProfile_ = nullptr;
|
|
255
|
+
connected_ = false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private:
|
|
259
|
+
using Clock = std::chrono::steady_clock;
|
|
260
|
+
|
|
261
|
+
void startWorker() {
|
|
262
|
+
std::lock_guard<std::mutex> lock(frameMutex_);
|
|
263
|
+
if (workerRunning_)
|
|
264
|
+
return;
|
|
265
|
+
stopWorker_ = false;
|
|
266
|
+
workerRunning_ = true;
|
|
267
|
+
worker_ = std::thread([this] { workerLoop(); });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
void stopWorker() {
|
|
271
|
+
{
|
|
272
|
+
std::lock_guard<std::mutex> lock(frameMutex_);
|
|
273
|
+
if (!workerRunning_)
|
|
274
|
+
return;
|
|
275
|
+
stopWorker_ = true;
|
|
276
|
+
}
|
|
277
|
+
frameCv_.notify_one();
|
|
278
|
+
if (worker_.joinable())
|
|
279
|
+
worker_.join();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
void workerLoop() {
|
|
283
|
+
std::vector<std::uint16_t> framePixels;
|
|
284
|
+
std::uint64_t consumedGeneration = 0;
|
|
285
|
+
for (;;) {
|
|
286
|
+
{
|
|
287
|
+
std::unique_lock<std::mutex> lock(frameMutex_);
|
|
288
|
+
frameCv_.wait(lock, [&] {
|
|
289
|
+
return stopWorker_ || pendingGeneration_ != consumedGeneration;
|
|
290
|
+
});
|
|
291
|
+
if (stopWorker_)
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
Clock::time_point frameStart{};
|
|
296
|
+
if (!beginFrame(&frameStart))
|
|
297
|
+
continue;
|
|
298
|
+
|
|
299
|
+
int frameWidth = 0;
|
|
300
|
+
int frameHeight = 0;
|
|
301
|
+
{
|
|
302
|
+
std::lock_guard<std::mutex> lock(frameMutex_);
|
|
303
|
+
if (stopWorker_)
|
|
304
|
+
break;
|
|
305
|
+
consumedGeneration = pendingGeneration_;
|
|
306
|
+
frameWidth = pendingWidth_;
|
|
307
|
+
frameHeight = pendingHeight_;
|
|
308
|
+
framePixels.assign(pendingPixels_.begin(), pendingPixels_.end());
|
|
309
|
+
}
|
|
310
|
+
@autoreleasepool {
|
|
311
|
+
submitRgb565Sync(framePixels.data(), frameWidth, frameHeight,
|
|
312
|
+
frameStart);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
close();
|
|
316
|
+
{
|
|
317
|
+
std::lock_guard<std::mutex> lock(frameMutex_);
|
|
318
|
+
workerRunning_ = false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
bool submitRgb565Sync(const std::uint16_t *pixels, int width, int height,
|
|
323
|
+
Clock::time_point frameStart) {
|
|
324
|
+
if (!pixels || width <= 0 || height <= 0)
|
|
325
|
+
return false;
|
|
326
|
+
bool ok = false;
|
|
327
|
+
if (!connect()) {
|
|
328
|
+
finishFrame(frameStart);
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const auto encodeStart = Clock::now();
|
|
333
|
+
NSData *jpeg = encodeJpeg(pixels, width, height);
|
|
334
|
+
const auto encodeEnd = Clock::now();
|
|
335
|
+
const auto payloadBytes =
|
|
336
|
+
jpeg ? static_cast<std::size_t>([jpeg length]) : 0;
|
|
337
|
+
const int reportCount = transferUnitCount(payloadBytes);
|
|
338
|
+
const auto writeStart = Clock::now();
|
|
339
|
+
if (jpeg && [jpeg length] > 0)
|
|
340
|
+
ok = writeJpeg(jpeg, width, height);
|
|
341
|
+
const auto writeEnd = Clock::now();
|
|
342
|
+
if (ok) {
|
|
343
|
+
recordFrame(payloadBytes, reportCount,
|
|
344
|
+
std::chrono::duration_cast<std::chrono::microseconds>(
|
|
345
|
+
encodeEnd - encodeStart)
|
|
346
|
+
.count(),
|
|
347
|
+
std::chrono::duration_cast<std::chrono::microseconds>(
|
|
348
|
+
writeEnd - writeStart)
|
|
349
|
+
.count());
|
|
350
|
+
}
|
|
351
|
+
finishFrame(frameStart);
|
|
352
|
+
if (!ok)
|
|
353
|
+
close();
|
|
354
|
+
return ok;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
std::chrono::microseconds frameInterval() const {
|
|
358
|
+
return std::chrono::microseconds((1000000 + fps_ / 2) / fps_);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
bool beginFrame(Clock::time_point *frameStart) {
|
|
362
|
+
auto now = Clock::now();
|
|
363
|
+
if (nextSubmit_.time_since_epoch().count() == 0)
|
|
364
|
+
nextSubmit_ = now;
|
|
365
|
+
if (now < nextSubmit_) {
|
|
366
|
+
std::this_thread::sleep_until(nextSubmit_);
|
|
367
|
+
now = Clock::now();
|
|
368
|
+
}
|
|
369
|
+
if (frameStart)
|
|
370
|
+
*frameStart = now;
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
void finishFrame(Clock::time_point frameStart) {
|
|
375
|
+
const auto interval = frameInterval();
|
|
376
|
+
const auto now = Clock::now();
|
|
377
|
+
if (nextSubmit_.time_since_epoch().count() == 0 ||
|
|
378
|
+
nextSubmit_ < frameStart) {
|
|
379
|
+
nextSubmit_ = frameStart;
|
|
380
|
+
}
|
|
381
|
+
do {
|
|
382
|
+
nextSubmit_ += interval;
|
|
383
|
+
} while (nextSubmit_ <= now);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
void recordFrame(std::size_t payloadBytes, int reportCount,
|
|
387
|
+
std::int64_t encodeUs, std::int64_t writeUs) {
|
|
388
|
+
const auto now = Clock::now();
|
|
389
|
+
if (statsWindowStart_.time_since_epoch().count() == 0)
|
|
390
|
+
statsWindowStart_ = now;
|
|
391
|
+
statsFrames_++;
|
|
392
|
+
statsPayloadBytes_ += payloadBytes;
|
|
393
|
+
statsReportCount_ += reportCount;
|
|
394
|
+
statsEncodeUs_ += encodeUs;
|
|
395
|
+
statsWriteUs_ += writeUs;
|
|
396
|
+
|
|
397
|
+
const auto elapsedUs =
|
|
398
|
+
std::chrono::duration_cast<std::chrono::microseconds>(now -
|
|
399
|
+
statsWindowStart_)
|
|
400
|
+
.count();
|
|
401
|
+
if (elapsedUs < 1000000)
|
|
402
|
+
return;
|
|
403
|
+
const double elapsedSeconds = static_cast<double>(elapsedUs) / 1000000.0;
|
|
404
|
+
const double frameCount = static_cast<double>(statsFrames_);
|
|
405
|
+
NSLog(@"[gea-thermalright] %.1ffps q=%.2f jpeg=%.1fKB reports=%.1f "
|
|
406
|
+
@"encode=%.1fms write=%.1fms",
|
|
407
|
+
frameCount / elapsedSeconds, jpegQuality_,
|
|
408
|
+
(static_cast<double>(statsPayloadBytes_) / frameCount) / 1024.0,
|
|
409
|
+
static_cast<double>(statsReportCount_) / frameCount,
|
|
410
|
+
(static_cast<double>(statsEncodeUs_) / frameCount) / 1000.0,
|
|
411
|
+
(static_cast<double>(statsWriteUs_) / frameCount) / 1000.0);
|
|
412
|
+
statsWindowStart_ = now;
|
|
413
|
+
statsFrames_ = 0;
|
|
414
|
+
statsPayloadBytes_ = 0;
|
|
415
|
+
statsReportCount_ = 0;
|
|
416
|
+
statsEncodeUs_ = 0;
|
|
417
|
+
statsWriteUs_ = 0;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
int transferUnitCount(std::size_t payloadBytes) const {
|
|
421
|
+
if (payloadBytes == 0)
|
|
422
|
+
return 0;
|
|
423
|
+
const DeviceProfile *profile = activeProfile();
|
|
424
|
+
if (profile && profile->transport == Transport::LyBulk) {
|
|
425
|
+
std::size_t chunks = payloadBytes / kLyChunkPayloadBytes + 1;
|
|
426
|
+
const std::size_t remainder = chunks % 4;
|
|
427
|
+
if (remainder != 0)
|
|
428
|
+
chunks += 4 - remainder;
|
|
429
|
+
return static_cast<int>(chunks);
|
|
430
|
+
}
|
|
431
|
+
return static_cast<int>(alignReport(20 + payloadBytes) / kReportBytes);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const DeviceProfile *activeProfile() const {
|
|
435
|
+
if (activeProfile_)
|
|
436
|
+
return activeProfile_;
|
|
437
|
+
if (const DeviceProfile *profile = profileForProduct(preferredProductId_))
|
|
438
|
+
return profile;
|
|
439
|
+
return profileForSize(width_, height_);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
bool connect() {
|
|
443
|
+
const DeviceProfile *profile = activeProfile();
|
|
444
|
+
if (connected_) {
|
|
445
|
+
if (profile && profile->transport == Transport::LyBulk)
|
|
446
|
+
return usbInterface_ != nullptr;
|
|
447
|
+
return device_ != nullptr;
|
|
448
|
+
}
|
|
449
|
+
const auto now = Clock::now();
|
|
450
|
+
if (lastConnectAttempt_.time_since_epoch().count() != 0 &&
|
|
451
|
+
now - lastConnectAttempt_ < std::chrono::milliseconds(1000)) {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
lastConnectAttempt_ = now;
|
|
455
|
+
|
|
456
|
+
close();
|
|
457
|
+
profile = activeProfile();
|
|
458
|
+
if (profile && profile->transport == Transport::LyBulk)
|
|
459
|
+
return connectLyBulk(profile);
|
|
460
|
+
return connectHid(profile);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
bool connectHid(const DeviceProfile *requestedProfile) {
|
|
464
|
+
manager_ = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
|
|
465
|
+
if (!manager_)
|
|
466
|
+
return false;
|
|
467
|
+
|
|
468
|
+
CFMutableDictionaryRef match = CFDictionaryCreateMutable(
|
|
469
|
+
kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks,
|
|
470
|
+
&kCFTypeDictionaryValueCallBacks);
|
|
471
|
+
if (!match) {
|
|
472
|
+
close();
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
addInt(match, CFSTR(kIOHIDVendorIDKey), kVendorId);
|
|
476
|
+
if (preferredProductId_ > 0) {
|
|
477
|
+
addInt(match, CFSTR(kIOHIDProductIDKey), preferredProductId_);
|
|
478
|
+
}
|
|
479
|
+
IOHIDManagerSetDeviceMatching(manager_, match);
|
|
480
|
+
CFRelease(match);
|
|
481
|
+
|
|
482
|
+
IOReturn opened = IOHIDManagerOpen(manager_, kIOHIDOptionsTypeNone);
|
|
483
|
+
if (opened != kIOReturnSuccess) {
|
|
484
|
+
NSLog(@"[gea-thermalright] IOHIDManagerOpen failed: 0x%08x", opened);
|
|
485
|
+
close();
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
CFSetRef devices = IOHIDManagerCopyDevices(manager_);
|
|
490
|
+
if (!devices || CFSetGetCount(devices) <= 0) {
|
|
491
|
+
if (devices)
|
|
492
|
+
CFRelease(devices);
|
|
493
|
+
NSLog(@"[gea-thermalright] no Thermalright HID display found (vid=0x%04x "
|
|
494
|
+
@"preferred pid=0x%04x)",
|
|
495
|
+
kVendorId, preferredProductId_);
|
|
496
|
+
close();
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const CFIndex count = CFSetGetCount(devices);
|
|
501
|
+
std::vector<const void *> values(static_cast<std::size_t>(count));
|
|
502
|
+
CFSetGetValues(devices, values.data());
|
|
503
|
+
const DeviceProfile *selectedProfile = nullptr;
|
|
504
|
+
for (const void *value : values) {
|
|
505
|
+
IOHIDDeviceRef candidate =
|
|
506
|
+
static_cast<IOHIDDeviceRef>(const_cast<void *>(value));
|
|
507
|
+
const int productId = deviceInt(candidate, CFSTR(kIOHIDProductIDKey));
|
|
508
|
+
const DeviceProfile *profile = profileForProduct(productId);
|
|
509
|
+
if (!profile)
|
|
510
|
+
continue;
|
|
511
|
+
if (profile->transport != Transport::Hid)
|
|
512
|
+
continue;
|
|
513
|
+
if (preferredProductId_ > 0 && productId != preferredProductId_)
|
|
514
|
+
continue;
|
|
515
|
+
if (requestedProfile && productId != requestedProfile->productId)
|
|
516
|
+
continue;
|
|
517
|
+
device_ = candidate;
|
|
518
|
+
selectedProfile = profile;
|
|
519
|
+
CFRetain(device_);
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
CFRelease(devices);
|
|
523
|
+
if (!device_) {
|
|
524
|
+
NSLog(@"[gea-thermalright] no supported HID profile found (vid=0x%04x "
|
|
525
|
+
@"preferred pid=0x%04x)",
|
|
526
|
+
kVendorId, preferredProductId_);
|
|
527
|
+
close();
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
opened = IOHIDDeviceOpen(device_, kIOHIDOptionsTypeNone);
|
|
532
|
+
if (opened != kIOReturnSuccess) {
|
|
533
|
+
NSLog(@"[gea-thermalright] IOHIDDeviceOpen failed: 0x%08x", opened);
|
|
534
|
+
close();
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
std::uint8_t init[kReportBytes] = {};
|
|
539
|
+
std::memcpy(init, kMagic, sizeof(kMagic));
|
|
540
|
+
init[12] = 0x01;
|
|
541
|
+
IOReturn result = IOHIDDeviceSetReport(device_, kIOHIDReportTypeOutput, 0,
|
|
542
|
+
init, sizeof(init));
|
|
543
|
+
if (result != kIOReturnSuccess) {
|
|
544
|
+
NSLog(@"[gea-thermalright] init report failed: 0x%08x", result);
|
|
545
|
+
close();
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(120));
|
|
550
|
+
connected_ = true;
|
|
551
|
+
activeProfile_ = selectedProfile;
|
|
552
|
+
NSLog(@"[gea-thermalright] connected %s pid=0x%04x canvas=%dx%d @ %dfps",
|
|
553
|
+
selectedProfile ? selectedProfile->name : "unknown",
|
|
554
|
+
selectedProfile ? selectedProfile->productId
|
|
555
|
+
: deviceInt(device_, CFSTR(kIOHIDProductIDKey)),
|
|
556
|
+
width_, height_, fps_);
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
io_service_t findUsbInterfaceService(int productId) {
|
|
561
|
+
CFMutableDictionaryRef match = IOServiceMatching("IOUSBHostInterface");
|
|
562
|
+
if (!match)
|
|
563
|
+
return IO_OBJECT_NULL;
|
|
564
|
+
|
|
565
|
+
io_iterator_t iterator = IO_OBJECT_NULL;
|
|
566
|
+
IOReturn result =
|
|
567
|
+
IOServiceGetMatchingServices(kIOMainPortDefault, match, &iterator);
|
|
568
|
+
if (result != kIOReturnSuccess || iterator == IO_OBJECT_NULL) {
|
|
569
|
+
NSLog(@"[gea-thermalright] USB interface match failed: 0x%08x", result);
|
|
570
|
+
return IO_OBJECT_NULL;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
io_service_t found = IO_OBJECT_NULL;
|
|
574
|
+
io_service_t service = IO_OBJECT_NULL;
|
|
575
|
+
while ((service = IOIteratorNext(iterator)) != IO_OBJECT_NULL) {
|
|
576
|
+
const bool isMatch =
|
|
577
|
+
serviceInt(service, CFSTR("idVendor")) == kVendorId &&
|
|
578
|
+
serviceInt(service, CFSTR("idProduct")) == productId &&
|
|
579
|
+
serviceInt(service, CFSTR("bInterfaceNumber")) == 0 &&
|
|
580
|
+
serviceInt(service, CFSTR("bInterfaceClass")) == 0xFF;
|
|
581
|
+
if (isMatch) {
|
|
582
|
+
found = service;
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
IOObjectRelease(service);
|
|
586
|
+
}
|
|
587
|
+
IOObjectRelease(iterator);
|
|
588
|
+
return found;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
bool openUsbInterface(const DeviceProfile *profile) {
|
|
592
|
+
if (!profile)
|
|
593
|
+
return false;
|
|
594
|
+
io_service_t service = findUsbInterfaceService(profile->productId);
|
|
595
|
+
if (service == IO_OBJECT_NULL) {
|
|
596
|
+
NSLog(@"[gea-thermalright] no Thermalright USB bulk interface found "
|
|
597
|
+
@"(vid=0x%04x pid=0x%04x)",
|
|
598
|
+
kVendorId, profile->productId);
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
IOCFPlugInInterface **plugin = nullptr;
|
|
603
|
+
SInt32 score = 0;
|
|
604
|
+
IOReturn result = IOCreatePlugInInterfaceForService(
|
|
605
|
+
service, kIOUSBInterfaceUserClientTypeID, kIOCFPlugInInterfaceID,
|
|
606
|
+
&plugin, &score);
|
|
607
|
+
IOObjectRelease(service);
|
|
608
|
+
if (result != kIOReturnSuccess || !plugin) {
|
|
609
|
+
NSLog(@"[gea-thermalright] IOUSB interface plugin failed: 0x%08x",
|
|
610
|
+
result);
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
IOUSBInterfaceInterface **interface = nullptr;
|
|
615
|
+
HRESULT query = (*plugin)->QueryInterface(
|
|
616
|
+
plugin, CFUUIDGetUUIDBytes(kIOUSBInterfaceInterfaceID),
|
|
617
|
+
reinterpret_cast<LPVOID *>(&interface));
|
|
618
|
+
(*plugin)->Release(plugin);
|
|
619
|
+
if (query != 0 || !interface) {
|
|
620
|
+
NSLog(@"[gea-thermalright] IOUSB interface QueryInterface failed: 0x%08x",
|
|
621
|
+
static_cast<unsigned>(query));
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
result = (*interface)->USBInterfaceOpen(interface);
|
|
626
|
+
if (result != kIOReturnSuccess) {
|
|
627
|
+
IOReturn seize = (*interface)->USBInterfaceOpenSeize(interface);
|
|
628
|
+
NSLog(
|
|
629
|
+
@"[gea-thermalright] USBInterfaceOpen failed: 0x%08x, seize: 0x%08x",
|
|
630
|
+
result, seize);
|
|
631
|
+
result = seize;
|
|
632
|
+
}
|
|
633
|
+
if (result != kIOReturnSuccess) {
|
|
634
|
+
(*interface)->Release(interface);
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
UInt8 endpointCount = 0;
|
|
639
|
+
result = (*interface)->GetNumEndpoints(interface, &endpointCount);
|
|
640
|
+
if (result != kIOReturnSuccess) {
|
|
641
|
+
NSLog(@"[gea-thermalright] GetNumEndpoints failed: 0x%08x", result);
|
|
642
|
+
(*interface)->USBInterfaceClose(interface);
|
|
643
|
+
(*interface)->Release(interface);
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
UInt8 inPipe = 0;
|
|
648
|
+
UInt8 outPipe = 0;
|
|
649
|
+
for (UInt8 pipe = 1; pipe <= endpointCount; pipe++) {
|
|
650
|
+
UInt8 direction = 0;
|
|
651
|
+
UInt8 number = 0;
|
|
652
|
+
UInt8 transferType = 0;
|
|
653
|
+
UInt16 maxPacketSize = 0;
|
|
654
|
+
UInt8 interval = 0;
|
|
655
|
+
result =
|
|
656
|
+
(*interface)
|
|
657
|
+
->GetPipeProperties(interface, pipe, &direction, &number,
|
|
658
|
+
&transferType, &maxPacketSize, &interval);
|
|
659
|
+
if (result != kIOReturnSuccess)
|
|
660
|
+
continue;
|
|
661
|
+
NSLog(@"[gea-thermalright] USB pipe %u ep=%u dir=%u type=%u mps=%u", pipe,
|
|
662
|
+
number, direction, transferType, maxPacketSize);
|
|
663
|
+
if (transferType != kUSBBulk)
|
|
664
|
+
continue;
|
|
665
|
+
if (direction == kUSBIn && !inPipe)
|
|
666
|
+
inPipe = pipe;
|
|
667
|
+
if (direction == kUSBOut && !outPipe)
|
|
668
|
+
outPipe = pipe;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (!inPipe || !outPipe) {
|
|
672
|
+
NSLog(@"[gea-thermalright] missing LY bulk pipes (in=%u out=%u)", inPipe,
|
|
673
|
+
outPipe);
|
|
674
|
+
(*interface)->USBInterfaceClose(interface);
|
|
675
|
+
(*interface)->Release(interface);
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
usbInterface_ = interface;
|
|
680
|
+
usbInPipe_ = inPipe;
|
|
681
|
+
usbOutPipe_ = outPipe;
|
|
682
|
+
(*usbInterface_)->ClearPipeStallBothEnds(usbInterface_, usbInPipe_);
|
|
683
|
+
(*usbInterface_)->ClearPipeStallBothEnds(usbInterface_, usbOutPipe_);
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
bool connectLyBulk(const DeviceProfile *profile) {
|
|
688
|
+
if (!openUsbInterface(profile))
|
|
689
|
+
return false;
|
|
690
|
+
|
|
691
|
+
std::vector<std::uint8_t> handshake(2048, 0);
|
|
692
|
+
handshake[0] = 0x02;
|
|
693
|
+
handshake[1] = 0xFF;
|
|
694
|
+
handshake[8] = 0x01;
|
|
695
|
+
if (!writeUsb(handshake.data(), handshake.size(), 1000, "LY handshake")) {
|
|
696
|
+
close();
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
std::uint8_t response[kReportBytes] = {};
|
|
701
|
+
UInt32 responseSize = sizeof(response);
|
|
702
|
+
IOReturn ack = (*usbInterface_)
|
|
703
|
+
->ReadPipeTO(usbInterface_, usbInPipe_, response,
|
|
704
|
+
&responseSize, 100, 100);
|
|
705
|
+
if (ack == kIOReturnSuccess && responseSize >= 9 && response[0] == 0x03 &&
|
|
706
|
+
response[1] == 0xFF && response[8] == 0x01) {
|
|
707
|
+
NSLog(@"[gea-thermalright] LY handshake ack %u bytes: %02x %02x %02x",
|
|
708
|
+
responseSize, response[0], response[1], response[8]);
|
|
709
|
+
} else {
|
|
710
|
+
NSLog(@"[gea-thermalright] LY handshake ack not available: 0x%08x", ack);
|
|
711
|
+
close();
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
connected_ = true;
|
|
716
|
+
activeProfile_ = profile;
|
|
717
|
+
lyAckWarningLogged_ = false;
|
|
718
|
+
NSLog(@"[gea-thermalright] connected %s pid=0x%04x canvas=%dx%d @ %dfps",
|
|
719
|
+
profile->name, profile->productId, width_, height_, fps_);
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
NSData *encodeJpeg(const std::uint16_t *pixels, int width, int height) {
|
|
724
|
+
const std::size_t pixelCount =
|
|
725
|
+
static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
|
726
|
+
rgbaScratch_.resize(pixelCount * 4);
|
|
727
|
+
const bool flip180 = rotate180();
|
|
728
|
+
for (int y = 0; y < height; y++) {
|
|
729
|
+
for (int x = 0; x < width; x++) {
|
|
730
|
+
const std::size_t dst =
|
|
731
|
+
static_cast<std::size_t>(y) * static_cast<std::size_t>(width) +
|
|
732
|
+
static_cast<std::size_t>(x);
|
|
733
|
+
const std::size_t src =
|
|
734
|
+
flip180 ? static_cast<std::size_t>(height - 1 - y) *
|
|
735
|
+
static_cast<std::size_t>(width) +
|
|
736
|
+
static_cast<std::size_t>(width - 1 - x)
|
|
737
|
+
: dst;
|
|
738
|
+
int r, g, b;
|
|
739
|
+
gea::framework::graphics::pixel::unpackRgb565(pixels[src], &r, &g, &b);
|
|
740
|
+
rgbaScratch_[dst * 4 + 0] =
|
|
741
|
+
static_cast<std::uint8_t>((r * 255 + 15) / 31);
|
|
742
|
+
rgbaScratch_[dst * 4 + 1] =
|
|
743
|
+
static_cast<std::uint8_t>((g * 255 + 31) / 63);
|
|
744
|
+
rgbaScratch_[dst * 4 + 2] =
|
|
745
|
+
static_cast<std::uint8_t>((b * 255 + 15) / 31);
|
|
746
|
+
rgbaScratch_[dst * 4 + 3] = 0xFF;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
|
751
|
+
CGDataProviderRef provider = CGDataProviderCreateWithData(
|
|
752
|
+
nullptr, rgbaScratch_.data(), rgbaScratch_.size(), nullptr);
|
|
753
|
+
if (!colorSpace || !provider) {
|
|
754
|
+
if (provider)
|
|
755
|
+
CGDataProviderRelease(provider);
|
|
756
|
+
if (colorSpace)
|
|
757
|
+
CGColorSpaceRelease(colorSpace);
|
|
758
|
+
return nil;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const CGBitmapInfo bitmapInfo =
|
|
762
|
+
static_cast<CGBitmapInfo>(kCGImageAlphaNoneSkipLast) | static_cast<CGBitmapInfo>(kCGBitmapByteOrder32Big);
|
|
763
|
+
CGImageRef image = CGImageCreate(
|
|
764
|
+
static_cast<std::size_t>(width), static_cast<std::size_t>(height), 8,
|
|
765
|
+
32, static_cast<std::size_t>(width) * 4, colorSpace, bitmapInfo,
|
|
766
|
+
provider, nullptr, false, kCGRenderingIntentDefault);
|
|
767
|
+
CGDataProviderRelease(provider);
|
|
768
|
+
CGColorSpaceRelease(colorSpace);
|
|
769
|
+
if (!image)
|
|
770
|
+
return nil;
|
|
771
|
+
|
|
772
|
+
NSMutableData *data = [NSMutableData data];
|
|
773
|
+
CGImageDestinationRef destination = CGImageDestinationCreateWithData(
|
|
774
|
+
(__bridge CFMutableDataRef)data, CFSTR("public.jpeg"), 1, nullptr);
|
|
775
|
+
if (!destination) {
|
|
776
|
+
CGImageRelease(image);
|
|
777
|
+
return nil;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
NSDictionary *properties = @{
|
|
781
|
+
(__bridge NSString *)
|
|
782
|
+
kCGImageDestinationLossyCompressionQuality : @(jpegQuality_)
|
|
783
|
+
};
|
|
784
|
+
CGImageDestinationAddImage(destination, image,
|
|
785
|
+
(__bridge CFDictionaryRef)properties);
|
|
786
|
+
const bool ok = CGImageDestinationFinalize(destination);
|
|
787
|
+
CFRelease(destination);
|
|
788
|
+
CGImageRelease(image);
|
|
789
|
+
return ok ? data : nil;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
bool rotate180() const {
|
|
793
|
+
if (const char *value = std::getenv("GEA_MACOS_THERMALRIGHT_ROTATE")) {
|
|
794
|
+
return std::atoi(value) == 180;
|
|
795
|
+
}
|
|
796
|
+
const DeviceProfile *profile = activeProfile();
|
|
797
|
+
return profile && profile->rotationDegrees == 180;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
bool writeReport(const std::uint8_t *report) {
|
|
801
|
+
if (!device_ || !report)
|
|
802
|
+
return false;
|
|
803
|
+
IOReturn result = IOHIDDeviceSetReport(device_, kIOHIDReportTypeOutput, 0,
|
|
804
|
+
report, kReportBytes);
|
|
805
|
+
if (result != kIOReturnSuccess) {
|
|
806
|
+
NSLog(@"[gea-thermalright] HID write failed: 0x%08x", result);
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
return true;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
bool writeJpeg(NSData *jpeg, int width, int height) {
|
|
813
|
+
const DeviceProfile *profile = activeProfile();
|
|
814
|
+
if (profile && profile->transport == Transport::LyBulk)
|
|
815
|
+
return writeLyJpeg(jpeg);
|
|
816
|
+
return writeHidJpeg(jpeg, width, height);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
bool writeHidJpeg(NSData *jpeg, int width, int height) {
|
|
820
|
+
const std::size_t payloadBytes = [jpeg length];
|
|
821
|
+
const std::size_t packetBytes = alignReport(20 + payloadBytes);
|
|
822
|
+
packetScratch_.assign(packetBytes, 0);
|
|
823
|
+
std::memcpy(packetScratch_.data(), kMagic, sizeof(kMagic));
|
|
824
|
+
packetScratch_[4] = 0x02;
|
|
825
|
+
packetScratch_[5] = 0x00;
|
|
826
|
+
packetScratch_[6] = 0x00; // JPEG
|
|
827
|
+
packetScratch_[7] = 0x00;
|
|
828
|
+
writeLe16(packetScratch_.data() + 8, static_cast<std::uint16_t>(width));
|
|
829
|
+
writeLe16(packetScratch_.data() + 10, static_cast<std::uint16_t>(height));
|
|
830
|
+
packetScratch_[12] = 0x02;
|
|
831
|
+
packetScratch_[13] = 0x00;
|
|
832
|
+
packetScratch_[14] = 0x00;
|
|
833
|
+
packetScratch_[15] = 0x00;
|
|
834
|
+
writeLe32(packetScratch_.data() + 16,
|
|
835
|
+
static_cast<std::uint32_t>(payloadBytes));
|
|
836
|
+
std::memcpy(packetScratch_.data() + 20, [jpeg bytes], payloadBytes);
|
|
837
|
+
|
|
838
|
+
for (std::size_t offset = 0; offset < packetScratch_.size();
|
|
839
|
+
offset += kReportBytes) {
|
|
840
|
+
if (!writeReport(packetScratch_.data() + offset))
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
bool writeUsb(const std::uint8_t *data, std::size_t size, UInt32 timeoutMs,
|
|
847
|
+
const char *label) {
|
|
848
|
+
if (!usbInterface_ || !usbOutPipe_ || !data)
|
|
849
|
+
return false;
|
|
850
|
+
IOReturn result =
|
|
851
|
+
(*usbInterface_)
|
|
852
|
+
->WritePipeTO(usbInterface_, usbOutPipe_,
|
|
853
|
+
const_cast<std::uint8_t *>(data),
|
|
854
|
+
static_cast<UInt32>(size), timeoutMs, timeoutMs);
|
|
855
|
+
if (result != kIOReturnSuccess) {
|
|
856
|
+
NSLog(@"[gea-thermalright] USB %s write failed: 0x%08x (%zu bytes)",
|
|
857
|
+
label ? label : "bulk", result, size);
|
|
858
|
+
(*usbInterface_)->ClearPipeStallBothEnds(usbInterface_, usbOutPipe_);
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
return true;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
void readLyAck() {
|
|
865
|
+
if (!usbInterface_ || !usbInPipe_)
|
|
866
|
+
return;
|
|
867
|
+
std::uint8_t ack[kReportBytes] = {};
|
|
868
|
+
UInt32 size = sizeof(ack);
|
|
869
|
+
IOReturn result =
|
|
870
|
+
(*usbInterface_)
|
|
871
|
+
->ReadPipeTO(usbInterface_, usbInPipe_, ack, &size, 1000, 1000);
|
|
872
|
+
if (result == kIOReturnSuccess)
|
|
873
|
+
return;
|
|
874
|
+
if (!lyAckWarningLogged_) {
|
|
875
|
+
NSLog(@"[gea-thermalright] LY frame ack not available: 0x%08x", result);
|
|
876
|
+
lyAckWarningLogged_ = true;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
bool writeLyJpeg(NSData *jpeg) {
|
|
881
|
+
const std::size_t payloadBytes = [jpeg length];
|
|
882
|
+
const std::uint8_t *payload =
|
|
883
|
+
static_cast<const std::uint8_t *>([jpeg bytes]);
|
|
884
|
+
if (!payload || payloadBytes == 0)
|
|
885
|
+
return false;
|
|
886
|
+
|
|
887
|
+
std::size_t chunkCount = payloadBytes / kLyChunkPayloadBytes + 1;
|
|
888
|
+
const std::size_t lastChunkBytes = payloadBytes % kLyChunkPayloadBytes;
|
|
889
|
+
std::size_t paddedChunkCount = chunkCount;
|
|
890
|
+
const std::size_t remainder = paddedChunkCount % 4;
|
|
891
|
+
if (remainder != 0)
|
|
892
|
+
paddedChunkCount += 4 - remainder;
|
|
893
|
+
|
|
894
|
+
lyScratch_.assign(paddedChunkCount * kLyChunkBytes, 0);
|
|
895
|
+
for (std::size_t i = 0; i < chunkCount; i++) {
|
|
896
|
+
std::uint8_t *chunk = lyScratch_.data() + i * kLyChunkBytes;
|
|
897
|
+
const bool isLast = i == chunkCount - 1;
|
|
898
|
+
const std::size_t dataBytes =
|
|
899
|
+
isLast ? lastChunkBytes : kLyChunkPayloadBytes;
|
|
900
|
+
chunk[0] = 0x01;
|
|
901
|
+
chunk[1] = 0xFF;
|
|
902
|
+
writeLe32(chunk + 2, static_cast<std::uint32_t>(payloadBytes));
|
|
903
|
+
writeLe16(chunk + 6, static_cast<std::uint16_t>(dataBytes));
|
|
904
|
+
chunk[8] = 0x01;
|
|
905
|
+
writeLe16(chunk + 9, static_cast<std::uint16_t>(chunkCount));
|
|
906
|
+
writeLe16(chunk + 11, static_cast<std::uint16_t>(i));
|
|
907
|
+
const std::size_t srcOffset = i * kLyChunkPayloadBytes;
|
|
908
|
+
if (dataBytes > 0) {
|
|
909
|
+
std::memcpy(chunk + kLyChunkHeaderBytes, payload + srcOffset,
|
|
910
|
+
dataBytes);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
std::size_t offset = 0;
|
|
915
|
+
while (offset < lyScratch_.size()) {
|
|
916
|
+
const std::size_t remaining = lyScratch_.size() - offset;
|
|
917
|
+
const std::size_t writeBytes =
|
|
918
|
+
remaining >= kLyBurstBytes ? kLyBurstBytes
|
|
919
|
+
: std::min<std::size_t>(2048, remaining);
|
|
920
|
+
if (!writeUsb(lyScratch_.data() + offset, writeBytes, 5000, "LY frame"))
|
|
921
|
+
return false;
|
|
922
|
+
offset += writeBytes;
|
|
923
|
+
}
|
|
924
|
+
readLyAck();
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
int width_ = kDefaultWidth;
|
|
929
|
+
int height_ = kDefaultHeight;
|
|
930
|
+
int preferredProductId_ = GEA_MACOS_THERMALRIGHT_PRODUCT_ID;
|
|
931
|
+
int fps_ = 30;
|
|
932
|
+
double jpegQuality_ = 0.45;
|
|
933
|
+
IOHIDManagerRef manager_ = nullptr;
|
|
934
|
+
IOHIDDeviceRef device_ = nullptr;
|
|
935
|
+
IOUSBInterfaceInterface **usbInterface_ = nullptr;
|
|
936
|
+
UInt8 usbInPipe_ = 0;
|
|
937
|
+
UInt8 usbOutPipe_ = 0;
|
|
938
|
+
const DeviceProfile *activeProfile_ = nullptr;
|
|
939
|
+
bool connected_ = false;
|
|
940
|
+
bool lyAckWarningLogged_ = false;
|
|
941
|
+
Clock::time_point nextSubmit_{};
|
|
942
|
+
Clock::time_point lastConnectAttempt_{};
|
|
943
|
+
Clock::time_point statsWindowStart_{};
|
|
944
|
+
int statsFrames_ = 0;
|
|
945
|
+
std::size_t statsPayloadBytes_ = 0;
|
|
946
|
+
int statsReportCount_ = 0;
|
|
947
|
+
std::int64_t statsEncodeUs_ = 0;
|
|
948
|
+
std::int64_t statsWriteUs_ = 0;
|
|
949
|
+
std::vector<std::uint8_t> rgbaScratch_;
|
|
950
|
+
std::vector<std::uint8_t> packetScratch_;
|
|
951
|
+
std::vector<std::uint8_t> lyScratch_;
|
|
952
|
+
std::mutex frameMutex_;
|
|
953
|
+
std::condition_variable frameCv_;
|
|
954
|
+
std::thread worker_;
|
|
955
|
+
bool workerRunning_ = false;
|
|
956
|
+
bool stopWorker_ = false;
|
|
957
|
+
std::vector<std::uint16_t> pendingPixels_;
|
|
958
|
+
int pendingWidth_ = 0;
|
|
959
|
+
int pendingHeight_ = 0;
|
|
960
|
+
std::uint64_t pendingGeneration_ = 0;
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
ThermalrightHidDisplay &display() {
|
|
964
|
+
static ThermalrightHidDisplay instance;
|
|
965
|
+
return instance;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
} // namespace
|
|
969
|
+
|
|
970
|
+
namespace gea::macos::thermalright {
|
|
971
|
+
|
|
972
|
+
bool enabled() {
|
|
973
|
+
#if GEA_MACOS_THERMALRIGHT_DISPLAY_TARGET
|
|
974
|
+
if (const char *value = std::getenv("GEA_MACOS_THERMALRIGHT_DISPLAY")) {
|
|
975
|
+
return truthy(value);
|
|
976
|
+
}
|
|
977
|
+
return true;
|
|
978
|
+
#else
|
|
979
|
+
return selectedByEnv();
|
|
980
|
+
#endif
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
int width() { return display().width(); }
|
|
984
|
+
int height() { return display().height(); }
|
|
985
|
+
int fps() { return display().fps(); }
|
|
986
|
+
|
|
987
|
+
bool submitRgb565(const std::uint16_t *pixels, int width, int height) {
|
|
988
|
+
if (!enabled())
|
|
989
|
+
return false;
|
|
990
|
+
return display().submitRgb565(pixels, width, height);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
void shutdown() { display().close(); }
|
|
994
|
+
|
|
995
|
+
} // namespace gea::macos::thermalright
|