-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpredictive_touch.zig
More file actions
260 lines (219 loc) · 14 KB
/
Copy pathpredictive_touch.zig
File metadata and controls
260 lines (219 loc) · 14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
// START_AI_HEADER
// MODULE: sys-daemon-zig/src/predictive_touch.zig
// PURPOSE: Predictive touch — 64-byte cache-line TouchEvent ring buffer + integer-only weighted-least-squares extrapolation that fires a CMD_PRE_THAW (id=4) packet to lifecycled ~75ms before the finger lands on an icon.
// INTENT: Whole module must fit in L1 instruction cache (no float, no allocator, no heap). BSS ring + atomic-free head/count. Only fires on hover/move (press/lift are post-commit and don't need prewarming). The whole 5-point ring + 4-byte packet is what we send across /var/run/bsdos-lifecycle.sock.
// DEPENDENCIES: std (time, math, net, debug).
// PUBLIC_API: TouchState enum, TouchEvent extern struct (64 B), push_coordinate(event), calculate_vector_target() ?u32, process_event(event), make_event(x, y, state) TouchEvent.
// END_AI_HEADER
// bsdOS Predictive Touch — предиктивный модуль тачскрина.
//
// Алгоритм:
// 1. Кольцевой буфер 5 точек (стек, 0 аллокаций)
// 2. Взвешенная линейная аппроксимация по последним 3 точкам
// 3. Экстраполяция на 75мс вперёд (фиксированная точка *16, только ALU)
// 4. Маппинг предсказанной координаты на сетку иконок → App_ID
// 5. CMD_PRE_THAW (id=4) binary packet → lifecycled Unix socket
//
// Нет float, нет аллокатора, нет heap — весь модуль живёт в L1-кэше инструкций.
// Компактность: zig build -Doptimize=ReleaseSmall → ~6-8 KiB
const std = @import("std");
// ── Типы данных ───────────────────────────────────────────────────────────────
pub const TouchState = enum(u8) {
hover = 0, // палец над стеклом (proximity sensor / capacitive hover)
move = 1, // касание + движение
press = 2, // статичное нажатие
lift = 3, // отрыв
};
// Структура выровнена на 64 байта = одна cache-line Cortex-A53.
// extern struct: C-совместимый layout, никаких zig-специфичных перестановок.
pub const TouchEvent = extern struct {
x: u16, // координата X (пиксели)
y: u16, // координата Y (пиксели)
_pad0: u32 = 0, // выравнивание u64 на 8 байт
timestamp_ms: u64, // монотонная метка (std.time.milliTimestamp)
touch_state: TouchState, // hover/move/press/lift
_pad1: [47]u8 = .{0} ** 47, // заполнение до 64 байт
comptime {
// Гарантия cache-line выравнивания на этапе компиляции
std.debug.assert(@sizeOf(TouchEvent) == 64);
std.debug.assert(@offsetOf(TouchEvent, "timestamp_ms") == 8);
}
};
// Бинарный пакет CMD_PRE_THAW — тот же 4-байтовый fixed-size протокол
// что в bsdos-agent и бинарной шине
const PreThawPacket = extern struct {
cmd_id: u8, // = CMD_PRE_THAW = 4
arg_len: u8, // = 0
payload: u16, // App_ID (little-endian)
comptime { std.debug.assert(@sizeOf(PreThawPacket) == 4); }
};
const CMD_PRE_THAW: u8 = 4;
// ── Параметры экрана и иконочной сетки ───────────────────────────────────────
// PinePhone / Banana Pi: 720×1440 px, 4×6 сетка иконок
const SCREEN_W: u16 = 720;
const SCREEN_H: u16 = 1440;
const GRID_COLS: u16 = 4;
const GRID_ROWS: u16 = 6;
const CELL_W: u16 = SCREEN_W / GRID_COLS; // 180 px
const CELL_H: u16 = SCREEN_H / GRID_ROWS; // 240 px
// Маппинг координаты → App_ID (101..124 для 4×6 сетки)
// Inline: компилятор вставит 2 деления и 1 сложение — 3 инструкции
inline fn app_id_at(x: u16, y: u16) ?u32 {
if (x >= SCREEN_W or y >= SCREEN_H) return null;
const col = x / CELL_W;
const row = y / CELL_H;
return @as(u32, row) * GRID_COLS + @as(u32, col) + 101;
}
// ── Кольцевой буфер (стек, 0 аллокаций) ─────────────────────────────────────
const RING_CAP: usize = 5;
// Глобальный state модуля — в BSS (zero-initialized, нет runtime cost).
// Без Allocator: вся жизнь в стековом/BSS-сегменте.
var ring: [RING_CAP]TouchEvent = undefined;
var head: usize = 0; // следующая позиция для записи
var count: usize = 0; // число заполненных слотов (0..5)
// push_coordinate:start
// purpose: append one TouchEvent into the 5-slot ring (overwriting the oldest once full) and advance head with wrapping add.
// input: event — full TouchEvent to record (64 bytes, copied by value into the BSS ring).
// output: void.
// sideEffects: mutates the module-global ring/head/count; no I/O, no allocation.
pub fn push_coordinate(event: TouchEvent) void {
ring[head] = event;
head = (head +% 1) % RING_CAP; // %% — wrapping add (нет UB при overflow)
if (count < RING_CAP) count += 1;
}
// push_coordinate:end
// i=0 → последняя точка, i=1 → предпоследняя, i=2 → ...
// Inline: развернётся в прямой доступ к массиву
inline fn peek(i: usize) ?TouchEvent {
if (i >= count) return null;
const idx = (head +% RING_CAP -% 1 -% i) % RING_CAP;
return ring[idx];
}
// ── Предиктивный движок (вся арифметика — целочисленная, на регистрах) ────────
// Фиксированная точка: координаты умножаем на FP_SCALE для субпиксельной точности.
// FP_SCALE=16 → 4 дробных бита → точность 0.0625 px — достаточно для иконок 180px
const FP_SCALE: i32 = 16;
const PREDICT_MS: i64 = 75; // середина окна 50-100мс
const MIN_MOTION_PX: i32 = 20; // минимальное суммарное движение для уверенного предсказания
// Возвращает App_ID если предсказание достоверно, иначе null.
// Нет float, нет аллокатора, нет ветвлений кроме guard-условий.
// calculate_vector_target:start
// purpose: read the last 3 points from the ring, fit a weighted (2:1) velocity vector, project 75 ms forward in fixed-point arithmetic, clamp to the screen, and return the App_ID under the predicted point.
// input: none (reads the module-global ring).
// output: ?u32 — the App_ID (101..124 for a 4×6 grid) at the predicted point, or null if fewer than 3 points, timestamps are not strictly increasing, or total motion < MIN_MOTION_PX.
// sideEffects: none (pure over the ring state).
pub fn calculate_vector_target() ?u32 {
if (count < 3) return null; // нужно минимум 3 точки
// Последние 3 точки: p0 → p1 → p2 (p2 = текущая)
const p0 = peek(2) orelse return null;
const p1 = peek(1) orelse return null;
const p2 = peek(0) orelse return null;
// Временные интервалы (мс), знаковые для безопасности
const dt01: i64 = @as(i64, @intCast(p1.timestamp_ms)) -
@as(i64, @intCast(p0.timestamp_ms));
const dt12: i64 = @as(i64, @intCast(p2.timestamp_ms)) -
@as(i64, @intCast(p1.timestamp_ms));
if (dt01 <= 0 or dt12 <= 0) return null; // отвергаем стаканированные события
// Пространственные дельты (пиксели, знаковые)
const dx01: i32 = @as(i32, @intCast(p1.x)) - @as(i32, @intCast(p0.x));
const dy01: i32 = @as(i32, @intCast(p1.y)) - @as(i32, @intCast(p0.y));
const dx12: i32 = @as(i32, @intCast(p2.x)) - @as(i32, @intCast(p1.x));
const dy12: i32 = @as(i32, @intCast(p2.y)) - @as(i32, @intCast(p1.y));
// Проверка достоверности — палец должен двигаться
const total_motion = @abs(dx12) + @abs(dx01) + @abs(dy12) + @abs(dy01);
if (total_motion < MIN_MOTION_PX) return null;
// Взвешенная скорость по методу трёх точек:
// v = (2*Δ12 + 1*Δ01) / (2*dt12 + dt01)
//
// Вес 2:1 — последнее движение важнее, сглаживает случайный дрейф.
// Дробь не вычисляем отдельно — сразу умножаем числитель на PREDICT_MS
// и на FP_SCALE для фиксированной точки.
//
// shift_x = dx_total * FP_SCALE * PREDICT_MS / dt_total
//
// Порядок умножения: сначала на FP_SCALE (малое), потом PREDICT_MS,
// чтобы не переполнить i32. Все промежуточные значения в i64.
const dt_total: i64 = 2 * dt12 + dt01; // всегда > 0
const shift_x: i32 = @intCast(
((@as(i64, 2 * dx12 + dx01)) * FP_SCALE * PREDICT_MS) / dt_total
);
const shift_y: i32 = @intCast(
((@as(i64, 2 * dy12 + dy01)) * FP_SCALE * PREDICT_MS) / dt_total
);
// Предсказанная координата (убираем FP_SCALE, остаётся в пикселях)
const xp = @as(i32, @intCast(p2.x)) + @divTrunc(shift_x, FP_SCALE);
const yp = @as(i32, @intCast(p2.y)) + @divTrunc(shift_y, FP_SCALE);
// Клампинг к экрану — @intCast не паникует после clamp
const xc: u16 = @intCast(std.math.clamp(xp, 0, SCREEN_W - 1));
const yc: u16 = @intCast(std.math.clamp(yp, 0, SCREEN_H - 1));
return app_id_at(xc, yc);
}
// calculate_vector_target:end
// ── Отправка CMD_PRE_THAW в lifecycled ───────────────────────────────────────
const LIFECYCLE_SOCK = "/var/run/bsdos-lifecycle.sock";
// send_pre_thaw:start
// purpose: pack the app_id into a 4-byte PreThawPacket (cmd_id=CMD_PRE_THAW=4, payload=app_id truncated to u16) and write it to /var/run/bsdos-lifecycle.sock for lifecycled to pre-warm the app's jail.
// input: app_id — the predicted App_ID; only the low 16 bits are transmitted (the grid fits in 101..124).
// output: void; lifecycle not listening is logged and ignored.
// sideEffects: opens (and immediately closes) AF_UNIX SOCK_STREAM to LIFECYCLE_SOCK; one 4-byte write.
fn send_pre_thaw(app_id: u32) void {
const pkt = PreThawPacket{
.cmd_id = CMD_PRE_THAW,
.arg_len = 0,
.payload = @truncate(app_id), // u32 → u16, App_ID 101-124 влезает
};
// Zero-copy: структура → байты без парсинга (тот же трюк что в bsdos-agent)
const bytes: [4]u8 = @bitCast(pkt);
const stream = std.net.connectUnixSocket(LIFECYCLE_SOCK) catch |err| {
std.debug.print("[touch] lifecycle connect err: {} (app_id={})\n",
.{ err, app_id });
return;
};
defer stream.close();
stream.writeAll(&bytes) catch |err| {
std.debug.print("[touch] pre_thaw send err: {}\n", .{err});
};
}
// send_pre_thaw:end
// ── Публичный API ─────────────────────────────────────────────────────────────
// Вызывать из цикла evdev-чтения в main.zig (~8мс = 120Hz polling)
// process_event:start
// purpose: per-touch entry point — push the event into the ring, then on hover/move (not on press/lift) try to predict a target App_ID and fire send_pre_thaw.
// input: event — TouchEvent straight from the evdev reader.
// output: void.
// sideEffects: mutates the ring; on a successful prediction, opens and writes 4 bytes to LIFECYCLE_SOCK.
pub fn process_event(event: TouchEvent) void {
push_coordinate(event);
// Предиктивная логика только в Hover и Move — до физического касания.
// В момент Press предсказание уже не нужно (пользователь уже нажал).
switch (event.touch_state) {
.hover, .move => {},
.press, .lift => return,
}
if (calculate_vector_target()) |app_id| {
std.debug.print("[touch] predict → app_id={d} ({}ms ahead)\n",
.{ app_id, PREDICT_MS });
send_pre_thaw(app_id);
}
}
// process_event:end
// ── Вспомогательное: создать TouchEvent из сырых evdev-данных ────────────────
// FreeBSD evdev: /dev/input/event0 → struct input_event { type, code, value }
// Этот хелпер конвертирует x+y из EV_ABS событий в наш TouchEvent.
// Вызывается из main.zig после чтения пары (ABS_X, ABS_Y).
// make_event:start
// purpose: helper for main.zig — wrap a raw (x, y, state) tuple from a pair of EV_ABS events into a full TouchEvent with the current monotonic timestamp.
// input: x, y — pixel coordinates from EV_ABS; state — TouchState to attach.
// output: a TouchEvent with timestamp_ms = std.time.milliTimestamp() and the 47-byte pad zeroed.
// sideEffects: none.
pub fn make_event(x: u16, y: u16, state: TouchState) TouchEvent {
return TouchEvent{
.x = x,
.y = y,
.timestamp_ms = @intCast(std.time.milliTimestamp()),
.touch_state = state,
._pad0 = 0,
._pad1 = .{0} ** 47,
};
}
// make_event:end