Writing a dedicated Allocator

(Note: all this is in the first stage it is windows oriented, later on I pick up new challenges for Linux).

I have this generic 2D struct, mostly used for Images, so let us assume T is a pixel.

pub fn Map(comptime T: type) type {
    return struct {
        const Self = @This();

        width: i32,
        height: i32,
        data: []T,
};

Map is currently the owner of the data, so there are some functions (like init, resize), taking a std.mem.Allocator for maintaining the correct amount of pixels.

Now an interesting challenge is ahead of me
I want to change the allocating code in such a way that Map is usable inside windows for BitBlt and StretchBlt. These function are - in my experience - insanely fast for graphics output in a very simple way: no need for OpenGL, Vulkan, SDL or whatever libs there are of which I have zero knowledge…

So… for this I need from the windows gdi the functions:
CreateDIBSection and CreateCompatibleDC.
This way I get a handle (dc) and a pointer to the pixels from windows, which I can manipulate.

So now my Map should no longer be the owner of its data but just using it as a raw ptr and len which is now owned by the windows gdi.

The most straightforward way to tackle this could maybe be writing a dedicated Allocator for this with a dedicated VTable.
Would this be true?
And I only need to write a smart way to implement these core functions?
alloc
resize
remap
free
Interacting with the windows stuff (dc and pixelptr) I have in hands?

std.mem.Allocator is an interface for general purpose memory allocation. While you could probably make one backed by win32 GDI functions, using it for 2D graphics like this would be confusing IMO. If you want to support multiple graphics backends, you’re better off creating a customized Renderer interface, and since you probably only need one implementation per platform/build target, there’s not much reason to use a VTable for it. There’s a variety of alternatives:

  • Use switch (@import("builtin").os.tag) for OS-specific rendering code
  • Use switch (@import("config").renderer) as above, but where config is an options module defined in build.zig
  • Put all rendering functions in a module and use build.zig logic to change which module gets imported by @import("Renderer"). This makes it easier to keep all code for a particular platform in the same place and gives more flexibility for differences between platforms, but has the disadvantage of making maintenance a bit more fragile. (e.g. if you add a new function in one renderer, it’s easy to forget to add it in the others)
  • Make your renderer a tagged union with functions that delegate to the payload struct corresponding to the active backend. This is like the previous one but is a little less fragile since all platforms will be analyzed for all builds, at the cost of an extra branch for each call into the interface, but still much cheaper than a vtable call.

Moreover, you’d be better off not having Map be responsible for creation/ownership/cleanup of the backing data at all. Just let the high level game code that initializes a Map variable call CreateDIBSection/CreateCompatibleDC directly and initialize the Map using the pointer to the actual image data.

Side note: you may want to add a stride: i32 parameter to Map, representing the number of bytes between the beginning of one line and the next. This would allow you to create a Map that’s backed by an image whose width is wider than your Map. You may need this for some graphics APIs. (e.g. older 3D APIs only allow power-of-two dimensioned textures)

Few things:

  1. You can just store [*]T instead of a slice inside map. However if you do so you should use accessor functions which assert bounds checks, eg Trying to use a two dimensional array as a a two dimensional slice - #8 by ScottRedig
  2. You could implement the allocator interface, but why do so? It’d be a poor fit, eg the “CreateDIBSection” doesn’t take an alignment (it says it must be double word aligned, that’s what you get), it takes arguments that an allocator doesn’t pass (eg bitmap layout), and you’re not going to be using this to allocate more than a few different types not a lot of generic types. There’s no rule of Zig that all memory must be allocated through the allocator interface. So, just write a function which calls into the windows functions and returns your map type, and a respective free function. Later on you can switch on target’s os to provide Linux functionality too.
  3. To answer the question specifically asked, though I don’t think you should use the interface in this case: Yes, an allocator implementation only provides those 4 vtable methods. In fact, both remap and resize are perfectly allowed to always fail, so you only need to implement alloc and free.
  4. You mentioned this being faster. Depends what you’re talking about. It can be faster to get set up and going with. Good for if you’re just playing around with cpu side code and want to get some pixels on the screen. However keep in mind this probably isn’t fast in a per-frame render time sense. With the high pixel resolution and memory speed on my laptop, eg, simply writing to every pixel on screen using one CPU core takes like 1/4 of a frame. (This surprised me when I noticed it, after which I did the math.). Also modern computers are set up to compose and display through the GPU anyways, so after rendering on the CPU it’ll send your pixels to the GPU anyways.

Thank you both. Useful remarks!
I will work on it further.