package sdl import ( "errors" "fmt" "os" "runtime" "unsafe" "github.com/ebitengine/purego" ) const ( InitVideo uint32 = 0x00000020 WindowVulkan uint64 = 0x0000000010000000 WindowResizable uint64 = 0x0000000000000020 EventQuit uint32 = 0x100 EventWindowResized uint32 = 0x206 EventPixelSizeChanged uint32 = 0x207 EventKeyDown uint32 = 0x300 EventKeyUp uint32 = 0x301 EventMouseMotion uint32 = 0x400 EventMouseButtonDown uint32 = 0x401 EventMouseButtonUp uint32 = 0x402 EventMouseWheel uint32 = 0x403 EventTextInput uint32 = 0x303 // it's about SDL_keycode, not SDL_scancode KeyEscape uint32 = 0x1B KeyF uint32 = 0x66 KeyQ uint32 = 0x71 KeyF1 uint32 = 0x4000003A KeyF2 uint32 = 0x4000003B InitAudio uint32 = 0x00000010 AudioDeviceDefaultPlayback uint32 = 0xFFFFFFFF AudioF32 uint16 = 0x8120 ) var ( sdlInit func(flags uint32) bool sdlQuit func() sdlGetError func() uintptr sdlCreateWindow func(title *byte, w, h int32, flags uint64) uintptr sdlDestroyWindow func(window uintptr) sdlVulkanGetInstanceExtensions func(count *uint32) uintptr sdlVulkanCreateSurface func(window, instance, allocator uintptr, surface *uint64) bool sdlPollEvent func(event unsafe.Pointer) bool sdlGetWindowSizeInPixels func(window uintptr, w, h *int32) bool sdlSetWindowFullscreen func(window uintptr, fullscreen bool) bool sdlOpenAudioDeviceStream func(devid uint32, spec unsafe.Pointer, callback uintptr, userdata uintptr) uintptr sdlResumeAudioStreamDevice func(stream uintptr) bool sdlPutAudioStreamData func(stream uintptr, buf unsafe.Pointer, length int32) bool sdlGetAudioStreamQueued func(stream uintptr) int32 sdlDestroyAudioStream func(stream uintptr) sdlGetAudioPlaybackDevices func(count *int32) uintptr sdlGetAudioDeviceName func(devid uint32) uintptr sdlStartTextInput func(window uintptr) sdlStopTextInput func(window uintptr) sdlGetClipboardText func() uintptr sdlSetClipboardText func(text *byte) bool sdlFree func(memory uintptr) ) func libraryCandidates() ([]string, error) { return libraryCandidatesForOS(runtime.GOOS) } func libraryCandidatesForOS(goos string) ([]string, error) { switch goos { case "linux": return []string{ "libSDL3.so.0", "libSDL3.so", }, nil case "darwin": return []string{ "libSDL3.0.dylib", "libSDL3.dylib", }, nil case "windows": return []string{ "SDL3.dll", }, nil default: return nil, fmt.Errorf("SDL3: unsupported operating system %q", goos) } } func openLibrary() (uintptr, error) { var candidates []string if explicit := os.Getenv("SDL3_LIBRARY"); explicit != "" { candidates = append(candidates, explicit) } defaults, err := libraryCandidates() if err != nil { return 0, err } candidates = append(candidates, defaults...) var attempts []error for _, name := range candidates { handle, err := purego.Dlopen( name, purego.RTLD_NOW|purego.RTLD_GLOBAL, ) if err == nil && handle != 0 { return handle, nil } if err == nil { err = errors.New("loader returned a zero handle") } attempts = append(attempts, fmt.Errorf("%s: %w", name, err)) } return 0, fmt.Errorf( "SDL3: unable to load shared library: %w", errors.Join(attempts...), ) } var loaded = false func Load() error { if loaded { return nil } h, err := openLibrary() if err != nil { return err } purego.RegisterLibFunc(&sdlInit, h, "SDL_Init") purego.RegisterLibFunc(&sdlQuit, h, "SDL_Quit") purego.RegisterLibFunc(&sdlGetError, h, "SDL_GetError") purego.RegisterLibFunc(&sdlCreateWindow, h, "SDL_CreateWindow") purego.RegisterLibFunc(&sdlDestroyWindow, h, "SDL_DestroyWindow") purego.RegisterLibFunc(&sdlVulkanGetInstanceExtensions, h, "SDL_Vulkan_GetInstanceExtensions") purego.RegisterLibFunc(&sdlVulkanCreateSurface, h, "SDL_Vulkan_CreateSurface") purego.RegisterLibFunc(&sdlPollEvent, h, "SDL_PollEvent") purego.RegisterLibFunc(&sdlGetWindowSizeInPixels, h, "SDL_GetWindowSizeInPixels") purego.RegisterLibFunc(&sdlSetWindowFullscreen, h, "SDL_SetWindowFullscreen") // audio purego.RegisterLibFunc(&sdlOpenAudioDeviceStream, h, "SDL_OpenAudioDeviceStream") purego.RegisterLibFunc(&sdlResumeAudioStreamDevice, h, "SDL_ResumeAudioStreamDevice") purego.RegisterLibFunc(&sdlPutAudioStreamData, h, "SDL_PutAudioStreamData") purego.RegisterLibFunc(&sdlGetAudioStreamQueued, h, "SDL_GetAudioStreamQueued") purego.RegisterLibFunc(&sdlDestroyAudioStream, h, "SDL_DestroyAudioStream") purego.RegisterLibFunc(&sdlGetAudioPlaybackDevices, h, "SDL_GetAudioPlaybackDevices") purego.RegisterLibFunc(&sdlGetAudioDeviceName, h, "SDL_GetAudioDeviceName") // input purego.RegisterLibFunc(&sdlStartTextInput, h, "SDL_StartTextInput") purego.RegisterLibFunc(&sdlStopTextInput, h, "SDL_StopTextInput") purego.RegisterLibFunc(&sdlGetClipboardText, h, "SDL_GetClipboardText") purego.RegisterLibFunc(&sdlSetClipboardText, h, "SDL_SetClipboardText") purego.RegisterLibFunc(&sdlFree, h, "SDL_free") loaded = true return nil } func Init(flags uint32) bool { return sdlInit(flags) } func Quit() { sdlQuit() } func GetError() string { return cstr(sdlGetError()) } func CreateWindow(title string, w, h int32, flags uint64) uintptr { return sdlCreateWindow(cbytes(title), w, h, flags) } func DestroyWindow(window uintptr) { sdlDestroyWindow(window) } func VulkanGetInstanceExtensions() []string { var count uint32 vkExts := sdlVulkanGetInstanceExtensions(&count) if vkExts == 0 { return nil } exts := make([]string, count) for i := uint32(0); i < count; i++ { p := *(*uintptr)(unsafe.Pointer(vkExts + uintptr(i)*unsafe.Sizeof(uintptr(0)))) exts[i] = cstr(p) } return exts } func VulkanCreateSurface(window, instance, allocator uintptr, surface *uint64) bool { return sdlVulkanCreateSurface(window, instance, allocator, surface) } func PollEvent(event unsafe.Pointer) bool { return sdlPollEvent(event) } func GetWindowSizeInPixels(window uintptr, w, h *int32) bool { return sdlGetWindowSizeInPixels(window, w, h) } func SetWindowFullscreen(window uintptr, fullscreen bool) bool { return sdlSetWindowFullscreen(window, fullscreen) } // cstr reads a NUL-terminated C string at p. func cstr(p uintptr) string { if p == 0 { return "" } var n int for *(*byte)(unsafe.Pointer(p + uintptr(n))) != 0 { n++ } return string(unsafe.Slice((*byte)(unsafe.Pointer(p)), n)) } // cbytes returns a NUL-terminated copy of s as *byte, kept alive by the caller. func cbytes(s string) *byte { b := make([]byte, len(s)+1) copy(b, s) runtime.KeepAlive(b) return &b[0] } type AudioSpec struct { Format uint16 Channels int32 Freq int32 } func OpenAudioDeviceStream(devid uint32, spec AudioSpec) uintptr { return sdlOpenAudioDeviceStream(devid, unsafe.Pointer(&spec), 0, 0) } func ResumeAudioStreamDevice(stream uintptr) bool { return sdlResumeAudioStreamDevice(stream) } func DestroyAudioStream(stream uintptr) { sdlDestroyAudioStream(stream) } func PutAudioStreamData(stream uintptr, buf []byte) bool { return sdlPutAudioStreamData(stream, unsafe.Pointer(&buf[0]), int32(len(buf))) } func GetAudioStreamQueued(stream uintptr) int32 { return sdlGetAudioStreamQueued(stream) } // playback device type AudioDevice struct { ID uint32 Name string } func GetAudioPlaybackDevices() []AudioDevice { var count int32 ptr := sdlGetAudioPlaybackDevices(&count) if ptr == 0 || count == 0 { return nil } ids := unsafe.Slice((*uint32)(unsafe.Pointer(ptr)), count) devs := make([]AudioDevice, 0, count) for _, id := range ids { name := cstr(sdlGetAudioDeviceName(id)) devs = append(devs, AudioDevice{ID: id, Name: name}) } return devs } // Input wrappers func StartTextInput(window uintptr) { sdlStartTextInput(window) } func StopTextInput(window uintptr) { sdlStopTextInput(window) } func GetClipboardText() string { text := sdlGetClipboardText() if text == 0 { return "" } result := cstr(text) sdlFree(text) return result } func SetClipboardText(text string) bool { bytes := make([]byte, len(text)+1) copy(bytes, text) result := sdlSetClipboardText(&bytes[0]) runtime.KeepAlive(bytes) return result }