I use bitfields all over the place in my emulator, in a way very similar to yours. I use them in the core CPU and PPU emulation, I use them in mapper registers, I use them in iNES and FDS header parsers.
That advice about non-portable or compiler-specific behavior? Yeah, the
standard might say that, but in practice, which compilers are you going to use? My emulator works on Windows, macOS, and Linux, using MSVC, clang/llvm, and gcc respectively, and the bitfields behave exactly the same way on those three compilers. What more do you want?
Bitfields aren't perfect, however. You have to pay close attention to bit alignment within bytes, byte alignment within words, endianness, and more. My bitfields look like this:
Code: Select all
enum NametableSource : uint8_t {
CIRAM,
CHRROM
};
union VRC6PpuBankingStyle {
VRC6PpuBankingStyle(uint8_t val) : value(val) {}
struct {
#if __BYTE_ORDER == __LITTLE_ENDIAN
uint8_t ppuBankingMode : 2;
uint8_t mirroring : 2;
NametableSource nametableSource : 1;
uint8_t chrA10Rule : 1;
uint8_t unused : 1;
uint8_t prgRamEnable : 1;
#else // __BIG_ENDIAN
uint8_t prgRamEnable : 1;
uint8_t unused : 1;
uint8_t chrA10Rule : 1;
NametableSource nametableSource : 1;
uint8_t mirroring : 2;
uint8_t ppuBankingMode : 2;
#endif
};
uint8_t value;
};
All of my bitfields are endian-safe by repeating themselves in reverse order for the other endianness mode. Boost.Endian provides the compile-time endianness detection. You can see I use a typesafe enum inside of this bitfield, and I declare the underlying integer data type with my enums, which
might be necessary to use them the way I do.
I often deal with 16-bit fields like this:
Code: Select all
#pragma pack(push, 1)
struct DiskInfo
{
uint8_t BlockCode;
char DiskVerification[14];
// ... snip ...
private:
uint8_t diskWriterSerialNumberLo;
uint8_t diskWriterSerialNumberHi;
// ... snip ...
public:
uint16_t GetDiskWriterSerialNumber() const { return diskWriterSerialNumberLo | (uint16_t(diskWriterSerialNumberHi) << 8); }
};
#pragma pack(pop)
Doing it this way means I don't even have to think about byte alignment / word alignment within the structure - I can just pluck a pointer out of the middle of an arbitrary buffer, cast it to the type I want, and get 16-bit integers out, regardless of the architecture or endianness. It's not like this is performance sensitive code.