Skip to content

Commit 13c978a

Browse files
Eric Migicovskyclaude
andcommitted
qemu-wasm: wire up the touchscreen and audio output
Touch: pebble_touch.c gains a browser injection path — the page writes {seq, down, x, y} in display pixels with Atomics and an 8 ms virtual-clock poll raises the touch IRQ on state changes, mirroring the input-layer handler. Canvas pointer events drive it; verified in Chromium by touch-scrolling the launcher. Audio: the pebble_audio drain stub now mirrors every DATA sample into a shared int16 ring with rate/volume/playing mirrors; the page drains it into WebAudio on a 60 ms poll, scheduling buffers at a running playhead. The context is created on the first canvas touch to satisfy autoplay policies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
1 parent ad14a94 commit 13c978a

6 files changed

Lines changed: 306 additions & 2 deletions

File tree

tools/qemu-wasm/jit/overlay/hw/misc/pebble_audio.c

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
#include "hw/core/qdev-properties.h"
3131
#include "hw/core/qdev-properties-system.h"
3232

33+
#ifdef EMSCRIPTEN
34+
#include <emscripten/emscripten.h>
35+
#endif
36+
3337
#define TYPE_PEBBLE_AUDIO "pebble-audio"
3438
OBJECT_DECLARE_SIMPLE_TYPE(PblAudio, PEBBLE_AUDIO)
3539

@@ -68,6 +72,39 @@ struct PblAudio {
6872
char *audiodev; /* accepted and ignored */
6973
};
7074

75+
#ifdef EMSCRIPTEN
76+
/* Browser playback: every sample the firmware pushes via DATA is also
77+
* appended to this ring; the page drains it into WebAudio. Head/tail
78+
* are free-running sample counters (index = counter % size). rate,
79+
* volume and playing mirror the device registers so the page can
80+
* (re)configure its AudioContext. */
81+
#define WASM_AUDIO_RING 32768
82+
83+
static int16_t s_wasm_audio_buf[WASM_AUDIO_RING];
84+
static struct {
85+
uint32_t buf, size;
86+
uint32_t head, tail;
87+
uint32_t rate, volume, playing;
88+
} s_wasm_audio_ctrl;
89+
90+
EMSCRIPTEN_KEEPALIVE void *pebble_wasm_audio_ctrl(void)
91+
{
92+
s_wasm_audio_ctrl.buf = (uint32_t)(uintptr_t)s_wasm_audio_buf;
93+
s_wasm_audio_ctrl.size = WASM_AUDIO_RING;
94+
return &s_wasm_audio_ctrl;
95+
}
96+
97+
static void pbl_audio_wasm_push(int16_t sample)
98+
{
99+
uint32_t head = s_wasm_audio_ctrl.head;
100+
if (head - qatomic_load_acquire(&s_wasm_audio_ctrl.tail) >= WASM_AUDIO_RING) {
101+
return; /* page not draining — drop */
102+
}
103+
s_wasm_audio_buf[head % WASM_AUDIO_RING] = sample;
104+
qatomic_store_release(&s_wasm_audio_ctrl.head, head + 1);
105+
}
106+
#endif
107+
71108
static void pbl_audio_update_irq(PblAudio *s)
72109
{
73110
qemu_set_irq(s->irq, (s->intstat & s->intctrl) != 0);
@@ -160,14 +197,23 @@ static void pbl_audio_write(void *opaque, hwaddr offset,
160197
} else {
161198
pbl_audio_stop(s);
162199
}
200+
#ifdef EMSCRIPTEN
201+
s_wasm_audio_ctrl.playing = s->ctrl & 1;
202+
#endif
163203
break;
164204
case AUDIO_SAMPLERATE:
165205
s->samplerate = value;
206+
#ifdef EMSCRIPTEN
207+
s_wasm_audio_ctrl.rate = s->samplerate;
208+
#endif
166209
break;
167210
case AUDIO_DATA:
168211
if (s->ring_count < RING_BUF_SAMPLES) {
169212
s->ring_count++;
170213
}
214+
#ifdef EMSCRIPTEN
215+
pbl_audio_wasm_push((int16_t)(value & 0xffff));
216+
#endif
171217
break;
172218
case AUDIO_INTCTRL:
173219
s->intctrl = value & INT_BUFAVAIL;
@@ -179,6 +225,9 @@ static void pbl_audio_write(void *opaque, hwaddr offset,
179225
break;
180226
case AUDIO_VOLUME:
181227
s->volume = value & 0x7F;
228+
#ifdef EMSCRIPTEN
229+
s_wasm_audio_ctrl.volume = s->volume;
230+
#endif
182231
break;
183232
default:
184233
qemu_log_mask(LOG_GUEST_ERROR,
@@ -206,6 +255,11 @@ static void pbl_audio_reset(DeviceState *dev)
206255
s->intctrl = 0;
207256
s->intstat = 0;
208257
s->volume = 100;
258+
#ifdef EMSCRIPTEN
259+
s_wasm_audio_ctrl.rate = s->samplerate;
260+
s_wasm_audio_ctrl.volume = s->volume;
261+
s_wasm_audio_ctrl.playing = 0;
262+
#endif
209263
pbl_audio_update_irq(s);
210264
}
211265

tools/qemu-wasm/jit/overlay/hw/misc/pebble_touch.c

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,17 @@
1717

1818
#include "qemu/osdep.h"
1919
#include "qemu/log.h"
20+
#include "qemu/timer.h"
2021
#include "hw/core/irq.h"
2122
#include "hw/core/sysbus.h"
2223
#include "hw/core/qdev-properties.h"
2324
#include "ui/console.h"
2425
#include "ui/input.h"
2526

27+
#ifdef EMSCRIPTEN
28+
#include <emscripten/emscripten.h>
29+
#endif
30+
2631
#define TYPE_PEBBLE_TOUCH "pebble-touch"
2732
OBJECT_DECLARE_SIMPLE_TYPE(PblTouch, PEBBLE_TOUCH)
2833

@@ -51,8 +56,30 @@ struct PblTouch {
5156
uint32_t display_h;
5257

5358
QemuInputHandlerState *input_handler;
59+
60+
#ifdef EMSCRIPTEN
61+
QEMUTimer *wasm_poll_timer;
62+
uint32_t wasm_last_seq;
63+
#endif
5464
};
5565

66+
#ifdef EMSCRIPTEN
67+
/* Browser touch injection: the page writes display-pixel coordinates
68+
* and the finger state, then bumps seq with Atomics; an 8 ms
69+
* virtual-clock poll applies changes exactly like input-layer events. */
70+
static struct {
71+
uint32_t seq;
72+
uint32_t down;
73+
uint32_t x;
74+
uint32_t y;
75+
} s_wasm_touch;
76+
77+
EMSCRIPTEN_KEEPALIVE void *pebble_wasm_touch_ctrl(void)
78+
{
79+
return &s_wasm_touch;
80+
}
81+
#endif
82+
5683
static void pbl_touch_update_irq(PblTouch *s)
5784
{
5885
qemu_set_irq(s->irq, (s->intctrl & INT_TOUCH_EVENT) &&
@@ -95,6 +122,32 @@ static const QemuInputHandler pbl_touch_input_handler = {
95122
.event = pbl_touch_input_event,
96123
};
97124

125+
#ifdef EMSCRIPTEN
126+
static void pbl_touch_wasm_poll(void *opaque)
127+
{
128+
PblTouch *s = opaque;
129+
uint32_t seq = qatomic_load_acquire(&s_wasm_touch.seq);
130+
131+
if (seq != s->wasm_last_seq) {
132+
s->wasm_last_seq = seq;
133+
uint32_t down = s_wasm_touch.down ? 1 : 0;
134+
uint32_t x = MIN(s_wasm_touch.x, s->display_w - 1);
135+
uint32_t y = MIN(s_wasm_touch.y, s->display_h - 1);
136+
bool changed = (down != s->state) ||
137+
(down && (x != s->x || y != s->y));
138+
s->x = x;
139+
s->y = y;
140+
s->state = down;
141+
if (changed) {
142+
s->intstat |= INT_TOUCH_EVENT;
143+
pbl_touch_update_irq(s);
144+
}
145+
}
146+
timer_mod(s->wasm_poll_timer,
147+
qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + 8);
148+
}
149+
#endif
150+
98151
/* === Register access === */
99152

100153
static uint64_t pbl_touch_read(void *opaque, hwaddr offset, unsigned size)
@@ -157,6 +210,13 @@ static void pbl_touch_realize(DeviceState *dev, Error **errp)
157210
s->input_handler = qemu_input_handler_register(dev,
158211
&pbl_touch_input_handler);
159212
qemu_input_handler_activate(s->input_handler);
213+
214+
#ifdef EMSCRIPTEN
215+
s->wasm_poll_timer = timer_new_ms(QEMU_CLOCK_VIRTUAL,
216+
pbl_touch_wasm_poll, s);
217+
timer_mod(s->wasm_poll_timer,
218+
qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + 8);
219+
#endif
160220
}
161221

162222
static void pbl_touch_init(Object *obj)

tools/qemu-wasm/site-dist/index.html

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ <h1>PebbleOS Emulator</h1>
119119
<button id="btn-select" title="Select (Right / Enter)">Select</button>
120120
<button id="btn-down" title="Down (Arrow Down)">Down</button>
121121
</div>
122-
<div class="hint">Keys: &larr; Back &middot; &uarr;/&darr; Navigate &middot; &rarr;/Enter Select &middot; buttons can be held</div>
122+
<div class="hint">Keys: &larr; Back &middot; &uarr;/&darr; Navigate &middot; &rarr;/Enter Select &middot; buttons can be held &middot; the screen is a touchscreen (click/drag)</div>
123123

124124
<div id="install-panel">
125125
<div id="install-row">
@@ -344,6 +344,101 @@ <h1>PebbleOS Emulator</h1>
344344
el.addEventListener('pointercancel', release);
345345
});
346346

347+
// ================================================================
348+
// Touch: pebble_touch.c polls a shared {seq, down, x, y} struct at
349+
// 8 ms (display-pixel coordinates). Pointer events on the canvas
350+
// write it with Atomics. Emery/gabbro only; harmless elsewhere.
351+
// ================================================================
352+
var touchAddr = 0;
353+
354+
function sendTouch(down, ev) {
355+
if (!qemu) return;
356+
if (!touchAddr) {
357+
if (typeof qemu._pebble_wasm_touch_ctrl !== 'function') return;
358+
touchAddr = Number(qemu._pebble_wasm_touch_ctrl());
359+
if (!touchAddr) return;
360+
}
361+
var r = canvas.getBoundingClientRect();
362+
var x = Math.max(0, Math.min(canvas.width - 1,
363+
Math.round((ev.clientX - r.left) * canvas.width / r.width)));
364+
var y = Math.max(0, Math.min(canvas.height - 1,
365+
Math.round((ev.clientY - r.top) * canvas.height / r.height)));
366+
var base = touchAddr >> 2;
367+
Atomics.store(qemu.HEAPU32, base + 1, down ? 1 : 0);
368+
Atomics.store(qemu.HEAPU32, base + 2, x);
369+
Atomics.store(qemu.HEAPU32, base + 3, y);
370+
Atomics.add(qemu.HEAPU32, base, 1); // seq++ publishes the update
371+
}
372+
373+
var touchActive = false;
374+
canvas.style.touchAction = 'none';
375+
canvas.addEventListener('pointerdown', function (e) {
376+
e.preventDefault();
377+
canvas.setPointerCapture(e.pointerId);
378+
touchActive = true;
379+
startAudio(); // user gesture: safe moment to unlock WebAudio
380+
sendTouch(true, e);
381+
});
382+
canvas.addEventListener('pointermove', function (e) {
383+
if (touchActive) sendTouch(true, e);
384+
});
385+
function touchEnd(e) {
386+
if (!touchActive) return;
387+
touchActive = false;
388+
sendTouch(false, e);
389+
}
390+
canvas.addEventListener('pointerup', touchEnd);
391+
canvas.addEventListener('pointercancel', touchEnd);
392+
393+
// ================================================================
394+
// Audio: pebble_audio.c mirrors every PCM sample the firmware
395+
// plays into a shared int16 ring; drain it into WebAudio on a
396+
// 60 ms poll, scheduling chunks at a running playhead.
397+
// ================================================================
398+
var audioCtx = null;
399+
var audioPlayhead = 0;
400+
var audioAddr = 0;
401+
402+
function startAudio() {
403+
if (audioCtx || !qemu) return;
404+
if (typeof qemu._pebble_wasm_audio_ctrl !== 'function') return;
405+
audioAddr = Number(qemu._pebble_wasm_audio_ctrl());
406+
if (!audioAddr) return;
407+
try {
408+
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
409+
} catch (e) { return; }
410+
setInterval(drainAudio, 60);
411+
log('[audio] output enabled');
412+
}
413+
414+
function drainAudio() {
415+
if (!audioCtx || audioCtx.state === 'suspended') { if (audioCtx) audioCtx.resume(); }
416+
var base = audioAddr >> 2;
417+
var u32 = qemu.HEAPU32;
418+
var buf = u32[base], size = u32[base + 1];
419+
var head = Atomics.load(u32, base + 2);
420+
var tail = Atomics.load(u32, base + 3);
421+
if (head === tail) return;
422+
var n = (head - tail) >>> 0;
423+
if (n > size) { tail = head - size; n = size; }
424+
var rate = u32[base + 4] || 16000;
425+
var vol = (u32[base + 5] || 100) / 100;
426+
var pcm = audioCtx.createBuffer(1, n, Math.max(3000, rate));
427+
var ch = pcm.getChannelData(0);
428+
var heap16 = new Int16Array(qemu.HEAPU8.buffer);
429+
for (var i = 0; i < n; i++) {
430+
ch[i] = (heap16[(buf >> 1) + ((tail + i) % size)] / 32768) * vol;
431+
}
432+
Atomics.store(u32, base + 3, head);
433+
var src = audioCtx.createBufferSource();
434+
src.buffer = pcm;
435+
src.connect(audioCtx.destination);
436+
var now = audioCtx.currentTime;
437+
if (audioPlayhead < now + 0.02) audioPlayhead = now + 0.02;
438+
src.start(audioPlayhead);
439+
audioPlayhead += n / Math.max(3000, rate);
440+
}
441+
347442
// ================================================================
348443
// Serial console: UART2 (debug console) is routed to a file in the
349444
// emscripten MEMFS; poll it and show printable text.
40 Bytes
Binary file not shown.
-31 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)