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 11111111The 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 1For 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 15Two 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 byte0Written 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 = 0x0FUsed 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 = 0xFFUsed 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 = 0xF0Used 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 — 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–16The 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 0Shifting 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.