esp32s3: fix Xtensa register window spill using recursive call4 in task switching

Replace the ROTW-based register flush with a recursive call4 approach
that properly triggers hardware window-overflow exceptions. ROTW only
modifies WindowBase without saving registers, causing corruption when
switching goroutines. The recursive call4 correctly spills all 15 window
panes. Also clear WindowStart after the stack switch to prevent stale
overflow of garbage register values.

Add tinygo_task_current export for C interop.

Signed-off-by: deadprogram <ron@hybridgroup.com>
This commit is contained in:
deadprogram
2026-04-07 12:15:22 +02:00
committed by Ron Evans
parent 5b7a2c73db
commit c3d514a751
2 changed files with 57 additions and 14 deletions
+52 -14
View File
@@ -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
+5
View File
@@ -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())
}