detached 585a4c5

purego: add support for s390x (#413)

Implements the necessary changes to support linux/s390x (with CGO_ENABLED=1).
For now we can't support without cgo until golang/go#77449 has been addressed
although I have a CL to implement the required changes.
commit
585a4c527464bb6ef91b22aef08d55219c8fab09
author
George Adams · · 2026-02-16 09:23 UTC
committer
GitHub · · 2026-02-16 09:23 UTC
parent
d685103
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.10.0-alpha.5
14 files · +4561 -20
M .github/workflows/test.yml +11 -0
@@ -72,6 +72,7 @@ jobs:
7272
7373 - name: go build (Linux minor architectures)
7474 # Test them only on the latest Go to reduce CI time.
75+ # s390x cannot be tested here as it requires Cgo.
7576 if: startsWith(matrix.go, '1.26.')
7677 run: |
7778 # Check cross-compiling Linux binaries for minor architectures.
@@ -179,6 +180,7 @@ jobs:
179180 gcc-14-loongarch64-linux-gnu g++-14-loongarch64-linux-gnu \
180181 gcc-powerpc64le-linux-gnu g++-powerpc64le-linux-gnu \
181182 gcc-riscv64-linux-gnu g++-riscv64-linux-gnu \
183+ gcc-s390x-linux-gnu g++-s390x-linux-gnu \
182184 qemu-user
183185
184186 - name: go test (Linux loong64)
@@ -216,6 +218,15 @@ jobs:
216218 go env -u CC
217219 go env -u CXX
218220
221+ - name: go test (Linux s390x)
222+ run: |
223+ go env -w CC=s390x-linux-gnu-gcc
224+ go env -w CXX=s390x-linux-gnu-g++
225+ env GOOS=linux GOARCH=s390x CGO_ENABLED=1 go test -c -o=purego-test-cgo .
226+ env QEMU_LD_PREFIX=/usr/s390x-linux-gnu qemu-s390x ./purego-test-cgo -test.shuffle=on -test.v -test.count=10
227+ go env -u CC
228+ go env -u CXX
229+
219230 bsd:
220231 strategy:
221232 matrix:
@@ -72,6 +72,7 @@ jobs:
72 72
73 - name: go build (Linux minor architectures)73 - name: go build (Linux minor architectures)
74 # Test them only on the latest Go to reduce CI time.74 # Test them only on the latest Go to reduce CI time.
75+ # s390x cannot be tested here as it requires Cgo.
75 if: startsWith(matrix.go, '1.26.')76 if: startsWith(matrix.go, '1.26.')
76 run: |77 run: |
77 # Check cross-compiling Linux binaries for minor architectures.78 # Check cross-compiling Linux binaries for minor architectures.
@@ -179,6 +180,7 @@ jobs:
179 gcc-14-loongarch64-linux-gnu g++-14-loongarch64-linux-gnu \180 gcc-14-loongarch64-linux-gnu g++-14-loongarch64-linux-gnu \
180 gcc-powerpc64le-linux-gnu g++-powerpc64le-linux-gnu \181 gcc-powerpc64le-linux-gnu g++-powerpc64le-linux-gnu \
181 gcc-riscv64-linux-gnu g++-riscv64-linux-gnu \182 gcc-riscv64-linux-gnu g++-riscv64-linux-gnu \
183+ gcc-s390x-linux-gnu g++-s390x-linux-gnu \
182 qemu-user184 qemu-user
183 185
184 - name: go test (Linux loong64)186 - name: go test (Linux loong64)
@@ -216,6 +218,15 @@ jobs:
216 go env -u CC218 go env -u CC
217 go env -u CXX219 go env -u CXX
218 220
221+ - name: go test (Linux s390x)
222+ run: |
223+ go env -w CC=s390x-linux-gnu-gcc
224+ go env -w CXX=s390x-linux-gnu-g++
225+ env GOOS=linux GOARCH=s390x CGO_ENABLED=1 go test -c -o=purego-test-cgo .
226+ env QEMU_LD_PREFIX=/usr/s390x-linux-gnu qemu-s390x ./purego-test-cgo -test.shuffle=on -test.v -test.count=10
227+ go env -u CC
228+ go env -u CXX
229+
219 bsd:230 bsd:
220 strategy:231 strategy:
221 matrix:232 matrix:
M README.md +1 -1
@@ -42,7 +42,7 @@ Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs o
4242
4343 - **Android**: 386<sup>1</sup>, arm<sup>1</sup>
4444 - **FreeBSD**: amd64<sup>2</sup>, arm64<sup>2</sup>
45-- **Linux**: 386, arm, loong64, ppc64le, riscv64
45+- **Linux**: 386, arm, loong64, ppc64le, riscv64, s390x<sup>1</sup>
4646 - **Windows**: 386<sup>3</sup>, arm<sup>3,4</sup>
4747
4848 #### Support Notes
@@ -42,7 +42,7 @@ Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs o
42 42
43 - **Android**: 386<sup>1</sup>, arm<sup>1</sup>43 - **Android**: 386<sup>1</sup>, arm<sup>1</sup>
44 - **FreeBSD**: amd64<sup>2</sup>, arm64<sup>2</sup>44 - **FreeBSD**: amd64<sup>2</sup>, arm64<sup>2</sup>
45-- **Linux**: 386, arm, loong64, ppc64le, riscv6445+- **Linux**: 386, arm, loong64, ppc64le, riscv64, s390x<sup>1</sup>
46 - **Windows**: 386<sup>3</sup>, arm<sup>3,4</sup>46 - **Windows**: 386<sup>3</sup>, arm<sup>3,4</sup>
47 47
48 #### Support Notes48 #### Support Notes
M callback_test.go +6 -1
@@ -1,7 +1,7 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2023 The Ebitengine Authors
33
4-//go:build darwin || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64))
4+//go:build darwin || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || s390x))
55
66 package purego_test
77
@@ -126,6 +126,11 @@ func TestNewCallbackFloat32(t *testing.T) {
126126 }
127127
128128 func TestNewCallbackFloat32AndFloat64(t *testing.T) {
129+ if runtime.GOARCH == "s390x" {
130+ // S390X has only 4 float registers (F0,F2,F4,F6), so 15 floats requires
131+ // 11 stack slots, but only 10 are available (maxArgs - numIntRegs = 15 - 5)
132+ t.Skip("Test requires too many stack arguments for s390x")
133+ }
129134 // This tests that calling a function with a mix of float32 and float64 arguments works
130135 const (
131136 expectedCbTotalF32 = float32(72)
@@ -1,7 +1,7 @@
1 // SPDX-License-Identifier: Apache-2.01 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: 2023 The Ebitengine Authors2 // SPDX-FileCopyrightText: 2023 The Ebitengine Authors
3 3
4-//go:build darwin || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64))4+//go:build darwin || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || s390x))
5 5
6 package purego_test6 package purego_test
7 7
@@ -126,6 +126,11 @@ func TestNewCallbackFloat32(t *testing.T) {
126 }126 }
127 127
128 func TestNewCallbackFloat32AndFloat64(t *testing.T) {128 func TestNewCallbackFloat32AndFloat64(t *testing.T) {
129+ if runtime.GOARCH == "s390x" {
130+ // S390X has only 4 float registers (F0,F2,F4,F6), so 15 floats requires
131+ // 11 stack slots, but only 10 are available (maxArgs - numIntRegs = 15 - 5)
132+ t.Skip("Test requires too many stack arguments for s390x")
133+ }
129 // This tests that calling a function with a mix of float32 and float64 arguments works134 // This tests that calling a function with a mix of float32 and float64 arguments works
130 const (135 const (
131 expectedCbTotalF32 = float32(72)136 expectedCbTotalF32 = float32(72)
M func.go +18 -4
@@ -133,7 +133,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
133133 panic("purego: cfn is nil")
134134 }
135135 if ty.NumOut() == 1 && (ty.Out(0).Kind() == reflect.Float32 || ty.Out(0).Kind() == reflect.Float64) &&
136- runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" {
136+ runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
137137 panic("purego: float returns are not supported")
138138 }
139139 {
@@ -273,7 +273,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
273273 var arm64_r8 uintptr
274274 if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {
275275 outType := ty.Out(0)
276- if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64") && outType.Size() > maxRegAllocStructSize {
276+ if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") && outType.Size() > maxRegAllocStructSize {
277277 val := reflect.New(outType)
278278 keepAlive = append(keepAlive, val)
279279 addInt(val.Pointer())
@@ -313,7 +313,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
313313 syscall := thePool.Get().(*syscall15Args)
314314 defer thePool.Put(syscall)
315315
316- if runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" {
316+ if runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x" {
317317 syscall.Set(cfn, sysargs[:], floats[:], 0)
318318 runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
319319 } else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {
@@ -356,11 +356,15 @@ func RegisterFunc(fptr any, cfn uintptr) {
356356 // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
357357 // On 386, x87 FPU returns floats as float64 in ST(0), so we read as float64 and convert.
358358 // On PPC64LE, C ABI converts float32 to double in FPR, so we read as float64.
359+ // On S390X (big-endian), float32 is in upper 32 bits of the 64-bit FP register.
359360 switch runtime.GOARCH {
360361 case "386":
361362 v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
362363 case "ppc64le":
363364 v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
365+ case "s390x":
366+ // S390X is big-endian: float32 in upper 32 bits of 64-bit register
367+ v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1 >> 32))))
364368 default:
365369 v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1))))
366370 }
@@ -411,7 +415,12 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat
411415 addInt(0)
412416 }
413417 case reflect.Float32:
414- addFloat(uintptr(math.Float32bits(float32(v.Float()))))
418+ // On S390X big-endian, float32 goes in upper 32 bits of 64-bit FP register
419+ if runtime.GOARCH == "s390x" {
420+ addFloat(uintptr(math.Float32bits(float32(v.Float()))) << 32)
421+ } else {
422+ addFloat(uintptr(math.Float32bits(float32(v.Float()))))
423+ }
415424 case reflect.Float64:
416425 if is32bit {
417426 bits := math.Float64bits(v.Float())
@@ -498,6 +507,8 @@ func numOfFloatRegisters() int {
498507 switch runtime.GOARCH {
499508 case "amd64", "arm64", "loong64", "ppc64le", "riscv64":
500509 return 8
510+ case "s390x":
511+ return 4
501512 case "arm":
502513 return 16
503514 case "386":
@@ -516,6 +527,9 @@ func numOfIntegerRegisters() int {
516527 return 8
517528 case "amd64":
518529 return 6
530+ case "s390x":
531+ // S390X uses R2-R6 for integer arguments
532+ return 5
519533 case "arm":
520534 return 4
521535 case "386":
@@ -133,7 +133,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
133 panic("purego: cfn is nil")133 panic("purego: cfn is nil")
134 }134 }
135 if ty.NumOut() == 1 && (ty.Out(0).Kind() == reflect.Float32 || ty.Out(0).Kind() == reflect.Float64) &&135 if ty.NumOut() == 1 && (ty.Out(0).Kind() == reflect.Float32 || ty.Out(0).Kind() == reflect.Float64) &&
136- runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" {136+ runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
137 panic("purego: float returns are not supported")137 panic("purego: float returns are not supported")
138 }138 }
139 {139 {
@@ -273,7 +273,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
273 var arm64_r8 uintptr273 var arm64_r8 uintptr
274 if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {274 if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {
275 outType := ty.Out(0)275 outType := ty.Out(0)
276- if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64") && outType.Size() > maxRegAllocStructSize {276+ if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") && outType.Size() > maxRegAllocStructSize {
277 val := reflect.New(outType)277 val := reflect.New(outType)
278 keepAlive = append(keepAlive, val)278 keepAlive = append(keepAlive, val)
279 addInt(val.Pointer())279 addInt(val.Pointer())
@@ -313,7 +313,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
313 syscall := thePool.Get().(*syscall15Args)313 syscall := thePool.Get().(*syscall15Args)
314 defer thePool.Put(syscall)314 defer thePool.Put(syscall)
315 315
316- if runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" {316+ if runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x" {
317 syscall.Set(cfn, sysargs[:], floats[:], 0)317 syscall.Set(cfn, sysargs[:], floats[:], 0)
318 runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))318 runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
319 } else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {319 } else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {
@@ -356,11 +356,15 @@ func RegisterFunc(fptr any, cfn uintptr) {
356 // On 32bit platforms syscall.r2 is the upper part of a 64bit return.356 // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
357 // On 386, x87 FPU returns floats as float64 in ST(0), so we read as float64 and convert.357 // On 386, x87 FPU returns floats as float64 in ST(0), so we read as float64 and convert.
358 // On PPC64LE, C ABI converts float32 to double in FPR, so we read as float64.358 // On PPC64LE, C ABI converts float32 to double in FPR, so we read as float64.
359+ // On S390X (big-endian), float32 is in upper 32 bits of the 64-bit FP register.
359 switch runtime.GOARCH {360 switch runtime.GOARCH {
360 case "386":361 case "386":
361 v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))362 v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
362 case "ppc64le":363 case "ppc64le":
363 v.SetFloat(math.Float64frombits(uint64(syscall.f1)))364 v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
365+ case "s390x":
366+ // S390X is big-endian: float32 in upper 32 bits of 64-bit register
367+ v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1 >> 32))))
364 default:368 default:
365 v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1))))369 v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1))))
366 }370 }
@@ -411,7 +415,12 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat
411 addInt(0)415 addInt(0)
412 }416 }
413 case reflect.Float32:417 case reflect.Float32:
414- addFloat(uintptr(math.Float32bits(float32(v.Float()))))418+ // On S390X big-endian, float32 goes in upper 32 bits of 64-bit FP register
419+ if runtime.GOARCH == "s390x" {
420+ addFloat(uintptr(math.Float32bits(float32(v.Float()))) << 32)
421+ } else {
422+ addFloat(uintptr(math.Float32bits(float32(v.Float()))))
423+ }
415 case reflect.Float64:424 case reflect.Float64:
416 if is32bit {425 if is32bit {
417 bits := math.Float64bits(v.Float())426 bits := math.Float64bits(v.Float())
@@ -498,6 +507,8 @@ func numOfFloatRegisters() int {
498 switch runtime.GOARCH {507 switch runtime.GOARCH {
499 case "amd64", "arm64", "loong64", "ppc64le", "riscv64":508 case "amd64", "arm64", "loong64", "ppc64le", "riscv64":
500 return 8509 return 8
510+ case "s390x":
511+ return 4
501 case "arm":512 case "arm":
502 return 16513 return 16
503 case "386":514 case "386":
@@ -516,6 +527,9 @@ func numOfIntegerRegisters() int {
516 return 8527 return 8
517 case "amd64":528 case "amd64":
518 return 6529 return 6
530+ case "s390x":
531+ // S390X uses R2-R6 for integer arguments
532+ return 5
519 case "arm":533 case "arm":
520 return 4534 return 4
521 case "386":535 case "386":
M func_test.go +4 -4
@@ -50,7 +50,7 @@ func TestRegisterFunc(t *testing.T) {
5050 }
5151
5252 func Test_qsort(t *testing.T) {
53- if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" {
53+ if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
5454 t.Skip("Platform doesn't support Floats")
5555 return
5656 }
@@ -79,7 +79,7 @@ func Test_qsort(t *testing.T) {
7979 }
8080
8181 func TestRegisterFunc_Floats(t *testing.T) {
82- if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" {
82+ if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
8383 t.Skip("Platform doesn't support Floats")
8484 return
8585 }
@@ -121,7 +121,7 @@ func TestRegisterFunc_Floats(t *testing.T) {
121121 }
122122
123123 func TestRegisterLibFunc_Bool(t *testing.T) {
124- if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" {
124+ if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
125125 t.Skip("Platform doesn't support callbacks")
126126 return
127127 }
@@ -376,7 +376,7 @@ func TestABI_ArgumentPassing(t *testing.T) {
376376 if tt.name == "20_int32" && (runtime.GOOS != "darwin" || runtime.GOARCH != "arm64") {
377377 t.Skip("20 int32 arguments only supported on Darwin ARM64 with smart stack checking")
378378 }
379- if tt.name == "10_float32" && (runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64") {
379+ if tt.name == "10_float32" && (runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") {
380380 t.Skip("float32 stack arguments not yet supported on this platform")
381381 }
382382 // Struct tests require Darwin ARM64 or AMD64
@@ -50,7 +50,7 @@ func TestRegisterFunc(t *testing.T) {
50 }50 }
51 51
52 func Test_qsort(t *testing.T) {52 func Test_qsort(t *testing.T) {
53- if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" {53+ if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
54 t.Skip("Platform doesn't support Floats")54 t.Skip("Platform doesn't support Floats")
55 return55 return
56 }56 }
@@ -79,7 +79,7 @@ func Test_qsort(t *testing.T) {
79 }79 }
80 80
81 func TestRegisterFunc_Floats(t *testing.T) {81 func TestRegisterFunc_Floats(t *testing.T) {
82- if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" {82+ if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
83 t.Skip("Platform doesn't support Floats")83 t.Skip("Platform doesn't support Floats")
84 return84 return
85 }85 }
@@ -121,7 +121,7 @@ func TestRegisterFunc_Floats(t *testing.T) {
121 }121 }
122 122
123 func TestRegisterLibFunc_Bool(t *testing.T) {123 func TestRegisterLibFunc_Bool(t *testing.T) {
124- if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" {124+ if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
125 t.Skip("Platform doesn't support callbacks")125 t.Skip("Platform doesn't support callbacks")
126 return126 return
127 }127 }
@@ -376,7 +376,7 @@ func TestABI_ArgumentPassing(t *testing.T) {
376 if tt.name == "20_int32" && (runtime.GOOS != "darwin" || runtime.GOARCH != "arm64") {376 if tt.name == "20_int32" && (runtime.GOOS != "darwin" || runtime.GOARCH != "arm64") {
377 t.Skip("20 int32 arguments only supported on Darwin ARM64 with smart stack checking")377 t.Skip("20 int32 arguments only supported on Darwin ARM64 with smart stack checking")
378 }378 }
379- if tt.name == "10_float32" && (runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64") {379+ if tt.name == "10_float32" && (runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") {
380 t.Skip("float32 stack arguments not yet supported on this platform")380 t.Skip("float32 stack arguments not yet supported on this platform")
381 }381 }
382 // Struct tests require Darwin ARM64 or AMD64382 // Struct tests require Darwin ARM64 or AMD64
A internal/fakecgo/asm_s390x.s +55 -0
@@ -0,0 +1,55 @@
1+// Copyright 2016 The Go Authors. All rights reserved.
2+// Use of this source code is governed by a BSD-style
3+// license that can be found in the LICENSE file.
4+
5+#include "textflag.h"
6+
7+// Called by C code generated by cmd/cgo.
8+// func crosscall2(fn, a unsafe.Pointer, n int32, ctxt uintptr)
9+// Saves C callee-saved registers and calls cgocallback with three arguments.
10+// fn is the PC of a func(a unsafe.Pointer) function.
11+TEXT crosscall2(SB), NOSPLIT|NOFRAME, $0
12+ // Start with standard C stack frame layout and linkage.
13+
14+ // Save R6-R15 in the register save area of the calling function.
15+ STMG R6, R15, 48(R15)
16+
17+ // Allocate 96 bytes on the stack.
18+ MOVD $-96(R15), R15
19+
20+ // Save F8-F15 in our stack frame.
21+ FMOVD F8, 32(R15)
22+ FMOVD F9, 40(R15)
23+ FMOVD F10, 48(R15)
24+ FMOVD F11, 56(R15)
25+ FMOVD F12, 64(R15)
26+ FMOVD F13, 72(R15)
27+ FMOVD F14, 80(R15)
28+ FMOVD F15, 88(R15)
29+
30+ // Initialize Go ABI environment.
31+ BL runtime·load_g(SB)
32+
33+ MOVD R2, 8(R15) // fn unsafe.Pointer
34+ MOVD R3, 16(R15) // a unsafe.Pointer
35+
36+ // Skip R4 = n uint32
37+ MOVD R5, 24(R15) // ctxt uintptr
38+ BL runtime·cgocallback(SB)
39+
40+ FMOVD 32(R15), F8
41+ FMOVD 40(R15), F9
42+ FMOVD 48(R15), F10
43+ FMOVD 56(R15), F11
44+ FMOVD 64(R15), F12
45+ FMOVD 72(R15), F13
46+ FMOVD 80(R15), F14
47+ FMOVD 88(R15), F15
48+
49+ // De-allocate stack frame.
50+ MOVD $96(R15), R15
51+
52+ // Restore R6-R15.
53+ LMG 48(R15), R6, R15
54+
55+ RET
@@ -0,0 +1,55 @@
1+// Copyright 2016 The Go Authors. All rights reserved.
2+// Use of this source code is governed by a BSD-style
3+// license that can be found in the LICENSE file.
4+
5+#include "textflag.h"
6+
7+// Called by C code generated by cmd/cgo.
8+// func crosscall2(fn, a unsafe.Pointer, n int32, ctxt uintptr)
9+// Saves C callee-saved registers and calls cgocallback with three arguments.
10+// fn is the PC of a func(a unsafe.Pointer) function.
11+TEXT crosscall2(SB), NOSPLIT|NOFRAME, $0
12+ // Start with standard C stack frame layout and linkage.
13+
14+ // Save R6-R15 in the register save area of the calling function.
15+ STMG R6, R15, 48(R15)
16+
17+ // Allocate 96 bytes on the stack.
18+ MOVD $-96(R15), R15
19+
20+ // Save F8-F15 in our stack frame.
21+ FMOVD F8, 32(R15)
22+ FMOVD F9, 40(R15)
23+ FMOVD F10, 48(R15)
24+ FMOVD F11, 56(R15)
25+ FMOVD F12, 64(R15)
26+ FMOVD F13, 72(R15)
27+ FMOVD F14, 80(R15)
28+ FMOVD F15, 88(R15)
29+
30+ // Initialize Go ABI environment.
31+ BL runtime·load_g(SB)
32+
33+ MOVD R2, 8(R15) // fn unsafe.Pointer
34+ MOVD R3, 16(R15) // a unsafe.Pointer
35+
36+ // Skip R4 = n uint32
37+ MOVD R5, 24(R15) // ctxt uintptr
38+ BL runtime·cgocallback(SB)
39+
40+ FMOVD 32(R15), F8
41+ FMOVD 40(R15), F9
42+ FMOVD 48(R15), F10
43+ FMOVD 56(R15), F11
44+ FMOVD 64(R15), F12
45+ FMOVD 72(R15), F13
46+ FMOVD 80(R15), F14
47+ FMOVD 88(R15), F15
48+
49+ // De-allocate stack frame.
50+ MOVD $96(R15), R15
51+
52+ // Restore R6-R15.
53+ LMG 48(R15), R6, R15
54+
55+ RET
A struct_s390x.go +143 -0
@@ -0,0 +1,143 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package purego
5+
6+import (
7+ "reflect"
8+ "unsafe"
9+)
10+
11+func getStruct(outType reflect.Type, syscall syscall15Args) reflect.Value {
12+ outSize := outType.Size()
13+
14+ switch {
15+ case outSize == 0:
16+ return reflect.New(outType).Elem()
17+
18+ case outSize <= 16:
19+ // Reconstruct from registers by copying raw bytes
20+ var buf [16]byte
21+
22+ // Integer registers
23+ *(*uintptr)(unsafe.Pointer(&buf[0])) = syscall.a1
24+ if outSize > 8 {
25+ *(*uintptr)(unsafe.Pointer(&buf[8])) = syscall.a2
26+ }
27+
28+ // Homogeneous float aggregates override integer regs
29+ if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
30+ if outType.Field(0).Type.Kind() == reflect.Float32 {
31+ // float32 values in FP regs
32+ f := []uintptr{syscall.f1, syscall.f2, syscall.f3, syscall.f4}
33+ for i := 0; i < numFields; i++ {
34+ *(*uint32)(unsafe.Pointer(&buf[i*4])) = uint32(f[i])
35+ }
36+ } else {
37+ // float64: whole register value is valid
38+ *(*uintptr)(unsafe.Pointer(&buf[0])) = syscall.f1
39+ if outSize > 8 {
40+ *(*uintptr)(unsafe.Pointer(&buf[8])) = syscall.f2
41+ }
42+ }
43+ }
44+
45+ return reflect.NewAt(outType, unsafe.Pointer(&buf[0])).Elem()
46+
47+ default:
48+ // Returned indirectly via pointer in a1
49+ ptr := *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))
50+ return reflect.NewAt(outType, ptr).Elem()
51+ }
52+}
53+
54+func addStruct(
55+ v reflect.Value,
56+ numInts, numFloats, numStack *int,
57+ addInt, addFloat, addStack func(uintptr),
58+ keepAlive []any,
59+) []any {
60+ size := v.Type().Size()
61+ if size == 0 {
62+ return keepAlive
63+ }
64+
65+ if size <= 16 {
66+ return placeSmallAggregateS390X(v, addFloat, addInt, keepAlive)
67+ }
68+
69+ return placeStack(v, keepAlive, addInt)
70+}
71+
72+func placeSmallAggregateS390X(
73+ v reflect.Value,
74+ addFloat, addInt func(uintptr),
75+ keepAlive []any,
76+) []any {
77+ size := v.Type().Size()
78+
79+ var ptr unsafe.Pointer
80+ if v.CanAddr() {
81+ ptr = v.Addr().UnsafePointer()
82+ } else {
83+ tmp := reflect.New(v.Type())
84+ tmp.Elem().Set(v)
85+ ptr = tmp.UnsafePointer()
86+ keepAlive = append(keepAlive, tmp.Interface())
87+ }
88+
89+ var buf [16]byte
90+ src := unsafe.Slice((*byte)(ptr), size)
91+ copy(buf[:], src)
92+
93+ w0 := *(*uintptr)(unsafe.Pointer(&buf[0]))
94+ w1 := uintptr(0)
95+ if size > 8 {
96+ w1 = *(*uintptr)(unsafe.Pointer(&buf[8]))
97+ }
98+
99+ if isFloats, _ := isAllSameFloat(v.Type()); isFloats {
100+ addFloat(w0)
101+ if size > 8 {
102+ addFloat(w1)
103+ }
104+ } else {
105+ addInt(w0)
106+ if size > 8 {
107+ addInt(w1)
108+ }
109+ }
110+
111+ return keepAlive
112+}
113+
114+// placeStack is a fallback for structs that are too large to fit in registers
115+func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any {
116+ if v.CanAddr() {
117+ addInt(v.Addr().Pointer())
118+ return keepAlive
119+ }
120+ ptr := reflect.New(v.Type())
121+ ptr.Elem().Set(v)
122+ addInt(ptr.Pointer())
123+ return append(keepAlive, ptr.Interface())
124+}
125+
126+func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
127+ // S390X does not bundle stack args
128+ return false
129+}
130+
131+func collectStackArgs(
132+ args []reflect.Value,
133+ i, numInts, numFloats int,
134+ keepAlive []any,
135+ addInt, addFloat, addStack func(uintptr),
136+ numIntsPtr, numFloatsPtr, numStackPtr *int,
137+) ([]reflect.Value, []any) {
138+ return nil, keepAlive
139+}
140+
141+func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
142+ panic("bundleStackArgs not supported on S390X")
143+}
@@ -0,0 +1,143 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package purego
5+
6+import (
7+ "reflect"
8+ "unsafe"
9+)
10+
11+func getStruct(outType reflect.Type, syscall syscall15Args) reflect.Value {
12+ outSize := outType.Size()
13+
14+ switch {
15+ case outSize == 0:
16+ return reflect.New(outType).Elem()
17+
18+ case outSize <= 16:
19+ // Reconstruct from registers by copying raw bytes
20+ var buf [16]byte
21+
22+ // Integer registers
23+ *(*uintptr)(unsafe.Pointer(&buf[0])) = syscall.a1
24+ if outSize > 8 {
25+ *(*uintptr)(unsafe.Pointer(&buf[8])) = syscall.a2
26+ }
27+
28+ // Homogeneous float aggregates override integer regs
29+ if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
30+ if outType.Field(0).Type.Kind() == reflect.Float32 {
31+ // float32 values in FP regs
32+ f := []uintptr{syscall.f1, syscall.f2, syscall.f3, syscall.f4}
33+ for i := 0; i < numFields; i++ {
34+ *(*uint32)(unsafe.Pointer(&buf[i*4])) = uint32(f[i])
35+ }
36+ } else {
37+ // float64: whole register value is valid
38+ *(*uintptr)(unsafe.Pointer(&buf[0])) = syscall.f1
39+ if outSize > 8 {
40+ *(*uintptr)(unsafe.Pointer(&buf[8])) = syscall.f2
41+ }
42+ }
43+ }
44+
45+ return reflect.NewAt(outType, unsafe.Pointer(&buf[0])).Elem()
46+
47+ default:
48+ // Returned indirectly via pointer in a1
49+ ptr := *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))
50+ return reflect.NewAt(outType, ptr).Elem()
51+ }
52+}
53+
54+func addStruct(
55+ v reflect.Value,
56+ numInts, numFloats, numStack *int,
57+ addInt, addFloat, addStack func(uintptr),
58+ keepAlive []any,
59+) []any {
60+ size := v.Type().Size()
61+ if size == 0 {
62+ return keepAlive
63+ }
64+
65+ if size <= 16 {
66+ return placeSmallAggregateS390X(v, addFloat, addInt, keepAlive)
67+ }
68+
69+ return placeStack(v, keepAlive, addInt)
70+}
71+
72+func placeSmallAggregateS390X(
73+ v reflect.Value,
74+ addFloat, addInt func(uintptr),
75+ keepAlive []any,
76+) []any {
77+ size := v.Type().Size()
78+
79+ var ptr unsafe.Pointer
80+ if v.CanAddr() {
81+ ptr = v.Addr().UnsafePointer()
82+ } else {
83+ tmp := reflect.New(v.Type())
84+ tmp.Elem().Set(v)
85+ ptr = tmp.UnsafePointer()
86+ keepAlive = append(keepAlive, tmp.Interface())
87+ }
88+
89+ var buf [16]byte
90+ src := unsafe.Slice((*byte)(ptr), size)
91+ copy(buf[:], src)
92+
93+ w0 := *(*uintptr)(unsafe.Pointer(&buf[0]))
94+ w1 := uintptr(0)
95+ if size > 8 {
96+ w1 = *(*uintptr)(unsafe.Pointer(&buf[8]))
97+ }
98+
99+ if isFloats, _ := isAllSameFloat(v.Type()); isFloats {
100+ addFloat(w0)
101+ if size > 8 {
102+ addFloat(w1)
103+ }
104+ } else {
105+ addInt(w0)
106+ if size > 8 {
107+ addInt(w1)
108+ }
109+ }
110+
111+ return keepAlive
112+}
113+
114+// placeStack is a fallback for structs that are too large to fit in registers
115+func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any {
116+ if v.CanAddr() {
117+ addInt(v.Addr().Pointer())
118+ return keepAlive
119+ }
120+ ptr := reflect.New(v.Type())
121+ ptr.Elem().Set(v)
122+ addInt(ptr.Pointer())
123+ return append(keepAlive, ptr.Interface())
124+}
125+
126+func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
127+ // S390X does not bundle stack args
128+ return false
129+}
130+
131+func collectStackArgs(
132+ args []reflect.Value,
133+ i, numInts, numFloats int,
134+ keepAlive []any,
135+ addInt, addFloat, addStack func(uintptr),
136+ numIntsPtr, numFloatsPtr, numStackPtr *int,
137+) ([]reflect.Value, []any) {
138+ return nil, keepAlive
139+}
140+
141+func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
142+ panic("bundleStackArgs not supported on S390X")
143+}
A sys_s390x.s +114 -0
@@ -0,0 +1,114 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build linux
5+
6+#include "textflag.h"
7+#include "go_asm.h"
8+#include "funcdata.h"
9+
10+// S390X ELF ABI:
11+// - Integer args: R2-R6 (5 registers)
12+// - Float args: F0, F2, F4, F6 (4 registers, even-numbered)
13+// - Return: R2 (integer), F0 (float)
14+// - Stack pointer: R15
15+// - Link register: R14
16+// - Callee-saved: R6-R13, F8-F15 (but R6 is also used for 5th param)
17+//
18+// Stack frame layout (aligned to 8 bytes):
19+// 0(R15) - back chain
20+// 8(R15) - reserved
21+// 16(R15) - reserved
22+// ... - register save area (R6-R15 at 48(R15))
23+// 160(R15) - parameter area start (args beyond registers)
24+//
25+// We need space for:
26+// - 160 bytes standard frame (with register save area)
27+// - Stack args a6-a15 (10 * 8 = 80 bytes)
28+// - Saved args pointer (8 bytes)
29+// - Padding for alignment
30+// Total: 264 bytes (rounded to 8-byte alignment)
31+
32+#define STACK_SIZE 264
33+#define STACK_ARGS 160
34+#define ARGP_SAVE 248
35+
36+GLOBL ·syscall15XABI0(SB), NOPTR|RODATA, $8
37+DATA ·syscall15XABI0(SB)/8, $syscall15X(SB)
38+
39+TEXT syscall15X(SB), NOSPLIT, $0
40+ // On entry, R2 contains the args pointer
41+ // Save callee-saved registers in caller's frame (per ABI)
42+ STMG R6, R15, 48(R15)
43+
44+ // Allocate our stack frame
45+ MOVD R15, R1
46+ SUB $STACK_SIZE, R15
47+ MOVD R1, 0(R15) // back chain
48+
49+ // Save args pointer
50+ MOVD R2, ARGP_SAVE(R15)
51+
52+ // R9 := args pointer (syscall15Args*)
53+ MOVD R2, R9
54+
55+ // Load float args into F0, F2, F4, F6 (s390x uses even-numbered FPRs)
56+ FMOVD syscall15Args_f1(R9), F0
57+ FMOVD syscall15Args_f2(R9), F2
58+ FMOVD syscall15Args_f3(R9), F4
59+ FMOVD syscall15Args_f4(R9), F6
60+
61+ // Load integer args into R2-R6 (5 registers)
62+ MOVD syscall15Args_a1(R9), R2
63+ MOVD syscall15Args_a2(R9), R3
64+ MOVD syscall15Args_a3(R9), R4
65+ MOVD syscall15Args_a4(R9), R5
66+ MOVD syscall15Args_a5(R9), R6
67+
68+ // Spill remaining args (a6-a15) onto the stack at 160(R15)
69+ MOVD ARGP_SAVE(R15), R9 // reload args pointer
70+ MOVD syscall15Args_a6(R9), R1
71+ MOVD R1, (STACK_ARGS+0*8)(R15)
72+ MOVD syscall15Args_a7(R9), R1
73+ MOVD R1, (STACK_ARGS+1*8)(R15)
74+ MOVD syscall15Args_a8(R9), R1
75+ MOVD R1, (STACK_ARGS+2*8)(R15)
76+ MOVD syscall15Args_a9(R9), R1
77+ MOVD R1, (STACK_ARGS+3*8)(R15)
78+ MOVD syscall15Args_a10(R9), R1
79+ MOVD R1, (STACK_ARGS+4*8)(R15)
80+ MOVD syscall15Args_a11(R9), R1
81+ MOVD R1, (STACK_ARGS+5*8)(R15)
82+ MOVD syscall15Args_a12(R9), R1
83+ MOVD R1, (STACK_ARGS+6*8)(R15)
84+ MOVD syscall15Args_a13(R9), R1
85+ MOVD R1, (STACK_ARGS+7*8)(R15)
86+ MOVD syscall15Args_a14(R9), R1
87+ MOVD R1, (STACK_ARGS+8*8)(R15)
88+ MOVD syscall15Args_a15(R9), R1
89+ MOVD R1, (STACK_ARGS+9*8)(R15)
90+
91+ // Call function
92+ MOVD syscall15Args_fn(R9), R1
93+ BL (R1)
94+
95+ // Restore args pointer for storing results
96+ MOVD ARGP_SAVE(R15), R9
97+
98+ // Store integer results back (R2, R3)
99+ MOVD R2, syscall15Args_a1(R9)
100+ MOVD R3, syscall15Args_a2(R9)
101+
102+ // Store float return values (F0, F2, F4, F6)
103+ FMOVD F0, syscall15Args_f1(R9)
104+ FMOVD F2, syscall15Args_f2(R9)
105+ FMOVD F4, syscall15Args_f3(R9)
106+ FMOVD F6, syscall15Args_f4(R9)
107+
108+ // Deallocate stack frame
109+ ADD $STACK_SIZE, R15
110+
111+ // Restore callee-saved registers from caller's save area
112+ LMG 48(R15), R6, R15
113+
114+ RET
@@ -0,0 +1,114 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build linux
5+
6+#include "textflag.h"
7+#include "go_asm.h"
8+#include "funcdata.h"
9+
10+// S390X ELF ABI:
11+// - Integer args: R2-R6 (5 registers)
12+// - Float args: F0, F2, F4, F6 (4 registers, even-numbered)
13+// - Return: R2 (integer), F0 (float)
14+// - Stack pointer: R15
15+// - Link register: R14
16+// - Callee-saved: R6-R13, F8-F15 (but R6 is also used for 5th param)
17+//
18+// Stack frame layout (aligned to 8 bytes):
19+// 0(R15) - back chain
20+// 8(R15) - reserved
21+// 16(R15) - reserved
22+// ... - register save area (R6-R15 at 48(R15))
23+// 160(R15) - parameter area start (args beyond registers)
24+//
25+// We need space for:
26+// - 160 bytes standard frame (with register save area)
27+// - Stack args a6-a15 (10 * 8 = 80 bytes)
28+// - Saved args pointer (8 bytes)
29+// - Padding for alignment
30+// Total: 264 bytes (rounded to 8-byte alignment)
31+
32+#define STACK_SIZE 264
33+#define STACK_ARGS 160
34+#define ARGP_SAVE 248
35+
36+GLOBL ·syscall15XABI0(SB), NOPTR|RODATA, $8
37+DATA ·syscall15XABI0(SB)/8, $syscall15X(SB)
38+
39+TEXT syscall15X(SB), NOSPLIT, $0
40+ // On entry, R2 contains the args pointer
41+ // Save callee-saved registers in caller's frame (per ABI)
42+ STMG R6, R15, 48(R15)
43+
44+ // Allocate our stack frame
45+ MOVD R15, R1
46+ SUB $STACK_SIZE, R15
47+ MOVD R1, 0(R15) // back chain
48+
49+ // Save args pointer
50+ MOVD R2, ARGP_SAVE(R15)
51+
52+ // R9 := args pointer (syscall15Args*)
53+ MOVD R2, R9
54+
55+ // Load float args into F0, F2, F4, F6 (s390x uses even-numbered FPRs)
56+ FMOVD syscall15Args_f1(R9), F0
57+ FMOVD syscall15Args_f2(R9), F2
58+ FMOVD syscall15Args_f3(R9), F4
59+ FMOVD syscall15Args_f4(R9), F6
60+
61+ // Load integer args into R2-R6 (5 registers)
62+ MOVD syscall15Args_a1(R9), R2
63+ MOVD syscall15Args_a2(R9), R3
64+ MOVD syscall15Args_a3(R9), R4
65+ MOVD syscall15Args_a4(R9), R5
66+ MOVD syscall15Args_a5(R9), R6
67+
68+ // Spill remaining args (a6-a15) onto the stack at 160(R15)
69+ MOVD ARGP_SAVE(R15), R9 // reload args pointer
70+ MOVD syscall15Args_a6(R9), R1
71+ MOVD R1, (STACK_ARGS+0*8)(R15)
72+ MOVD syscall15Args_a7(R9), R1
73+ MOVD R1, (STACK_ARGS+1*8)(R15)
74+ MOVD syscall15Args_a8(R9), R1
75+ MOVD R1, (STACK_ARGS+2*8)(R15)
76+ MOVD syscall15Args_a9(R9), R1
77+ MOVD R1, (STACK_ARGS+3*8)(R15)
78+ MOVD syscall15Args_a10(R9), R1
79+ MOVD R1, (STACK_ARGS+4*8)(R15)
80+ MOVD syscall15Args_a11(R9), R1
81+ MOVD R1, (STACK_ARGS+5*8)(R15)
82+ MOVD syscall15Args_a12(R9), R1
83+ MOVD R1, (STACK_ARGS+6*8)(R15)
84+ MOVD syscall15Args_a13(R9), R1
85+ MOVD R1, (STACK_ARGS+7*8)(R15)
86+ MOVD syscall15Args_a14(R9), R1
87+ MOVD R1, (STACK_ARGS+8*8)(R15)
88+ MOVD syscall15Args_a15(R9), R1
89+ MOVD R1, (STACK_ARGS+9*8)(R15)
90+
91+ // Call function
92+ MOVD syscall15Args_fn(R9), R1
93+ BL (R1)
94+
95+ // Restore args pointer for storing results
96+ MOVD ARGP_SAVE(R15), R9
97+
98+ // Store integer results back (R2, R3)
99+ MOVD R2, syscall15Args_a1(R9)
100+ MOVD R3, syscall15Args_a2(R9)
101+
102+ // Store float return values (F0, F2, F4, F6)
103+ FMOVD F0, syscall15Args_f1(R9)
104+ FMOVD F2, syscall15Args_f2(R9)
105+ FMOVD F4, syscall15Args_f3(R9)
106+ FMOVD F6, syscall15Args_f4(R9)
107+
108+ // Deallocate stack frame
109+ ADD $STACK_SIZE, R15
110+
111+ // Restore callee-saved registers from caller's save area
112+ LMG 48(R15), R6, R15
113+
114+ RET
A sys_unix_s390x.s +109 -0
@@ -0,0 +1,109 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build linux
5+
6+#include "textflag.h"
7+#include "go_asm.h"
8+#include "funcdata.h"
9+
10+// S390X ELF ABI callbackasm1 implementation
11+// On entry, R0 contains the callback index (set by callbackasm)
12+// NOTE: We use R0 instead of R11 because R11 is callee-saved on S390X.
13+//
14+// S390X stack frame layout:
15+// 0(R15) - back chain
16+// 48(R15) - register save area (R6-R15)
17+// 160(R15) - parameter area
18+//
19+// S390X uses R2-R6 for integer arguments (5 registers) and F0,F2,F4,F6 for floats (4 registers).
20+//
21+// Our frame layout (total 264 bytes, 8-byte aligned):
22+// 0(R15) - back chain
23+// 48(R15) - saved R6-R15 (done by STMG)
24+// 160(R15) - callbackArgs struct (32 bytes: index, args, result, stackArgs)
25+// 192(R15) - args array start
26+//
27+// Args array layout:
28+// - floats F0,F2,F4,F6 (32 bytes)
29+// - ints R2-R6 (40 bytes)
30+// Total args array: 72 bytes, ends at 264
31+//
32+// Stack args in caller's frame start at old_R15+160
33+
34+#define FRAME_SIZE 264
35+#define CB_ARGS 160
36+#define ARGS_ARRAY 192
37+#define FLOAT_OFF 0
38+#define INT_OFF 32
39+
40+TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0
41+ NO_LOCAL_POINTERS
42+
43+ // On entry, the trampoline in zcallback_s390x.s left
44+ // the callback index in R0 (NOT R11, since R11 is callee-saved).
45+ // R6 contains the 5th integer argument.
46+
47+ // Save R6-R15 in caller's frame (per S390X ABI) BEFORE allocating our frame
48+ // STMG stores R6's current value (the 5th arg) at 48(R15)
49+ STMG R6, R15, 48(R15)
50+
51+ // Save current stack pointer (will be back chain)
52+ MOVD R15, R1
53+
54+ // Allocate our stack frame
55+ SUB $FRAME_SIZE, R15
56+ MOVD R1, 0(R15) // back chain
57+
58+ // Save R0 (callback index) immediately - it's volatile
59+ MOVD R0, (CB_ARGS+0)(R15)
60+
61+ // Save callback arguments to args array.
62+ // Layout: floats first (F0,F2,F4,F6), then ints (R2-R6)
63+ FMOVD F0, (ARGS_ARRAY+FLOAT_OFF+0*8)(R15)
64+ FMOVD F2, (ARGS_ARRAY+FLOAT_OFF+1*8)(R15)
65+ FMOVD F4, (ARGS_ARRAY+FLOAT_OFF+2*8)(R15)
66+ FMOVD F6, (ARGS_ARRAY+FLOAT_OFF+3*8)(R15)
67+
68+ MOVD R2, (ARGS_ARRAY+INT_OFF+0*8)(R15)
69+ MOVD R3, (ARGS_ARRAY+INT_OFF+1*8)(R15)
70+ MOVD R4, (ARGS_ARRAY+INT_OFF+2*8)(R15)
71+ MOVD R5, (ARGS_ARRAY+INT_OFF+3*8)(R15)
72+
73+ // R6 (5th int arg) was saved at 48(old_R15) by STMG
74+ // old_R15 = current R15 + FRAME_SIZE, so R6 is at 48+FRAME_SIZE(R15) = 312(R15)
75+ MOVD (48+FRAME_SIZE)(R15), R1
76+ MOVD R1, (ARGS_ARRAY+INT_OFF+4*8)(R15)
77+
78+ // Finish setting up callbackArgs struct at CB_ARGS(R15)
79+ // struct { index uintptr; args unsafe.Pointer; result uintptr; stackArgs unsafe.Pointer }
80+ // Note: index was already saved earlier
81+ ADD $ARGS_ARRAY, R15, R1
82+ MOVD R1, (CB_ARGS+8)(R15) // args = address of register args
83+ MOVD $0, (CB_ARGS+16)(R15) // result = 0
84+
85+ // stackArgs points to caller's stack arguments at old_R15+160 = R15+FRAME_SIZE+160
86+ ADD $(FRAME_SIZE+160), R15, R1
87+ MOVD R1, (CB_ARGS+24)(R15) // stackArgs = &caller_stack_args
88+
89+ // Call crosscall2 with arguments in registers:
90+ // R2 = fn (from callbackWrap_call closure)
91+ // R3 = frame (address of callbackArgs)
92+ // R5 = ctxt (0)
93+ MOVD ·callbackWrap_call(SB), R2
94+ MOVD (R2), R2 // dereference closure to get fn
95+ ADD $CB_ARGS, R15, R3 // frame = &callbackArgs
96+ MOVD $0, R5 // ctxt = 0
97+
98+ BL crosscall2(SB)
99+
100+ // Get callback result into R2
101+ MOVD (CB_ARGS+16)(R15), R2
102+
103+ // Deallocate frame
104+ ADD $FRAME_SIZE, R15
105+
106+ // Restore R6-R15 from caller's frame
107+ LMG 48(R15), R6, R15
108+
109+ RET
@@ -0,0 +1,109 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build linux
5+
6+#include "textflag.h"
7+#include "go_asm.h"
8+#include "funcdata.h"
9+
10+// S390X ELF ABI callbackasm1 implementation
11+// On entry, R0 contains the callback index (set by callbackasm)
12+// NOTE: We use R0 instead of R11 because R11 is callee-saved on S390X.
13+//
14+// S390X stack frame layout:
15+// 0(R15) - back chain
16+// 48(R15) - register save area (R6-R15)
17+// 160(R15) - parameter area
18+//
19+// S390X uses R2-R6 for integer arguments (5 registers) and F0,F2,F4,F6 for floats (4 registers).
20+//
21+// Our frame layout (total 264 bytes, 8-byte aligned):
22+// 0(R15) - back chain
23+// 48(R15) - saved R6-R15 (done by STMG)
24+// 160(R15) - callbackArgs struct (32 bytes: index, args, result, stackArgs)
25+// 192(R15) - args array start
26+//
27+// Args array layout:
28+// - floats F0,F2,F4,F6 (32 bytes)
29+// - ints R2-R6 (40 bytes)
30+// Total args array: 72 bytes, ends at 264
31+//
32+// Stack args in caller's frame start at old_R15+160
33+
34+#define FRAME_SIZE 264
35+#define CB_ARGS 160
36+#define ARGS_ARRAY 192
37+#define FLOAT_OFF 0
38+#define INT_OFF 32
39+
40+TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0
41+ NO_LOCAL_POINTERS
42+
43+ // On entry, the trampoline in zcallback_s390x.s left
44+ // the callback index in R0 (NOT R11, since R11 is callee-saved).
45+ // R6 contains the 5th integer argument.
46+
47+ // Save R6-R15 in caller's frame (per S390X ABI) BEFORE allocating our frame
48+ // STMG stores R6's current value (the 5th arg) at 48(R15)
49+ STMG R6, R15, 48(R15)
50+
51+ // Save current stack pointer (will be back chain)
52+ MOVD R15, R1
53+
54+ // Allocate our stack frame
55+ SUB $FRAME_SIZE, R15
56+ MOVD R1, 0(R15) // back chain
57+
58+ // Save R0 (callback index) immediately - it's volatile
59+ MOVD R0, (CB_ARGS+0)(R15)
60+
61+ // Save callback arguments to args array.
62+ // Layout: floats first (F0,F2,F4,F6), then ints (R2-R6)
63+ FMOVD F0, (ARGS_ARRAY+FLOAT_OFF+0*8)(R15)
64+ FMOVD F2, (ARGS_ARRAY+FLOAT_OFF+1*8)(R15)
65+ FMOVD F4, (ARGS_ARRAY+FLOAT_OFF+2*8)(R15)
66+ FMOVD F6, (ARGS_ARRAY+FLOAT_OFF+3*8)(R15)
67+
68+ MOVD R2, (ARGS_ARRAY+INT_OFF+0*8)(R15)
69+ MOVD R3, (ARGS_ARRAY+INT_OFF+1*8)(R15)
70+ MOVD R4, (ARGS_ARRAY+INT_OFF+2*8)(R15)
71+ MOVD R5, (ARGS_ARRAY+INT_OFF+3*8)(R15)
72+
73+ // R6 (5th int arg) was saved at 48(old_R15) by STMG
74+ // old_R15 = current R15 + FRAME_SIZE, so R6 is at 48+FRAME_SIZE(R15) = 312(R15)
75+ MOVD (48+FRAME_SIZE)(R15), R1
76+ MOVD R1, (ARGS_ARRAY+INT_OFF+4*8)(R15)
77+
78+ // Finish setting up callbackArgs struct at CB_ARGS(R15)
79+ // struct { index uintptr; args unsafe.Pointer; result uintptr; stackArgs unsafe.Pointer }
80+ // Note: index was already saved earlier
81+ ADD $ARGS_ARRAY, R15, R1
82+ MOVD R1, (CB_ARGS+8)(R15) // args = address of register args
83+ MOVD $0, (CB_ARGS+16)(R15) // result = 0
84+
85+ // stackArgs points to caller's stack arguments at old_R15+160 = R15+FRAME_SIZE+160
86+ ADD $(FRAME_SIZE+160), R15, R1
87+ MOVD R1, (CB_ARGS+24)(R15) // stackArgs = &caller_stack_args
88+
89+ // Call crosscall2 with arguments in registers:
90+ // R2 = fn (from callbackWrap_call closure)
91+ // R3 = frame (address of callbackArgs)
92+ // R5 = ctxt (0)
93+ MOVD ·callbackWrap_call(SB), R2
94+ MOVD (R2), R2 // dereference closure to get fn
95+ ADD $CB_ARGS, R15, R3 // frame = &callbackArgs
96+ MOVD $0, R5 // ctxt = 0
97+
98+ BL crosscall2(SB)
99+
100+ // Get callback result into R2
101+ MOVD (CB_ARGS+16)(R15), R2
102+
103+ // Deallocate frame
104+ ADD $FRAME_SIZE, R15
105+
106+ // Restore R6-R15 from caller's frame
107+ LMG 48(R15), R6, R15
108+
109+ RET
M syscall_cgo_linux.go +2 -2
@@ -1,7 +1,7 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
33
4-//go:build cgo && !(386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64)
4+//go:build cgo && !(386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || s390x)
55
66 package purego
77
@@ -17,5 +17,5 @@ func syscall_syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a
1717 }
1818
1919 func NewCallback(_ any) uintptr {
20- panic("purego: NewCallback on Linux is only supported on 386/amd64/arm64/arm/loong64/ppc64le/riscv64")
20+ panic("purego: NewCallback on Linux is only supported on 386/amd64/arm64/arm/loong64/ppc64le/riscv64/s390x")
2121 }
@@ -1,7 +1,7 @@
1 // SPDX-License-Identifier: Apache-2.01 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3 3
4-//go:build cgo && !(386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64)4+//go:build cgo && !(386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || s390x)
5 5
6 package purego6 package purego
7 7
@@ -17,5 +17,5 @@ func syscall_syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a
17 }17 }
18 18
19 func NewCallback(_ any) uintptr {19 func NewCallback(_ any) uintptr {
20- panic("purego: NewCallback on Linux is only supported on 386/amd64/arm64/arm/loong64/ppc64le/riscv64")20+ panic("purego: NewCallback on Linux is only supported on 386/amd64/arm64/arm/loong64/ppc64le/riscv64/s390x")
21 }21 }
M syscall_sysv.go +52 -7
@@ -1,7 +1,8 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
33
4-//go:build darwin || freebsd || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64)) || netbsd
4+// TODO: remove s390x cgo dependency once golang/go#77449 is resolved
5+//go:build darwin || freebsd || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || (cgo && s390x))) || netbsd
56
67 package purego
78
@@ -164,15 +165,25 @@ func callbackWrap(a *callbackArgs) {
164165 // Darwin ARM64: read from packed stack with proper alignment
165166 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
166167 } else if stackFrame != nil {
167- // ppc64le: stack args are in separate stackFrame
168- args[i] = reflect.NewAt(inType, unsafe.Pointer(&stackFrame[stackSlot])).Elem()
168+ // ppc64le/s390x: stack args are in separate stackFrame
169+ if runtime.GOARCH == "s390x" {
170+ // s390x big-endian: sub-8-byte values are right-justified
171+ args[i] = callbackArgFromSlotBigEndian(unsafe.Pointer(&stackFrame[stackSlot]), inType)
172+ } else {
173+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&stackFrame[stackSlot])).Elem()
174+ }
169175 stackSlot += slots
170176 } else {
171177 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
172178 stackSlot += slots
173179 }
174180 } else {
175- args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[floatsN])).Elem()
181+ if runtime.GOARCH == "s390x" {
182+ // s390x big-endian: float32 is right-justified in 8-byte FPR slot
183+ args[i] = callbackArgFromSlotBigEndian(unsafe.Pointer(&frame[floatsN]), inType)
184+ } else {
185+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[floatsN])).Elem()
186+ }
176187 }
177188 floatsN += slots
178189 case reflect.Struct:
@@ -185,8 +196,13 @@ func callbackWrap(a *callbackArgs) {
185196 // Darwin ARM64: read from packed stack with proper alignment
186197 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
187198 } else if stackFrame != nil {
188- // ppc64le: stack args are in separate stackFrame
189- args[i] = reflect.NewAt(inType, unsafe.Pointer(&stackFrame[stackSlot])).Elem()
199+ // ppc64le/s390x: stack args are in separate stackFrame
200+ if runtime.GOARCH == "s390x" {
201+ // s390x big-endian: sub-8-byte values are right-justified
202+ args[i] = callbackArgFromSlotBigEndian(unsafe.Pointer(&stackFrame[stackSlot]), inType)
203+ } else {
204+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&stackFrame[stackSlot])).Elem()
205+ }
190206 stackSlot += slots
191207 } else {
192208 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
@@ -195,7 +211,12 @@ func callbackWrap(a *callbackArgs) {
195211 } else {
196212 // the integers begin after the floats in frame
197213 pos := intsN + numOfFloatRegisters()
198- args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[pos])).Elem()
214+ if runtime.GOARCH == "s390x" {
215+ // s390x big-endian: sub-8-byte values are right-justified in GPR slot
216+ args[i] = callbackArgFromSlotBigEndian(unsafe.Pointer(&frame[pos]), inType)
217+ } else {
218+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[pos])).Elem()
219+ }
199220 }
200221 intsN += slots
201222 }
@@ -246,6 +267,27 @@ func callbackArgFromStack(argsBase unsafe.Pointer, stackSlot int, stackByteOffse
246267 return reflect.NewAt(inType, ptr).Elem()
247268 }
248269
270+// callbackArgFromSlotBigEndian reads an argument from an 8-byte slot on big-endian architectures.
271+// On s390x:
272+// - Integer types are right-justified in GPRs: sub-8-byte values are at offset (8 - size)
273+// - Float32 in FPRs is left-justified: stored in upper 32 bits, so at offset 0
274+// - Float64 occupies the full 8-byte slot
275+func callbackArgFromSlotBigEndian(slotPtr unsafe.Pointer, inType reflect.Type) reflect.Value {
276+ size := inType.Size()
277+ if size >= 8 {
278+ // 8-byte values occupy the entire slot
279+ return reflect.NewAt(inType, slotPtr).Elem()
280+ }
281+ // Float32 is left-justified in FPRs (upper 32 bits), so offset is 0
282+ if inType.Kind() == reflect.Float32 {
283+ return reflect.NewAt(inType, slotPtr).Elem()
284+ }
285+ // Integer types are right-justified: offset = 8 - size
286+ offset := 8 - size
287+ ptr := unsafe.Add(slotPtr, offset)
288+ return reflect.NewAt(inType, ptr).Elem()
289+}
290+
249291 // callbackasmAddr returns address of runtime.callbackasm
250292 // function adjusted by i.
251293 // On x86 and amd64, runtime.callbackasm is a series of CALL instructions,
@@ -270,6 +312,9 @@ func callbackasmAddr(i int) uintptr {
270312 // On ARM, ARM64, Loong64, PPC64LE and RISCV64, each entry is a MOV instruction
271313 // followed by a branch instruction
272314 entrySize = 8
315+ case "s390x":
316+ // On S390X, each entry is LGHI (4 bytes) + JG (6 bytes)
317+ entrySize = 10
273318 }
274319 return callbackasmABI0 + uintptr(i*entrySize)
275320 }
@@ -1,7 +1,8 @@
1 // SPDX-License-Identifier: Apache-2.01 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3 3
4-//go:build darwin || freebsd || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64)) || netbsd4+// TODO: remove s390x cgo dependency once golang/go#77449 is resolved
5+//go:build darwin || freebsd || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || (cgo && s390x))) || netbsd
5 6
6 package purego7 package purego
7 8
@@ -164,15 +165,25 @@ func callbackWrap(a *callbackArgs) {
164 // Darwin ARM64: read from packed stack with proper alignment165 // Darwin ARM64: read from packed stack with proper alignment
165 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)166 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
166 } else if stackFrame != nil {167 } else if stackFrame != nil {
167- // ppc64le: stack args are in separate stackFrame168+ // ppc64le/s390x: stack args are in separate stackFrame
168- args[i] = reflect.NewAt(inType, unsafe.Pointer(&stackFrame[stackSlot])).Elem()169+ if runtime.GOARCH == "s390x" {
170+ // s390x big-endian: sub-8-byte values are right-justified
171+ args[i] = callbackArgFromSlotBigEndian(unsafe.Pointer(&stackFrame[stackSlot]), inType)
172+ } else {
173+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&stackFrame[stackSlot])).Elem()
174+ }
169 stackSlot += slots175 stackSlot += slots
170 } else {176 } else {
171 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()177 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
172 stackSlot += slots178 stackSlot += slots
173 }179 }
174 } else {180 } else {
175- args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[floatsN])).Elem()181+ if runtime.GOARCH == "s390x" {
182+ // s390x big-endian: float32 is right-justified in 8-byte FPR slot
183+ args[i] = callbackArgFromSlotBigEndian(unsafe.Pointer(&frame[floatsN]), inType)
184+ } else {
185+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[floatsN])).Elem()
186+ }
176 }187 }
177 floatsN += slots188 floatsN += slots
178 case reflect.Struct:189 case reflect.Struct:
@@ -185,8 +196,13 @@ func callbackWrap(a *callbackArgs) {
185 // Darwin ARM64: read from packed stack with proper alignment196 // Darwin ARM64: read from packed stack with proper alignment
186 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)197 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
187 } else if stackFrame != nil {198 } else if stackFrame != nil {
188- // ppc64le: stack args are in separate stackFrame199+ // ppc64le/s390x: stack args are in separate stackFrame
189- args[i] = reflect.NewAt(inType, unsafe.Pointer(&stackFrame[stackSlot])).Elem()200+ if runtime.GOARCH == "s390x" {
201+ // s390x big-endian: sub-8-byte values are right-justified
202+ args[i] = callbackArgFromSlotBigEndian(unsafe.Pointer(&stackFrame[stackSlot]), inType)
203+ } else {
204+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&stackFrame[stackSlot])).Elem()
205+ }
190 stackSlot += slots206 stackSlot += slots
191 } else {207 } else {
192 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()208 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
@@ -195,7 +211,12 @@ func callbackWrap(a *callbackArgs) {
195 } else {211 } else {
196 // the integers begin after the floats in frame212 // the integers begin after the floats in frame
197 pos := intsN + numOfFloatRegisters()213 pos := intsN + numOfFloatRegisters()
198- args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[pos])).Elem()214+ if runtime.GOARCH == "s390x" {
215+ // s390x big-endian: sub-8-byte values are right-justified in GPR slot
216+ args[i] = callbackArgFromSlotBigEndian(unsafe.Pointer(&frame[pos]), inType)
217+ } else {
218+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[pos])).Elem()
219+ }
199 }220 }
200 intsN += slots221 intsN += slots
201 }222 }
@@ -246,6 +267,27 @@ func callbackArgFromStack(argsBase unsafe.Pointer, stackSlot int, stackByteOffse
246 return reflect.NewAt(inType, ptr).Elem()267 return reflect.NewAt(inType, ptr).Elem()
247 }268 }
248 269
270+// callbackArgFromSlotBigEndian reads an argument from an 8-byte slot on big-endian architectures.
271+// On s390x:
272+// - Integer types are right-justified in GPRs: sub-8-byte values are at offset (8 - size)
273+// - Float32 in FPRs is left-justified: stored in upper 32 bits, so at offset 0
274+// - Float64 occupies the full 8-byte slot
275+func callbackArgFromSlotBigEndian(slotPtr unsafe.Pointer, inType reflect.Type) reflect.Value {
276+ size := inType.Size()
277+ if size >= 8 {
278+ // 8-byte values occupy the entire slot
279+ return reflect.NewAt(inType, slotPtr).Elem()
280+ }
281+ // Float32 is left-justified in FPRs (upper 32 bits), so offset is 0
282+ if inType.Kind() == reflect.Float32 {
283+ return reflect.NewAt(inType, slotPtr).Elem()
284+ }
285+ // Integer types are right-justified: offset = 8 - size
286+ offset := 8 - size
287+ ptr := unsafe.Add(slotPtr, offset)
288+ return reflect.NewAt(inType, ptr).Elem()
289+}
290+
249 // callbackasmAddr returns address of runtime.callbackasm291 // callbackasmAddr returns address of runtime.callbackasm
250 // function adjusted by i.292 // function adjusted by i.
251 // On x86 and amd64, runtime.callbackasm is a series of CALL instructions,293 // On x86 and amd64, runtime.callbackasm is a series of CALL instructions,
@@ -270,6 +312,9 @@ func callbackasmAddr(i int) uintptr {
270 // On ARM, ARM64, Loong64, PPC64LE and RISCV64, each entry is a MOV instruction312 // On ARM, ARM64, Loong64, PPC64LE and RISCV64, each entry is a MOV instruction
271 // followed by a branch instruction313 // followed by a branch instruction
272 entrySize = 8314 entrySize = 8
315+ case "s390x":
316+ // On S390X, each entry is LGHI (4 bytes) + JG (6 bytes)
317+ entrySize = 10
273 }318 }
274 return callbackasmABI0 + uintptr(i*entrySize)319 return callbackasmABI0 + uintptr(i*entrySize)
275 }320 }
R syscall_sysv_ppc64le.go → syscall_sysv_stackargs.go +1 -1 renamed, 95% similar
@@ -1,7 +1,7 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2026 The Ebitengine Authors
33
4-//go:build linux
4+//go:build linux && (ppc64le || s390x)
55
66 package purego
77
@@ -1,7 +1,7 @@
1 // SPDX-License-Identifier: Apache-2.01 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: 2026 The Ebitengine Authors2 // SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3 3
4-//go:build linux4+//go:build linux && (ppc64le || s390x)
5 5
6 package purego6 package purego
7 7
M wincallback.go +30 -0
@@ -221,6 +221,35 @@ TEXT callbackasm(SB), NOSPLIT|NOFRAME, $0
221221 }
222222 }
223223
224+func genasmS390x() {
225+ var buf bytes.Buffer
226+
227+ buf.WriteString(`// Code generated by wincallback.go using 'go generate'. DO NOT EDIT.
228+
229+//go:build linux
230+
231+// External code calls into callbackasm at an offset corresponding
232+// to the callback index. Callbackasm is a table of MOVD and BR instructions.
233+// The MOVD instruction loads R0 with the callback index, and the
234+// BR instruction branches to callbackasm1.
235+// callbackasm1 takes the callback index from R0 and
236+// indexes into an array that stores information about each callback.
237+// It then calls the Go implementation for that callback.
238+// NOTE: We use R0 instead of R11 because R11 is callee-saved on S390X.
239+#include "textflag.h"
240+
241+TEXT callbackasm(SB), NOSPLIT|NOFRAME, $0
242+`)
243+ for i := 0; i < maxCallback; i++ {
244+ fmt.Fprintf(&buf, "\tMOVD $%d, R0\n", i)
245+ buf.WriteString("\tBR callbackasm1(SB)\n")
246+ }
247+ if err := os.WriteFile("zcallback_s390x.s", buf.Bytes(), 0644); err != nil {
248+ fmt.Fprintf(os.Stderr, "wincallback: %s\n", err)
249+ os.Exit(2)
250+ }
251+}
252+
224253 func main() {
225254 genasm386()
226255 genasmAmd64()
@@ -229,4 +258,5 @@ func main() {
229258 genasmLoong64()
230259 genasmPpc64le()
231260 genasmRiscv64()
261+ genasmS390x()
232262 }
@@ -221,6 +221,35 @@ TEXT callbackasm(SB), NOSPLIT|NOFRAME, $0
221 }221 }
222 }222 }
223 223
224+func genasmS390x() {
225+ var buf bytes.Buffer
226+
227+ buf.WriteString(`// Code generated by wincallback.go using 'go generate'. DO NOT EDIT.
228+
229+//go:build linux
230+
231+// External code calls into callbackasm at an offset corresponding
232+// to the callback index. Callbackasm is a table of MOVD and BR instructions.
233+// The MOVD instruction loads R0 with the callback index, and the
234+// BR instruction branches to callbackasm1.
235+// callbackasm1 takes the callback index from R0 and
236+// indexes into an array that stores information about each callback.
237+// It then calls the Go implementation for that callback.
238+// NOTE: We use R0 instead of R11 because R11 is callee-saved on S390X.
239+#include "textflag.h"
240+
241+TEXT callbackasm(SB), NOSPLIT|NOFRAME, $0
242+`)
243+ for i := 0; i < maxCallback; i++ {
244+ fmt.Fprintf(&buf, "\tMOVD $%d, R0\n", i)
245+ buf.WriteString("\tBR callbackasm1(SB)\n")
246+ }
247+ if err := os.WriteFile("zcallback_s390x.s", buf.Bytes(), 0644); err != nil {
248+ fmt.Fprintf(os.Stderr, "wincallback: %s\n", err)
249+ os.Exit(2)
250+ }
251+}
252+
224 func main() {253 func main() {
225 genasm386()254 genasm386()
226 genasmAmd64()255 genasmAmd64()
@@ -229,4 +258,5 @@ func main() {
229 genasmLoong64()258 genasmLoong64()
230 genasmPpc64le()259 genasmPpc64le()
231 genasmRiscv64()260 genasmRiscv64()
261+ genasmS390x()
232 }262 }
A zcallback_s390x.s +4015 -0 large diff (4015 lines changed): collapsed

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 102

↑↓ moveenter openesc close