Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wishlist - What would you like to have prioritized? #4

Open
JupiterRider opened this issue Feb 1, 2025 · 23 comments
Open

Wishlist - What would you like to have prioritized? #4

JupiterRider opened this issue Feb 1, 2025 · 23 comments

Comments

@JupiterRider
Copy link
Owner

SDL3 has a large API and it will take some time, until everything is carefully implemented in this Golang binding.

Please let me know, if there is a function or type you would like to prioritize.

@clseibold
Copy link

clseibold commented Feb 1, 2025

When the new version of SDL_ttf v3 comes out, I would need bindings for those (font rendering is very important for my application). The same for SDL_image.

@mewmew
Copy link
Contributor

mewmew commented Feb 2, 2025

I like that you use the phrasing carefully implemented.

I think this is my main wish and priority for the purego-sdl3 repository. To allow it to take time before freezing the API.

I'd love for this repo to become the way to interact with SDL from Go. And I'd love for it to be done through idiomatic and consistent approaches.

To give an example, functions which return errors could be updated to return the Go error type instead of bool. Essentially this boils down to calling errors.New(sdl.GetError()) from the wrapper functions. The benefit would be that purego-sdl3 could handle such error creation boiler plate code in a single place, instead of pushing such responsibilities to each call site of the user.

@JupiterRider
Copy link
Owner Author

JupiterRider commented Feb 2, 2025

@mewmew Returning errors instead of false in case of a failure sounds good. We can really think about doing that!

Today I implement another function, where returning an error is very useful too:

// GetMouseNameForID returns the name of the selected mouse, or error on failure.
//
// This function returns "" if the mouse doesn't have a name.
func GetMouseNameForID(instanceId MouseID) (string, error) {
	ret := sdlGetMouseNameForID(instanceId)
	if ret == nil {
		return "", errors.New(GetError())
	}
	return convert.ToString(ret), nil
}

Without the error type, the user wouldn't know, if the mouse has no name or a failure occurred.

@clseibold
Copy link

clseibold commented Feb 2, 2025

Wanted to mention that SDL_image got a 3.2 release yesterday: https://github.com/libsdl-org/SDL_image/releases
And SDL_ttf has a v3.1 pre-release: https://github.com/libsdl-org/SDL_ttf/releases

SDL_net and SDL_mixer still have not released v3.

Finally, none of the above are in Fedora's repos, and Debian/Ubuntu don't have libsdl3 itself, let alone these others, lol.

@clseibold
Copy link

clseibold commented Feb 2, 2025

Here's a list of stuff that I use and will need for Profectus, my Gemini Protocol browser application:

  • Init: Timer, Video, Events
  • Keyboard and Mouse Motion, Button Up/Down, and Wheel events
  • Text Input events, PumpEvents, WaitEvent(), Quit()
  • EventWatch
  • HiDPI stuff - when switching to SDL3, I will need the 5 functions and 6 constants listed in here: https://wiki.libsdl.org/SDL3/README/highdpi
  • Window: Create, set size, position, SDL_WINDOW_HIGH_PIXEL_DENSITY, SDL_SyncWindow
  • Window events - hidden, maximize, minimize, shown, restored, resized, size_changed, etc., including the new events for HiDPI in v3 (SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED, SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED)
  • Performance counter/timer stuff
  • Rect Intersect
  • SetCursor(), SystemCursor()
  • Texture: Destroy()
  • Surface: Set(), Duplicate(), Bounds(), Free()
  • Renderer: CreateRenderer, Set Clip Rect, Set Draw Color, Set Draw Blend Mode, CreateTextureFromSurface, Clear, DrawRect, FillRect, DrawLine, Copy() texture to destination rect, Present
    • Include the Float variants of the Drawing functions
  • SDL_SetTextureScaleMode()
  • Hints: HINT_IME_INTERNAL_EDITING, and HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK
  • CreateRGBSurfaceWithFormat, Pixelformat stuff

Aside from the SDL_ttf and SDL_image stuff, this should be pretty much everything I use for SDL.

@mewmew
Copy link
Contributor

mewmew commented Feb 10, 2025

Edit: By the way Arch Linux has SDL3 in its official repositories, I don't know how complete it is though since traditionally they split the sdl2 packages into sdl2_image, sdl2_mixer etc and there's only one sdl3 package.

It seems that the sdl3 package on Arch only provides the main library (similar to how was done for sdl2).

$ pacman -Ql sdl3 | grep "\.so"
sdl3 /usr/lib/libSDL3.so
sdl3 /usr/lib/libSDL3.so.0
sdl3 /usr/lib/libSDL3.so.0.2.4

The sdl3_ttf, sdl3_image and sdl3_mixer packages are made available through AUR, probably until they are officially released.

9 aur/sdl3_mixer-git r1879.e32ba24-1 (+1 0.10) 
    A simple multi-channel audio mixer (Version 3)
8 aur/sdl3_ttf-git r987.5986e15-1 (+0 0.00) 
    Support for TrueType (.ttf) font files with Simple Directmedia Layer (Version 3)
7 aur/sdl3_image 3.2.0-1 (+0 0.00) 
    SDL3 image loading library

@Jipok
Copy link

Jipok commented Feb 13, 2025

Will support for graphics (drawing primitives) be added? Also, could you please provide some insight into performance, especially compared to cgo. Does purego introduce any overhead? I want to create something similar to awesomewm, i.e., draw my own panel and dashboards. Since it will always be running and displayed, I'm concerned about performance.

@JupiterRider
Copy link
Owner Author

Will support for graphics (drawing primitives) be added?

@Jipok Yes, I am planning to add all relevant functions, types and #defines.

Does purego introduce any overhead?

ebitengine/purego#202

@JupiterRider
Copy link
Owner Author

@clseibold ttf is implemented now and ready for usage.

@clseibold
Copy link

@JupiterRider Thanks! I think most of the core things I need are implemented, so I will try it soon. I appreciate the work you've put into this!

@Jipok
Copy link

Jipok commented Feb 15, 2025

ebitengine/purego#202

There are not enough details, and I myself do not understand it to analyze the code.
I am sure that the overhead is acceptable if we are talking about calls to create a window and handle input events. But what if these are thousands of calls to draw primitives every 8 milliseconds(120 Hz)? Will this not overload the CPU?
I wanted to make a really effective UI, although I do not mind losing a couple of percent of performance.
I understand well how Go is doing with performance for the web, but such interaction with the GUI is beyond my competence, how much exactly does purego take on? Should I look for another language?

@JupiterRider
Copy link
Owner Author

JupiterRider commented Feb 16, 2025

@clseibold SDL_image is implemented now too!

@JupiterRider
Copy link
Owner Author

JupiterRider commented Feb 16, 2025

@Jipok I re-created raylib's bunnnymark example in purego-sdl3 und C SDL:

Go code:

package main

import (
	_ "embed"
	"fmt"
	"image/color"
	"math/rand"

	sdl "github.com/jupiterrider/purego-sdl3/sdl"
)

const maxBunnies = 50_000
const width, height = 1280, 720

func main() {
	defer sdl.Quit()
	if !sdl.Init(sdl.InitVideo) {
		panic(sdl.GetError())
	}

	var window *sdl.Window
	var renderer *sdl.Renderer
	if !sdl.CreateWindowAndRenderer("Bunnymark", width, height, sdl.WindowResizable, &window, &renderer) {
		panic(sdl.GetError())
	}
	defer sdl.DestroyRenderer(renderer)
	defer sdl.DestroyWindow(window)

	surface := sdl.LoadBMP("wabbit_alpha.bmp")
	if surface == nil {
		panic(sdl.GetError())
	}

	texture := sdl.CreateTextureFromSurface(renderer, surface)
	if texture == nil {
		panic(sdl.GetError())
	}
	defer sdl.DestroyTexture(texture)
	sdl.DestroySurface(surface)

	var bunniesCount int = 0
	var bunnies [maxBunnies]Bunny

	last := sdl.GetTicksNS()

Outer:
	for {
		var event sdl.Event
		for sdl.PollEvent(&event) {
			switch event.Type() {
			case sdl.EventQuit:
				break Outer
			case sdl.EventKeyDown:
				if event.Key().Scancode == sdl.ScancodeEscape {
					break Outer
				}
			case sdl.EventMouseButtonDown:
				if btn := event.Button(); btn.Button == uint8(sdl.ButtonLeft) {
					for i := 0; i < 100; i++ {
						if bunniesCount < maxBunnies {
							bunnies[bunniesCount].PositionX = btn.X
							bunnies[bunniesCount].PositionY = btn.Y
							bunnies[bunniesCount].SpeedX = float32(rand.Intn(501) - 250)
							bunnies[bunniesCount].SpeedY = float32(rand.Intn(501) - 250)
							bunnies[bunniesCount].Color.R = byte(rand.Intn(240+1-50) + 50)
							bunnies[bunniesCount].Color.G = byte(rand.Intn(240+1-80) + 80)
							bunnies[bunniesCount].Color.B = byte(rand.Intn(240+1-100) + 100)
							bunniesCount++
						}
					}
				}
			}
		}

		sdl.SetRenderDrawColor(renderer, 255, 255, 255, 255)
		sdl.RenderClear(renderer)

		now := sdl.GetTicksNS()
		ticks := now - last
		last = now
		delta := float64(ticks) / 1_000_000_000.0

		for i := 0; i < bunniesCount; i++ {
			bunnies[i].PositionX += bunnies[i].SpeedX * float32(delta)
			bunnies[i].PositionY += bunnies[i].SpeedY * float32(delta)

			if ((bunnies[i].PositionX + float32(texture.W)/2) > width) || ((bunnies[i].PositionX + float32(texture.W)/2) < 0) {
				bunnies[i].SpeedX *= -1
			}

			if ((bunnies[i].PositionY + float32(texture.H)/2) > width) || ((bunnies[i].PositionY + float32(texture.H)/2) < 0) {
				bunnies[i].SpeedY *= -1
			}

			var dstrect sdl.FRect
			dstrect.X = bunnies[i].PositionX
			dstrect.Y = bunnies[i].PositionY
			dstrect.W = float32(texture.W)
			dstrect.H = float32(texture.H)
			sdl.SetTextureColorMod(texture, bunnies[i].Color.R, bunnies[i].Color.G, bunnies[i].Color.B)
			sdl.RenderTexture(renderer, texture, nil, &dstrect)
		}

		sdl.SetRenderDrawColor(renderer, 0, 0, 0, 255)
		sdl.RenderFillRect(renderer, &sdl.FRect{W: 120, H: 20})
		sdl.SetRenderDrawColor(renderer, 0, 255, 0, 255)
		sdl.RenderDebugText(renderer, 5, 5, fmt.Sprintf("bunnies %d", bunniesCount))

		sdl.RenderPresent(renderer)
	}
}

type Bunny struct {
	PositionX float32
	PositionY float32
	SpeedX    float32
	SpeedY    float32
	Color     color.RGBA
}

C code:

#include <SDL3/SDL.h>
#include <stdio.h>

#define MAX_BUNNIES 50000
#define WIDTH 1280
#define HEIGHT 720

typedef struct
{
    float PositionX;
    float PositionY;
    float SpeedX;
    float SpeedY;
    SDL_Color Color;
} Bunny;

int main(int argc, char *argv[])
{
    SDL_Init(SDL_INIT_VIDEO);
    SDL_Window *window;
    SDL_Renderer *renderer;

    SDL_CreateWindowAndRenderer("Bunnymark", WIDTH, HEIGHT, SDL_WINDOW_RESIZABLE, &window, &renderer);

    SDL_Surface *surface = SDL_LoadBMP("wabbit_alpha.bmp");
    SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface);
    SDL_DestroySurface(surface);

    int bunniesCount = 0;
    Bunny *bunnies = (Bunny *)SDL_calloc(MAX_BUNNIES, sizeof(Bunny));

    bool running = true;
    Uint64 last = SDL_GetTicksNS();

    while (running)
    {
        SDL_Event event;
        while (SDL_PollEvent(&event))
        {
            switch (event.type)
            {
            case SDL_EVENT_QUIT:
                running = 0;
                break;
            case SDL_EVENT_KEY_DOWN:
                if (event.key.scancode == SDL_SCANCODE_ESCAPE)
                {
                    running = 0;
                }
                break;
            case SDL_EVENT_MOUSE_BUTTON_DOWN:
                if (event.button.button == SDL_BUTTON_LEFT)
                {
                    for (int i = 0; i < 100; i++)
                    {
                        if (bunniesCount < MAX_BUNNIES)
                        {
                            bunnies[bunniesCount].PositionX = event.button.x;
                            bunnies[bunniesCount].PositionY = event.button.y;
                            bunnies[bunniesCount].SpeedX = (float)(SDL_rand(501) - 250);
                            bunnies[bunniesCount].SpeedY = (float)(SDL_rand(501) - 250);
                            bunnies[bunniesCount].Color.r = (Uint8)(SDL_rand(240 + 1 - 50) + 50);
                            bunnies[bunniesCount].Color.g = (Uint8)(SDL_rand(240 + 1 - 80) + 80);
                            bunnies[bunniesCount].Color.b = (Uint8)(SDL_rand(240 + 1 - 100) + 100);
                            bunniesCount++;
                        }
                    }
                }
                break;
            }
        }

        SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
        SDL_RenderClear(renderer);

        Uint64 now = SDL_GetTicksNS();
        Uint64 ticks = now - last;
        last = now;
        double delta = (double)(ticks) / 1000000000.0;

        for (int i = 0; i < bunniesCount; i++)
        {
            bunnies[i].PositionX += bunnies[i].SpeedX * (float)(delta);
            bunnies[i].PositionY += bunnies[i].SpeedY * (float)(delta);

            if (((bunnies[i].PositionX + (float)(texture->w) / 2) > WIDTH) ||
                ((bunnies[i].PositionX + (float)(texture->w) / 2) < 0))
            {
                bunnies[i].SpeedX *= -1;
            }

            if (((bunnies[i].PositionY + (float)(texture->h) / 2) > HEIGHT) ||
                ((bunnies[i].PositionY + (float)(texture->h) / 2) < 0))
            {
                bunnies[i].SpeedY *= -1;
            }

            SDL_FRect dstrect = {0};
            dstrect.x = bunnies[i].PositionX;
            dstrect.y = bunnies[i].PositionY;
            dstrect.w = (float)(texture->w);
            dstrect.h = (float)(texture->h);

            SDL_SetTextureColorMod(texture, bunnies[i].Color.r, bunnies[i].Color.g, bunnies[i].Color.b);

            SDL_RenderTexture(renderer, texture, NULL, &dstrect);
        }

        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
        SDL_FRect rect = {0};
        rect.w = 120;
        rect.h = 20;
        SDL_RenderFillRect(renderer, &rect);
        SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
        char buffer[100];
        sprintf(buffer, "bunnies %d", bunniesCount);
        SDL_RenderDebugText(renderer, 5, 5, buffer);

        SDL_RenderPresent(renderer);
    }

    SDL_DestroyTexture(texture);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}

This is the Result:

Go:

Image

C:

Image

@Jipok
Copy link

Jipok commented Feb 17, 2025

But why such a difference in gpu?

@clseibold
Copy link

Oof, I didn't realize calling into C code from Go was so slow! Jumping up over 40 ms per frame is quite a lot.

@JupiterRider
Copy link
Owner Author

I'm afraid that this is a purego issue, because cgo is much faster:

package main

import (
	"fmt"
	"image/color"
	"math/rand"
	"runtime"
	"unsafe"
)

// #cgo LDFLAGS: -lSDL3
// #include <SDL3/SDL.h>
import "C"

func init() {
	runtime.LockOSThread()
}

const maxBunnies = 50_000
const width, height = 1280, 720

func main() {
	defer C.SDL_Quit()
	if !C.SDL_Init(C.SDL_INIT_VIDEO) {
		panic(C.GoString(C.SDL_GetError()))
	}

	var window *C.SDL_Window
	var renderer *C.SDL_Renderer
	title := C.CString("Bunnymark")
	if !C.SDL_CreateWindowAndRenderer(title, width, height, C.SDL_WINDOW_RESIZABLE, &window, &renderer) {
		panic(C.GoString(C.SDL_GetError()))
	}
	C.SDL_free(unsafe.Pointer(title))
	defer C.SDL_DestroyRenderer(renderer)
	defer C.SDL_DestroyWindow(window)

	file := C.CString("wabbit_alpha.bmp")
	surface := C.SDL_LoadBMP(file)
	if surface == nil {
		panic(C.GoString(C.SDL_GetError()))
	}
	C.SDL_free(unsafe.Pointer(file))

	texture := C.SDL_CreateTextureFromSurface(renderer, surface)
	if texture == nil {
		panic(C.GoString(C.SDL_GetError()))
	}
	defer C.SDL_DestroyTexture(texture)
	C.SDL_DestroySurface(surface)

	var bunniesCount int = 0
	var bunnies [maxBunnies]Bunny

	last := C.SDL_GetTicksNS()

Outer:
	for {
		var event C.SDL_Event
		for C.SDL_PollEvent(&event) {
			eventType := *(*uint32)(unsafe.Pointer(&event))
			switch eventType {
			case C.SDL_EVENT_QUIT:
				break Outer
			case C.SDL_EVENT_KEY_DOWN:
				key := *(*C.SDL_KeyboardEvent)(unsafe.Pointer(&event))
				if key.scancode == C.SDL_SCANCODE_ESCAPE {
					break Outer
				}
			case C.SDL_EVENT_MOUSE_BUTTON_DOWN:
				btn := *(*C.SDL_MouseButtonEvent)(unsafe.Pointer(&event))
				if btn.button == C.SDL_BUTTON_LEFT {
					for i := 0; i < 100; i++ {
						if bunniesCount < maxBunnies {
							bunnies[bunniesCount].PositionX = float32(btn.x)
							bunnies[bunniesCount].PositionY = float32(btn.y)
							bunnies[bunniesCount].SpeedX = float32(rand.Intn(501) - 250)
							bunnies[bunniesCount].SpeedY = float32(rand.Intn(501) - 250)
							bunnies[bunniesCount].Color.R = byte(rand.Intn(240+1-50) + 50)
							bunnies[bunniesCount].Color.G = byte(rand.Intn(240+1-80) + 80)
							bunnies[bunniesCount].Color.B = byte(rand.Intn(240+1-100) + 100)
							bunniesCount++
						}
					}
				}
			}
		}

		C.SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255)
		C.SDL_RenderClear(renderer)

		now := C.SDL_GetTicksNS()
		ticks := now - last
		last = now
		delta := float64(ticks) / 1_000_000_000.0

		for i := 0; i < bunniesCount; i++ {
			bunnies[i].PositionX += bunnies[i].SpeedX * float32(delta)
			bunnies[i].PositionY += bunnies[i].SpeedY * float32(delta)

			if ((bunnies[i].PositionX + float32(texture.w)/2) > width) || ((bunnies[i].PositionX + float32(texture.w)/2) < 0) {
				bunnies[i].SpeedX *= -1
			}

			if ((bunnies[i].PositionY + float32(texture.h)/2) > height) || ((bunnies[i].PositionY + float32(texture.h)/2) < 0) {
				bunnies[i].SpeedY *= -1
			}

			var dstrect C.SDL_FRect
			dstrect.x = C.float(bunnies[i].PositionX)
			dstrect.y = C.float(bunnies[i].PositionY)
			dstrect.w = C.float(texture.w)
			dstrect.h = C.float(texture.h)
			C.SDL_SetTextureColorMod(texture, C.Uint8(bunnies[i].Color.R), C.Uint8(bunnies[i].Color.G), C.Uint8(bunnies[i].Color.B))
			C.SDL_RenderTexture(renderer, texture, nil, &dstrect)
		}

		C.SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255)
		C.SDL_RenderFillRect(renderer, &C.SDL_FRect{w: 120, h: 20})
		C.SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255)
		text := C.CString(fmt.Sprintf("bunnies %d", bunniesCount))
		C.SDL_RenderDebugText(renderer, 5, 5, text)
		C.SDL_free(unsafe.Pointer(text))
		C.SDL_RenderPresent(renderer)
	}
}

type Bunny struct {
	PositionX float32
	PositionY float32
	SpeedX    float32
	SpeedY    float32
	Color     color.RGBA
}

Image

@Jipok
Copy link

Jipok commented Feb 17, 2025

Well, this is what I assumed and feared.

@Jipok
Copy link

Jipok commented Feb 17, 2025

@TotallyGamerJet can you clarify this issue? Is there any hope for performance improvement?

@TotallyGamerJet
Copy link

The best suggestion I can give for performance is to avoid RegisterLib as much as possible. If you can replace uses with Purego.SyscallN you should see similar performance characteristics to native Cgo. You'll have to use RegisterFunc for functions that take or return structs or have both floats and int arguments. All other cases should be able to use SyscallN. Also, SyscallN could probably be optimized slightly more since it makes an allocation every time but it basically does just what a normal Cgo call does. Hope this helps.

@Zyko0
Copy link

Zyko0 commented Feb 17, 2025

^
I'm doing exactly this here: https://github.com/Zyko0/go-sdl3/blob/main/sdl_functions.gen_impl.go#L4685-L9065, feel free to take what you want from this file / copy it!
I made a tool to facilitate the generation of purego.SyscallN function a few months ago, that I also used here if you're interested/curious: https://github.com/Zyko0/purego-gen

edit: I re-generated the file after TotallyGamerJet's observation!

@TotallyGamerJet
Copy link

@Zyko0 After looking at the generated code I noticed that it is invalid as it breaks what is guaranteed by the unsafe package. See point 4 in https://pkg.go.dev/unsafe#Pointer

If a pointer argument must be converted to uintptr for use as an argument, that conversion must appear in the call expression itself:
syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))
The compiler handles a Pointer converted to a uintptr in the argument list of a call to a function implemented in assembly by arranging that the referenced allocated object, if any, is retained and not moved until the call completes, even though from the types alone it would appear that the object is no longer needed during the call.

Perhaps I should add this part to purego's documentation.

@Zyko0
Copy link

Zyko0 commented Feb 17, 2025

@TotallyGamerJet That's interesting, thanks! I didn't know about that, I'll need to refactor a few things haha

edit: I made the changes, but now I'm also thinking about some function arguments that I'm taking as uintptr already, which is probably incorrect too, the conversion happening too early (in the parent context), breaking the guarantee.

@JupiterRider
Copy link
Owner Author

Thanks everyone! I will start to move performance critical functions (like drawing, rendering) to purego.SyscallN.

And special thanks to @Jipok . If you hadn't asked for a benchmark, I wouldn't have noticed.

TotallyGamerJet added a commit to ebitengine/purego that referenced this issue Mar 6, 2025
It was unclear how to properly use this function. Add some clarification.

JupiterRider/purego-sdl3#4 (comment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants