Seventh in a series on building a firmware-free VideoBIOS for RISC-V. In Part 6 a cold GK208, driven only by my own modeset, put out a live 720×400 analog signal the monitor recognized — but the picture it scanned held nothing I had put there. This post is about crossing that last gap: making the card show pixels I chose, and then making text with no text mode to lean on.
The story so far. With the x86 firmware gone, my own EVO modeset locked the pixel clock and the monitor read the exact mode back to me. The display pipe was live. What it presented was not — the head was scanning a surface I did not control.
An empty raster
A locked clock gives you timing, not a picture. The head was scanning, the sync was clean, and what reached the glass was whatever happened to sit in the surface the head pointed at — not a framebuffer I had filled. To present pixels I choose, the base channel has to show a real surface out of VRAM with an output LUT in the path, and that is where First Light stopped.
My first instinct was to add it on top of what already worked: keep the modeset that locked the clock, and poke in the base surface and the LUT after the fact.
INVALID_STATE
The display engine refused. The atomic update that added the surface and the LUT came
back with an error I would stare at for days — 0x6101f0 reading
0x5080, a type-5 INVALID_STATE — and it refused every variation of the
same idea. The reason turned out to be a property of the hardware, not a wrong value of
mine: you cannot extend a head that has no coherent surface state by poking new pieces
into it. The engine takes exactly one coherent initialization, and half a modeset plus
later additions is not a state it will accept.
That is the same shape of wall as Part 4, and the same lesson in a new place: the engine advances when the state is real and whole, and rejects it when it is assembled in pieces.
One commit
So I threw out the bolt-on and built the modeset the way nouveau builds it — as
a single atomic commit. Everything the head needs goes in together: mode timing, the core
backing surface, the output LUT, the usage bounds, the DAC, procamp and dither and the
viewport, all pushed into the 907D core channel; the real framebuffer, at VRAM
0x02000000, pushed into the 907C base channel; and the whole assembly made
to take at once by one interlocked UPDATE — the core committing interlocked with
the base, the base interlocked with the core, so neither lands without the other. Pushed
as one consistent state, the engine accepted it. The wall that had stood for a very long
time was gone in the time it takes to issue two methods.
First pixel
On the 25th of June 2026 I painted the letters GK208 into the framebuffer
and they stood on the monitor — white glyphs on black, cold card, no nouveau
anywhere, no firmware underneath. After First Light, where the monitor knew the mode but
showed nothing of mine, this was the first time the screen carried something I had put
there on purpose — five characters, and I sat looking at them.
The one register with colour in it
For a while everything I drew was some shade of gray, and I did not question it, because everything I drew was white on black anyway. When I finally pushed a colour through the pipe, the head handed me gray. The saturation had been zero the whole time — Kepler's procamp block wants a specific neutral value, and zero is not it:
push_mthd(0x0498, 1);
push_data(0x00040000); /* SAT_COS = 0x400 (unity). Writing 0 here
zeroes saturation and the head outputs GRAYSCALE. */
One method, one value — 0x400, unity saturation — and the
colour I was already sending arrived as colour. It was the one bit of Part 4 again, this
time a single register: nothing downstream was wrong, one field upstream was neutralizing
everything.
A character generator, not a mode
Here is what a cold Kepler will not give you: a text mode. The legacy VGA text path
lights nothing on this card, which Part 6 settled. So text is not a mode you select
— it is glyphs you draw, one bitmap at a time, into the framebuffer you now control.
The first GK208 was exactly that, done on the CPU: walk the string, look up
each glyph in the font, set the pixels.
That prints a logo. It is not a controller. A controller cannot spend a CPU drawing every character of every repaint, so the drawing has to move onto the card's own engines — and on these Kepler parts those engines run on nouveau's open microcode, with no signed firmware blob to load, so I could bring them up firmware-free. To put five letters on a screen I had ended up bringing a 2D engine and then a compute pipeline to life on a cold GPU, a stretch of yak-shaving I can only defend by where it led. It led to a character generator: a compute kernel of my own, hand-written and compiled to the card's native instruction set, that reads a grid of cells and a font and writes the glyph pixels itself. Each cell is one word:
[31:16] symbol (UCS-2) [15] blink [14:8] bg [7:0] fg
The screen becomes a grid of those cells, and turning the grid into pixels is the kernel's work, not the CPU's.
It was never a demo
Once text is a cell array that the card's own engine turns into pixels, the program I had been writing quietly changed into something else. It was not a thing that draws a logo and holds it. It was a video controller — a text surface an operating system could write into and repaint, the way a PC's software has always driven the text console it was handed. I had set out to give my computer a voice at power-on and built, without quite naming it, the controller that voice would speak through. Handing that controller to an operating system — the ABI, the doorbell, the exit — is the last post.
Next: The Datasheet — Handing a Live Controller to an Operating System.
No comments:
Post a Comment