01The Canonical Model
uint8 and uint16 are the canonical integer representations. They are mutually coherent: bit replication and truncation between them is lossless, and their float conversions agree:
All other bit depths are defined by their relationship to uint8.
Float is defined as uint8/255 (or equivalently uint16/65535). This is the canonical exit to floating point.
02Bitwise Conversions
Bit replication and truncation are the convention in hardware and in software rendering engines: they are how a uint5 or uint3 asset yields its canonical uint8 values. Formats that specify a conversion of their own are listed in Appendix A.
Bit extension
lower → higher bit depth. Replicate the source bits to fill the destination:
Bit truncation
higher → lower bit depth. Keep the most significant bits:
uint8 → uint3: value >> 5
uint8 → uint4: value >> 4
uint16 → uint8: value >> 8
Integer to float
Extend to canonical depth first, then divide:
uint3 → float: extend to uint8, then /255
uint8 → float: /255
uint16 → float: /65535
Float to integer
Multiply, then truncate to desired depth:
float → uint8: round(clamp(f * 255, 0, 255)) — or dither; truncate bits if needed
Truncation is the inverse of replication. It reads only the top bits, never overflows, and its error is symmetric within one code and unbiased — on the replication lattice it is neither nearest nor floor: 0xFF00 truncates to 0xFF, whose extension 0xFFFF sits almost a full code above the original. Rounding during truncation (adding half before truncating), a naive form of rounding, is incorrect; it rounds on the power-of-two stride, but replicated codes are spaced slightly wider.
Conversion labDetails
03Exact Rounding
When the content is genuinely higher-precision — not a replicated code — reduction is quantization: read the source by its own divisor and round to the destination's, the exact v / (2^n - 1) model in both directions. Between coherent depths that is the nearest code on the replication lattice, and it has a closed form:
(v * 255 + 32767) / 65535= round(v / 257)(v * max_dst + max_src/2) / max_src(v + 128 - ((v + 128) >> 8)) >> 8uint16 → uint8, no multiplyReduction lab: uint16 → uint8Details
No candidate search and no tie-break rule is needed: the max values are odd, so a value is never exactly halfway between two codes.
The same oddness makes exact rounding compose across coherent depths — the refinement factor (2^m−1)/(2^n−1) is odd, so every coarse decision boundary falls halfway between fine lattice points, out of reach of a nearest step: uint16 → uint8 → uint4 equals uint16 → uint4, always.
And like truncation, exact rounding inverts replication, so after one reduction any further sequence of extension and reduction — truncating or exact rounding, even mixed — is the identity.
Truncation is the fastest option with the same round-trip guarantees; exact rounding only tightens the first, lossy step — within half a code of the source instead of within one.
Extension is still replication. Quantized or not, uint8 → uint16 is v * 0x101, never the mid-cell padding (v << 8) | 0x80. Mid padding cannot reach either endpoint (0 becomes 0x0080 and 255 becomes 0xFF80 — full scale is lost), pulls every value toward mid gray by up to half a low code (+128/65535 at 0, −127/65535 at 255, nothing at 128), and puts every value but 128 off-lattice, so it is not a fixed point of the conversions: reducing and re-extending yields v * 0x101, not the value it came from.
v * 0x101
0 → 0x0000
1 → 0x0101
128 → 0x8080
255 → 0xFFFF
(v << 8) | 0x80
0 → 0x0080
1 → 0x0180
128 → 0x8080
255 → 0xFF80
04Coherent Bit Depths
Bit depths that divide evenly into a larger depth are fully coherent: bit replication, truncation, and float conversion all align perfectly.
| Depth | Divides evenly into | ||||
|---|---|---|---|---|---|
| 2 | 4 | 8 | 16 | 32 | |
| uint1 | ✓ | ✓ | ✓ | ✓ | ✓ |
| uint2 | · | ✓ | ✓ | ✓ | ✓ |
| uint4 | · | · | ✓ | ✓ | ✓ |
| uint8 | · | · | · | ✓ | ✓ |
| uint16 | · | · | · | · | ✓ |
For these, bit replication to the larger depth followed by division yields identical float values to direct division by (2^n - 1).
05Non-Divisor Bit Depths
Bit depths that don't divide evenly into 8:
| Depth | Used in |
|---|---|
| uint3 | RGB332 |
| uint5 | RGB565 |
| uint6 | RGB565 |
For these depths:
- Bit replication is the definition. This is what the format means, unless the format specifies otherwise (Appendix A).
- Bit truncation is lossless. It is the inverse of replication.
- Float conversion via /255 is the definition. The value
extend_to_uint8(v) / 255.0is the correct float representation.
Replication is not exact rounding up. Extending by the exact model, round(v * 255 / max), gives the same uint8 codes as replication for uint2 and uint3, so for RGB332 the two agree on every code; for uint5 they differ on 4 codes of 32 and for uint6 on 10 codes of 64, always by one:
| Depth | Codes | Replicated | Exact rounding up |
|---|---|---|---|
| uint5 | 3 · 7 · 24 · 28 | 24 · 57 · 198 · 231 | 25 · 58 · 197 · 230 |
| uint6 | 11 to 15 · 48 to 52 | 44 48 52 56 60 · 195 199 203 207 211 | 45 49 53 57 61 · 194 198 202 206 210 |
The differing codes are those whose exact value lies within a quarter code of a halfway point, with the replicated low bits landing on the other side of it. RGB565 content extended by replication and by exact rounding are therefore different uint8 images; the format defines the first.
06Incorrect Conversions
Dividing by (2^n - 1) directly
// WRONG: uint3 → float as v/7
float f = uint3_value / 7.0;
// CORRECT: extend first
float f = replicate_to_uint8(uint3_value) / 255.0;
The exact interpretation, each bit depth with its own divisor, skips the canonical form: the divisor belongs to the uint8 value the format defines, not to the low-depth code. Formats that define the float at their own depth are the exception: Direct3D's UNORM textures read B5G6R5_UNORM's red as v/31 (Appendix A).
Rounding during truncation
// WRONG: rounding during truncation
uint3 = (uint8 + 16) >> 5;
// CORRECT: truncation
uint3 = uint8 >> 5;
Rounding during truncation targets a lattice that doesn't exist: 0xFE80 is nearest 0xFE (extension 0xFEFE), yet (0xFE80 + 128) >> 8 gives 0xFF, and 0xFFFF overflows. Exact rounding, nearest on the true lattice, is round(v * 255 / 65535) — for quantizing genuine 16-bit content, never for reinterpreting codes. Plain truncation is always more correct than rounding during truncation; where precision matters, exact rounding is the correct way; both keep the round-trip guarantee.
In the conversion lab above, keep uint3 and step through the codes: from code 4 upward, the “Rounding during truncation” result no longer matches, and code 7 overflows the 3-bit range. The reduction lab shows 0xFE80 and 0xFFFF the same way.
Mixing divisors across bit depths
// WRONG: comparing values by their exact divisors
float a = uint3_value / 7.0;
float b = uint5_value / 31.0;
// CORRECT: extend both to canonical depth
float a = replicate_3_to_8(uint3_value) / 255.0;
float b = replicate_5_to_8(uint5_value) / 255.0;
07Why This Works: The Infinite Bit Replication Model
A uint_n value represents an infinitely repeating binary fraction:
This infinite series sums to exactly v / (2^n - 1), which is why the endpoints work:
0.111111... (binary) = 1.0 (exactly, like 0.999... = 1 in decimal)
Bit extension reveals more digits of this infinite pattern. Bit truncation recovers the original pattern. These operations are inverses by construction.
When extending to a non-multiple bit depth (e.g., 3→8), the pattern truncates at a non-aligned boundary. The subsequent /255 then interprets an 8-bit repeating pattern rather than the original 3-bit repeating pattern. This is not an error; it is the definition. The uint8 form is canonical.
08Summary
| Operation | Method | Lossless? |
|---|---|---|
| uint_n → uint8 | bit replication | ✓ lossless (by definition) |
| uint8 → uint_n | bit truncation (>>) | ✓ lossless (inverse of above) |
| uint8 ↔ uint16 | bit replication / truncation | ✓ lossless (8 divides 16) |
| uint8 → float | /255 | ✓ lossless (by definition) |
| uint16 → float | /65535 | ✓ lossless (coherent with uint8) |
| uint3 → float | extend to uint8, then /255 | ✓ lossless (by definition) |
The system is fully coherent when you respect uint8 or uint16 as canonical. The only "incoherence" comes from applying the exact v / (2^n - 1) model directly between two arbitrary bit depths instead of converting to the canonical uint8 form first, and that holds whether a format defines its uint8 form bitwise or by a rounding of its own.
AAppendix A: Canonical Forms by Format
Some formats, DXT and BC7 among them, specify their own conversion for obtaining the uint8 value. Where a format does, that specification is authoritative: the uint8 value it produces is the canonical value, and everything in this article applies from that value onward. Bit replication is what hardware does for low bit depth formats, so outside those specified behaviors it is the correct option. The table lists what each format defines as its canonical value and the operation its specification names for getting there; where a row quotes a specification, the wording is the specification's own, and the sources follow the table.
| Format | Depths | Canonical form | How it is obtained |
|---|---|---|---|
| RGB888, RGBA8888, L8, A8 | 8 | uint8, itself | — |
| RGB16, RGBA16, L16; 16-bit PNG | 16 | uint16, itself | To uint8 by exact rounding, or by truncation as the faster approximation. PNG's decoder guidance calls the linear equation ROUND(input * MAXOUTSAMPLE / MAXINSAMPLE) "the most accurate" and shifting "slightly less accurate" ("discard the low-order byte"); with an sBIT chunk shifting right recovers the original data exactly, "the only case in which shifting might be said to be more accurate than linear scaling". |
| RGB332, RGB565, RGB555, ARGB1555, ARGB4444, L1, L2, L4, A4 | 1 to 6 | uint8 | Bit replication. As an integer image format this is the convention of display hardware and rendering engines; PNG describes it as "left bit replication" and states that it "usually gives the same value as linear scaling and is never off by more than one". A1 replicates to 0x00 or 0xFF. |
| RGB101010A2 | 10 / 10 / 10 / 2 | uint16 for the color, uint8 for the alpha | Bit replication upward (10 → 16: the top six bits repeated); to uint8 as genuine content, by exact rounding or by truncation as the faster approximation. |
| PNG with sBIT | 1 to 16 stored, fewer significant | The stored sample depth | The encoder scales up so that "the high-order S bits of the stored data must agree with the original S-bit data values"; the decoder recovers the original by shifting right. For the scaling itself PNG names the linear equation ROUND(input * MAXOUTSAMPLE / MAXINSAMPLE), which is exact rounding, as "the most accurate", with replication as a close approximation. Zero fill is "distinctly less accurate": it "cannot reproduce white exactly". |
| GPU UNORM textures, any depth (B5G6R5_UNORM, R10G10B10A2_UNORM, R16_UNORM, …) | 1 to 16, 24 for depth buffers | Float at the component's own depth | Ideal rounding. Direct3D: the n-bit value "is converted to float (0.0f, 1.0f, 2.0f, etc.) and then divided by (2n−1)"; back again by c * (2^n - 1) + 0.5 truncated, within 0.6 ULP. UNORM is a float-canonical format: it defines no integer form, so it is outside the rest of this article. |
| BC1, BC2, BC3 endpoints (DXT1 to DXT5) | 5 / 6 / 5 | uint8 | Bit replication: Direct3D's reference decoder promotes each endpoint with promoteToUNORM8, and "Promotion is defined to utilize MSB extension to define the new LSBs". Implementations of BC1 to BC5 "may optionally promote or do round-to-nearest division" within a stated error tolerance; 0.0 and 1.0 must be exact. |
| BC7 endpoints | 4 to 7 per mode, plus P-bit | uint8 | Bit replication, bit-accurate: "left shift endpoint components so that their MSB lies in bit 7", then "Replicate each component's MSB into the LSBs revealed by the left-shift". BC7 decompression "must be bit accurate". |
| ETC1, ETC2 endpoints | 4 (individual), 5 (differential), 6 and 7 (planar) | uint8 | Bit replication: four-bit values "are extended to RGB888 by replicating the four higher order bits in the four lower order bits"; five-bit codewords "by replicating the top three highest order bits to the three lowest order bits"; planar six-bit channels by the top two bits. |
| ASTC endpoints | quantized in bits, trits and quints | uint8 (uint16 for HDR) | For bit-only values, "simple bit replication from the most significant bit of the value". For trit and quint values a procedure of its own, "a set of bit manipulations and adjustments to avoid the expense of full-width multipliers", which "ensures correct scaling": not replication. |
| Limited-range video (BT.601, BT.709, BT.2020, BT.2100) | 8, 10, 12 | The 8-bit code scaled by 2n−8: black at 16·2n−8, white at 235·2n−8 | Shift with zero fill. The nominal range is defined that way in the standards' quantization tables (16 to 235 at 8 bits, 64 to 940 at 10), so the endpoints are not the code extremes and replication would be wrong here. Float is (v − 16·2^(n−8)) / (219·2^(n−8)). |
| Indexed, palettized | index | Not a color depth | The palette entry's format applies. |
Read down the third column, every integer color format that specifies its own expansion specifies bit replication, and the two that do not are the two whose codes do not mean fractions of full scale: limited-range video, where the scale is defined by the shift, and ASTC's trit and quint endpoints, which are not binary fractions at all. PNG is the one specification that ranks the methods, and it ranks them as this article does: linear scaling for precision, replication within one code of it, zero fill wrong at white. UNORM stands apart by defining a float, not an integer.
Sources: PNG Specification 1.2, sections 9.1 "Sample depth scaling" and 10.4 "Sample depth rescaling" (encoders, decoders); Direct3D 11.3 Functional Specification, 3.2.3 "Integer Conversion" and 19.5.2 to 19.5.6 (block compression error tolerance, promotion, BC1); BC7 Format, "Decoding the BC7 Format"; OES_compressed_ETC1_RGB8_texture and the Khronos Data Format Specification, ETC2 and ASTC chapters ("Endpoint Unquantization"); ITU-R BT.709 and BT.2100, digital representation tables.