Refresh and audio rates on the Retroid Pocket Nova (SM8550)¶
RetroArch defaults to variable refresh from 80 to 120.198154 Hz on the built-in panel when it is the only connected display and no display mode is pinned. It uses the fastest mode for every system and times frames at the core's reported rate; the display driver repeats them to keep the panel within its range. See variable refresh for the launch conditions, rate limits and measurements.
Fixed-refresh modes¶
The mode selection and display-paced rendering described below apply to
RetroArch with variable refresh disabled (global.vrr=0 in
/storage/.config/system/configs/system.cfg). The preferred mode is
119.880120 Hz, twice NTSC's 59.94 Hz. For systems whose native rate is
noticeably different, setsettings.sh (set_ra_refresh_rate) asks for a
panel mode at exactly twice that rate, and RetroArch switches to it when
the game starts.
Where each system's native rate comes from, crystal by crystal, and where an emulator's number differs from the console's, is derived in CONSOLE_CLOCKS.md. Which systems have their mode, and which are still open, is in PortareOS_Modelines.md.
Every mode uses the panel's own timing, 1302 × 998, and changes only the pixel clock (see below). Steam's 120.0004 Hz mode is Android's own 120 Hz mode exactly. Nothing else picks it: RetroArch asks for each system's exact rate, and mpv, ARMSX2, xemu and the launcher take the preferred mode. All of them keep the panel in its 120 Hz class. The modes are defined in the panel driver, projects/PortareOS/devices/SM8550/patches/linux/0105-drm-panel-Add-Retroid-Pocket-Nova-panel.patch.
| Panel mode | Pixel clock | Off the console's rate | Used by |
|---|---|---|---|
| 119.88031 Hz | 155772 kHz | +1.6 ppm | default (launcher, everything else) |
| 119.45473 Hz | 155219 kHz | -2.2 ppm | gambatte, mgba |
| 120.198154 Hz | 156185 kHz | +4.4 ppm, rounded up | snes9x, mesen2; variable refresh runs on it |
| 119.65252 Hz | 155476 kHz | +2.6 ppm | parallel_n64, swanstation, ymir |
| 119.84568 Hz | 155727 kHz | +1.7 ppm | genesis_plus_gx |
| 118.36038 Hz | 153797 kHz | +3.2 ppm | fbneo, for neogeo only |
| 119.20000 Hz | 154888 kHz | +1.7 ppm | neocd |
| 120.00037 Hz | 155928 kHz | +3.1 ppm | gamescope, for Steam |
Why every mode has the panel's own timing¶
The panel's own timing, Android's for both of its rates, is 12 lines of vertical front porch, 2 of sync and 24 of back porch: 998 lines. Every mode here uses it and moves only the pixel clock, so none is exact to the console; all are within 3.2 ppm, which at fixed refresh RetroArch's audio rate control absorbs. Variable refresh runs on the fastest, the SNES mode, and that one is rounded up, +4.4 ppm: its refresh must be no longer than half the fastest content's frame, or that content falls a refresh behind about once an hour.
Two departures from it were tried, and the panel took neither under variable refresh.
The SNES mode was 1302 x 1116 at 174651 kHz: a 142-line back porch, cut to
widen black frame insertion's flip window, and exact to 0.00 ppm. The panel
does not lock to that frame at all. It scans out
of its own memory at about 120.14 Hz, and the frames arriving on the link
land in that memory at a point of its scan that drifts with the difference
between the two rates. The DPU's CRC cannot see it, since every refresh
leaves the controller whole; tools/tear-tap shows it as a bar that tears
on a beat:
| Panel mode | Content, refresh | Tears seen |
|---|---|---|
| 120.198 Hz, vtotal 1116, variable refresh | 59.94 fps, 119.88 Hz | every 3.5 to 4.7 s |
| the same | 60 fps, 120 Hz | every 6.5 to 7.8 s |
| the same | 50 fps, 100 Hz | all the time |
| 119.880 Hz, vtotal 1001, fixed refresh | 59.94 fps | none |
Each interval is one over the gap between the panel's ~120.14 Hz and the refresh: 0.26 Hz, 0.14 Hz, 20 Hz.
Every other mode had 27 lines of back porch, three more than the panel's, so
that 1302 x 1001 would make 119.88 Hz exactly 120000/1001. The panel locks to
that, but holds the lock only while variable refresh stretches a frame by
about 0.3 ms; past that it refreshes on its own and the frame arriving later
tears. Measured with the bar on the 1001-line SNES mode, and through
vrr-probe custom for Android's timing against ours at the same pacing:
| Timing, variable refresh | Content, refresh | Stretch | Tears seen |
|---|---|---|---|
| 1302 x 1001, 156654 kHz | 59.0 fps | +0.16 ms | 1 in 20 s |
| the same | 58.0 fps | +0.30 ms | none in 20 s |
| the same | 56.0 fps | +0.61 ms | 4 in 20 s |
| the same | 54.0 fps | +0.94 ms | 6 in 20 s |
| the same | 50 fps | +1.68 ms | many |
| 1302 x 1001 against 1302 x 998, blind | 50 fps | +1.67 ms | 2 against none |
| 1302 x 998, 155928 kHz, again | 50 fps | +1.67 ms | none in 30 s |
The rolling line seen with BFI on the wide mode, below, has the same shape.
The DPU keeps the rate it is given. Measured with vblank-rate
on the default mode, whose pixel clock the DSI PLL synthesises as
156239991 Hz against the 156240000 asked for: 119.88005 Hz over 20 s
and 119.88009 over 60, -0.50 and -0.17 ppm from the 119.880113 those
timings work out to: the pixel clock over htotal x vtotal. That is the
link's rate; on its own porches the panel follows it, on the wide SNES mode
it did not (above).
Measure against CLOCK_MONOTONIC_RAW, not CLOCK_MONOTONIC. DRM
timestamps vblanks with ktime_get(), which NTP slews; the pixel clock
is not slewed, so on a device running timesyncd the slew is measured as
if it were panel error. Here that was +68 ppm drifting to +70, and the
slew was separately seen swinging between -114 and -142 ppm between
runs - orders of magnitude more than the mode errors this table is cut
to. vblank-rate samples both clocks and reports both rates.
BFI was tested fairly afterwards and does not work here. The first
attempt was not a fair test: setsettings gives BFI a swap interval of 1 so a
frame reaches the display on every refresh, and the timed-present path was
gated on an interval above 1, so BFI fell back to the repeated presents that
patch exists to replace - visibly slowed, frames arriving a refresh late. #427
opened the gate. With it open, and on the wide-blanking mode cut for it, BFI
still flickers and a line still rolls down the panel; more swapchain images
help but do not remove it, at two or at three. That mode is the one the panel
does not lock to (above), so the rolling line may be its own scan; BFI has not
been measured on the panel's own porches.
Measured on the same session, in flips per second against the 120.198 the mode produces: 101.764 with the CRT shader on, 120.075 with it off, 120.195 with three swapchain images. So the band is GPU throughput and swapchain depth, not blanking - a frame that is not ready inverts the light/dark alternation however wide the window is. The 156 lines are not earning their bit clock, and were kept only because nothing measured them costing anything either; #623 measured it.
What the stock Android install says about this panel¶
The device still has its Android partitions, and the panel's qualified
configuration is in dtbo_a, on the node named il97680a amoled panel
without DSC. None of this was known here:
- The panel is an AMOLED. Pixel response is microseconds, so there is no transition smear on a moving edge. What softens a left-to-right scroll is persistence: each SNES frame is held for two refreshes, 16.6 ms, and a tracked edge smears by however far it travels in that time. Black frame insertion is the remedy, and it does not work here - so the softness on a scrolling game is the display being sample-and-hold, and nothing in software reaches it.
- Its own timings are 1302 x 998, at 60 and 120 Hz. This driver used 1001 lines, three more of back porch, until #623 showed what they cost (above).
- The init sequence already matches the vendor's, command for command and
byte for byte, for the first fourteen: the same register pages, the same
0x0d 0x75 0x00 0x00brightness, the same 120 ms and 20 ms waits, and the same reset pulse once the active-low flag is accounted for.
The vendor sends three more after the display is on - page 0x22, register
0xe1 = 0x01, back to page 0x00 - which this driver does not. Whoever
transcribed the other fourteen exactly had those in front of them and stopped,
so the omission is more likely a decision than an oversight, and the reasoning
did not survive: the driver reached this tree with the ROCKNIX rename. What
0xe1 does is undocumented, and a register written once after display-on in a
vendor page is the shape of a compensation setting. That is the argument
against sending it speculatively rather than for it - getting it wrong degrades
the image subtly and attributes itself to nothing.
Why video mode, when the vendor uses command mode¶
That node drives the panel in command mode, with a hardware TE pin. This driver uses video mode, and that is deliberate.
A CRT has no memory: the host drives H and V sync and the beam paints what arrives, when it arrives. Video-mode DSI is the same contract - the DPU owns the timing, the panel holds no framebuffer, pixels stream. Command mode is the other arrangement: the panel keeps its own GRAM, refreshes itself at its own rate, and the host writes into it when TE allows.
Every mode in the table above exists because of that difference. The timings are ours to choose, so each console gets a panel mode at exactly twice its frame rate; in command mode the panel self-refreshes at a rate it decides and there is no modeline to cut. The strategy does not survive the switch.
It is also worse on both things this device is tuned for. TE gating puts a
variable wait between a finished frame and the panel showing it, where video
mode scans out whatever was latched at vblank. And writing into a buffer that
is being scanned out is the one arrangement that tears for real, which
tear-test shows does not happen here at all.
What command mode buys is panel self-refresh while nothing moves, a power saving on a static screen and the opposite of this workload. Considered and declined.
Fixed-refresh pacing: how a frame lands on a frame¶
A mode at twice the console's rate is only half of it. The other half is RetroArch holding the core to that mode, one frame for every two refreshes, with nothing else setting the pace.
RetroArch reaches the panel through Vulkan's VK_KHR_display, with no compositor. Its swap interval is left on automatic, which resolves to 2 when the mode is within the audio timing skew of twice the core's rate; that is what the modes are chosen for. Vulkan has no swap interval, so stock RetroArch keeps one by presenting the same frame twice into the FIFO swapchain. With two swapchain images, which the image uses for latency, the second present's acquire returns at the vblank between the two, and the core has one refresh period, 8.36 ms, to produce the next frame. A frame that takes longer costs a whole refresh. SwanStation at 4x with a CRT shader missed often enough for 55.65 fps against 59.83, with the audio buffer running dry every 50 to 100 ms and 5 to 8 ms gaps in the sound 7.5% of the time. That was the "slowed down" audio after threaded video was turned off.
Patch 0014-vulkan-khr-display-timed-presents.patch presents each frame once instead, with a desired present time through VK_GOOGLE_display_timing: two refresh periods after the last present the driver has timed, counted per present issued since it. Mesa's KMS backend flips on exactly that vblank. The acquire then returns the moment the previous frame reaches the display, the core has the whole frame period, and the loop runs at the mode's rate, which is the console's. The last present's actual time is the anchor, so a late frame moves the cadence by a refresh and the next one is placed from where the display is, not from a schedule that has drifted. The repeat stays for contexts that cannot time a present. Two things in RetroArch had to be fixed for the extension to be usable at all: its entry points were only loaded on Windows, and a device negotiated by a hardware core, which is how SwanStation and the other Vulkan cores work, never had the extension enabled or recorded.
Measured on Tekken 3, threaded video off:
| Presents | Core fps | Audio gaps in 15 s |
|---|---|---|
| The same frame twice | 55.65 | 161 gaps of 5 to 8 ms, 7.5% of the time |
| One, timed | 59.64 | none |
The dropped-frame count was about ten in every run, all in the first seconds, while the core loads and the pipelines compile.
Threaded video is off (video_threaded = "false"). With it on, a worker thread presents whatever frame is latest and the core is paced by the audio sink instead: full speed and clean audio, but a thread hop of latency, frames reaching the panel unevenly, the core drifting against the panel by the difference of two clocks, and nothing for the automatic frame delay to measure. The exact modes buy nothing in that arrangement. It was the stopgap (#337) between finding the cause and fixing the driver (#339). A core that cannot make a frame inside 16.7 ms can still have it on in its own override file, config/<core>/<core>.cfg; better to find out why it is that slow.
With the core paced by the panel, audio rate control only absorbs the residual between the mode and the panel's crystal, a few parts per million, rather than the difference between the audio and video clocks. Where the mode is not exactly twice the core's rate, the arcade boards for instance, the skew is larger and rate control bends the pitch accordingly, as on any other device.
To see it working: the log has Timed presents: swap interval 2, one present a frame, 16.683 ms apart. when the path is in effect. A hardware-rendered core that creates its own Vulkan device has to enable VK_GOOGLE_display_timing for this, because RetroArch cannot add it afterwards. Flycast ignored the list it was given until its patch 002, and ran on the repeated presents. A core that still does gets VK_GOOGLE_display_timing: not enabled on the device, presents are not timed. in the log and falls back. RetroArch's statistics overlay (statistics_show) reports the core's frame rate, the loop deviation and the audio buffer's underruns. vblank-rate measures the panel's real rate without DRM master, from a queued DRM vblank event; /sys/kernel/debug/dri/0/crtc-0/status also counts vblanks but its counter only advances while the vblank IRQ is enabled, so it cannot be sampled against wall clock - an idle panel reads about 27 Hz on a 120 Hz mode. A capture of the PipeWire sink (pw-record --target <sink> -P '{ stream.capture.sink = true }') shows audio gaps as runs of zero samples.
Systems and fixed-refresh modes¶
The rates are the original NTSC hardware's frame rate, or for handhelds the hardware's own rate.
Where an emulator runs at a different rate from the hardware, the table says so. On the N64, the hardware rate follows from the video chip's 48.681812 MHz clock: a 3094-clock line and a 263-line frame in 240p, or 262.5 lines in 480i. ParaLLEl N64 as shipped upstream does not emulate the line length: it derives the frame period from a nominal 60 Hz and the frame height the game sets, so it runs at about 60.02 Hz. PortareOS patches it (003-vi-frame-period-from-h-sync.patch) to take the period from the V_SYNC and H_SYNC registers, which gives the console's 59.826 Hz, and matches a panel mode to it.
The PlayStation's line is 3412.5 GPU clocks, the broadcast line. SwanStation rounds it to 3413 and so runs at 59.8173 Hz; PortareOS patches it (001-ntsc-line-is-3412-5-ticks.patch) to alternate 3413 and 3412 like the console, which gives 59.826 Hz, the same as the N64, so the two share a mode. SwanStation keeps 263 lines in 480i as well, where the console has 262.5, so a 480i game runs at 59.826 rather than the console's 59.94: 0.19 % slow, the same compromise as the N64's.
| Systems (default emulator) | Native rate (Hz) | Panel mode (Hz) |
|---|---|---|
| gb, gbh, gbc, gbch (Gambatte) | 59.7275 | 119.455 |
| gba, gbah, gbav (mGBA) | 59.7275 | 119.455 |
| snes, snesh, sfc, satellaview, snesmsu1 (snes9x) | 60.0988 | 120.198 |
| nes, famicom, fds (MesenCE) | 60.0988 | 120.198 |
| psx (SwanStation) | 59.826 (480i too, see above) | 119.652 |
| saturn (Ymir) | 59.826 (455 x 4 master clocks a line, 263 lines; the core reports it exactly with our patch) | 119.652 |
| mastersystem, sg-1000, gamegear, ggh (Genesis Plus GX) | 59.9227 | 119.846 |
| megadrive, megadrive-japan, megadriveh, genesis, genh (Genesis Plus GX) | 59.9227 | 119.846 |
| segacd, megacd (Genesis Plus GX) | 59.9227 | 119.846 |
| sega32x (PicoDrive) | 59.9227 (the emulator reports 60) | 119.880 |
| n64, n64dd (ParaLLEl N64) | 59.826 in 240p, 59.94 in 480i | 119.652 |
| neogeo (FBNeo) | 59.1856 (the emulator keeps hundredths: 59.18) | 118.360 |
| neocd (NeoCD) | 59.5999 (the CD's 24.168 MHz crystal) | 119.200 |
| arcade (FBNeo) | depends on the game's board (about 54–61) | 119.880 |
| dreamcast, naomi, atomiswave (Flycast) | 59.94 | 119.880 (exactly 2×) |
| gamecube, triforce, wii, wiiware (Dolphin) | 59.94 | 119.880 (exactly 2×) |
| ps2 (ARMSX2) | 59.94 | 119.880 (exactly 2×) |
| xbox (xemu) | 59.94 | 119.880 (exactly 2×) |
| psp, pspminis (PPSSPP) | 59.94 | 119.880 (exactly 2×) |
| scummvm, steam, ports | no fixed rate (PC games) | 119.880 |
| movies (mpv) | the video's frame rate (23.976, 25, 29.97 …) | 119.880; mpv syncs video to audio |
| music, tools, imageviewer | – | 119.880 |
Audio¶
The Nova's speaker and headphone link runs at 48 kHz, 44.1 kHz or 32 kHz. Three things had to allow that: the AudioReach topology blob, which caps the playback PCMs at 48 kHz until extra-firmware's tplg-playback-rates.py widens it; kernel patches 1052–1055, which open the DSP ports and the machine driver's fixup to 44.1 and 32 kHz; and the I2S bit clock, which follows the stream (1054). PipeWire allows all three rates (default.clock.allowed-rates = [ 48000 44100 32000 ]). AUDIO_SAMPLE_RATES.md describes the whole procedure, for carrying it to another distribution. No rate is a resampling stage in itself: the link switches to the rate of the stream that opens it.
RetroArch picks the link rate from the core's (audio_out_rate = "0", RetroArch patch 0015): the smallest of 32, 44.1 and 48 kHz that the core's rate divides into within 0.5 % (32,040 → 32,000; 22,050 → 44,100; 44,100 → 44,100), else the smallest above it so nothing is cut (32,768 → 44,100), else 48 kHz (65,536 → 48,000). The pick is made every time the audio driver initialises, so a core that changes its rate through SET_SYSTEM_AV_INFO, as ParaLLEl N64 does once the game has programmed its DAC, reopens the device at the new game's rate. A non-zero audio_out_rate in a per-core config (config/<core>/<core>.cfg) still overrides the pick; the shipped ones for SwanStation, Flycast, PPSSPP, NeoCD, Genesis Plus GX, PicoDrive (44.1 kHz) and Snes9x (32 kHz, 44.1 kHz for snesmsu1) name the rate the pick would make anyway. RetroArch resamples each core's audio to that rate with its sinc resampler, and dynamic rate control keeps the stream in step with the display. So every core is resampled at least a little, even one whose rate matches the output, because rate control adjusts the ratio by up to 0.5 %. mpv plays a file at its own rate.
The first rate column is the console's own: the rate its sound hardware produces samples at, or "analog" where the chip's channels are mixed as analog signals and there is no sample rate to speak of. The second is what the emulator hands RetroArch, taken from its source code at the pinned commit with PortareOS's options.
| Systems (default emulator) | Console's audio rate (Hz) | Emulator outputs (Hz) | Played at (Hz) |
|---|---|---|---|
| gb, gbh, gbc, gbch (Gambatte) | analog (the APU runs at 1,048,576) | 32,768 | 48,000 |
| gba, gbah, gbav (mGBA) | 32,768 by default; a game can pick up to 262,144 | 65,536 | 48,000 |
| snes, snesh, sfc, satellaview (snes9x) | 32,000 nominal (about 32,040 on real consoles) | 32,040 (the core's own) | 32,000 |
| snesmsu1 (snes9x) | 32,000, plus the MSU-1's 44,100 | 44,100 with MSU-1 enhanced audio, the core's default | 44,100 |
| nes, famicom, fds (MesenCE) | analog (the APU's channels are mixed as analog signals) | 32,000 (mixed at the APU clock, lowpassed and resampled; our patch) | 32,000 |
| psx (SwanStation) | 44,100 | 44,100 | 44,100 |
| saturn (Ymir) | 44,100 (SCSP) | 44,100 | 44,100 |
| mastersystem, sg-1000, gamegear, ggh (Genesis Plus GX) | analog (SN76489, 223,722 per channel step) | 44,100 | 44,100 |
| megadrive, megadrive-japan, megadriveh, genesis, genh (Genesis Plus GX) | 53,267 (YM2612), plus the SN76489 | 44,100 | 44,100 |
| segacd, megacd (Genesis Plus GX) | as the Mega Drive, plus 32,552 (RF5C164 PCM) and 44,100 (CD audio) | 44,100 | 44,100 |
| sega32x (PicoDrive) | as the Mega Drive, plus the 32X's PWM at a rate the game sets | 44,100 (native would give 53,267) |
44,100 |
| n64, n64dd (ParaLLEl N64) | set by the game (commonly 22,050 to 44,100) | the game's rate, exactly, e.g. 22,037.94; 32,040 until the game sets one | the link rate picked for the game's: 32,000 for ~32 kHz games, 44,100 for ~22 kHz and 44.1 kHz ones |
| neogeo (FBNeo) | 55,555 (YM2610) | about 48,000 (47,990 at 59.18 fps) | 48,000 |
| neocd (NeoCD) | 55,555 (YM2610), plus 44,100 (CD audio) | 44,100 | 44,100 |
| arcade (FBNeo) | depends on the board | about 48,000, depending on the game's frame rate | 48,000 |
| dreamcast, naomi, atomiswave (Flycast) | 44,100 (AICA) | 44,100 | 44,100 |
| gamecube, triforce (Dolphin) | 32,029 (DSP), 48,000 (streamed disc audio) | 48,000 (asks RetroArch for its rate) | 48,000 |
| wii, wiiware (Dolphin) | 32,000 (DSP), 48,000 (streamed disc audio) | 48,000 (asks RetroArch for its rate) | 48,000 |
| ps2 (ARMSX2) | 48,000 (SPU2) | 48,000 (44,100 in PS1 mode) | 48,000 |
| xbox (xemu) | 48,000 (AC'97) | 48,000 | 48,000 |
| psp, pspminis (PPSSPP) | 44,100 | 44,100 | 44,100 |
| movies, music (mpv, PORTAMP) | the file's rate | the file's rate | the file's rate |
In short:
- No resampling needed: the 44.1 kHz cores play at 44.1 kHz: PS1, Saturn, Dreamcast, NAOMI, Atomiswave, PSP, NeoCD and every other Sega system. Xbox, PS2 and Dolphin already produce 48 kHz, and the NES 32 kHz.
- Nearly native: the SNES plays at 32 kHz. The 32,040 → 32,000 conversion (0.125 %) is smaller than what rate control adjusts anyway.
- Resampled as on any other device: Game Boy, GBA and Neo Geo. Their rates match none of the link's rates, so they need resampling either way. The N64 is per game: ~32 kHz games get the 32 kHz link, ~22 kHz games an exact 2:1 into 44.1 kHz, 44.1 kHz games play native.
Adding a mode¶
- Find the system's exact native rate
f, preferably from the rate the emulator logs at startup. - Compute the pixel clock
round(2 × f × 1302 × 998 / 1000)in kHz, add the mode to the driver patch next to the others, and update the hunk's line count. - Add the emulator and the rate
2 × ftoset_ra_refresh_rateinsetsettings.sh. RetroArch accepts a mode within 1 Hz of the requested rate, andsetsettings.shonly asks for a mode that the panel lists within 0.002 Hz. - Test the mode on the device:
modetest -clists it, and the picture stays stable while a game runs.
Open work is tracked in #284.