Hacker News new | ask | show | jobs
by mort96 124 days ago
It doesn't just have to be files, FWIW. I once worked in a Go project which used SDL through CGO for drawing. "Widgets" were basically functions which would allocate an SDL surface, draw to it using Cairo, and return it to Go code. That SDL surface would be wrapped in a Go wrapper with a Destroy method which would call SDL_DestroySurface.

And to draw a surface to the screen, you need to create an SDL texture from it. If that's all you want to do, you can then destroy the SDL surface.

So you could imagine code like this:

    strings := []string{"Lorem", "ipsum", "dolor", "sit", "amet"}
    
    stringTextures := []SDLTexture{}
    for _, s := range strings {
        surface := RenderTextToSurface(s)
        defer surface.Destroy()
        stringTextures = append(stringTextures, surface.CreateTexture())
    }
Oops, you're now using way more memory than you need!
1 comments

Why would you allocate/destroy memory in each iteration when you can reuse it to much greater effect? Aside from bad API design, but a language isn't there to paper over bad design decisions. A good language makes bad design decisions painful.
The surfaces are all of different size, so the code would have to be more complex, resizing some underlying buffer on demand. You'd have to split up the text rendering into an API to measure the text and an API to render the text, so that you could resize the buffer. So you'd introduce quite a lot of extra complexity.

And what would be the benefit? You save up to one malloc and free per string you want to render, but text rendering is so demanding it completely drowns out the cost of one allocation.

Why does the buffer need to be resized? Your malloc version allocates a fixed amount of memory on each iteration. You can allocate the same amount of memory ahead of time.

If you were dynamically changing the malloc allocation size on each iteration then you have a case for a growable buffer to do the same, but in that case you would already have all the complexity of which you speak as required to support a dynamically-sized malloc.

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?

> 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.

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.