thecodingidiot.com

The InfiniteBits

Bits

In f03a, AND, NAND, and NOT were physical circuits made of transistors. The 6502 in f03c lifted those same operations into CPU instructions — and, ora — working directly on bits. C gives you the same operations as operators you can compose in expressions.

Earlier in this chapter, the render loop used << 16 to slot a value into a specific byte position and the explanation said to move on. This page is where you stop and understand what that means. Pixel packing is the application at the end; the operators are the tool.


Binary representation

Every integer in a C program is stored in memory as a sequence of bits — binary digits, each either 0 or 1. Eight bits form one byte, which can hold values from 0 to 255.

decimal   binary
      0   00000000
      1   00000001
      2   00000010
      4   00000100
     15   00001111
    127   01111111
    255   11111111

The value of a binary number is the sum of the bit positions that are set. Position 0 (rightmost) is worth 1, position 1 is worth 2, position 2 is worth 4, and so on — each position is double the previous:

bit position:   7    6    5    4    3    2    1    0
place value:  128   64   32   16    8    4    2    1

For the value 200 (11001000): bits 7, 6, and 3 are set. 128 + 64 + 8 = 200.

A uint32_t is 32 bits — four bytes. It holds values from 0 to 4,294,967,295. SDL2 stores each pixel as a uint32_t with one independent value packed into each of its four byte positions. To place a value into a specific byte position inside a 32-bit integer, you need the bitwise operators.


Hexadecimal

Writing 11001000 for 200 is tedious and error-prone. Hexadecimal is a shorthand: one hex digit represents exactly four bits.

binary   hex   decimal
0000      0        0
0001      1        1
...
1001      9        9
1010      A       10
1011      B       11
1100      C       12
1101      D       13
1110      E       14
1111      F       15

Two hex digits represent one byte (four bits + four bits = eight bits):

0xFF  =  1111 1111  =  255   (all bits set)
0x0F  =  0000 1111  =   15   (low four bits set)
0xC8  =  1100 1000  =  200
0x00  =  0000 0000  =    0   (all bits clear)

The 0x prefix tells the C compiler the literal is hexadecimal. A four-byte SDL2 pixel uses eight hex digits — two per byte:

0xFF C8 64 32
 └──┘ └──┘ └──┘ └──┘
byte3 byte2 byte1 byte0

Written as one value: 0xFFC86432. In decimal that is 4291314738 — unreadable. Hex makes the byte boundaries obvious at a glance. What each byte position represents is the subject of the next page.


The six bitwise operators

C has six operators that work on individual bits.

~ NOT — invert every bit

~x flips every bit: 0 becomes 1, 1 becomes 0.

uint8_t a = 0x0F;   /* 0000 1111 */
uint8_t b = ~a;     /* 1111 0000 = 0xF0 */

& AND — both bits must be 1

x & y produces 1 in each position where both inputs have a 1.

0xFF & 0x0F   =  1111 1111
               & 0000 1111
               = 0000 1111   =  0x0F

Used to mask — isolate specific bits while zeroing the rest.

| OR — at least one bit must be 1

x | y produces 1 in each position where either input has a 1.

0xF0 | 0x0F   =  1111 0000
               | 0000 1111
               = 1111 1111   =  0xFF

Used to combine — merge bit fields that occupy different positions.

^ XOR — exactly one bit must be 1

x ^ y produces 1 where the bits differ.

0xFF ^ 0x0F   =  1111 1111
               ^ 0000 1111
               = 1111 0000   =  0xF0

Used to toggle — flip specific bits. Less common in pixel work; common in encryption and checksums.

<< Left shift — move bits toward the high end

x << n moves every bit n positions to the left. Zeros fill the low end; bits that fall off the high end are discarded.

uint8_t x = 0x01;   /* 0000 0001 */
x = x << 3;         /* 0000 1000 = 8 */

Shifting left by n multiplies by 2n2^n — the bit in position 0 (worth 1) moves to position 3 (worth 8). But the reason this chapter needs left shift has nothing to do with multiplication.

The problem: a byte value starts in the wrong position.

You have a byte value — say 200, which is 0xC8. It fits in 8 bits and starts at bits 7–0, the low byte of whatever integer holds it. You need it in byte 2 (bits 23–16) of a 32-bit integer. It is in the wrong place.

Left shift by 16 moves it there. Visualised as four bytes side by side:

                      byte 3   byte 2   byte 1   byte 0
before 0xC8:          00       00       00       C8
after  0xC8 << 16:    00       C8       00       00
                               └──── now in byte 2, bits 23–16

The eight bits have travelled 16 positions to the left. Everything to the right of them is now zero — those slots are empty and ready for other values. Each byte position in a 32-bit integer requires a different shift count:

byte 3  << 24   →   bits 31–24
byte 2  << 16   →   bits 23–16
byte 1  <<  8   →   bits 15–8
byte 0  <<  0   →   bits  7–0   (no shift — already there)

>> Right shift — move bits toward the low end

x >> n moves every bit n positions to the right. For unsigned integers, zeros fill the high end; bits that fall off the low end are discarded.

uint32_t x = 0x00C80000;   /* byte 2 sitting in bits 23–16 */
x = x >> 16;               /* 0x000000C8 — byte 2 back in bits 7–0 */

Right shift is the reverse of left shift. Once a value has been packed into a higher byte position, right shift brings it back down to bits 7–0 where it can be read as a plain number.

Extracting a byte from a packed integer. Given 0xFFC86432, to recover the value in byte 2 (bits 23–16):

0xFFC86432 >> 16  =  0x0000FFC8   ← byte 2 moved to byte 0

Shifting brought byte 2 to the low position — but byte 3 travelled along into byte 1. The result is 0xFFC8, not 0xC8. AND with 0xFF removes the unwanted high byte:

  0x0000FFC8
& 0x000000FF
= 0x000000C8   =  200   ← byte 2 extracted

& 0xFF is a mask: it keeps only the lowest 8 bits and zeroes everything above them. The pattern (value >> 16) & 0xFF is the general recipe for reading byte 2 back out of any packed 32-bit integer. Right shift positions the target byte at the low end; AND removes everything above it.


What comes next

Those four operators — shift left, shift right, OR, AND — are all that pixel packing requires. The next page explains what the four byte positions in an SDL2 pixel actually represent, and uses these operators to build the colour functions.