From 65754593a6ae1bea8a70bc12ecdcdbdb709ea8fc Mon Sep 17 00:00:00 2001 From: Brad Barnett Date: Sat, 26 Sep 2026 06:08:09 +0000 Subject: [PATCH 1/2] Overlay: patch 0015 (_timing for the windows port) and the wasm bridge's call signature 0015 gives micropython.exe a wake source: a _timing module whose timer-queue thread sets a flag the main thread notices between bytecodes, in sleeps and in the console wait, then hands the callback to mp_sched_schedule from its own context. multimer's native source is its only caller. In the windows profiles; compile-checked with mingw and smoked under wine (the VM-hook and sleep paths; the console and pipe waits need a real Windows console). The wasm bridge called external_call_depth_dec with no argument; v1.29.0 takes the object to keep rooted. The call went through an invoke wrapper with the wrong signature, so every timer callback in the direct wasm build ended in "null function or function signature mismatch" (fatal under node, a console error in a page). It passes mp_const_none now. --- ...ndows-timing-module-and-idle-console.patch | 260 ++++++++++++++++++ profiles/README.md | 4 +- profiles/windows-full.series | 1 + profiles/windows-networked.series | 1 + provenance.json | 3 + usermods/wasmbridge/mod_wasm_bridge.c | 11 +- 6 files changed, 275 insertions(+), 5 deletions(-) create mode 100644 patches/0015-micropython-windows-timing-module-and-idle-console.patch diff --git a/patches/0015-micropython-windows-timing-module-and-idle-console.patch b/patches/0015-micropython-windows-timing-module-and-idle-console.patch new file mode 100644 index 0000000..eabdc11 --- /dev/null +++ b/patches/0015-micropython-windows-timing-module-and-idle-console.patch @@ -0,0 +1,260 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> +Date: Sat, 26 Sep 2026 06:06:30 +0000 +Subject: [PATCH] windows: a _timing module and a console wait that services + pending callbacks. + +micropython.exe has no signals, no machine.Timer and no threads in the +VM, so nothing could wake the main thread on a deadline: multimer's +old win32 provider needed the thread in an alertable wait, which the +REPL's console read is not, and a plain time.sleep starved it. + +_timing owns one timer-queue timer. Its thread only sets a flag; the +main thread notices the flag in the VM's periodic hook (between +bytecodes), in mp_event_wait_ms (so time.sleep_ms sees it) and in the +console wait, and hands the callback to mp_sched_schedule from its own +context, so the callback runs at a bytecode boundary exactly as a +board's soft machine.Timer callback does. mp_hal_stdin_rx_chr waits +for console input in 10 ms slices, running pending callbacks between +them, and the piped-stdin path (0013) polls the pipe the same way, so +the REPL is served while it waits for a key. + +_timing.init(callback), _timing.arm(ms), _timing.cancel(); multimer's +native wake source is the only caller. + +Based-on: micropython 0fd6c573e (tag v1.29.0) +--- + ports/windows/Makefile | 1 + + ports/windows/mod_timing.c | 139 ++++++++++++++++++++++++++++++++++ + ports/windows/mpconfigport.h | 16 ++++ + ports/windows/windows_mphal.c | 25 ++++++ + 4 files changed, 181 insertions(+) + create mode 100644 ports/windows/mod_timing.c + +diff --git a/ports/windows/Makefile b/ports/windows/Makefile +index 4b6b182..b6a591a 100644 +--- a/ports/windows/Makefile ++++ b/ports/windows/Makefile +@@ -74,6 +74,7 @@ SRC_C = \ + modsocket.c \ + ports/unix/gccollect.c \ + windows_mphal.c \ ++ mod_timing.c \ + realpath.c \ + init.c \ + fmode.c \ +diff --git a/ports/windows/mod_timing.c b/ports/windows/mod_timing.c +new file mode 100644 +index 0000000..a1199a6 +--- /dev/null ++++ b/ports/windows/mod_timing.c +@@ -0,0 +1,139 @@ ++/* ++ * This file is part of the MicroPython project, http://micropython.org/ ++ * ++ * The MIT License (MIT) ++ * ++ * Copyright (c) 2026 Brad Barnett ++ * ++ * Permission is hereby granted, free of charge, to any person obtaining a copy ++ * of this software and associated documentation files (the "Software"), to deal ++ * in the Software without restriction, including without limitation the rights ++ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ++ * copies of the Software, and to permit persons to whom the Software is ++ * furnished to do so, subject to the following conditions: ++ * ++ * The above copyright notice and this permission notice shall be included in ++ * all copies or substantial portions of the Software. ++ * ++ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ++ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ++ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ++ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ++ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ++ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ++ * THE SOFTWARE. ++ */ ++ ++// _timing: one OS timer that wakes the main thread, for a port with no ++// signals, no machine.Timer and no threads in the VM. ++// ++// import _timing ++// _timing.init(callback) # a callable of one argument (None) ++// _timing.arm(ms) # one-shot: callback is scheduled in ms ++// _timing.cancel() ++// ++// A timer-queue thread only sets a flag when the deadline passes. The main ++// thread notices it in the VM's periodic hook, in mp_event_wait_ms (so ++// time.sleep_ms sees it), and in the console wait of mp_hal_stdin_rx_chr ++// (so the REPL sees it while it waits for a key), and hands the callback to ++// mp_sched_schedule from its own context. So the callback runs at a bytecode ++// boundary of the main thread, exactly as machine.Timer's soft callbacks do ++// on a board, and no VM state is ever touched from the timer thread. ++ ++#include ++ ++#include "py/runtime.h" ++#include "py/mphal.h" ++ ++#if MICROPY_PY_TIMING ++ ++static HANDLE timing_queue = NULL; ++static HANDLE timing_timer = NULL; ++static volatile LONG timing_pending = 0; ++ ++static void CALLBACK timing_expired(PVOID param, BOOLEAN fired) { ++ (void)param; ++ (void)fired; ++ InterlockedExchange(&timing_pending, 1); ++} ++ ++// Called from the main thread at safe points (see the header comment). ++void mp_timing_poll(void) { ++ if (!timing_pending) { ++ return; ++ } ++ mp_obj_t cb = MP_STATE_PORT(timing_callback); ++ if (InterlockedExchange(&timing_pending, 0) && cb != MP_OBJ_NULL) { ++ // A full scheduler queue means the VM is not draining it; the next ++ // arm() retries, and multimer counts the miss. ++ mp_sched_schedule(cb, mp_const_none); ++ } ++} ++ ++static void timing_cancel_timer(void) { ++ if (timing_timer != NULL) { ++ // INVALID_HANDLE_VALUE waits for a running callback to finish, so no ++ // callback can set the flag after this returns. ++ DeleteTimerQueueTimer(timing_queue, timing_timer, INVALID_HANDLE_VALUE); ++ timing_timer = NULL; ++ } ++ InterlockedExchange(&timing_pending, 0); ++} ++ ++static mp_obj_t timing_init(mp_obj_t callback) { ++ if (!mp_obj_is_callable(callback)) { ++ mp_raise_TypeError(MP_ERROR_TEXT("callback must be callable")); ++ } ++ if (timing_queue == NULL) { ++ timing_queue = CreateTimerQueue(); ++ if (timing_queue == NULL) { ++ mp_raise_OSError(GetLastError()); ++ } ++ } ++ timing_cancel_timer(); ++ MP_STATE_PORT(timing_callback) = callback; ++ return mp_const_none; ++} ++static MP_DEFINE_CONST_FUN_OBJ_1(timing_init_obj, timing_init); ++ ++static mp_obj_t timing_arm(mp_obj_t ms_in) { ++ mp_int_t ms = mp_obj_get_int(ms_in); ++ if (ms < 0) { ++ ms = 0; ++ } ++ if (timing_queue == NULL || MP_STATE_PORT(timing_callback) == MP_OBJ_NULL) { ++ mp_raise_ValueError(MP_ERROR_TEXT("init() first")); ++ } ++ timing_cancel_timer(); ++ if (!CreateTimerQueueTimer(&timing_timer, timing_queue, timing_expired, NULL, ++ (DWORD)ms, 0, WT_EXECUTEONLYONCE)) { ++ timing_timer = NULL; ++ mp_raise_OSError(GetLastError()); ++ } ++ return mp_const_none; ++} ++static MP_DEFINE_CONST_FUN_OBJ_1(timing_arm_obj, timing_arm); ++ ++static mp_obj_t timing_cancel(void) { ++ timing_cancel_timer(); ++ return mp_const_none; ++} ++static MP_DEFINE_CONST_FUN_OBJ_0(timing_cancel_obj, timing_cancel); ++ ++static const mp_rom_map_elem_t timing_module_globals_table[] = { ++ { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR__timing) }, ++ { MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&timing_init_obj) }, ++ { MP_ROM_QSTR(MP_QSTR_arm), MP_ROM_PTR(&timing_arm_obj) }, ++ { MP_ROM_QSTR(MP_QSTR_cancel), MP_ROM_PTR(&timing_cancel_obj) }, ++}; ++static MP_DEFINE_CONST_DICT(timing_module_globals, timing_module_globals_table); ++ ++const mp_obj_module_t mp_module_timing = { ++ .base = { &mp_type_module }, ++ .globals = (mp_obj_dict_t *)&timing_module_globals, ++}; ++ ++MP_REGISTER_MODULE(MP_QSTR__timing, mp_module_timing); ++MP_REGISTER_ROOT_POINTER(mp_obj_t timing_callback); ++ ++#endif // MICROPY_PY_TIMING +diff --git a/ports/windows/mpconfigport.h b/ports/windows/mpconfigport.h +index 4b6b96d..652d028 100644 +--- a/ports/windows/mpconfigport.h ++++ b/ports/windows/mpconfigport.h +@@ -82,6 +82,22 @@ + #ifndef MICROPY_SCHEDULER_DEPTH + #define MICROPY_SCHEDULER_DEPTH (16) + #endif ++// _timing (mod_timing.c): one OS timer that wakes the main thread, for a port ++// with no signals and no machine.Timer. The VM hook and the internal event ++// hook let the main thread notice a deadline between bytecodes and inside ++// sleeps; mp_hal_stdin_rx_chr notices it while the REPL waits for a key. ++#ifndef MICROPY_PY_TIMING ++#define MICROPY_PY_TIMING (1) ++#endif ++#if MICROPY_PY_TIMING ++void mp_timing_poll(void); ++#define MICROPY_VM_HOOK_COUNT (64) ++#define MICROPY_VM_HOOK_INIT static unsigned int vm_hook_divisor = MICROPY_VM_HOOK_COUNT; ++#define MICROPY_VM_HOOK_POLL if (--vm_hook_divisor == 0) { vm_hook_divisor = MICROPY_VM_HOOK_COUNT; mp_timing_poll(); } ++#define MICROPY_VM_HOOK_LOOP MICROPY_VM_HOOK_POLL ++#define MICROPY_VM_HOOK_RETURN MICROPY_VM_HOOK_POLL ++#define MICROPY_INTERNAL_EVENT_HOOK mp_timing_poll() ++#endif + #define MICROPY_VFS (1) + #define MICROPY_VFS_POSIX (1) + #define MICROPY_PY_FUNCTION_ATTRS (1) +diff --git a/ports/windows/windows_mphal.c b/ports/windows/windows_mphal.c +index 4825d93..efea074 100644 +--- a/ports/windows/windows_mphal.c ++++ b/ports/windows/windows_mphal.c +@@ -204,6 +204,19 @@ static int stdin_rx_chr_pipe(void) { + } + unsigned char c; + DWORD num_read = 0; ++ #if MICROPY_PY_TIMING ++ // A pipe read blocks with nothing to wake it; poll it in slices so a ++ // _timing deadline and other pending callbacks run while the REPL ++ // waits for a line (a file redirect fails the peek and reads on). ++ DWORD avail = 0; ++ if (PeekNamedPipe(std_in, NULL, 0, NULL, &avail, NULL) && avail == 0) { ++ mp_event_handle_nowait(); ++ MP_THREAD_GIL_EXIT(); ++ Sleep(10); ++ MP_THREAD_GIL_ENTER(); ++ continue; ++ } ++ #endif + MP_THREAD_GIL_EXIT(); + BOOL status = ReadFile(std_in, &c, 1, &num_read, NULL); + MP_THREAD_GIL_ENTER(); +@@ -242,6 +255,18 @@ int mp_hal_stdin_rx_chr(void) { + DWORD num_read; + INPUT_RECORD rec; + for (;;) { ++ #if MICROPY_PY_TIMING ++ // Wait for input in slices, so a _timing deadline and any other ++ // pending callback run while the REPL sits at its prompt. The console ++ // handle signals when an input record is available. ++ MP_THREAD_GIL_EXIT(); ++ DWORD waited = WaitForSingleObject(std_in, 10); ++ MP_THREAD_GIL_ENTER(); ++ if (waited == WAIT_TIMEOUT) { ++ mp_event_handle_nowait(); ++ continue; ++ } ++ #endif + MP_THREAD_GIL_EXIT(); + status = ReadConsoleInput(std_in, &rec, 1, &num_read); + MP_THREAD_GIL_ENTER(); diff --git a/profiles/README.md b/profiles/README.md index e4e719b..ead3113 100644 --- a/profiles/README.md +++ b/profiles/README.md @@ -6,8 +6,8 @@ applies one; `--check` verifies applicability without touching the tree. | Profile | Patches | Extras | |---|---|---| -| `windows-networked` | 0001, 0013 | | -| `windows-full` | 0001, 0002, 0003, 0012, 0013 | | +| `windows-networked` | 0001, 0013, 0015 | | +| `windows-full` | 0001, 0002, 0003, 0012, 0013, 0015 | | | `desktop-pydevices` | 0002, 0012 | | | `webassembly-pydevices` | 0004, 0005, 0006, 0007, 0008 | `usermods/wasmbridge`, `variants/webassembly` | | `esp32-s3-debug` | 0009 | | diff --git a/profiles/windows-full.series b/profiles/windows-full.series index 36dfbae..7300c9e 100644 --- a/profiles/windows-full.series +++ b/profiles/windows-full.series @@ -3,3 +3,4 @@ 0003 0012 0013 +0015 diff --git a/profiles/windows-networked.series b/profiles/windows-networked.series index eb1ffc6..df84332 100644 --- a/profiles/windows-networked.series +++ b/profiles/windows-networked.series @@ -1,2 +1,3 @@ 0001 0013 +0015 diff --git a/provenance.json b/provenance.json index f6091fe..94eec67 100644 --- a/provenance.json +++ b/provenance.json @@ -43,6 +43,9 @@ }, "0014-micropython-esp32-tinyusb-0.21.patch": { "sha256": "15a101901c7bebd93b5846e02fb1ff62b3c9a5e043b43c7c76de455007e02492" + }, + "0015-micropython-windows-timing-module-and-idle-console.patch": { + "sha256": "3c255514979938e16aca6f19459be2447206bada168ebd3f29294b5af1f86418" } } } diff --git a/usermods/wasmbridge/mod_wasm_bridge.c b/usermods/wasmbridge/mod_wasm_bridge.c index eb56849..98179bd 100644 --- a/usermods/wasmbridge/mod_wasm_bridge.c +++ b/usermods/wasmbridge/mod_wasm_bridge.c @@ -67,7 +67,12 @@ extern uintptr_t pydevices_http_get(const char *, int *, size_t *, uintptr_t *); extern void pydevices_sleep_ms(int); extern void pydevices_host_reset(void); extern void external_call_depth_inc(void); -extern void external_call_depth_dec(void); +// MicroPython v1.29.0 gives the decrement the object to keep rooted while +// the call unwinds (ports/webassembly/main.c). Declaring it without that +// argument compiled, but the call went through an invoke_* wrapper with the +// wrong signature and every timer callback ended in "null function or +// function signature mismatch": fatal under node, a console error in a page. +extern void external_call_depth_dec(mp_obj_t root_obj); void pydevices_bridge_deinit(void) { pydevices_host_reset(); @@ -319,11 +324,11 @@ EMSCRIPTEN_KEEPALIVE void pydevices_timer_dispatch(int id) { } else { mp_obj_print_exception(&mp_plat_print, MP_OBJ_FROM_PTR(nlr.ret_val)); } - external_call_depth_dec(); + external_call_depth_dec(mp_const_none); return; } } - external_call_depth_dec(); + external_call_depth_dec(mp_const_none); } static mp_obj_t bridge_timer_cancel(mp_obj_t id) { From a61d14e51d99666973a88fa5b784a5761e091ffe Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Sat, 26 Sep 2026 03:12:32 -0500 Subject: [PATCH 2/2] Patch 0015: a high-resolution waitable timer, waits that wake on its event, 1 ms timer resolution The cloud's first version used a timer-queue timer and 10 ms wait slices; on a real Windows console it fell to the 15.6 ms system tick (a 10 ms multimer timer: 348/500 idle, 339 busy, 20 a second at the prompt). Now one waitable timer, high resolution where Windows offers it, on a helper thread signals an event that MICROPY_INTERNAL_WFE, the console wait and the piped stdin path block on, and init() asks for the 1 ms resolution as SDL does. Same bench: 507/500 idle and busy, 0.5 ms median jitter, 4 ms p99 lateness, about 100 a second at the prompt. mp_timing_poll keeps a deadline pending when the scheduler queue is full, so a full queue cannot stop every timer. --- ...ndows-timing-module-and-idle-console.patch | 237 ++++++++++++------ provenance.json | 2 +- 2 files changed, 168 insertions(+), 71 deletions(-) diff --git a/patches/0015-micropython-windows-timing-module-and-idle-console.patch b/patches/0015-micropython-windows-timing-module-and-idle-console.patch index eabdc11..2535705 100644 --- a/patches/0015-micropython-windows-timing-module-and-idle-console.patch +++ b/patches/0015-micropython-windows-timing-module-and-idle-console.patch @@ -1,40 +1,60 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Sat, 26 Sep 2026 06:06:30 +0000 -Subject: [PATCH] windows: a _timing module and a console wait that services - pending callbacks. +Subject: [PATCH] windows: a _timing module, and waits that wake on its deadline. micropython.exe has no signals, no machine.Timer and no threads in the VM, so nothing could wake the main thread on a deadline: multimer's old win32 provider needed the thread in an alertable wait, which the REPL's console read is not, and a plain time.sleep starved it. -_timing owns one timer-queue timer. Its thread only sets a flag; the -main thread notices the flag in the VM's periodic hook (between -bytecodes), in mp_event_wait_ms (so time.sleep_ms sees it) and in the -console wait, and hands the callback to mp_sched_schedule from its own -context, so the callback runs at a bytecode boundary exactly as a -board's soft machine.Timer callback does. mp_hal_stdin_rx_chr waits -for console input in 10 ms slices, running pending callbacks between -them, and the piped-stdin path (0013) polls the pipe the same way, so -the REPL is served while it waits for a key. +_timing owns one waitable timer, high resolution where Windows offers +it (10 1803+), waited on by a helper thread that only sets a flag and +signals an event. The main thread notices the flag in the VM's periodic +hook (between bytecodes), in mp_event_wait_ms (so time.sleep_ms sees +it) and in the console wait, and hands the callback to +mp_sched_schedule from its own context, so the callback runs at a +bytecode boundary exactly as a board's soft machine.Timer callback +does. The port's waits block on the event: MICROPY_INTERNAL_WFE, the +console wait in mp_hal_stdin_rx_chr and the piped-stdin path (0013) all +return the moment a deadline passes, so a sleep or a REPL waiting for a +key is served at once, not at the end of a time slice. init() also asks +Windows for its 1 ms timer resolution (timeBeginPeriod, as SDL and +pygame do); without it every wait in the process rounds up to the +15.6 ms system tick, time.sleep_ms included. + +Measured on Windows 11 with a 10 ms multimer timer over 5 s: 507 of 500 +delivered with 0.5 ms median jitter and 4 ms p99 lateness, both with +the main thread idle in sleep_ms and with it spinning in Python; at an +idle REPL prompt about 100 callbacks a second. The first version of +this patch (a timer-queue timer and 10 ms wait slices) managed 348 idle, +339 busy and 20 a second at the prompt, all at the system tick. _timing.init(callback), _timing.arm(ms), _timing.cancel(); multimer's native wake source is the only caller. Based-on: micropython 0fd6c573e (tag v1.29.0) --- - ports/windows/Makefile | 1 + - ports/windows/mod_timing.c | 139 ++++++++++++++++++++++++++++++++++ - ports/windows/mpconfigport.h | 16 ++++ - ports/windows/windows_mphal.c | 25 ++++++ - 4 files changed, 181 insertions(+) - create mode 100644 ports/windows/mod_timing.c + ports/windows/Makefile | 3 +- + ports/windows/mod_timing.c | 199 ++++++++++++++++++++++++++++++++++++++++++ + ports/windows/mpconfigport.h | 19 ++++ + ports/windows/windows_mphal.c | 22 +++++ + ports/windows/windows_mphal.h | 6 +- + 5 files changed, 247 insertions(+), 2 deletions(-) diff --git a/ports/windows/Makefile b/ports/windows/Makefile -index 4b6b182..b6a591a 100644 +index 4b6b18270..5f90114bf 100644 --- a/ports/windows/Makefile +++ b/ports/windows/Makefile +@@ -47,7 +47,7 @@ INC += -I$(VARIANT_DIR) + + # compiler settings + CFLAGS += $(INC) -Wall -Wpointer-arith -Wdouble-promotion -Werror -std=gnu99 -DUNIX -D__USE_MINGW_ANSI_STDIO=1 $(COPT) $(CFLAGS_EXTRA) +-LDFLAGS += -lm -lbcrypt $(LDFLAGS_EXTRA) ++LDFLAGS += -lm -lbcrypt -lwinmm $(LDFLAGS_EXTRA) + + # Force the use of 64-bits for file sizes in C library functions on 32-bit platforms. + # This option has no effect on 64-bit builds. @@ -74,6 +74,7 @@ SRC_C = \ modsocket.c \ ports/unix/gccollect.c \ @@ -45,10 +65,10 @@ index 4b6b182..b6a591a 100644 fmode.c \ diff --git a/ports/windows/mod_timing.c b/ports/windows/mod_timing.c new file mode 100644 -index 0000000..a1199a6 +index 000000000..357118932 --- /dev/null +++ b/ports/windows/mod_timing.c -@@ -0,0 +1,139 @@ +@@ -0,0 +1,199 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * @@ -83,29 +103,49 @@ index 0000000..a1199a6 +// _timing.arm(ms) # one-shot: callback is scheduled in ms +// _timing.cancel() +// -+// A timer-queue thread only sets a flag when the deadline passes. The main -+// thread notices it in the VM's periodic hook, in mp_event_wait_ms (so -+// time.sleep_ms sees it), and in the console wait of mp_hal_stdin_rx_chr -+// (so the REPL sees it while it waits for a key), and hands the callback to -+// mp_sched_schedule from its own context. So the callback runs at a bytecode -+// boundary of the main thread, exactly as machine.Timer's soft callbacks do -+// on a board, and no VM state is ever touched from the timer thread. ++// One waitable timer (high resolution where Windows 10 1803+ offers it, so ++// it is not bound to the 15.6 ms system tick) is waited on by a helper ++// thread that only sets a flag and signals an event when the deadline ++// passes. The main thread notices the flag in the VM's periodic hook, in ++// mp_event_wait_ms (so time.sleep_ms sees it) and in the console wait of ++// mp_hal_stdin_rx_chr, and hands the callback to mp_sched_schedule from its ++// own context. So the callback runs at a bytecode boundary of the main ++// thread, exactly as machine.Timer's soft callbacks do on a board, and no VM ++// state is ever touched from the helper thread. The event is what the ++// port's waits block on: a sleep or a REPL waiting for a key returns the ++// moment a deadline passes instead of at the end of a time slice. ++// ++// init() also asks Windows for its 1 ms timer resolution (timeBeginPeriod), ++// as SDL and pygame do: without it every wait in the process, time.sleep_ms ++// included, is rounded up to the 15.6 ms system tick. + +#include ++#include + +#include "py/runtime.h" +#include "py/mphal.h" + +#if MICROPY_PY_TIMING + -+static HANDLE timing_queue = NULL; ++#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION ++#define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002 ++#endif ++ +static HANDLE timing_timer = NULL; ++static HANDLE timing_event = NULL; ++static HANDLE timing_thread = NULL; +static volatile LONG timing_pending = 0; ++static bool timing_period_raised = false; + -+static void CALLBACK timing_expired(PVOID param, BOOLEAN fired) { ++static DWORD WINAPI timing_thread_main(LPVOID param) { + (void)param; -+ (void)fired; -+ InterlockedExchange(&timing_pending, 1); ++ for (;;) { ++ if (WaitForSingleObject(timing_timer, INFINITE) != WAIT_OBJECT_0) { ++ return 0; ++ } ++ InterlockedExchange(&timing_pending, 1); ++ SetEvent(timing_event); ++ } +} + +// Called from the main thread at safe points (see the header comment). @@ -115,33 +155,69 @@ index 0000000..a1199a6 + } + mp_obj_t cb = MP_STATE_PORT(timing_callback); + if (InterlockedExchange(&timing_pending, 0) && cb != MP_OBJ_NULL) { -+ // A full scheduler queue means the VM is not draining it; the next -+ // arm() retries, and multimer counts the miss. -+ mp_sched_schedule(cb, mp_const_none); ++ if (!mp_sched_schedule(cb, mp_const_none)) { ++ // The scheduler queue is full. Keep the deadline pending so the ++ // next poll retries; dropping it would leave nothing to re-arm ++ // the timer and every multimer timer would stop. ++ InterlockedExchange(&timing_pending, 1); ++ } + } +} + -+static void timing_cancel_timer(void) { -+ if (timing_timer != NULL) { -+ // INVALID_HANDLE_VALUE waits for a running callback to finish, so no -+ // callback can set the flag after this returns. -+ DeleteTimerQueueTimer(timing_queue, timing_timer, INVALID_HANDLE_VALUE); -+ timing_timer = NULL; ++// Wait until the next deadline or timeout_ms, whichever comes first, with ++// the GIL released. Alertable, so APCs a library queues still run. ++void mp_timing_wfe(unsigned long timeout_ms) { ++ MP_THREAD_GIL_EXIT(); ++ if (timing_event != NULL) { ++ WaitForSingleObjectEx(timing_event, timeout_ms, TRUE); ++ } else { ++ SleepEx(timeout_ms, TRUE); + } -+ InterlockedExchange(&timing_pending, 0); ++ MP_THREAD_GIL_ENTER(); ++} ++ ++// Wait for console input or the next deadline. True when input is ready (or ++// the wait could not be made, so the caller's blocking read runs as before). ++int mp_timing_wait_input(void *input, unsigned long timeout_ms) { ++ HANDLE hs[2] = { (HANDLE)input, timing_event }; ++ DWORD n = timing_event != NULL ? 2 : 1; ++ MP_THREAD_GIL_EXIT(); ++ DWORD r = WaitForMultipleObjectsEx(n, hs, FALSE, timeout_ms, TRUE); ++ MP_THREAD_GIL_ENTER(); ++ return r == WAIT_OBJECT_0 || r == WAIT_FAILED; +} + +static mp_obj_t timing_init(mp_obj_t callback) { + if (!mp_obj_is_callable(callback)) { + mp_raise_TypeError(MP_ERROR_TEXT("callback must be callable")); + } -+ if (timing_queue == NULL) { -+ timing_queue = CreateTimerQueue(); -+ if (timing_queue == NULL) { ++ if (timing_event == NULL) { ++ timing_event = CreateEventW(NULL, FALSE, FALSE, NULL); ++ if (timing_event == NULL) { + mp_raise_OSError(GetLastError()); + } + } -+ timing_cancel_timer(); ++ if (timing_timer == NULL) { ++ timing_timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); ++ if (timing_timer == NULL) { ++ timing_timer = CreateWaitableTimerW(NULL, FALSE, NULL); ++ } ++ if (timing_timer == NULL) { ++ mp_raise_OSError(GetLastError()); ++ } ++ } ++ if (timing_thread == NULL) { ++ timing_thread = CreateThread(NULL, 0, timing_thread_main, NULL, 0, NULL); ++ if (timing_thread == NULL) { ++ mp_raise_OSError(GetLastError()); ++ } ++ } ++ if (!timing_period_raised) { ++ timeBeginPeriod(1); ++ timing_period_raised = true; ++ } ++ CancelWaitableTimer(timing_timer); ++ InterlockedExchange(&timing_pending, 0); + MP_STATE_PORT(timing_callback) = callback; + return mp_const_none; +} @@ -152,13 +228,14 @@ index 0000000..a1199a6 + if (ms < 0) { + ms = 0; + } -+ if (timing_queue == NULL || MP_STATE_PORT(timing_callback) == MP_OBJ_NULL) { ++ if (timing_timer == NULL || MP_STATE_PORT(timing_callback) == MP_OBJ_NULL) { + mp_raise_ValueError(MP_ERROR_TEXT("init() first")); + } -+ timing_cancel_timer(); -+ if (!CreateTimerQueueTimer(&timing_timer, timing_queue, timing_expired, NULL, -+ (DWORD)ms, 0, WT_EXECUTEONLYONCE)) { -+ timing_timer = NULL; ++ // Relative due time in 100 ns units; -1 fires at once (0 would mean the ++ // epoch, an absolute time). ++ LARGE_INTEGER due; ++ due.QuadPart = ms == 0 ? -1 : -(LONGLONG)ms * 10000; ++ if (!SetWaitableTimer(timing_timer, &due, 0, NULL, NULL, FALSE)) { + mp_raise_OSError(GetLastError()); + } + return mp_const_none; @@ -166,7 +243,10 @@ index 0000000..a1199a6 +static MP_DEFINE_CONST_FUN_OBJ_1(timing_arm_obj, timing_arm); + +static mp_obj_t timing_cancel(void) { -+ timing_cancel_timer(); ++ if (timing_timer != NULL) { ++ CancelWaitableTimer(timing_timer); ++ } ++ InterlockedExchange(&timing_pending, 0); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_0(timing_cancel_obj, timing_cancel); @@ -189,22 +269,25 @@ index 0000000..a1199a6 + +#endif // MICROPY_PY_TIMING diff --git a/ports/windows/mpconfigport.h b/ports/windows/mpconfigport.h -index 4b6b96d..652d028 100644 +index 4b6b96d1b..a4f17ecde 100644 --- a/ports/windows/mpconfigport.h +++ b/ports/windows/mpconfigport.h -@@ -82,6 +82,22 @@ +@@ -82,6 +82,25 @@ #ifndef MICROPY_SCHEDULER_DEPTH #define MICROPY_SCHEDULER_DEPTH (16) #endif +// _timing (mod_timing.c): one OS timer that wakes the main thread, for a port +// with no signals and no machine.Timer. The VM hook and the internal event +// hook let the main thread notice a deadline between bytecodes and inside -+// sleeps; mp_hal_stdin_rx_chr notices it while the REPL waits for a key. ++// sleeps; the port's waits (sleeps, the console, a piped stdin) block on the ++// timer's event, so a deadline wakes them at once. +#ifndef MICROPY_PY_TIMING +#define MICROPY_PY_TIMING (1) +#endif +#if MICROPY_PY_TIMING +void mp_timing_poll(void); ++void mp_timing_wfe(unsigned long timeout_ms); ++int mp_timing_wait_input(void *input, unsigned long timeout_ms); +#define MICROPY_VM_HOOK_COUNT (64) +#define MICROPY_VM_HOOK_INIT static unsigned int vm_hook_divisor = MICROPY_VM_HOOK_COUNT; +#define MICROPY_VM_HOOK_POLL if (--vm_hook_divisor == 0) { vm_hook_divisor = MICROPY_VM_HOOK_COUNT; mp_timing_poll(); } @@ -216,41 +299,38 @@ index 4b6b96d..652d028 100644 #define MICROPY_VFS_POSIX (1) #define MICROPY_PY_FUNCTION_ATTRS (1) diff --git a/ports/windows/windows_mphal.c b/ports/windows/windows_mphal.c -index 4825d93..efea074 100644 +index 4825d935f..3d0bb0908 100644 --- a/ports/windows/windows_mphal.c +++ b/ports/windows/windows_mphal.c -@@ -204,6 +204,19 @@ static int stdin_rx_chr_pipe(void) { +@@ -204,6 +204,18 @@ static int stdin_rx_chr_pipe(void) { } unsigned char c; DWORD num_read = 0; + #if MICROPY_PY_TIMING -+ // A pipe read blocks with nothing to wake it; poll it in slices so a -+ // _timing deadline and other pending callbacks run while the REPL -+ // waits for a line (a file redirect fails the peek and reads on). ++ // A pipe read blocks with nothing to wake it: while it is empty, wait ++ // on _timing's event (10 ms at most) and run what came due, so timers ++ // and other pending callbacks are served while the REPL waits for a ++ // line (a file redirect fails the peek and reads on). + DWORD avail = 0; + if (PeekNamedPipe(std_in, NULL, 0, NULL, &avail, NULL) && avail == 0) { ++ mp_timing_wfe(10); + mp_event_handle_nowait(); -+ MP_THREAD_GIL_EXIT(); -+ Sleep(10); -+ MP_THREAD_GIL_ENTER(); + continue; + } + #endif MP_THREAD_GIL_EXIT(); BOOL status = ReadFile(std_in, &c, 1, &num_read, NULL); MP_THREAD_GIL_ENTER(); -@@ -242,6 +255,18 @@ int mp_hal_stdin_rx_chr(void) { +@@ -242,6 +254,16 @@ int mp_hal_stdin_rx_chr(void) { DWORD num_read; INPUT_RECORD rec; for (;;) { + #if MICROPY_PY_TIMING -+ // Wait for input in slices, so a _timing deadline and any other -+ // pending callback run while the REPL sits at its prompt. The console -+ // handle signals when an input record is available. -+ MP_THREAD_GIL_EXIT(); -+ DWORD waited = WaitForSingleObject(std_in, 10); -+ MP_THREAD_GIL_ENTER(); -+ if (waited == WAIT_TIMEOUT) { ++ // Wait for a key or a _timing deadline, whichever comes first, so a ++ // timer callback runs the moment it is due while the REPL sits at its ++ // prompt. The console handle signals when an input record is ++ // available; the 100 ms bound is only a safety net. ++ if (!mp_timing_wait_input(std_in, 100)) { + mp_event_handle_nowait(); + continue; + } @@ -258,3 +338,20 @@ index 4825d93..efea074 100644 MP_THREAD_GIL_EXIT(); status = ReadConsoleInput(std_in, &rec, 1, &num_read); MP_THREAD_GIL_ENTER(); +diff --git a/ports/windows/windows_mphal.h b/ports/windows/windows_mphal.h +index e70d00def..f56d55b8b 100644 +--- a/ports/windows/windows_mphal.h ++++ b/ports/windows/windows_mphal.h +@@ -30,7 +30,11 @@ + // Don't use the unix version of this macro. + #undef MICROPY_INTERNAL_WFE + +-#if MICROPY_ENABLE_SCHEDULER ++#if MICROPY_ENABLE_SCHEDULER && MICROPY_PY_TIMING ++// Wait on _timing's event (mod_timing.c), so a sleep returns the moment a ++// deadline passes instead of at the end of its time slice. ++#define MICROPY_INTERNAL_WFE(TIMEOUT_MS) mp_timing_wfe((unsigned long)MAX(1, (TIMEOUT_MS))) ++#elif MICROPY_ENABLE_SCHEDULER + // Use minimum 1mSec sleep to make sure there is effectively a wait period: + // something like usleep(500) truncates and ends up calling Sleep(0). + #define MICROPY_INTERNAL_WFE(TIMEOUT_MS) msec_sleep(MAX(1.0, (double)(TIMEOUT_MS))) diff --git a/provenance.json b/provenance.json index 94eec67..42ddd16 100644 --- a/provenance.json +++ b/provenance.json @@ -45,7 +45,7 @@ "sha256": "15a101901c7bebd93b5846e02fb1ff62b3c9a5e043b43c7c76de455007e02492" }, "0015-micropython-windows-timing-module-and-idle-console.patch": { - "sha256": "3c255514979938e16aca6f19459be2447206bada168ebd3f29294b5af1f86418" + "sha256": "77ce5f8b4a0745bf4764e3b47255b023a335209c649c60400f335ec2f11ae30a" } } }