Skip to content

Free-threaded builds: lvgl leaves the GIL off; one thread drives LVGL - #37

Merged
bdbarnett merged 3 commits into
mainfrom
ostinato/gil-free
Oct 15, 2026
Merged

bdbarnett merged 3 commits into
mainfrom
ostinato/gil-free

Conversation

@bdbarnett

Copy link
Copy Markdown
Contributor

On a free-threaded Python (3.15t wheels are published), importing lvgl turned the GIL back on for the whole process. This syncs PyDevices/lvgl-bindings#36, which declares the module GIL-free, so other threads keep running in parallel with drawing.

The rule that makes it safe, now in the README and docs/threads.md: make every lvgl call on one thread, and let go of every lvgl object there. Other threads can run any Python that doesn't touch lvgl. LVGL isn't thread-safe, and the binding's own shared state (struct-conversion scratch buffers, the callback registry, deferred releases) has no lock, so two threads calling lvgl at once can corrupt it on any build.

Tests. tests/test_free_threading.py runs on free-threaded builds and skips elsewhere (so it skips in this repo's 3.12 CI). It checks that importing lvgl leaves the GIL off, and that 200 frames drawn on one thread while other threads run Python are byte for byte the frames drawn alone, in 3 rounds out of 3. On 3.15t (Linux, 3.15.0b3) it fails without the declaration (the GIL is on after import) and passes with it, five runs out of five. With one LVGL call planted in a worker thread (bar.set_size, since undone) the frame check fails, 0 rounds of 3 matching. The cp315t wheel test now also asserts the GIL is off after import.

This branch carries #36's sync too until that merges; then LVGL_BINDINGS_COMMIT needs a rebase, and after lvgl-bindings#36 merges it should be resynced to the merged commit (squash merges change the SHA). Nothing else in the sync or release flow changes.

…er threads

docs/threads.md states the rule that lets lvgl run without the GIL: every
lvgl call, and every lvgl object's release, on one thread. The test checks
that importing lvgl leaves the GIL off, and that frames drawn while other
threads run Python match frames drawn alone. The wheel test on cp315t
checks the GIL stays off after import.
@bdbarnett

Copy link
Copy Markdown
Contributor Author

Windows check: wheels built from a clean export of this branch for cp315 and cp315t (Python 3.15.0, MSVC), installed into fresh venvs, with the suite plus #36's test_callback_references.py run twice each. cp315: 32 tests OK, the 2 free-threading tests skipped. cp315t: sys._is_gil_enabled() is False after import lvgl, 32 tests OK including both free-threading tests.

The generated files, lv_conf.h and fs_driver.py are byte-identical to
lvgl-bindings at 22f8383, and the lvgl submodule pin is unchanged; only
LVGL_BINDINGS_COMMIT moves from the pre-merge be7654f.
@bdbarnett
bdbarnett merged commit a489bcf into main Oct 15, 2026
1 check passed
@bdbarnett
bdbarnett deleted the ostinato/gil-free branch October 15, 2026 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant