Monday, August 29, 2022

Packed structs in Zig make bit/flag sets trivial

As we’ve been building Mach engine, we’ve been using a neat little pattern in Zig that enables writing flag sets more nicely in Zig than in other languages.

What is a flag set?

We’ve been rewriting mach/gpu (WebGPU bindings for Zig) from scratch recently, so let’s take a flag set from the WebGPU C API:

typedef uint32_t WGPUFlags;
typedef WGPUFlags WGPUColorWriteMaskFlags;

Effectively, WGPUColorWriteMaskFlags here is a 32-bit unsigned integer where you can set specific bits in it to represent whether or not to write certain colors:

typedef enum WGPUColorWriteMask {
    WGPUColorWriteMask_None = 0x00000000,
    WGPUColorWriteMask_Red = 0x00000001,
    WGPUColorWriteMask_Green = 0x00000002,
    WGPUColorWriteMask_Blue = 0x00000004,
    WGPUColorWriteMask_Alpha = 0x00000008,
    WGPUColorWriteMask_All = 0x0000000F,
    WGPUColorWriteMask_Force32 = 0x7FFFFFFF
} WGPUColorWriteMask;

Then to use it you’d use the various bit operations with those masks, e.g.:

WGPUColorWriteMaskFlags mask = WGPUColorWriteMask_Red | WGPUColorWriteMask_Green;
mask |= WGPUColorWriteMask_Blue; // set blue bit

This all works, people have been doing it for years in C, C++, Java, Rust, and more. In Zig, we can do better.

Zig packed structs

Zig has packed structs: these let us pack memory tightly, where a bool is actually a single bit (in most other languages, this is not true.) Zig also has arbitrary bit-width integers, like u28, u1 and so on.

We can write WGPUColorWriteMaskFlags from earlier in Zig using:

pub const ColorWriteMaskFlags = packed struct {
    red: bool = false,
    green: bool = false,
    blue: bool = false,
    alpha: bool = false,

    _padding: u28 = 0,
};

This is still just 32 bits of memory, and so can be passed to the same C APIs that expect a WGPUColorWriteMaskFlags - but interacting with it is much nicer:

var mask = ColorWriteMaskFlags{.red = true, .green = true};
mask.blue = true; // set blue bit

In C you would need to write code like this:

if (mask & WGPUColorWriteMask_Alpha) {
    // alpha is set..
}
if (mask & (WGPUColorWriteMask_Alpha|WGPUColorWriteMask_Blue)) {
    // alpha and blue are set..
}
if ((mask & WGPUColorWriteMask_Green) == 0) {
    // green not set
}

In Zig it’s just:

if (mask.alpha) {
    // alpha is set..
}
if (mask.alpha and mask.blue) {
    // alpha is set..
}
if (!mask.green) {
    // green not set
}

Comptime validation

Making sure that our ColorWriteMaskFlags ends up being the same size could be a bit tricky: what if we count the number of bool wrong? Or what if we accidently get the padding size wrong? Then it might not be the same size as a uint32 anymore.

Luckily, we can verify our expectations at comptime:

pub const ColorWriteMaskFlags = packed struct {
    red: bool = false,
    green: bool = false,
    blue: bool = false,
    alpha: bool = false,

    _padding: u28 = 0,

    comptime {
        std.debug.assert(@sizeOf(@This()) == @sizeOf(u32));
        std.debug.assert(@bitSizeOf(@This()) == @bitSizeOf(u32));
    }
}

The Zig compiler will take care of running the comptime code block here for us when building, and it will verify that the byte size of @This() (the type we’re inside of, the ColorWriteMaskFlags struct in this case) matches the @sizeOf(u32).

Similarly we could check the @bitSizeOf both types if we like.

Note that @sizeOf may include the size of padding for more complex types, while @bitSizeOf returns the number of bits it takes to store T in memory if the type were a field in a packed struct/union. For flag sets like this, it doesn’t matter and either will do. For more complex types, be sure to recall this.

Thanks for reading

Be sure to join the new Mach engine Discord server where we’re building the future of Zig game development.

You can also sponsor my work if you like what I’m doing! :)



from Hacker News https://ift.tt/gMu6Ta1

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.