Repository navigation
Performance and Timing
The PICO9918 has no frame buffer. Core 1 builds each display line during the line before it goes out, so every line carries a hard deadline, and the renderer either finishes inside it or the line is lost. Two configuration options move that balance (the system clock preset and the sprites-per-scanline limit) and one board difference decides what 80-column text can do at all.
This page is about the deadline: what has to fit inside it, what raising the clock buys, what raising the sprite limit costs, and how to tell which side of the line you are on. For how the pipeline is wired, see Display Pipeline; for the host side of the bus, see Host Bus Timing.
VGA output is 640x480 at 60 Hz: 800 pixel times per line at a 25.175 MHz pixel clock, which is 31.8 us of video. Each TMS9918A line is scaled 2x vertically, so the renderer only has to produce a new line every second VGA line:
| Output | Rows | Rendered lines | Vertical scale | Budget per rendered line |
|---|---|---|---|---|
| VGA 640x480@60 | 24 or 30 | 192 or 240 | 2x | 63.6 us |
| VGA 640x480@60 | 48 or 60 | 384 or 480 | 1x | 31.8 us |
| SCART PAL 576i | any | 268 per field | 1x | 64.0 us |
| SCART NTSC 480i | any | 220 per field | 1x | 63.6 us |
The second row is the one that surprises people. Setting R0 bit 3 (double-row mode, the 48- and 60-row displays) stops the vertical doubling and renders every VGA line individually, so the row count doubles and the time available per line halves. Those are one fact stated twice, not two costs multiplying: rendering every VGA line instead of doubling each one is why there are twice as many lines, and why each gets half the period. What a line costs to render does not change. In absolute terms a 60-row screen renders 480 lines where a 30-row screen renders 240 and a 24-row screen renders 192 (src/renderer.c:143-145).
SCART is different again: interlaced output never doubles vertically, so a line always gets a whole field line, and double-row mode takes its extra rows from the two interlaced fields instead of from the budget. See SCART Output Reference.
Inside that window core 1 has to run vrEmuTms9918ScanLine() (the whole composite of bitmap layer, sprites, both tile layers and the backdrop), then expand the resulting palette indices into 12-bit BGR, and service any host bus transaction that arrives while it does. At the 252 MHz default that is about 16,000 CPU cycles for a full-height line and about 8,000 for a doubled-row one.
This is the mechanism behind nearly every "flickering", "missing lines" or "corrupt rows" report, and it is worth understanding exactly.
Core 0's DMA interrupt handler pushes one request per rendered line into the inter-core FIFO. Core 1 pops a request, renders that line, and comes back for the next. If it is still working when the next request arrives, the request queues. When core 1 does come back, it drains everything queued and renders only the newest line: because by then the earlier line's slot has already been clocked out to the display, and drawing it would be drawing into the past.
Important
A scanline that misses its deadline is never drawn late: the next one is dropped instead. There are only two line buffers, indexed by line parity, so a dropped line outputs whatever was last written to its buffer: the same-parity line from two rendered lines earlier, or the same slot from the previous frame. It looks like a stale, repeated or torn row, not a black one, which is exactly why it reads as a rendering bug rather than as a timing problem.
Two consequences follow from that, and both matter when diagnosing a report:
- Going over budget does not drop a line immediately. The lag accumulates. One committed capture has eight consecutive rows that each run a few microseconds past their 63.6 us line (about 27 us of lag between them, still short of a whole line) and it drops nothing. The skip happens only once accumulated lag crosses one full line period, which is why a scene can sit visibly over budget on some rows and produce a perfect picture.
-
Every one of those rows is still finished after the display has started reading it out. The stale sliver is at the left of the picture area, not the right. The left border is DMA-filled at the top of the line (
src/renderer.c:238-239), the expensive part of the render only fills an index buffer, and the RGB line is written left to right in one burst at the very end (src/renderer.c:271onward). So a late render leaves the leftmost pixels unwritten when readout passes them, and it cannot leave the right-hand end short, because once that burst starts it outruns the readout. Tearing on a handful of rows precedes dropped rows.
Neither symptom is counted by anything on the display. The FPS figure on the performance overlay counts frames the host never acknowledged, an unread interrupt, not lines the renderer missed. Renderer overruns are only observable directly through the firmware's live test harness.
The system clock is a saved setting with three presets. Raising it does not change the deadline; it changes how much work fits inside it.
| Preset | VGA | Core voltage | Cycles in a 63.6 us line | Versus preset 0 |
|---|---|---|---|---|
| 0 (default) | 252 MHz | 1.15 V | ~16,000 | - |
| 1 | 302.4 MHz | 1.20 V | ~19,200 | +20% |
| 2 | 352 MHz | 1.30 V | ~22,400 | +40% |
With a SCART AV Dongle attached the firmware switches to a second preset table at boot, chosen so the system clock is an exact multiple of the 13.5 MHz SCART pixel clock: 270 MHz (20 x 13.5), then 324 MHz (24 x 13.5), then 324 MHz again. Presets 1 and 2 are therefore the same clock on SCART, so there is nothing above preset 1 to gain.
252 MHz is the floor the whole design is specified against: the published per-scanline budget assumes it, the firmware's own performance records are all taken at it, and the VGA timing generator refuses any system clock too slow to produce the active mode at all. Raising the preset is therefore pure headroom: the payoff is the dense 80-column, ECM, two-layer, 32-sprite and 48/60-row scenes that do not fit at 252 MHz, and nothing else changes.
Warning
Raising the preset raises the RP2040/RP2350 core voltage as well as the clock. On VGA that is 1.15 V at the default, then 1.20 V, then 1.30 V; on SCART it is 1.15 V then 1.20 V, and preset 2 is the same clock at the same voltage as preset 1 (src/clocks.c:49-57). A clock change takes effect on the next power cycle and reverts by itself on the power cycle after that unless you confirm it in the Configurator, so a preset a particular board will not run cannot lock you out: power-cycle twice and the previous setting returns. Do not treat 352 MHz as a free upgrade. Verify a board at it before relying on it, and record the clock against any timing measurement, since two readings taken at different presets can differ by a third and read as a regression.
A real TMS9918A draws at most four sprites on any one line; the fifth and beyond are simply not drawn, and its number is reported in status register 0. Decades of software is built on that limit: some of it deliberately, cycling sprites between frames to flicker rather than vanish.
The PICO9918 makes the limit a setting. Register 30 holds the number of sprites drawn per line, and its power-on value comes from the Scanline sprites option in the Configurator:
| Configurator option | R30 power-on value | Effect |
|---|---|---|
| 4 (default) | 4 | Original TMS9918A behaviour |
| 8 | 8 | |
| 16 | 16 | |
| 32 | 32 | Every sprite the hardware processes |
Two details make this more useful than it first looks, and both are deliberate:
- It works on unmodified software. R30 governs the sprite loop whether or not the VDP is unlocked, and registers above R7 cannot be written while locked, so the configured value is what a plain TMS9918A game gets, with no patching.
- Reporting is separate from drawing. Status register 0 still flags a fifth sprite on the line by default, even when all 32 are being displayed, because software uses that flag for raster timing. R50 bit 3 switches reporting over to R30 instead. See Sprite Limits for the register-level detail, including that completing the unlock sequence overwrites R30 with 31.
Register 51 is the other half: it caps how many sprites are examined per frame, up to the hardware maximum of 32.
Caution
Raising the limit spends scanline budget directly. Every extra sprite on a line is more work inside the deadline, and 32 magnified 16x16 sprites tiled across a line do not fit 63.6 us at 252 MHz: the firmware's own live-test scene library halves the count for that reason. Raise it on the lines that need it, not as a default, and remember that some titles depend on the four-sprite limit for their intended look.
The PICO9918 v1.x boards use an RP2040 (Cortex-M0+); the PRO v2.x uses an RP2350 (Cortex-M33). Most of the firmware is identical (see RP2040 vs RP2350 Differences) and both boards today emulate the same 64 KB F18A address space, so there is no VRAM difference between them. One display feature does differ, and it differs because of the budget on this page.
An 80-column line is twice as wide as any other mode: 480 visible pixels inside a 512-pixel line. Carrying the tile palette select, ECM levels and the bitmap layer through it needs eight bits per pixel, because four bits have no room left for a palette selector once they are holding the colour. Eight bits doubles the cost of the line in two places at once: the three per-line composite buffers grow from 264 to 520 bytes each, and the palette expansion has 512 indices to look up instead of 256.
The development branch therefore carries two tiers, chosen by board at build time:
| PICO9918 PRO (RP2350) | PICO9918 v1.x (RP2040) | |
|---|---|---|
| 80-column line | 8 bits per pixel, 512 bytes | 4 bits per pixel, 256 bytes |
| T80 render path | the shared enhanced tile path, same as every other mode | a narrower dedicated text path with sprites added on |
| ECM 1-3, bitmap layer, palette select in T80 | yes | no |
| Sprites, both scroll axes, position attributes, tile layer 2, tile layer 1 disable in T80 | yes | yes |
Important
The 8bpp tier is not in any release. PICO9918_TEXT80_8BPP exists only on the development branch (v1.2.0, the newest release, has no such option) so every downloadable build, PRO included, takes the 4bpp path. The table above describes where the firmware is going, not what is on the Releases page today.
The full capability list, including the two features that need R50 bit 1 set on the 4bpp tier, is in Text Modes.
This is a budget decision, not an oversight, and the cost is measurable on both sides. The 4bpp RP2040 tier fits 80 columns at 24, 30, 48 and 60 rows: though t80-60rows is one of the five scenes that start dropping lines the moment the diagnostic panels go on, so the margin at 60 rows is thin rather than generous. The 8bpp tier does not always: on a PRO, an 80-column 60-row screen: a 512-byte line against a 31.8 us budget: is the one configuration in the firmware's whole test suite that drops scanlines at 252 MHz with nothing else running. That is the price of the features, paid by the board that has the cycles for them, and it is a case for clocking up.
Enable the performance overlay (see Diagnostic Overlays) and two numbers matter:
-
RENDER: microseconds spent inside
vrEmuTms9918ScanLine(), averaged per scanline. This is the emulation core alone, and it is comparable between any two scenes and any two boards. - FRAME: milliseconds of per-line work summed across one frame. It includes the palette expansion and everything else the renderer does outside the emulation core. Because it is a sum over the frame's rows, it cannot be compared between scenes with different row counts.
FRAME * 1000 / rows gives the per-line total. Subtract RENDER and the remainder is everything outside the emulation core: a figure that stays near-constant across wildly different content, which makes it a good check that a reading is sane rather than a transcription error.
Three things to know before trusting a number:
- Record the board, the scene and the clock preset with every reading. A figure with none of those attached cannot be compared to anything.
- A comfortable average is not proof. Scenes have been recorded dropping a scanline while their own average sat 8 to 25 us inside budget, because the cost is not evenly distributed across rows. Only the firmware's live test harness can answer "did any line actually get skipped".
- The overlay is not free, and its cost lands partly outside its own measurement. It draws on every scanline, after RENDER's timer has stopped, so RENDER under-reports what the board is really doing while you are watching it.
Note
The overlay setting lives in flash, so it survives a firmware update and stays on until you turn it off. On an RP2040 board, turning all four panels on is enough to push five scenes of the firmware's 111-scene test suite from fitting to dropping lines: every one of which fits with the overlay off. Turn the overlay off before judging performance, and before reporting a display fault.
For repeatable measurement against known content rather than whatever demo is to hand, the firmware repository carries a benchmark ROM: one image with eleven fixed, entirely static scenes for the TI-99/4A, ColecoVision and MSX, stepped with any key, documented so a number can be attributed to specific content. The same two counters can also be read over SWD with no camera and no person, across a whole scene library at once. Both are covered in Firmware Testing.
| Symptom | Most likely cause |
|---|---|
| Rows flickering, or a band of rows that will not settle | Scanlines being skipped: the renderer is over budget on those rows. Turn off the diagnostic overlay first, then raise the clock preset |
| Individual lines showing stale or duplicated content | The same thing. A skipped line reuses its buffer's previous contents; it is not a VRAM or palette fault |
| A row whose left-hand edge is stale while the rest is correct | A marginal line: finished after the display began reading it out, so its leftmost pixels were read before they were written. This precedes dropped lines |
| Trouble that only appears in 48- or 60-row mode | The budget is halved there (R0 bit 3), and there are twice as many lines to fill |
| Trouble that only appears in 80-column mode | The widest line in the firmware. On a PRO it is also the deepest. See the tier table above |
| Sprites missing beyond the fourth on a line | Working as configured. Raise Scanline sprites in the Configurator, or set R30 |
| Sprites missing beyond the limit you configured | R30 is drawn from, not reported from. Check R50 bit 3, and note that unlocking overwrites R30 with 31 |
| Game logic running at half speed, or FPS below the mode's field rate | A host-side miss, not a renderer one: the host is not reading the status register in time to acknowledge each frame interrupt |
| No picture at all after a clock change | Power-cycle twice: an unconfirmed clock preset reverts by itself |
- Troubleshooting: symptom-first index, if you arrived here from a display fault
- Diagnostic Overlays: the performance panel, its fields and how to enable it
- Firmware Testing: the benchmark ROM and the SWD harness that read these counters
- Firmware: the clock preset table and the confirm-or-revert flow for display settings
- Firmware Architecture: the dual-core split, the display pipeline and the DMA channels behind this budget
- Configurator: where the clock preset and the scanline sprite limit are set
- F18A Programmers Reference: R30, R50 and R51, the text modes and double-row mode at register level
- PICO9918 Programmers Reference: the extended 48- and 60-row modes and their scaling
- Host Bus Timing: the other real-time deadline the firmware meets, on the host side
- SCART Output Reference: interlaced field timing, where the budget rules differ
-
Building from Source: build options, including the unreleased 80-column tier flag (
PICO9918_TEXT80_8BPP)
Hardware
Supported Devices
Firmware
- Firmware
- Firmware Version History
- Display Output
- Building from Source
- Firmware Architecture
- Firmware Testing
Configurator
Programming
Reference