Repository navigation
A Core Haptics engine that stopped under a call raises an NSException: handle it as a failed engine instead of ending the app - #1694
oliverames wants to merge 1 commit into
Conversation
…: the rumble adapter handles it as a failed engine instead of ending the app Mario Kart World 1.8.0 on a physical iPhone died after about 85 s of a race with 'CHHapticEngine checkEngineRunning: self.running' uncaught, a few seconds after two focus changes. Core Haptics raises, not returns an NSError, for a player or parameter call on an engine that is not running, and the engine stops on its own while its stopped handler is only queued. apply: and the pulse renewal now run in @Try and handle a caught exception like a failed start (stop, drop the engine, back off, clear the output, log a [RED] line); t_haptics_apple raises from a player and ends in the same uncaught exception without the fix.
4cc18cb to
ecf3ac0
Compare
…the rumble adapter drops it as a failed engine instead of ending the app (PR #1694) Mario Kart World 1.8.0 on an iPhone ended after about 85 s of a race on an uncaught '-[CHHapticEngine(CHHapticEngineInternal) checkEngineRunning:]: self.running', a few seconds after a focus loss and its return. Core Haptics raises, rather than returning an NSError, for a player call on an engine that is not running, and has no running state to ask first: the engine stops on its own while its stopped handler is only queued, and the adapter's own stop is asynchronous. Which call raised is not known (no backtrace). Every entry into a rumble channel (apply:, stop, the queued pulse renewal and delayed engine stop) now runs its Core Haptics calls through guarded:work:. A raise drops the engine and its players without calling them again (a stop sends a plain player zero, which raises once more), backs off like a failed start, clears the queued output and logs a rate-limited [RED] line; the next effect makes a new engine. From PR #1694 (oliverames), which guarded apply: and the pulse renewal and recovered by stopping the channel: on the plain-player route (Xbox controllers, one engine for both handles) that stop raised again inside the catch. t_haptics_apple raises on the advanced and the plain-player route, from a level change and from the idle release; the PR's version ends in an uncaught NSException on the plain-player case. Each raise leaks what ARC held in the unwound frames (no -fobjc-arc-exceptions; about 300 bytes a raise in the test), noted in the code. Checked: t_haptics_apple passes; haptics_apple.m compiles for macOS and iOS arm64 with -Wall -Werror; test_docs passes. Not checked: a forced engine stop on a device, haptics felt with the change. Co-authored-by: Oliver Ames <7366664+oliverames@users.noreply.github.com>
|
Thanks — landed on main as 7238bfd, with you as co-author. The diagnosis, the raising test fake and the docs are yours. Two changes on top:
Still untested: a forced engine stop on a device. If you hit the crash again, a log with the new |
…the rumble adapter drops it as a failed engine instead of ending the app (PR seedds#1694) Mario Kart World 1.8.0 on an iPhone ended after about 85 s of a race on an uncaught '-[CHHapticEngine(CHHapticEngineInternal) checkEngineRunning:]: self.running', a few seconds after a focus loss and its return. Core Haptics raises, rather than returning an NSError, for a player call on an engine that is not running, and has no running state to ask first: the engine stops on its own while its stopped handler is only queued, and the adapter's own stop is asynchronous. Which call raised is not known (no backtrace). Every entry into a rumble channel (apply:, stop, the queued pulse renewal and delayed engine stop) now runs its Core Haptics calls through guarded:work:. A raise drops the engine and its players without calling them again (a stop sends a plain player zero, which raises once more), backs off like a failed start, clears the queued output and logs a rate-limited [RED] line; the next effect makes a new engine. From PR seedds#1694 (oliverames), which guarded apply: and the pulse renewal and recovered by stopping the channel: on the plain-player route (Xbox controllers, one engine for both handles) that stop raised again inside the catch. t_haptics_apple raises on the advanced and the plain-player route, from a level change and from the idle release; the PR's version ends in an uncaught NSException on the plain-player case. Each raise leaks what ARC held in the unwound frames (no -fobjc-arc-exceptions; about 300 bytes a raise in the test), noted in the code. Checked: t_haptics_apple passes; haptics_apple.m compiles for macOS and iOS arm64 with -Wall -Werror; test_docs passes. Not checked: a forced engine stop on a device, haptics felt with the change. Co-authored-by: Oliver Ames <7366664+oliverames@users.noreply.github.com>
…the rumble adapter drops it as a failed engine instead of ending the app (PR seedds#1694) Mario Kart World 1.8.0 on an iPhone ended after about 85 s of a race on an uncaught '-[CHHapticEngine(CHHapticEngineInternal) checkEngineRunning:]: self.running', a few seconds after a focus loss and its return. Core Haptics raises, rather than returning an NSError, for a player call on an engine that is not running, and has no running state to ask first: the engine stops on its own while its stopped handler is only queued, and the adapter's own stop is asynchronous. Which call raised is not known (no backtrace). Every entry into a rumble channel (apply:, stop, the queued pulse renewal and delayed engine stop) now runs its Core Haptics calls through guarded:work:. A raise drops the engine and its players without calling them again (a stop sends a plain player zero, which raises once more), backs off like a failed start, clears the queued output and logs a rate-limited [RED] line; the next effect makes a new engine. From PR seedds#1694 (oliverames), which guarded apply: and the pulse renewal and recovered by stopping the channel: on the plain-player route (Xbox controllers, one engine for both handles) that stop raised again inside the catch. t_haptics_apple raises on the advanced and the plain-player route, from a level change and from the idle release; the PR's version ends in an uncaught NSException on the plain-player case. Each raise leaks what ARC held in the unwound frames (no -fobjc-arc-exceptions; about 300 bytes a raise in the test), noted in the code. Checked: t_haptics_apple passes; haptics_apple.m compiles for macOS and iOS arm64 with -Wall -Werror; test_docs passes. Not checked: a forced engine stop on a device, haptics felt with the change. Co-authored-by: Oliver Ames <7366664+oliverames@users.noreply.github.com> (cherry picked from commit 7238bfd)
What and why
Mario Kart World 1.8.0 on a physical iPhone ended about 85 s into a race with
*** Terminating app due to uncaught exception 'com.apple.CoreHaptics', reason: '-[CHHapticEngine(CHHapticEngineInternal) checkEngineRunning:]: self.running'and[dm] CRASH sig=6, a few seconds after the log'soe focus statechanged twice (2, then 1). It was not memory: the footprint was flat at about 3.7 GiB.Core Haptics reports a player or parameter call on an engine that is not running by raising an
NSException, not by returning anNSError. The engine stops on its own (audio interruption, focus change, helper exit) and its stopped handler is only queued onto the adapter's serial queue, so a pulse renewal or a level change can reach the engine first. Everyerror:path was already handled; this is not an error return.apply:and the pulse renewal now run in@try; a caught exception is handled as a failed engine (stop, drop the engine, back off like a failed start, clear the queued output,[RED] haptics: ... on a stopped engine), so the next effect builds a new engine. Reference entry: A haptic engine that stops under a call raises; game note line indm/games/0400C3F00006E000/README.md.Checked
t_haptics_applegains a case that raises from a player'ssendParameters: it passes with the change and ends in the same uncaughtNSExceptionwithout it (built and run on the Mac);dm/tools/tests/test_docs.pypasses.scripts/regress.py check --games: not run: the change only adds an exception handler around Core Haptics calls on the Mac and iOS front ends; nothing runs differently unless Core Haptics raises.Front ends
None: no front end has to act on this.
Docs
docs/reference/services.mdand the game note;python3 dm/tools/docs_symptoms.pywas rundmsh_chain.py --headerwas run anddm/gfx/nvn/dmsh_chain.his part of the change. Not applicable: no chain tool changed.