Tsdl API confusion: How do I get "pixel_format" from "Pixel.format_enum" or from a "surface"?

Hi, I’m trying to learn OCaml and the SDL graphics library simultaneously at the moment, and I’m still struggling with how to eat my way through an OCaml API.
I’m using the Tsdl library, and it seems to have two types to work with SDL formats. Given an “Sdl.surface”, to use the function

Sdl.map_rgb: pixel_format -> uint8 -> uint8 -> uint8 -> uint32

I need its “pixel_format”. But I can only find a function

Sdl.get_surface_format_enum: surface -> Pixel.format_enum

I don’t see a way to get from the enum to the “pixel_format” type anywhere (except for allocating a new format and releasing it after, which works, but I only want to read the format, not allocate anything), but that might be because I’m not at all used to reading OCaml APIs.

Thanks for any input!

Tsdl doesn’t give access to the format field of surfaces to prevent memory ownership problems. As far as I remember this is simply a non-essential data structure to deal with pixel formats in C.

If you want to access the pixel data of a surface lock it and use Sdl.get_surface_pixel using the bigarray kind you’d like to read the surface raw memory with and work directly with that according to the surface’s Pixel.format_enum.

I agree with @kenranunderscore that there is a small inconsistency in the API, making
Sdl.map_rgb delicate to use. I had to face the same problem and here is what I did:

  1. fork tsdl and make the surface format field readable. But in the long run it’s not a good idea to have my own separate version of tsdl, so then I came back to official tsdl and

  2. allocated a format by Sdl.alloc_format (Sdl.get_surface_format_enum surface), and letting the ocaml GC dispose of this format. But finally, I wasn’t sure it was a good idea and hence I

  3. made sure all my surfaces are Sdl.Pixel.format_argb8888, access the pixels with
    let pixels = Sdl.get_surface_pixels surface Bigarray.int8_unsigned ...
    and check the format with
    Sdl.get_surface_format_enum surface = Sdl.Pixel.format_argb8888
    (this is essentially what @dbuenzli suggested above, and if you want to test several cases you end up rewriting your own version of Sdl.map_rgb…)

If there is a better way I’d be happy to know

The point here is that if you go through SDL’s Sdl.map_rgb it will terribly inefficient: for each pixel update you’ll call to C and box (allocate) an int32.

With a little bit of care you should be able to write tight loops in OCaml to update the surface’s bigarray such that the compiler won’t box your pixel updates even if using a format with int32 values.

So you may find the design odd/unconvenient but it is in the tradition of not giving you the tools to allow yourself to write grossly inefficient code.

1 Like

thanks for the explanation. I don’t really know the internals of ocaml’s allocation, but if I understand correctly, you agree with my point 3. above because

  • we test the format only once with Sdl functions
  • when looping the surface’s bigarray we don’t do any more C calls

it this correct?

Yes you avoid the C-OCaml boundary cost. But depending on which buffer layout you choose there’s a little bit more attention needed in how you write your loop to avoid boxing the scalars you are using to update the surface.

If you want to avoid surprises the easiest is to use a buffer format whose associated type (the first parameter of Bigarray.kind) is int since these integers are always unboxed.

Otherwise if you are using say int32 you should likely avoid calling functions and do everything inline but I fail to find a reference to point to with the exact rules (I suspect they are similar to those of float boxing – but that document may be outdated aswell). Best in this case is to checkout the assembly (ocamlopt -S) to make sure you are not allocating int32 when you do your update.

If you have code in the open we can have a look.

Besides here are a few references you may want to peruse at some point if you want to understand a little bit better how OCaml represents values at runtime.

You say you don’t want to alloc anything but you don’t say why.
If it’s outside of a loop (just inside your mainloop) I think it’s ok to use alloc_format and free_format after.
This is what I’m doing in this mini-game, you can see at line 636 in the function pixel_for_surface (the equivalent of Surface.get_pixelformat_t is probably get_surface_format_enum). This function is called everytime a new enemy is allocated, so not too often, and the mini-game runs just fine even on a slow computer.

1 Like

thanks for the hints. Having to look at the assembly to determine which values are allocated sounds a bit scary though. What would be the minimal program to compile with -S in order to see what happens when manipulating ints, for instance, Bigarray.int8_unsigned ?

Are there available tests which tell me the cost of allocating and/or calling C? For instance I have a function for drawing simple shape to an SDL renderer, which calls
Sdl.render_draw_points a number of times. If I understand correctly it would be more efficient to first store alll computed pixels in an array, and do a final, single Sdl.render_draw_points. Oh well, this sounds kind of obvious :wink: But if the gain is little, that would be premature optimization.

Don’t be scared ! It’s just a computer and you don’t have to understand everything of it. You can find a few reading hints here.

For the rest just perform measurements on your program and locate your bottlenecks. In the end this is what matters.

As @fccm showed there are patterns where using Sdl.map_rgb may be harmless and others where it’s clearly not (e.g. if you go through it to read/update each of the pixels of your surface).

Measure don’t take guesses.