Hacker News new | ask | show | jobs
by mort96 124 days ago
The example allocates an SDL_Surface large enough to fit the text string each iteration.

Granted, you could do a pre-pass to find the largest string and allocate enough memory for that once, then use that buffer throughout the loop.

But again, what do you gain from that complexity?

2 comments

> The example allocates an SDL_Surface large enough to fit the text string each iteration.

Impossible without knowing how much to allocate, which you indicate would require adding a bunch of complexity. However, I am willing to chalk that up to being a typo. Given that we are now calculating how much to allocate on each iteration, where is the meaningful complexity? I see almost no difference between:

    while (next()) {
        size_t size = measure_text(t);
        void *p = malloc(size);
        draw_text(p, t);
        free(p);
    }
and

    void *p = NULL;
    while (next()) {
        size_t size = measure_text(t);
        void *p = galloc(p, size);
        draw_text(p, t);
    }
    free(p);
>> The example allocates an SDL_Surface large enough to fit the text string each iteration.

> Impossible without knowing how much to allocate

But we do know how much to allocate? The implementation of this example's RenderTextToSurface function would use SDL functions to measure the text, then allocate an SDL_Surface large enough, then draw to that surface.

> I see almost no difference between: (code example) and (code example)

What? Those two code examples aren't even in the same language as the code I showed.

The difference would be between the example I gave earlier:

    stringTextures := []SDLTexture{}
    for _, str := range strings {
        surface := RenderTextToSurface(str)
        defer surface.Destroy()
        stringTextures = append(stringTextures, surface.CreateTexture())
    }
and:

    surface := NewSDLSurface(0, 0)
    defer surface.Destroy()
    stringTextures := []SDLTexture{}
    for _, str := range strings {
        size := MeasureText(s)
        if size.X > surface.X || size.Y > surface.Y {
            surface.Destroy()
            surface = NewSDLSurface(size.X, size.Y)
        }

        surface.Clear()
        RenderTextToSurface(surface, str)
        stringTextures = append(stringTextures, surface.CreateTextureFromRegion(0, 0, size.X, size.Y))
    }
Remember, I'm talking about the API to a Go wrapper around SDL. How the C code would've looked if you wrote it in C is pretty much irrelevant.

I have to ask again though, since you ignored me the first time: what do you gain? Text rendering is really really slow compared to memory allocation.

> Remember, I'm talking about the API to a Go wrapper around SDL.

We were talking about using malloc/free vs. a resizable buffer. Happy to progress the discussion towards a Go API, however. That, obviously, is going to look something more like this:

    renderer := SDLRenderer()
    defer renderer.Destroy()
    for _, str := range strings {
        surface := renderer.RenderTextToSurface(str)
        textures = append(textures, renderer.CreateTextureFromSurface(surface))
    }
I have no idea why you think it would look like that monstrosity you came up with.
> No. We were talking about using malloc/free vs. a resizable buffer.

No. This is a conversation about Go. My example[1], that you responded to, was an example taken from a real-world project I've worked on which uses Go wrappers around SDL functions to render text. Nowhere did I mention malloc or free, you brought those up.

The code you gave this time is literally my first example (again, [1]), which allocates a new surface every time, except that you forgot to destroy the surface. Good job.

Can this conversation be over now?

[1] https://news.ycombinator.com/item?id=47088409

I invite you to read the code again. You missed a few things. Notably it uses a shared memory buffer, as discussed, and does free it upon defer being executed. It is essentially equivalent to the second C snippet above, while your original example is essentially equivalent to the first C snippet.
Wait, so your wrapper around SDL_Renderer now also inexplicably contains a scratch buffer? I guess that explains why you put RenderTextToSurface on your SDL_Renderer wrapper, but ... that's some really weird API design. Why does the SDL_Renderer wrapper know how to use SDL_TTF or PangoCairo to draw text to a surface? Why does SDL_Renderer then own the resulting surface?

To anyone used to SDL, your proposed API is extremely surprising.

It would've made your point clearer if you'd explained this coupling between SDL_Renderer and text rendering in your original post.

But yes, I concede that if there was any reason to do so, putting a scratch surface into your SDL_Renderer that you can auto-resize and render text to would be a solution that makes for slightly nicer API design. Your SDL_Renderer now needs to be passed around as a parameter to stuff which only ought to need to concern itself with CPU rendering, and you now need to deal with mutexes if you have multiple goroutines rendering text, but those would've been alright trade-offs -- again, if there was a reason to do so. But there's not; the allocation is fast and the text rendering is slow.

I think I've been successfully nerd sniped.

It might be preferable to create a font atlas and just allocate printable ASCII characters as a spritesheet (a single SDL_Texture* reference and an array of rects.) Rather than allocating a texture for each string, you just iterate the string and blit the characters, no new allocations necessary.

If you need something more complex, with kerning and the like, the current version of SDL_TTF can create font atlases for various backends.

Completely depends on context. If you're rendering dynamically changing text, you should do as you say. If you have some completely static text, there's really nothing wrong with doing the text rendering once using PangoCairo and then re-using that texture. Doing it with PangoCairo also lets you do other fancy things like drop shadows easier.