-
Notifications
You must be signed in to change notification settings - Fork 0
Performance
English · Deutsch
The rendering budget for anyone adding or changing a screen.
The frame rate on this panel is limited by PSRAM (pseudo-static random-access memory) bandwidth, not by the CPU. The LCD (liquid-crystal display) peripheral scans one framebuffer out of PSRAM continuously at about 30 MB/s, and the framebuffers sit behind a write-back, write-allocate data cache (64 KB, 8-way, 64-byte lines). Every pixel the CPU writes costs a 64-byte line fill and a 64-byte write-back unless the line is already resident. The cost of a frame is therefore a count of cache-line fills, which can be measured exactly on the host.
tools/frame_cost.py builds the real screens for the host and runs them under
cachegrind with the ESP32-S3's cache geometry. It reports the difference
between rendering one frame and eleven, so process start-up and the first paint
of each buffer cancel out.
$ python3 tools/frame_cost.py
panel 39.0 Hz, ~39 MB/s effective -> 976 KiB of traffic per panel frame
mode lines/frame traffic est. ms est. fps
-------------------------------------------------------
frame 8,856 1107 KiB 29.1 19.5
frame-idle 1,274 159 KiB 4.2 39.0
held 4,777 597 KiB 15.7 39.0
sim 9,817 1227 KiB 32.2 19.5
throttle 10,598 1325 KiB 34.8 19.5
chrome 33,106 4138 KiB 108.7 7.8
overview 953 119 KiB 3.1 39.0
servo 15,759 1970 KiB 51.7 13.0
servo-grip 3,026 378 KiB 9.9 39.0
analyser 871 109 KiB 2.9 39.0
logs 937 117 KiB 3.1 39.0
settings 862 108 KiB 2.8 39.0
battery 871 109 KiB 2.9 39.0
balance 865 108 KiB 2.8 39.0
programmer 895 112 KiB 2.9 39.0
balance-sim 2,395 299 KiB 7.9 39.0
settings-sim 2,407 301 KiB 7.9 39.0
battery-sim 2,401 300 KiB 7.9 39.0
analyser-chrome 39,234 4904 KiB 128.8 6.5
logs-chrome 16,061 2008 KiB 52.7 13.0
settings-chrome 23,591 2949 KiB 77.4 9.8
battery-chrome 36,987 4623 KiB 121.4 7.8
balance-chrome 40,685 5086 KiB 133.5 6.5
programmer-chrome 28,480 3560 KiB 93.5 9.8
picker 902 113 KiB 3.0 39.0
picker-chrome 15,883 1985 KiB 52.1 13.0
clear 12,006 1501 KiB 39.4 19.5
vlines 8,160 1020 KiB 26.8 19.5
hlines 0 0 KiB 0.0 39.0
| Mode | What it measures |
|---|---|
frame |
the motor bench on a frame where a telemetry sample lands |
frame-idle |
the motor bench on a frame between samples, nothing touched |
held |
the motor bench between runs: live readouts over a plot that is holding the last one |
sim |
as frame, with the SIMULATION watermark |
throttle |
the motor bench with a finger on the throttle, the drag case |
chrome |
the motor bench with nothing cached, repainted in full |
overview |
the menu, chrome cached |
servo |
the servo screen with the arm redrawn |
servo-grip |
the servo screen with only the grip repainted |
analyser, logs, settings, battery, balance, programmer, picker
|
one steady frame of that screen, chrome cached |
<screen>-sim |
the same screen with the SIMULATION watermark |
<screen>-chrome |
the same screen invalidated on every frame |
clear |
a full-screen clear |
vlines |
seventeen full-height vertical lines |
hlines |
the same pixel count as horizontal lines |
Absolute counts shift by a few fills between machines, because argv and the
environment share the cache with the framebuffer. frame_cost.py --check-doc
therefore checks the table to a tolerance of 1%.
Draw row-major. Seventeen full-height vertical lines cost 8,160 fills; the same pixel count as horizontal lines costs zero, because each line is resident from the previous pixel. The shell is laid out in horizontal bands, and a vertical rule is a deliberate expense.
Cache the chrome. Repainting everything costs about thirty times the steady
state. Every screen keeps a per-framebuffer bitmask of what it has already
painted; that is what the buffer_index argument of render() is for. The
panel alternates between two buffers, so a screen that invalidates only the
buffer being drawn leaves the other one a frame behind, which reads as flicker.
Cache a stencil that does not move. SIMULATION is drawn on every frame
whenever the bench numbers carry LINK_BN_SIMULATED, which includes a
coprocessor answering with simulated numbers. Rotated text scans its rotated
bounding box, which corner to corner is the whole canvas, and rotates and
divides per pixel to write the 3,439 pixels the mark covers, 0.9% of the
canvas. ui_watermark records those points once and writes them thereafter:
144,721 instructions per frame in place of 8,412,078.
A count of fills is not a count of cycles. The table above measures cache-line fills, and the mark costs only 1,586 of them: it is arithmetic per pixel, not traffic. It was 58 times the cost the table implied, and the table could not show it. Where a mode's measured frame time exceeds what its fills predict, count instructions before trusting the estimate.
Give a control that moves every frame its own counter. A drag delivers a
touch per frame. The motor screen's buttons and its throttle shared one
revision, so dragging repainted every control beside it. The throttle now
carries its own, and repaints the readout's box and the slider's painted
region: the throttle mode in the table above measures it against a sample
frame. The flip quantises to whole panel frames, so a drag either lands
inside two of them or waits for a third: 19.5 fps or 13.0. Measured on
hardware at 50.5 ms, the old drag took the third.
Clear what a widget paints, not what it occupies. The slider's thumb and its
shadow stand proud of the track by 5 px above and 7 px below, and the hero
numerals are 32 px tall with 3 px of slant on a 30 px row, so a clear sized
to the track or to the row leaves a thumb at every position the finger passed
and the numerals' feet behind. ui_slider_painted_rect() reports the region
rather than leaving a caller to derive it.
Paint only on frames that have something to paint. Samples arrive at 20 Hz
and the panel refreshes at 39 Hz, so about every other frame has nothing new.
The bench screen keeps the plot's push count and a control revision, each per
framebuffer, and repaints the plot, the readouts and the controls only when the
corresponding counter has moved. each_framebuffer_is_updated_independently in
test_motor pins the per-buffer counters.
The panel moves 976 KiB per frame at 39 Hz. A render costing twice that lands at 19.5 fps, which is one frame per 20 Hz telemetry sample; drawing faster would repaint identical pixels, drawing slower would drop samples. CI (continuous integration) holds each mode to a ceiling:
| Modes | Ceiling (fills) | Catches |
|---|---|---|
frame, sim
|
15,600 | a bench frame that exceeds one telemetry sample |
overview |
2,000 | a chrome-cached screen that has started repainting |
servo |
17,000 | the arm and grip drawing growing |
servo-grip |
4,000 | a breath repainting the whole card |
| the seven per-screen modes | 1,200 | a screen that has started repainting |
the three -sim modes |
2,800 | the watermark growing past a full canvas |
the seven -chrome modes |
45,000 | a full repaint growing |
If a future pane needs more room, the remaining levers in order of bluntness are the plot's height, its width, and clipping the simulation watermark to the region that was repainted.
The ESP32-S3 log line DRAW … WAIT …, printed every 300 frames, is the
on-hardware check: DRAW is the paint time, WAIT is how long the flip blocked. A
healthy frame is mostly WAIT.
rcbench
English
Using the bench
- What this is for
- Building
- Bringing up the link
- First run on hardware
- Screens
- Balancing
- Servo procedures
- Receiver buses
- Safety
- BLHeli_32 parameters
Reference
Deutsch
Den Prüfstand benutzen
- Worum es geht
- Bauen
- Den Link in Betrieb nehmen
- Erster Lauf auf der Hardware
- Bildschirme
- Auswuchten
- Servoverfahren
- Empfängerbusse
- Sicherheit
- BLHeli_32-Parameter
Referenz