@daugasauron/dolly-host-touch 0.0.0-stage → 0.1.4-rc.0

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 CHANGED
@@ -1,3 +1,4 @@
1
- # Temporary Holding Version
1
+ # @daugasauron/dolly-host-touch
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Dolly's `touch@0` host module: fingers on a touchscreen: every one that is down, each with its id. Give the URL of its
4
+ `module.json` to `boot()`'s `modules`; @daugasauron/dolly's README says how.
package/abi.mjs ADDED
@@ -0,0 +1,19 @@
1
+ // Generated from host/touch/dolly-touch-0.wat, host/touch/touch.h, include/dolly/process.h.
2
+ export const DOLLY_TOUCH_HEADER_SIZE = 16;
3
+ export const DOLLY_TOUCH_RECORD_SIZE = 32;
4
+ export const DOLLY_TOUCH_CAPACITY = 512;
5
+ export const DOLLY_TOUCH_WORD_READ = 0;
6
+ export const DOLLY_TOUCH_WORD_WRITE = 1;
7
+ export const DOLLY_TOUCH_WORD_LEASE = 2;
8
+ export const DOLLY_TOUCH_WORD_ENABLED = 3;
9
+ export const DOLLY_TOUCH_DOWN = 1;
10
+ export const DOLLY_TOUCH_MOVE = 2;
11
+ export const DOLLY_TOUCH_UP = 3;
12
+ export const DOLLY_TOUCH_CANCEL = 4;
13
+ export const DOLLY_TOUCH_FINGERS = 10;
14
+ export const DOLLY_TOUCH_ACQUIRE = 92;
15
+ export const DOLLY_TOUCH_READ = 93;
16
+ export const DOLLY_TOUCH_RELEASE = 94;
17
+ export const DOLLY_TOUCH_READ_RECORDS = 64;
18
+ export const DOLLY_ERRNO = Object.freeze({ E2BIG: 1, EACCES: 2, EADDRINUSE: 3, EADDRNOTAVAIL: 4, EAFNOSUPPORT: 5, EAGAIN: 6, EALREADY: 7, EBADF: 8, EBADMSG: 9, EBUSY: 10, ECANCELED: 11, ECHILD: 12, ECONNABORTED: 13, ECONNREFUSED: 14, ECONNRESET: 15, EDEADLK: 16, EDESTADDRREQ: 17, EDOM: 18, EDQUOT: 19, EEXIST: 20, EFAULT: 21, EFBIG: 22, EHOSTUNREACH: 23, EIDRM: 24, EILSEQ: 25, EINPROGRESS: 26, EINTR: 27, EINVAL: 28, EIO: 29, EISCONN: 30, EISDIR: 31, ELOOP: 32, EMFILE: 33, EMLINK: 34, EMSGSIZE: 35, EMULTIHOP: 36, ENAMETOOLONG: 37, ENETDOWN: 38, ENETRESET: 39, ENETUNREACH: 40, ENFILE: 41, ENOBUFS: 42, ENODEV: 43, ENOENT: 44, ENOEXEC: 45, ENOLCK: 46, ENOLINK: 47, ENOMEM: 48, ENOMSG: 49, ENOPROTOOPT: 50, ENOSPC: 51, ENOSYS: 52, ENOTCONN: 53, ENOTDIR: 54, ENOTEMPTY: 55, ENOTRECOVERABLE: 56, ENOTSOCK: 57, ENOTTY: 59, ENXIO: 60, EOVERFLOW: 61, EOWNERDEAD: 62, EPERM: 63, EPIPE: 64, EPROTO: 65, EPROTONOSUPPORT: 66, EPROTOTYPE: 67, ERANGE: 68, EROFS: 69, ESPIPE: 70, ESRCH: 71, ESTALE: 72, ETIMEDOUT: 73, ETXTBSY: 74, EXDEV: 75, ENODATA: 116, ENOTSUP: 138 });
19
+ export const DOLLY_TOUCH_ABI_DIGEST = "515bdcabf63b7396d485abded3a066c0c55771b0eedf4de824f3f728e645cf1c";
package/module.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "touch",
3
+ "version": 0,
4
+ "gives": "fingers on a touchscreen: every one that is down, each with its id",
5
+ "dependencies": ["runtime@0"],
6
+ "phase": "image",
7
+ "imports": [],
8
+ "host": "touch.mjs",
9
+ "contracts": ["dolly-touch-0.wat"],
10
+ "process": [],
11
+ "headers": ["touch.h", "touch-abi.h"],
12
+ "kernel": ["kernel.c"],
13
+ "client": ["client.c"],
14
+ "package": "libdolly-touch"
15
+ }
package/package.json CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
+ "version": "0.1.4-rc.0",
3
+ "type": "module",
2
4
  "name": "@daugasauron/dolly-host-touch",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
5
+ "license": "MIT",
6
+ "dependencies": {
7
+ "@daugasauron/dolly": "0.1.4-rc.0"
8
+ },
9
+ "description": "Dolly's touch@0 host module: fingers on a touchscreen: every one that is down, each with its id.",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/daugasauron/dolly.git"
13
+ }
14
+ }
package/touch.mjs ADDED
@@ -0,0 +1,166 @@
1
+ import * as T from "./abi.mjs";
2
+ export { DOLLY_TOUCH_ABI_DIGEST as digest } from "./abi.mjs";
3
+
4
+ const word = { read: T.DOLLY_TOUCH_WORD_READ, write: T.DOLLY_TOUCH_WORD_WRITE,
5
+ lease: T.DOLLY_TOUCH_WORD_LEASE, enabled: T.DOLLY_TOUCH_WORD_ENABLED };
6
+ // Moves leave room for every finger's down and its end.
7
+ const moveReserve = 2 * T.DOLLY_TOUCH_FINGERS;
8
+
9
+ // The Worker's end of the touch mailbox, the one producer of its records: the
10
+ // page sends it the fingers it took. A finger is delivered under the lease its
11
+ // down was written for; the fingers of an earlier lease, and those refused,
12
+ // are nobody's until they end.
13
+ export class TouchTransport {
14
+ // The ring's size is the contract's; a test may shrink it.
15
+ constructor(buffer, address, { capacity = T.DOLLY_TOUCH_CAPACITY } = {}) {
16
+ if (!(buffer instanceof SharedArrayBuffer)) throw new Error("Dolly touch transport requires shared Wasm memory");
17
+ if (!Number.isSafeInteger(address) || address <= 0 || address % 4 !== 0 || !Number.isSafeInteger(capacity) ||
18
+ capacity <= moveReserve || (capacity & (capacity - 1)) !== 0 ||
19
+ address > buffer.byteLength - T.DOLLY_TOUCH_HEADER_SIZE - capacity * T.DOLLY_TOUCH_RECORD_SIZE) {
20
+ throw new Error("Dolly supplied an invalid touch mailbox");
21
+ }
22
+ this.view = new DataView(buffer);
23
+ this.words = new Int32Array(buffer);
24
+ this.address = address;
25
+ this.capacity = capacity;
26
+ this.fingers = new Map();
27
+ this.ignored = new Set();
28
+ this.heldLease = 0;
29
+ this.nextId = 1;
30
+ this.dropped = 0;
31
+ }
32
+
33
+ load(name) { return Atomics.load(this.words, this.address / 4 + word[name]) >>> 0; }
34
+
35
+ // The holder's lease word: zero while no program holds the lease.
36
+ lease() { return this.load("lease"); }
37
+
38
+ free() { return this.capacity - ((this.load("write") - this.load("read")) >>> 0); }
39
+
40
+ write(id, phase, { x, y, pressure, time }) {
41
+ const write = this.load("write");
42
+ const offset = this.address + T.DOLLY_TOUCH_HEADER_SIZE + (write & (this.capacity - 1)) * T.DOLLY_TOUCH_RECORD_SIZE;
43
+ [id, phase, x, y, pressure, this.dropped, time, this.heldLease]
44
+ .forEach((value, index) => this.view.setUint32(offset + index * 4, value >>> 0, true));
45
+ this.dropped = 0;
46
+ Atomics.store(this.words, this.address / 4 + word.write, (write + 1) | 0);
47
+ }
48
+
49
+ // One Pointer Event of a finger as samples ({ x, y, pressure, time }, the
50
+ // coalesced moves of a pointermove). Returns whether the finger is this
51
+ // module's, so that it is not also the pointer.
52
+ finger(phase, pointerId, samples) {
53
+ const lease = this.lease();
54
+ if (lease !== this.heldLease) {
55
+ for (const pointer of this.fingers.keys()) this.ignored.add(pointer);
56
+ this.fingers.clear();
57
+ this.heldLease = lease;
58
+ }
59
+ const id = this.fingers.get(pointerId), ends = phase === T.DOLLY_TOUCH_UP || phase === T.DOLLY_TOUCH_CANCEL;
60
+ if (id === undefined) {
61
+ if (this.ignored.has(pointerId)) {
62
+ if (ends) this.ignored.delete(pointerId);
63
+ return true;
64
+ }
65
+ if (phase !== T.DOLLY_TOUCH_DOWN || !lease) return false;
66
+ // The ring keeps room for this down and the end of every finger that is down.
67
+ if (this.fingers.size === T.DOLLY_TOUCH_FINGERS || this.free() < this.fingers.size + 2) {
68
+ this.dropped++;
69
+ this.ignored.add(pointerId);
70
+ return true;
71
+ }
72
+ const assigned = this.nextId;
73
+ this.nextId = assigned === 0xffffffff ? 1 : assigned + 1;
74
+ this.fingers.set(pointerId, assigned);
75
+ this.write(assigned, phase, samples.at(-1));
76
+ } else if (ends) {
77
+ this.fingers.delete(pointerId);
78
+ this.write(id, phase, samples.at(-1));
79
+ } else {
80
+ for (const sample of samples) {
81
+ if (this.free() > moveReserve) this.write(id, T.DOLLY_TOUCH_MOVE, sample);
82
+ else this.dropped++;
83
+ }
84
+ }
85
+ Atomics.notify(this.words, this.address / 4 + word.write);
86
+ return true;
87
+ }
88
+
89
+ // Tells the kernel whether this page listens: without it no lease is given.
90
+ enable(enabled) {
91
+ Atomics.store(this.words, this.address / 4 + word.enabled, enabled ? 1 : 0);
92
+ }
93
+ }
94
+
95
+ const phases = { pointerdown: T.DOLLY_TOUCH_DOWN, pointermove: T.DOLLY_TOUCH_MOVE,
96
+ pointerup: T.DOLLY_TOUCH_UP, pointercancel: T.DOLLY_TOUCH_CANCEL };
97
+
98
+ // The page's fingers on the canvas. They are taken before input@0's listeners
99
+ // on the canvas see them, and only while a program holds the lease, as the
100
+ // Worker last reported it: without one the terminal's scroll and pinch are
101
+ // input@0's. A finger taken stays this module's until it ends.
102
+ export function browser({ canvas, surfaceSize, send }) {
103
+ let started = false, lease = 0;
104
+ const taken = new Set();
105
+ // Positions are pixels of the frame the canvas shows, as input@0's pointer records are.
106
+ function sample(event) {
107
+ const bounds = canvas.getBoundingClientRect(), { width, height } = surfaceSize() ?? canvas;
108
+ return { x: Math.round(bounds.width === 0 ? 0 : (event.clientX - bounds.left) * width / bounds.width),
109
+ y: Math.round(bounds.height === 0 ? 0 : (event.clientY - bounds.top) * height / bounds.height),
110
+ pressure: Math.round(event.pressure * 1000), time: Math.floor(event.timeStamp) };
111
+ }
112
+ function listen(event) {
113
+ if (!started || event.pointerType !== "touch" || event.target !== canvas) return;
114
+ const phase = phases[event.type], { pointerId } = event;
115
+ if (phase === T.DOLLY_TOUCH_DOWN) {
116
+ if (!lease) return;
117
+ taken.add(pointerId);
118
+ } else if (!taken.has(pointerId)) return;
119
+ else if (phase !== T.DOLLY_TOUCH_MOVE) taken.delete(pointerId);
120
+ const coalesced = phase === T.DOLLY_TOUCH_MOVE ? event.getCoalescedEvents?.() ?? [] : [];
121
+ send({ type: "touch-finger", phase, pointerId, samples: (coalesced.length ? coalesced : [event]).map(sample) });
122
+ event.preventDefault();
123
+ event.stopPropagation();
124
+ }
125
+ // A finger held still is a long press, the program's to read, not the page's menu.
126
+ function menu(event) {
127
+ if (lease) event.preventDefault();
128
+ }
129
+ for (const type of Object.keys(phases)) window.addEventListener(type, listen, { capture: true });
130
+ canvas.addEventListener("contextmenu", menu);
131
+ return {
132
+ page: { get touchTransport() { return started ? { lease: () => lease } : undefined; } },
133
+ messages: { "touch-lease"(message) { lease = message.lease; } },
134
+ start() { started = true; },
135
+ dispose() {
136
+ for (const type of Object.keys(phases)) window.removeEventListener(type, listen, { capture: true });
137
+ canvas.removeEventListener("contextmenu", menu);
138
+ started = false;
139
+ },
140
+ };
141
+ }
142
+
143
+ // The holder's lease is read at each service tick and reported when it changes.
144
+ export function worker({ get, send }) {
145
+ let transport, stop, reported = 0;
146
+ return {
147
+ messages: { "touch-finger"({ phase, pointerId, samples }) { transport?.finger(phase, pointerId, samples); } },
148
+ service() {
149
+ const lease = transport?.lease() ?? 0;
150
+ if (transport && lease !== reported) send({ type: "touch-lease", lease: reported = lease });
151
+ },
152
+ start({ kernel, memory }) {
153
+ const address = Number(kernel.dolly_touch_mailbox_address());
154
+ transport = new TouchTransport(memory.buffer, address);
155
+ transport.enable(true);
156
+ // The finger's records notify the write word: a program waiting for a finger resumes then.
157
+ stop = get("runtime").serviceOnNotify(transport.words, address / 4 + word.write);
158
+ return {};
159
+ },
160
+ dispose() {
161
+ transport?.enable(false);
162
+ transport = undefined;
163
+ stop?.();
164
+ },
165
+ };
166
+ }