aioscilloscope / Notes / October 2026

The density display: every waveform counted

A density display shows how often each pixel was hit, so a glitch in one waveform of a million shows as a faint branch off a bright clock. It can only show the glitch if the waveform that carried it was counted. In NovaOS the CPU counts every waveform the FPGA delivers into a per-pixel hit map, and the GPU fades, grades and draws it. On our DHO924S that is about 58,600 waveforms a second at 20 ns/div, all of them on the screen.

A Rigol DHO924S running NovaOS: a 10 MHz square in the density display with the temperature palette, 57.7k waveforms per second
A 10 MHz square at 20 ns/div · temperature palette, 57.7k waveforms a second, every one counted (DHO924S).

CH1Counting has to be complete

The FPGA records triggered waveforms into its own memory and plays them out in batches over PCIe, by DMA, into a ring of four slots in shared memory. The stack note covers that path. If the screen draws only the batches it gets to between frames, it misses waveforms: measured that way on our DHO924S, 33 to 89 % of the acquired waveforms reached the display, depending on the time base.

So the acquisition engine counts. Every batch it reads goes to worker threads while the acquisition thread arms the next one, and a ring slot is reused only after the workers are done with it. Every waveform is counted by construction. If counting ever fell behind, it would show as a lower waveform rate, and the engine measures the time it spends waiting for the workers so that cost stays visible.

CH2The counting kernel

The hit map is the plot in pixels, about 900 by 430, with a 16-bit saturating count per pixel, stored column by column. Each channel's samples arrive scaled to 7,500 codes a division around mid-scale, whatever the volts per division, so a code's row comes from one integer expression, and the division by a constant compiles to a multiply. The kernel reads the 16-bit codes where the DMA left them, with no copy.

CH3Threads and the scheduler

Four workers run at nice 10, free on all six cores; the first two take any batch and the other two only the large ones, so the slower time bases' small batches are not spread thinner. The UI's render thread runs on one Cortex-A72 and the acquisition thread on the other. The placement was measured: workers confined to the acquisition thread's core cut the rate at 20 ns/div from 28,602 to 20,285 waveforms a second, and confined to the four A53s they fell behind at 20 ns/div.

Three scheduling details decided whether counting costs anything:

Nothing runs while nobody looks: batches are handed to the workers only while a client has taken counts in the last 2 s, and the workers sleep on a condition variable.

CH4The GPU side

Each frame, the UI takes the counts gathered since its last take. Only the band of rows that has hits crosses the socket, which brought the UI's take thread from 30 % to 5 % of a core. The Mali-T860 does the rest in OpenGL ES 3:

Weighted vectors are an option: a waveform that lights L rows in a column adds about 16/L to each, so a flat stretch shows brighter than a fast edge through the same rows, as an analog beam would draw it. The newest trace, the grid and the menus are composited on top. On a busy screen the DHO's panel runs at 56 to 59 frames a second, and with the engine counting the UI draws two to four times as many frames as when it drew the batches' samples itself.

What it gives

Our DHO924S, the built-in generator's 1 MHz square on CH4, 10 kpts, vectors, infinite persistence:

SceneWaveforms a second, four workersTwo workersCounted
20 ns/div, one channel58,57341,328every one
2 µs/div, one channel11,78511,784every one
2 µs/div, four channels3,5463,546every one

The counted share is the engine's count of waveforms counted against waveforms acquired, read a moment apart, so it moves a few tenths of a percent either side of 100. At 2 µs/div counting costs nothing measurable: 10,702 waveforms a second counting against 10,703 with the workers off, in one A/B. At 20 ns/div the workers are the limit; with counting off the engine acquires about 77,800 a second. On our MHO934 the engine counts 50,365 waveforms a second at 20 ns/div and 4 GSa/s, and 13,765 at 2 µs/div. Against Rigol's firmware, counted the same way on the trigger output, see NovaOS vs stock.

Because every waveform is counted, a box drawn on the display can act as a trigger. When the hit density inside the box crosses a level (or, for a pulse that must always pass through, falls under it), NovaOS stops, beeps or keeps the waveform that did it, which it finds again in the ring by testing only the box's columns of the frames still there.

Measured on our own DHO924S and MHO934 in October 2026. The per-pixel hit count with graded intensity and exponential persistence is an old technique whose original patents have expired; this implementation is our own.