#715·pyxel

Arm64 Windows freeze when closing Pyxel

Author: tingtronCreated Jul 2, 2026Updated Aug 29, 2026
Labelsbug

What happened? / 何が起きましたか?

When exiting any Pyxel examples or my own code, using normal exit even (Q key, or Esc), when the code calls pyxel.quit(), the window disappears, but the application keeps running (it does not return to the CMD prompt.)

Workarounds, not really pretty:

  • To kill the hidden application and release the command prompt, use Task Manager, Kill Process pyxel.exe.
  • Prefix command line with start, which will pop up a new console. It stays frozen after Quit, but can be closed with [X], which will also kill the child pyxel.exe and python.exe. This will keep your main Command shell going.

Configuration:

  • The Arm64 Windows has the latest updates and 25H2
  • The Python is installed using the recommended PIM (Windows Store) and the latest Python version
  • Pyxel was installed in a separate venv, and no other packages are installed

Note: if replacing pyxel.quit() with sys.exit(0) it exits normally.

Here's some additional info after chatting with GPT:

An arm64 Windows freeze when closing Pyxel (which wraps Rust and SDL) on Snapdragon chips is almost always caused by a cross-language thread orchestration deadlock during garbage collection.

When Python shuts down or garbage collects Pyxel objects, the destruction sequence runs on Python's main thread. If Pyxel’s underlying Rust-SDL thread tries to uninitialize audio, video, or joystick subsystems while Python/OS threads are locked out, the process hangs indefinitely.

The primary triggers and how to resolve them include:

  1. The Windows Sensor Manager Bug

On Windows ARM64 (specifically Snapdragon PCs), SDL has a known edge-case bug where it attempts to release system sensors (ISensorManager_Release). Because Snapdragon hardware reports different topology hooks to Windows, this function frequently blocks indefinitely on shutdown.

  • The Fix: Explicitly tell SDL to ignore sensors before Pyxel boots up. In Python, add this environment variable at the absolute top of your entry script (before importing pyxel):
python
import os
os.environ["SDL_HINT_MAIN_CALLBACK_READY"] = "1"
# This stops SDL from polling and destroying tablet/sensor hooks on Windows
os.environ["SDL_HINT_TRACKPAD_IS_TOUCHSCREEN"] = "0" 
  1. Audio Device Thread Race Condition

Pyxel spawns its own background thread to feed the audio callback ring buffer. On Windows ARM64, if the Python interpreter kills the process main thread before Rust finishes the SDL_CloseAudioDevice() loop, the WASAPI / Windows sound driver locks up.

  • The Fix: Rather than letting the script hit the natural end and panic, force Pyxel to cleanly tear down its window and audio states from within your code:
python
import pyxel
# Inside your update() function when detecting an exit:
if pyxel.btnp(pyxel.KEY_ESCAPE):
    pyxel.quit()  # Tells Pyxel's Rust core to safely stop audio threads first
  1. Multi-Threaded GIL Deadlock

If Pyxel is called inside a sub-thread or alongside custom Python threads (like threading.Thread), Python's Global Interpreter Lock (GIL) can clash with Rust's drop implementations. SDL requires SDL_Quit() to be called entirely from the OS main thread.

  • The Fix: Ensure pyxel.run() is called strictly from your python file's main thread. Avoid using Python's atexit module to clean up Pyxel objects, as atexit hooks on Windows ARM64 run at a point where the main window thread handles have already been restricted by the OS.
  1. Workaround: Forcing a Clean Hard-Exit

If the underlying issue is deep within the compiled Rust sdl2 crate binaries binding to Windows ARM64, you can bypass the hanging SDL_Quit() hook entirely by telling Python to immediately kill the process when exiting.

  • Replace standard exits or window closes with a direct OS termination. This ensures the app closes instantly for the end user without lingering as a ghost process in Task Manager:
python
import sys
import os

# Force-exit hook
def safe_exit():
    os._exit(0) # Skips normal GC teardown and stops the hang

OS

Windows 11 Arm64 Snapdragon

Pyxel Version / Pyxel バージョン

2.9.6

Python Version / Python バージョン

3.14.6

Screenshots or code (optional) / スクリーンショットやコード(任意)

Additional alternatives, specifically for Windows 11 Arm64 and Pyxel.

If even the simplest 01_hello_pyxel.py hangs on exit via Esc or Q, it confirms the problem is an architecture-specific deadlock during SDL's sub-system cleanup on Windows ARM64 (Snapdragon).

Pyxel's underlying Rust core links to SDL2. When you exit a standard example, Pyxel initiates a sequence that destroys the OpenGL graphics context, unregisters Windows input devices, and kills the audio thread buffer. On Qualcomm Snapdragon chips running Windows 11, the native OS drivers for either audio (WASAPI) or input (Human Interface Devices) fail to respond to SDL's cleanup signal, halting the entire Python process.

You can fix or completely bypass this issue using the following strategies:

  1. Apply the Global SDL Audio/Sensor Hints

Before Pyxel initializes its Rust backend, you can pass environment variables to force SDL to skip the specific Windows ARM64 hardware loops known to hang during teardown.

Open 01_hello_pyxel.py and modify the top of the file to include these os.environ keys before import pyxel:

python
import os
import sys

# Force SDL to use a generic audio driver fallback if WASAPI hangs
os.environ["SDL_AUDIODRIVER"] = "directsound"

# Block SDL from querying or cleaning up advanced Windows tablet/sensor APIs 
os.environ["SDL_HINT_MAIN_CALLBACK_READY"] = "1"
os.environ["SDL_HINT_TRACKPAD_IS_TOUCHSCREEN"] = "0"

import pyxel
# ... rest of the 01_hello_pyxel.py code follows
  1. Monkey-Patch Pyxel's Exit with a Hard OS Termination

In Pyxel version 2.8.0 and later, the native pyxel.quit() mechanism was changed to allow a standard Python object cleanup rather than instantly terminating the process. On ARM64, this delayed teardown is what triggers the hang.

You can bypass the broken SDL_Quit() loop entirely by intercepting the exit and forcing Windows to kill the process immediately. Modify the example's update() function to look like this:

python
def update():
    # If Esc or Q is pressed, kill the process instantly before SDL can hang
    if pyxel.btnp(pyxel.KEY_ESCAPE) or pyxel.btnp(pyxel.KEY_Q):
        os._exit(0)  # Immediate hard exit, bypasses standard SDL_Quit() safely
        
    # Your normal update logic...

Note: Using os._exit(0) is safe for a Pyxel game because it forces the OS to immediately reclaim the memory and window handles, giving the user an instant, smooth close.

  1. Native Windows ARM64 Driver Check

Because Snapdragon X Elite/Plus chips rely heavily on Qualcomm's Adreno graphics and audio pipelines, old or generic Windows Update drivers frequently fail to respond to standard SDL closure events.

  • Open your device's manufacturer application (e.g., Lenovo Vantage, Dell Command, or Samsung Update).Install the latest Qualcomm Adreno Graphics Driver and Qualcomm Audio Subsystem updates.

  • Install the latest Qualcomm Adreno Graphics Driver and Qualcomm Audio Subsystem updates.

If you don't want to modify every Pyxel example manually, implementing Method 2 (os._exit(0)) is the most reliable workaround for the Snapdragon platform until the upstream sdl2 Rust crate fully updates its Windows ARM64 topology fixes.

  1. Combination of the methods using a Wrapper Script

Save the following code as run_pyxel.py in the same folder where your Pyxel examples are located:

python
import sys
import os
import importlib.util

def main():
    # 1. Ensure a target script was provided
    if len(sys.argv) < 2:
        print("Usage: python run_pyxel.py <target_example.py>")
        sys.argv = ["", "01_hello_pyxel.py"] # Default if no arg provided
        print("No script specified. Attempting to run default: 01_hello_pyxel.py\n")

    target_script = sys.argv[1]
    if not os.path.exists(target_script):
        print(f"Error: File '{target_script}' not found.")
        sys.exit(1)

    # 2. Inject SDL environment fixes before Pyxel can load
    os.environ["SDL_AUDIODRIVER"] = "directsound"
    os.environ["SDL_HINT_MAIN_CALLBACK_READY"] = "1"
    os.environ["SDL_HINT_TRACKPAD_IS_TOUCHSCREEN"] = "0"

    # 3. Import pyxel and monkey-patch its quit behavior
    try:
        import pyxel
        
        # We override pyxel.quit so that whenever an example calls it,
        # it forces an immediate hard exit, bypassing the broken SDL_Quit hook.
        def arm64_safe_quit():
            os._exit(0)
            
        pyxel.quit = arm64_safe_quit
    except ImportError:
        print("Error: Pyxel is not installed in this Python environment.")
        sys.exit(1)

    # 4. Dynamically load and run the target Pyxel script
    print(f"Launching {target_script} with Windows ARM64 compatibility patches...")
    
    # Adjust sys.argv so the child script thinks it was run normally
    sys.argv = sys.argv[1:] 
    
    # Load the module context
    spec = importlib.util.spec_from_file_location("__main__", target_script)
    module = importlib.util.module_from_spec(spec)
    sys.modules["__main__"] = module
    
    try:
        spec.loader.exec_module(module)
    except KeyboardInterrupt:
        os._exit(0)

if __name__ == "__main__":
    main()