#3657·ebiten

internal/hook: SuspendAudio/ResumeAudio flips the flag before running hooks, diverging permanently on hook error

Author: kumagiCreated Sep 4, 2026Updated Sep 4, 2026
Labelsbugos:linuxos:windowsos:macosos:androidos:iosos:js

Ebitengine Version

main @ 4f474b00d (after v2.9.11)

Operating System

  • Windows
  • macOS
  • Linux
  • FreeBSD
  • OpenBSD
  • Android
  • iOS
  • Nintendo Switch
  • PlayStation 5
  • Xbox
  • Web Browsers

Go Version (paste your go version output)

go version go1.26.7 linux/amd64

What steps will reproduce the problem?

  1. Register audio suspend/resume hooks with an implementation that can fail (e.g. an audio-device operation failing during backgrounding on mobile):
go
hook.OnSuspendAudio(func() error {
    return errors.New("device busy") // e.g. transient OS failure
})
  1. Trigger hook.SuspendAudio(). It returns the error — but the damage is done. Trigger it again (or check the state): the second call is a silent no-op even though the device was never actually suspended.

Root cause (internal/hook/hook.go:86-110 on current main):

go
func SuspendAudio() error {
    ...
    audioSuspended = true      // committed BEFORE the hook runs (line 92)
    if onSuspendAudio != nil {
        return onSuspendAudio() // error leaves the flag flipped (line 94)
    }
    return nil
}

ResumeAudio mirrors this (flag cleared at line 105 before onResumeAudio() at line 107). When a hook fails, the flag permanently disagrees with reality: every later SuspendAudio no-ops ("already suspended") and every later ResumeAudio no-ops ("not suspended"). The audio state machine is wedged until process restart — typically permanent wrong behavior (stuck muted, or suspend never re-applied).

What is the expected result?

The hooks should run first, and the audioSuspended flag should only be committed on success, so a transient hook failure leaves the state machine consistent and retryable.

What happens instead?

One transient failure (exactly the kind that happens when an app is backgrounded/foregrounded on mobile) permanently desynchronizes the flag from the device. From a player's perspective: after returning to the game once, audio is stuck off (or misbehaves) forever, with no recovery short of restarting the app.

Anything else you feel useful to add?

  • Severity: high for game users, especially on mobile where suspend/resume around backgrounding is a core OS interaction, not an edge case.
  • Suggested fix: reorder to invoke onSuspendAudio/onResumeAudio first and set audioSuspended only when the hook returns nil.