Understanding Binary and Bitwise Operations

This tutorial explains how computers represent numbers in binary, and how you can manipulate individual bits using bitwise operators. Understanding these concepts will help you write more efficient code and understand how low-level systems work.

In modern development, these 'low-level' operations are often tucked away behind abstractions and higher-level APIs. It's not surprising that many programmers, even seasoned ones, aren't familiar with bitwise operations. When Mark Sibly was introducing collisions in his last project (SGDLib) and using flags in collisions, it became apparent just how many developers hadn't encountered these concepts before.

However, bitwise operations are handy to know and will go a long way in understanding how software works at a fundamental level. In future, I may implement features in BambooBasic that require the use of bitwise operations for setting and receiving flags and other such things, so this knowledge will become essential.



Table of Contents

What is Binary?

Computers store all data as binary - sequences of 0s and 1s called bits. Unlike our decimal system (base 10) which uses digits 0-9, binary (base 2) only uses 0 and 1.

A byte is 8 bits (half a byte is a nibble). For example, the number 42 in binary is 00101010.

Each position in a binary number represents a power of 2:

Position 7 6 5 4 3 2 1 0
Power of 2 2^7 2^6 2^5 2^4 2^3 2^2 2^1 2^0
Decimal Value 128 64 32 16 8 4 2 1
Binary (42) 0 0 1 0 1 0 1 0
Calculation: 0 + 0 + 32 + 0 + 8 + 0 + 2 + 0 = 42


Binary Basics

Converting decimal to binary:

Example: Convert 42 to binary

42 / 2 = 21 remainder 0  <-- Least significant bit (rightmost)
21 / 2 = 10 remainder 1
10 / 2 = 5  remainder 0
5  / 2 = 2  remainder 1
2  / 2 = 1  remainder 0
1  / 2 = 0  remainder 1  <-- Most significant bit (leftmost)

Read upwards: 101010 = 42

Common power-of-2 values to memorize:

Power Value Notes
2^01
2^12
2^24
2^38
2^416
2^532
2^664
2^7128
2^82561 byte can hold 0-255
2^9512
2^101,0241 Kilobyte (KB)
2^1665,53664 KB
2^201,048,5761 Megabyte (MB)


Hexadecimal Notation

Binary numbers get long quickly. Hexadecimal (base 16) is a shorthand that uses digits 0-9 and letters A-F.

Each hex digit represents exactly 4 bits:

Decimal Binary Hex
000000
100011
200102
300113
401004
501015
601106
701117
810008
910019
101010A
111011B
121100C
131101D
141110E
151111F

Example: The number 255 in different bases:

Decimal:     255
Binary:      11111111
Hexadecimal: $FF

In BambooBasic, use $ prefix for hex: Local color:Int = $FF0000



Bitwise AND - Testing and Masking

The BitAnd operator compares each bit. The result bit is 1 only if both input bits are 1.

Truth Table:

A | B | A BitAnd B
--|---|------------
0 | 0 |     0
0 | 1 |     0
1 | 0 |     0
1 | 1 |     1

Example:

  00101010  (42 in binary)
BitAnd
  00001111  (15 in binary - a "mask")
  --------
  00001010  (10 in binary)

Code Example:

Local a:Int = 42
Local mask:Int = 15
Local result:Int = a BitAnd mask
Print result  ; Output: 10

; Common use: Extract lower 4 bits
Local value:Int = $AB
Local lowerNibble:Int = value BitAnd $0F
Print lowerNibble  ; Output: 11 (B in hex = 11)

Practical Use - Testing Flags:

Const FLAG_VISIBLE:Int = 1    ; Binary: 00000001
Const FLAG_SOLID:Int = 2      ; Binary: 00000010
Const FLAG_ANIMATED:Int = 4   ; Binary: 00000100

Local objectFlags:Int = FLAG_VISIBLE BitOr FLAG_SOLID  ; Binary: 00000011

; Check if object is visible
If (objectFlags BitAnd FLAG_VISIBLE) <> 0 Then
    Print "Object is visible"
EndIf

; Check if object is solid
If (objectFlags BitAnd FLAG_SOLID) <> 0 Then
    Print "Object is solid"
EndIf

; Check if object is animated
If (objectFlags BitAnd FLAG_ANIMATED) <> 0 Then
    Print "Object is NOT animated"
EndIf


Bitwise OR - Setting Bits

The BitOr (or &) operator compares each bit. The result bit is 1 if either or both input bits are 1.

Truth Table:

A | B | A BitOr B
--|---|----------
0 | 0 |    0
0 | 1 |    1
1 | 0 |    1
1 | 1 |    1

Example:

  00101010  (42)
BitOr
  00001111  (15)
  --------
  00101111  (47)

Code Example:

Local a:Int = 42
Local b:Int = 15
Local result:Int = a BitOr b
; Or using the & symbol:
Local result2:Int = a & b
Print result  ; Output: 47

Practical Use - Combining Flags:

; Windows API style - combining multiple window styles
Const WS_VISIBLE:Int = $10000000
Const WS_CHILD:Int = $40000000
Const WS_BORDER:Int = $00800000

; Combine multiple flags into one value
Local windowStyle:Int = WS_VISIBLE BitOr WS_CHILD BitOr WS_BORDER
; Or with & symbol:
Local windowStyle2:Int = WS_VISIBLE & WS_CHILD & WS_BORDER

; This is how BGI functions work internally:
; CreateWindow(..., windowStyle)

Setting Individual Bits:

Local flags:Int = 0          ; Start with all bits off: 00000000
flags = flags BitOr 1        ; Turn on bit 0: 00000001
flags = flags BitOr 4        ; Turn on bit 2: 00000101
Print flags                  ; Output: 5


Bitwise XOR - Toggling Bits

The BitXor operator compares each bit. The result bit is 1 if the bits are different.

Truth Table:

A | B | A BitXor B
--|---|------------
0 | 0 |     0
0 | 1 |     1
1 | 0 |     1
1 | 1 |     0

Example:

  00101010  (42)
BitXor
  00001111  (15)
  --------
  00100101  (37)

Practical Use - Toggling Flags:

Const FLAG_MUTE:Int = 1

Local audioSettings:Int = 0  ; Sound is on

; Toggle mute on
audioSettings = audioSettings BitXor FLAG_MUTE
Print audioSettings  ; Output: 1 (muted)

; Toggle mute off again
audioSettings = audioSettings BitXor FLAG_MUTE
Print audioSettings  ; Output: 0 (unmuted)

XOR Swap Trick (for fun):

; Swap two variables without a temp variable
Local a:Int = 42
Local b:Int = 15

a = a BitXor b
b = a BitXor b  ; b now contains original a
a = a BitXor b  ; a now contains original b

Print a  ; Output: 15
Print b  ; Output: 42


Bitwise NOT - Inverting Bits

The BitNot operator flips all bits: 0 becomes 1, and 1 becomes 0.

Example (8-bit):

BitNot 00101010 = 11010101
BitNot 42       = -43 (in signed integers)

Code Example:

Local value:Int = 42
Local inverted:Int = BitNot value
Print inverted  ; Output will be negative due to two's complement

Practical Use - Clearing Specific Bits:

Const FLAG_VISIBLE:Int = 1
Const FLAG_SOLID:Int = 2

Local flags:Int = FLAG_VISIBLE BitOr FLAG_SOLID  ; Both flags set

; Clear the SOLID flag (turn it off)
flags = flags BitAnd (BitNot FLAG_SOLID)
; Result: Only VISIBLE flag remains set

Print flags  ; Output: 1


Bit Shifting - Moving Bits Left or Right

Shift Left (Shl) - Moves bits to the left, filling with zeros on the right.

00000101 Shl 1 = 00001010  (5 shifted left by 1 = 10)
00000101 Shl 2 = 00010100  (5 shifted left by 2 = 20)

Effect: Each left shift multiplies by 2.

Local value:Int = 5
Print value Shl 1  ; Output: 10  (5 * 2)
Print value Shl 2  ; Output: 20  (5 * 4)
Print value Shl 3  ; Output: 40  (5 * 8)

Shift Right (Shr) - Moves bits to the right, filling with zeros on the left.

00101000 Shr 1 = 00010100  (40 shifted right by 1 = 20)
00101000 Shr 2 = 00001010  (40 shifted right by 2 = 10)

Effect: Each right shift divides by 2 (integer division).

Local value:Int = 40
Print value Shr 1  ; Output: 20  (40 / 2)
Print value Shr 2  ; Output: 10  (40 / 4)
Print value Shr 3  ; Output: 5   (40 / 8)

Performance Note: Bit shifts are much faster than multiplication/division, though modern compilers often optimize this automatically.



Practical Example: Flags and Permissions

One of the most common uses of bitwise operations is managing multiple boolean flags in a single integer.

Traditional Approach (wasteful):

; Uses 4 bytes minimum (one Int per flag)
Local isVisible:Int = True
Local isSolid:Int = True
Local isAnimated:Int = False
Local canMove:Int = True

Bitwise Approach (efficient):

; Define flags as powers of 2 (each uses one bit)
Const FLAG_VISIBLE:Int = 1    ; Bit 0: 00000001
Const FLAG_SOLID:Int = 2      ; Bit 1: 00000010
Const FLAG_ANIMATED:Int = 4   ; Bit 2: 00000100
Const FLAG_CAN_MOVE:Int = 8   ; Bit 3: 00001000

; Store all flags in one integer (uses only 4 bytes total)
Local objectFlags:Int = 0

; Set flags (turn bits on)
objectFlags = FLAG_VISIBLE BitOr FLAG_SOLID BitOr FLAG_CAN_MOVE
; Result: 00001011 (bits 0, 1, and 3 are on)

; Check if a flag is set
If (objectFlags BitAnd FLAG_VISIBLE) <> 0 Then
    Print "Object is visible"
EndIf

; Toggle a flag
objectFlags = objectFlags BitXor FLAG_ANIMATED  ; Turn on animation
objectFlags = objectFlags BitXor FLAG_ANIMATED  ; Turn off animation

; Clear a specific flag (turn bit off)
objectFlags = objectFlags BitAnd (BitNot FLAG_SOLID)  ; Remove solid flag

; Check multiple flags at once
If (objectFlags BitAnd (FLAG_VISIBLE BitOr FLAG_CAN_MOVE)) <> 0 Then
    Print "Object is visible OR can move"
EndIf

Real-World Example - File Permissions:

; Unix-style file permissions
Const PERM_READ:Int = 4     ; 100 in binary
Const PERM_WRITE:Int = 2    ; 010 in binary
Const PERM_EXECUTE:Int = 1  ; 001 in binary

; Read + Write permission (110 in binary = 6)
Local filePerms:Int = PERM_READ BitOr PERM_WRITE

; Check if file is readable
If (filePerms BitAnd PERM_READ) <> 0 Then
    Print "File is readable"
EndIf


Practical Example: Color Manipulation

RGB colors are often stored as a single 32-bit integer, with each color component occupying 8 bits.

Color Format: $AARRGGBB

Bits 24-31: Alpha (transparency)
Bits 16-23: Red
Bits 8-15:  Green
Bits 0-7:   Blue

Creating a Color from Components:

Function MakeColor:Int(red:Int, green:Int, blue:Int, alpha:Int)
    ; Shift each component into position and combine with OR
    Local color:Int = (alpha Shl 24) BitOr (red Shl 16) BitOr (green Shl 8) BitOr blue
    Return color
EndFunction

; Example: Create opaque red color (255, 0, 0, 255)
Local redColor:Int = MakeColor(255, 0, 0, 255)
Print "Color: $" + Hex$(redColor)  ; Output: Color: $FF0000FF

Extracting Color Components:

Function GetRed:Int(color:Int)
    Return (color Shr 16) BitAnd $FF  ; Shift red to position 0-7, mask other bits
EndFunction

Function GetGreen:Int(color:Int)
    Return (color Shr 8) BitAnd $FF   ; Shift green to position 0-7, mask other bits
EndFunction

Function GetBlue:Int(color:Int)
    Return color BitAnd $FF           ; Blue is already in position 0-7, just mask
EndFunction

Function GetAlpha:Int(color:Int)
    Return (color Shr 24) BitAnd $FF  ; Shift alpha to position 0-7, mask other bits
EndFunction

; Example: Extract components from a color
Local color:Int = $FF8844AA
Print "Red: " + GetRed(color)      ; Output: Red: 136
Print "Green: " + GetGreen(color)  ; Output: Green: 68
Print "Blue: " + GetBlue(color)    ; Output: Blue: 170
Print "Alpha: " + GetAlpha(color)  ; Output: Alpha: 255

Color Manipulation:

; Darken a color by reducing all components
Function DarkenColor:Int(color:Int, amount:Int)
    Local r:Int = Max(0, GetRed(color) - amount)
    Local g:Int = Max(0, GetGreen(color) - amount)
    Local b:Int = Max(0, GetBlue(color) - amount)
    Local a:Int = GetAlpha(color)
    Return MakeColor(r, g, b, a)
EndFunction

; Set alpha (transparency) of a color
Function SetAlpha:Int(color:Int, newAlpha:Int)
    ; Clear alpha bits, then OR in new alpha
    color = color BitAnd $00FFFFFF  ; Clear alpha (keep RGB)
    color = color BitOr (newAlpha Shl 24)
    Return color
EndFunction


Practical Example: Bit Masking for Data Extraction

Bit masking is used to isolate specific bits from a larger value.

Example: Parsing Network Packet Header

; Packet header format (16 bits):
; Bits 0-3:   Protocol version (4 bits)
; Bits 4-7:   Message type (4 bits)
; Bits 8-15:  Sequence number (8 bits)

Function ParsePacketHeader(header:Int)
    ; Extract version (bits 0-3)
    Local version:Int = header BitAnd $000F

    ; Extract message type (bits 4-7)
    Local msgType:Int = (header Shr 4) BitAnd $000F

    ; Extract sequence number (bits 8-15)
    Local seqNum:Int = (header Shr 8) BitAnd $00FF

    Print "Version: " + version
    Print "Message Type: " + msgType
    Print "Sequence: " + seqNum
EndFunction

; Example packet: $4523
; Binary: 0100 0101 0010 0011
;         ^^^^ ^^^^ ^^^^ ^^^^
;          Seq  Seq  Type Ver
ParsePacketHeader($4523)
; Output:
; Version: 3      (0011)
; Message Type: 2 (0010)
; Sequence: 69    (01000101)


Practical Example: Data Packing

You can pack multiple small values into a single integer to save memory.

Example: Packing Player Stats

; Pack 4 values into one 32-bit integer:
; Bits 0-7:   Health (0-255)
; Bits 8-15:  Mana (0-255)
; Bits 16-23: Level (0-255)
; Bits 24-31: Class (0-255)

Function PackPlayerStats:Int(health:Int, mana:Int, level:Int, class:Int)
    Local packed:Int = health BitOr (mana Shl 8) BitOr (level Shl 16) BitOr (class Shl 24)
    Return packed
EndFunction

Function UnpackHealth:Int(packed:Int)
    Return packed BitAnd $FF
EndFunction

Function UnpackMana:Int(packed:Int)
    Return (packed Shr 8) BitAnd $FF
EndFunction

Function UnpackLevel:Int(packed:Int)
    Return (packed Shr 16) BitAnd $FF
EndFunction

Function UnpackClass:Int(packed:Int)
    Return (packed Shr 24) BitAnd $FF
EndFunction

; Example usage
Local stats:Int = PackPlayerStats(100, 50, 12, 3)
Print "Health: " + UnpackHealth(stats)  ; Output: Health: 100
Print "Mana: " + UnpackMana(stats)      ; Output: Mana: 50
Print "Level: " + UnpackLevel(stats)    ; Output: Level: 12
Print "Class: " + UnpackClass(stats)    ; Output: Class: 3


Windows API and Bitwise Flags

The Windows API extensively uses bitwise flags. Understanding this is crucial for BGI and native Windows programming.

Window Styles Example:

; Common Windows API flags (from winuser.h)
Const WS_OVERLAPPED:Int = $00000000
Const WS_CAPTION:Int = $00C00000
Const WS_SYSMENU:Int = $00080000
Const WS_THICKFRAME:Int = $00040000
Const WS_MINIMIZEBOX:Int = $00020000
Const WS_MAXIMIZEBOX:Int = $00010000
Const WS_VISIBLE:Int = $10000000

; Combine multiple styles
Const WS_OVERLAPPEDWINDOW:Int = WS_OVERLAPPED BitOr WS_CAPTION BitOr _
                                WS_SYSMENU BitOr WS_THICKFRAME BitOr _
                                WS_MINIMIZEBOX BitOr WS_MAXIMIZEBOX

; Create window with multiple styles
Local style:Int = WS_OVERLAPPEDWINDOW BitOr WS_VISIBLE

BGI Message Box Example:

; MessageBox button types
Const MB_OK:Int = $00000000
Const MB_OKCANCEL:Int = $00000001
Const MB_YESNO:Int = $00000004

; MessageBox icons
Const MB_ICONINFORMATION:Int = $00000040
Const MB_ICONWARNING:Int = $00000030
Const MB_ICONERROR:Int = $00000010

; Combine button type and icon
Local flags:Int = MB_YESNO BitOr MB_ICONWARNING
Local result:Int = BGI_MessageBox("Save changes?", "Confirm", flags, 0, window)

File Access Modes:

; File access flags
Const FILE_READ:Int = 1
Const FILE_WRITE:Int = 2
Const FILE_CREATE:Int = 4
Const FILE_APPEND:Int = 8

; Open file for reading and writing
Local mode:Int = FILE_READ BitOr FILE_WRITE

; Check if file can be written to
If (mode BitAnd FILE_WRITE) <> 0 Then
    Print "File is writable"
EndIf


Performance Benefits

Bitwise operations are among the fastest operations a CPU can perform because they work directly with the binary representation of data.

Fast Multiplication/Division by Powers of 2:

; Slow (uses CPU multiply instruction)
Local result:Int = value * 16

; Fast (bit shift)
Local result:Int = value Shl 4  ; Same as * 16

; Slow (uses CPU divide instruction)
Local result:Int = value / 8

; Fast (bit shift)
Local result:Int = value Shr 3  ; Same as / 8

Memory Efficiency:

; Store 32 boolean flags in one integer instead of 32 separate variables
Local flags:Int = 0

; Instead of:
Local flag0, flag1, flag2... flag31:Int

When to Use Bitwise Operations:

When NOT to Use Bitwise Operations:



Summary

Bitwise operations are a fundamental tool in a programmer's toolkit. They enable:

While modern high-level programming often abstracts these details away, understanding bitwise operations makes you a more capable programmer and is essential for systems programming, game development, and working with APIs like POSIX and Vulkan.



Related Documentation


BambooBasic © 2026 Michael Denathorn