← Firmware research

On-screen text colours

The fp's display text colour is not a colour value. It is a palette index, one byte wide, and the other three bytes of the word are ignored. All 256 entries are below, read out of the firmware.

0xC0BB1208
Ink — the glyph strokes. Low byte = palette index.
0xC0BB120C
Background — the rest of the glyph cell. Same encoding; the firmware ships it at 0, which is transparent.
white text
mem set 0xC0BB1208 0x00000001

Why it is an index

The OSD sub-layer is 8 bits per pixel through a colour lookup table. The glyph blitter (FUN_c052b620) writes one byte per pixel — the low byte of the colour word for a stroke pixel, the low byte of the background word for the rest. Whatever you put in the upper three bytes never reaches the screen.

The palette itself is a ROM constant at 0xC0B520BC, and it is 256 entries long — four bytes each, A Y U V, with U and V signed offsets from 128. The layer's descriptor advertises only the first thirty, which is why display palette 2 0 prints thirty rows. Everything on this page is read from that table, so it is exact rather than sampled.

What fpSup sets, and what the camera ships

fpSup's AutoRun patches three words before it draws its banner (fp_usb_shell/patches.py, the SCREEN table). Two of them move the text out from under the battery indicator; the third picks the colour.

stock firmwarefpSup
0xC0BB1208 0xFFFFFFFF — index 255, a dark green 0xFFFFF8B2 — index 178, #FF6668
0xC03E4698MOV r8,#0 — y = 0 MOV r8,#16
0xC03E46A0MOV r5,r8 — x follows r8, so 0 MOV r5,#120

So the pink the loading bar is drawn in is a deliberate choice, but index 178 sits in the block below that nothing else documents. Index 1, 3, 4, 5 or 7 would be the conservative pick.

The documented palette — 0 to 29

This is what display palette 2 0 prints. The usable solid colours are 1, 3, 4, 5 and 7; 8–15 are empty, and 17–29 are a green ramp the camera uses to antialias its own green text. RGB is a full-range BT.601 conversion of the AYUV beside it.

idxcolourRGBAYUV
0transparent—0000
1white#FFFFFF25525500
2black#000000255000
3red#FF010325577-42127
4green#00FE00255149-84-106
5blue#0100FE25529127-20
6black, 73%#000000187000
7dark grey#4444442556800
8unset—0000
9unset—0000
10unset—0000
11unset—0000
12unset—0000
13unset—0000
14unset—0000
15unset—0000
16black, 73%#000000186000
17green ramp#27522520764-15-18
18green ramp#306A3021482-19-24
19green ramp#3B813B221100-23-29
20green ramp#469544227116-27-33
21green ramp#4A9F4A232124-28-36
22green ramp#4FAC51236134-30-39
23green ramp#55B754240142-33-41
24green ramp#59BF59243149-34-43
25green ramp#5ECB5E247158-36-46
26green ramp#62D262251164-37-47
27green ramp#66DD67255172-39-50
28green ramp#6EEF6E255186-43-54
29green ramp#75FF74255198-46-58

30 to 160 — a dimming ramp, not empty space

Every entry in this range is pure black (Y, U and V all zero) at a different alpha: 120 populated entries spanning 25 distinct opacities, from 1/255 up to a fully opaque black at index 158. This is what the camera darkens the picture with behind its own readouts. Drawn as ink they are invisible on anything dark, which is what made them look empty the first time round — but as a background index (0xC0BB120C) they are the tidy way to put a readable shadow behind text without covering the picture.

idxalpha
3000%
3100%
3210%
6421%
9642%
12810%
152125%
156166%
158255100%
15913653%
1606827%

Eleven entries in the range are alpha 0 and draw nothing at all: 30, 31, 54, 55, 84, 85, 117, 124, 127, 143 and 144. The three at the top of the range are the heavy ones — 158 is opaque black, 159 is 53%, 160 is 27%.

161 to 255 — the second colour block

The firmware never exposes this range through display palette, but it is in the same ROM table and the hardware CLUT is 256 entries wide, so it works. Alpha is shown where it is not 255 — roughly half of these are semi-transparent, and over a dark picture they land much darker than the swatch suggests.

Where these numbers come from

Straight out of out/MAIN_c0000000.bin at 0xC0B520BC. As a check, all 256 indices were then filled across the whole OSD layer on a camera (display osd 1 0x000000<idx> fills the layer with that index) over an opaque black backdrop, and captured. 245 of 256 agree within four counts. The eleven that do not are the most saturated blues and magentas, where a full-range BT.601 conversion and the camera's own converter part company at the edge of the gamut — the AYUV in the table is the value that is actually true, and the RGB is a preview of it.

Two earlier readings of this were wrong, and both were measurement artefacts. The first pass drew each index as text and sampled the strokes: JPEG chroma subsampling averages a two-pixel stroke with the black behind it, which dragged every colour dark and shifted its hue — index 178 came out #895087 instead of #FF6668. The second pass concluded 30–160 were empty, when they are a black ramp that simply cannot be seen against black. Filling the whole layer removes the first problem, and reading the ROM table removes both.

While you are in here

There is one font and one size: 16×32 cells, 96 glyphs, 1 bpp, with 0 meaning ink and 1 meaning background. The second font object shares the same descriptor, so it is not a second size. The descriptor is in RAM at 0xC37830E8 — glyph width at +2, height at +4, bytes per row at +6, bitmap pointer at +0xC — so a larger font is a matter of uploading a scaled bitmap and repointing it. A 2× version is 24 KB and lands in under a second. The draw rectangle has to grow with it: DrawTextSimpleFont::v1 opens with if (rect.h < glyphHeight) return 0, so a taller font with the stock 32-pixel rectangle draws nothing at all.