emulator: wake the Events() select on SDCard push

Eject button wasn't taking effect immediately because Platform.Events()
was blocked in a select that only woke on button-channel input or
timer expiry. SDCardEvents go through a separate pending queue and the
select had no way to notice when they arrived — the eject sat in
pending until the next button press or timer expiry, both of which
could take seconds.

Fix: 1-buffered wake channel. signalWake() pokes it (non-blocking),
the select now reads from it as a third case, and the drain-loop at
the end re-drains pending so any event pushed while we waited gets
returned in the same call.

  exportSetSDCard() calls signalWake() after appending to pending.
  Wakeup() (gui.Platform method, previously no-op) now also calls it,
  so any future gui-side wake request works the same way.

The bug was specific to non-button events — button events go through
the events chan directly so they always woke the select.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mineracks 2026-05-28 21:09:58 +10:00
parent b4635f4efc
commit cf68d6c451

View File

@ -41,6 +41,11 @@ const (
type browserPlatform struct { type browserPlatform struct {
frame *image.RGBA frame *image.RGBA
events chan v1.Event events chan v1.Event
// wake is a 1-buffered channel that the select in Events() reads
// from. exportSetSDCard, exportCameraFrame (future), and any other
// non-button event source pokes wake after appending to pending so
// the Events() wait returns immediately and drains the new event.
wake chan struct{}
mu sync.Mutex mu sync.Mutex
pending []gui.Event pending []gui.Event
@ -52,6 +57,14 @@ func newBrowserPlatform() *browserPlatform {
return &browserPlatform{ return &browserPlatform{
frame: image.NewRGBA(image.Rect(0, 0, lcdWidth, lcdHeight)), frame: image.NewRGBA(image.Rect(0, 0, lcdWidth, lcdHeight)),
events: make(chan v1.Event, 64), events: make(chan v1.Event, 64),
wake: make(chan struct{}, 1),
}
}
func (p *browserPlatform) signalWake() {
select {
case p.wake <- struct{}{}:
default:
} }
} }
@ -76,14 +89,20 @@ func (p *browserPlatform) Events(deadline time.Time) []gui.Event {
select { select {
case ev := <-p.events: case ev := <-p.events:
out = append(out, p.toGuiEvent(ev)) out = append(out, p.toGuiEvent(ev))
case <-p.wake:
// Non-button event arrived (e.g. SDCardEvent). Fall through to
// the pending drain at the bottom.
case <-timer.C: case <-timer.C:
} }
// Drain any extras that piled up while we were waiting. // Drain any extras (button events) that piled up while we waited.
for { for {
select { select {
case ev := <-p.events: case ev := <-p.events:
out = append(out, p.toGuiEvent(ev)) out = append(out, p.toGuiEvent(ev))
default: default:
// Also re-drain pending — if signalWake fired, the SDCard
// or other event is sitting there now.
out = append(out, p.drainPending()...)
return out return out
} }
} }
@ -113,7 +132,7 @@ func (p *browserPlatform) push(button v1.Button, pressed bool) {
} }
func (p *browserPlatform) Wakeup() { func (p *browserPlatform) Wakeup() {
// no-op — JS-driven runtime; nothing to wake from. p.signalWake()
} }
func (p *browserPlatform) PlateSizes() []backup.PlateSize { func (p *browserPlatform) PlateSizes() []backup.PlateSize {
@ -274,6 +293,7 @@ func exportSetSDCard(this js.Value, args []js.Value) any {
plat.mu.Lock() plat.mu.Lock()
plat.pending = append(plat.pending, gui.SDCardEvent{Inserted: inserted}.Event()) plat.pending = append(plat.pending, gui.SDCardEvent{Inserted: inserted}.Event())
plat.mu.Unlock() plat.mu.Unlock()
plat.signalWake() // unblock any in-flight Events() wait
return nil return nil
} }