diff --git a/src/internal/task/task_stack_esp32.S b/src/internal/task/task_stack_esp32.S index f07f7e3ae..1b1608d06 100644 --- a/src/internal/task/task_stack_esp32.S +++ b/src/internal/task/task_stack_esp32.S @@ -46,26 +46,40 @@ tinygo_swapTask: // arbitrary register while registers are flushed. rsil a4, 3 // XCHAL_EXCM_LEVEL - // Flush all unsaved registers to the stack. - // This trick has been borrowed from the Zephyr project: - // https://github.com/zephyrproject-rtos/zephyr/blob/d79b003758/arch/xtensa/include/xtensa-asm2-s.h#L17 - and a12, a12, a12 - rotw 3 - and a12, a12, a12 - rotw 3 - and a12, a12, a12 - rotw 3 - and a12, a12, a12 - rotw 3 - and a12, a12, a12 - rotw 4 + // Save the old PS (in a4) to the stack because call4 clobbers a4-a7. + s32i a4, sp, 4 + + // Flush all register windows to the stack using recursive call4. + // + // The previous ROTW-based approach (borrowed from Zephyr) does NOT + // actually trigger window overflow exceptions on Xtensa LX7 — ROTW + // simply modifies WindowBase without saving any registers to the stack. + // This leaves stale window data in the physical register file after a + // goroutine switch, corrupting the overflow save chain when the new + // goroutine's deeper calls trigger overflow of old goroutine windows. + // + // Instead, we recursively call a small function via call4. Each call4 + + // entry triggers the hardware window-overflow mechanism for any occupied + // pane being reused, correctly saving registers to the stack. + // For NAREG=64 (16 panes), 15 recursive levels cover all panes except + // the current one (tinygo_swapTask), which must stay active. + movi a6, 15 + call4 .Lspill_windows + + // After the recursive spill returns, the physical register file still + // has WindowStart bits set for the spill helper frames. + // We will clear WindowStart completely (to 0) right before the retw.n + // below, after the stack switch is done. This prevents stale overflow + // when the new goroutine's calls rotate back into these panes. // Restore interrupts. + l32i a4, sp, 4 // reload saved PS wsr.ps a4 + rsync // At this point, the following is true: // WindowStart == 1 << WindowBase - // Therefore, we don't need to do this manually. + // All other windows have been properly flushed to their stacks. // It also means that the stack pointer can now be safely modified. // Save a0, which stores the return address and the parent register window @@ -83,6 +97,30 @@ tinygo_swapTask: // register also stores the parent register window. l32i.n a0, sp, 0 + // Clear ALL WindowStart bits. With all windows spilled to the stack, + // we must ensure no stale WS bits remain: the retw.n below will trigger + // underflow4 to load the new goroutine's registers from the new stack + // (which sets the appropriate WS bit via rfwu). Any stale WS bits + // (from spill helpers or the old goroutine) would cause spurious + // overflows of garbage register values into memory. + movi a5, 0 + wsr a5, WINDOWSTART + rsync + // Return into the new stack. This instruction will trigger a window // underflow, reloading the saved registers from the stack. retw.n + +// Recursive helper for flushing all register windows. +// Parameter: a2 = remaining recursion depth (passed via caller's a6). +// Each call4 + entry triggers the hardware overflow mechanism for any +// occupied pane at the new WindowBase position. + .balign 4 +.Lspill_windows: + entry a1, 16 + beqz a2, .Lspill_done + addi a2, a2, -1 + mov a6, a2 + call4 .Lspill_windows +.Lspill_done: + retw.n diff --git a/src/internal/task/task_stack_esp32.go b/src/internal/task/task_stack_esp32.go index e32df88e3..06613b296 100644 --- a/src/internal/task/task_stack_esp32.go +++ b/src/internal/task/task_stack_esp32.go @@ -74,3 +74,8 @@ func (s *state) pause() { func SystemStack() uintptr { return systemStack } + +//export tinygo_task_current +func tinygo_task_current() unsafe.Pointer { + return unsafe.Pointer(Current()) +}