Files
MobileGL/docs/Performance/glmark2-baseline.md
T

32 KiB

glmark2 baseline: MobileGL (Espryt, Magma) vs the stock kgsl driver stack on anland

Measured 2026-10-04 on the anland Plasma desktop. What was optimized since, the current numbers and the remaining work: glmark2-handoff.md. This is the phase-1 baseline for the performance work: approximate numbers, how they were taken, where MobileGL loses, and a ranked list of general fixes. The goal was "where does MobileGL fall short", not lab-grade accuracy, so the scores are approximate (1-3 valid runs per configuration, see n in the tables); the profiling evidence is the load-bearing part.

Headline (pinned clocks, glmark2 score; ratio to stock over the scenes all three stacks ran):

stock Espryt Magma
es2, 800x600 window ~1076 ~1319 (1.19x) ~778 (0.69x)
es2, fullscreen 2560x1600 ~1177 ~572 (0.47x) ~354 (0.29x)
desktop GL, 800x600 window ~1161 ~1283 (1.12x) ~743 (0.64x)
desktop GL, fullscreen ~1152 ~559 (0.49x) ~353 (0.31x)

The windowed averages hide two stories. On light scenes (one or two draws per frame) Espryt is already 1.2-1.5x the stock stack and Magma 0.7-0.9x. Every scene with many draws, client-side vertex arrays or many render-target switches, and everything at full screen, falls far behind:

es2 window, FPS stock Espryt Magma
desktop (blur) 976 275 (0.28) 157 (0.16)
desktop (shadow) 1044 234 (0.22) 125 (0.12)
ideas 688 138 (0.20) 66 (0.10)
terrain 443 149 (0.34) 115 (0.26)
build use-vbo=false 1095 467 (0.43) 472 (0.43)
refract 838 981 (1.17) 331 (0.39)
texture, fullscreen ~1430 ~726 (0.51) ~445 (0.31)

Stacks and environment

stock MobileGL
Android app com.anland.consumer 5.22-367a523 (the original anland) com.anland.consumer.mobilegl 5.22-yuv-video-decode (anland legacy-mobilegl-unified)
Container arch-kde arch-kde-mgl (a clone of it, plus /opt/mobilegl)
Display daemon the anland module's, /data/local/tmp/display_daemon.sock the app's, /data/local/tmp/anland-mobilegl/display.sock
Compositor /usr/bin/kwin_wayland 6.7.4 (anland producer patch) /opt/mobilegl/kwin/bin/kwin_wayland 6.7.4 (anland backend, renders through the server)
Client GL the stock kgsl driver stack: Mesa 26.3.0 freedreno over kgsl (MESA_LOADER_DRIVER_OVERRIDE=kgsl, GALLIUM_DRIVER=kgsl, FD_FORCE_KGSL=1 from /etc/environment); glvnd vendor 50_mesa.json only /opt/mobilegl/lib/libMobileGL.so via glvnd 10_mobilegl.json (sorts before 50_mesa.json); MOBILEGL_TRANSPORT=spawn, MOBILEGL_IPC_DATA=shm; run-ahead armed, present credit 1
Server - the app's :mobilegl service; Espryt = DirectGLES, Magma = DirectVulkan (app setting)
Window presentation Mesa's Wayland EGL, its own dma-bufs 3 server-allocated AHB shared images per window over zwp_linux_dmabuf_v1 (client log: presented through linux-dmabuf shared images (ABGR8888))
  • Both containers run byte-identical glmark2 binaries (glmark2 2023.01-2: md5 048dcd34... glmark2-es2-wayland, fde07ceb... glmark2-wayland in both rootfs images) and the same Mesa package; only the GL stack under the client changes.
  • MobileGL server in the APK: built from 18a5dba0, code-identical to 3a289222 (the only commit between them is a doc). Client deployed from the same tree.
  • One desktop owns the display at a time. stock -> MobileGL: am force-stop com.anland.consumer, droidspaces -C /data/local/Droidspaces/Containers/arch-kde/container.config stop, sh /data/local/tmp/anl/switch.sh DirectGLES|DirectVulkan (after a server crash switch.sh does not restart a dead :mobilegl: send the STOP intent, force-stop the app, open it). MobileGL -> stock: STOP intent + force-stop com.anland.consumer.mobilegl, droidspaces -C .../arch-kde/container.config start, monkey -p com.anland.consumer -c android.intent.category.LAUNCHER 1.
  • The MobileGL app shows its extra-keys bar by default (extra_keys_mode unset = always), which shrinks the desktop to about 2560x1410; the original app has with_keyboard. For equal fullscreen sizes the MobileGL app got extra_keys_mode=with_keyboard in shared_prefs/anland_settings.xml for the measurements. Fullscreen = 2560x1600; the tablet was turned to portrait for the late stock runs (1600x2560, same pixel count; the stock app follows auto-rotate).
  • The MobileGL session restores a System Settings window at login; it was closed before each batch.

Device state and clocks

Lenovo TB321FU (Snapdragon 8 Gen 3, Adreno 750), Android 15, AC power (battery 98-100%), screen on, auto brightness, Plasma idle, no Chrome. All numbers are at pinned clocks (the user allowed it; originals restored afterwards):

original pinned
cpufreq policy0 (cpu0-1) walt 902400-2265600 min = max = 1804800
policy2 (cpu2-4) walt 614400-3148800 min = max = 2246400
policy5 (cpu5-6) walt 499200-2956800 min = max = 2246400
policy7 (cpu7) walt 672000-3302400 min = max = 2246400
kgsl-3d0 devfreq msm-adreno-tz, 231-903 MHz min_freq = max_freq = 680000000
C=/sys/devices/system/cpu/cpufreq; G=/sys/class/kgsl/kgsl-3d0/devfreq
for e in policy0:1804800 policy2:2246400 policy5:2246400 policy7:2246400; do
  p=${e%%:*}; f=${e##*:}
  echo $f > $C/$p/scaling_max_freq; echo $f > $C/$p/scaling_min_freq; echo $f > $C/$p/scaling_max_freq
done
echo 680000000 > $G/max_freq; echo 680000000 > $G/min_freq; echo 680000000 > $G/max_freq

kgsl idle collapse was left on (idle_timer 80, force_* 0); sampled cur_freq read 680 MHz in every run, the CPU clocks held, CPU/GPU zones stayed at 40-62 C, nothing throttled. Pinning is required: unpinned, the stock stack scored 425 (es2 window) with the GPU at 310 MHz and the big cores at 672 MHz, every scene stuck at 330-440 fps; the stacks' different load patterns drive DVFS differently, so unpinned numbers do not compare.

Method

  • glmark2 default scene list with -b :duration=5 (5 s per scene; same list). Configurations: glmark2-es2-wayland and glmark2-wayland, each 800x600 windowed and --fullscreen.
  • MobileGL runs: MGLOG=/tmp/pb-<tag>.log mgrun offscreen bash -c '<glmark2 command>' (as root in the container; mgrun is the anland-mobilegl-plasma skill's). Stock runs: sudo -u swung0x48 bash -c 'set -a; . /etc/environment; set +a; export XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-0; <glmark2 command>'.
  • Runs interleaved across stacks; before each run all CPU/GPU zones below 46 C (min 45 s wait).
  • Swap interval: glmark2 sets interval 0 on Wayland (--swap-mode default). Neither stack throttles the client: Mesa's interval 0 ignores frame callbacks; MobileGL's FrameThrottle waits only after a callback has been outstanding 200 ms. Both compositors repaint at ~125 Hz on the 165 Hz panel.
  • Frames shown: two screenshots 1 s apart 25 s into each run must differ, and SurfaceFlinger must latch buffers for the anland SurfaceView (64-126 per second during runs): dumpsys SurfaceFlinger --latency "<SurfaceView[com.anland.consumer...](BLAST)#N>", counting present times within the last second. The stock desktop intermittently turns black mid-run (a pure-black 21707-byte screenshot) while SurfaceFlinger keeps latching ~125 buffers/s, and its score then jumps to ~1420-1500; a powerdevil inhibit (kde-inhibit --power --screenSaver + kscreen-doctor --dpms on) did not stop it, restarting the stock container + app clears it. Stock runs were therefore probed every 15 s and every run with a black probe was discarded (3 of 8, plus a first pinned round with a black screen throughout), so stock n is 1-2 per configuration.
  • Overall glmark2 score = mean scene FPS of the scenes that ran. MobileGL does not run the two buffer ... update-method=map scenes (GL_OES_mapbuffer is not in its ES extension string), whose stock FPS is low (330-408), so its raw score is flattered; the "common scenes" ratio is the fair one.
  • --off-screen is not comparable: on MobileGL glFinish does not wait for the GPU (C1 below), so glmark2-wayland --off-screen reported 6846 (Espryt) and 12117 (Magma) against 791 on stock, and Magma's server crashed in it (C2); glmark2-es2-wayland --off-screen does not start on MobileGL ("Could not initialize canvas": glmark2 picks GL_RGBA4 and no depth because GL_OES_rgb8_rgba8 and GL_OES_depth24 are not advertised).

Profiling method

Single scenes, 18-30 s (-b <scene>:duration=18), after 7-9 s of warm-up: simpleperf record -e cpu-clock -f 4000 -p <client>,<server>,<kwin> --duration 8, top -H -d 8, /sys/class/kgsl/kgsl-3d0/gpubusy, and MOBILEGL_PIPE_STATS=1 on the client (P65LinkMetrics kind=frame|frame-op|frame-sent per-frame lines: wall, client CPU, transport wait, blocking replies by wire op, records by wire op). Call graphs: simpleperf record --trace-offcpu -g. The container client is an ordinary process on the same kernel, so Android's simpleperf samples it. Symbolization: the APK's server lib is stripped; a RelWithDebInfo build of the same tree (build-android.sh) has every function at +0x80 (checked on all 7970 exported functions), so a server vaddr_in_file + 0x80 goes into llvm-symbolizer. For the container client simpleperf cannot read the ELF and prints offsets from the start of the executable mapping: add 0x4fb000 (the r-x segment's page-aligned vaddr) for the deployed client lib.

Scores (approximate)

Overall scores (median of runs; min-max in brackets; n = runs)

Configuration stock Espryt Magma Espryt/stock Magma/stock Espryt/stock (common scenes) Magma/stock (common scenes)
glmark2-es2-wayland, 800x600 window 1076 [1076-1076] n=1 1319 [1298-1363] n=3 778 [743-813] n=2 1.23 0.72 1.19 0.69
glmark2-es2-wayland, fullscreen 1177 [1172-1182] n=2 572 [556-577] n=3 354 [352-357] n=2 0.49 0.30 0.47 0.29
glmark2-wayland (desktop GL), 800x600 window 1161 [1156-1166] n=2 1283 [1248-1299] n=3 743 [718-768] n=2 1.11 0.64 1.12 0.64
glmark2-wayland (desktop GL), fullscreen 1152 [1152-1152] n=1 559 [558-566] n=3 353 [350-356] n=2 0.49 0.31 0.49 0.31

Per-scene FPS: glmark2-es2-wayland, 800x600 window (median of runs)

Scene stock Espryt Magma Espryt/stock Magma/stock
build use-vbo=false 1095 467 472 0.43 0.43
build use-vbo=true 1287 1675 1062 1.30 0.83
texture texture-filter=nearest 1276 1482 948 1.16 0.74
texture texture-filter=linear 1269 1672 915 1.32 0.72
texture texture-filter=mipmap 1283 1689 860 1.32 0.67
shading shading=gouraud 1157 1433 873 1.24 0.75
shading shading=blinn-phong-inf 1143 1779 997 1.56 0.87
shading shading=phong 1137 1582 966 1.39 0.85
shading shading=cel 1237 1786 854 1.44 0.69
bump bump-render=high-poly 1265 1777 984 1.40 0.78
bump bump-render=normals 1225 1647 1018 1.34 0.83
bump bump-render=height 1232 1738 920 1.41 0.75
effect2d kernel=0,1,0;1,-4,1;0,1,0; 1270 1706 929 1.34 0.73
effect2d kernel=1,1,1,1,1;1,1,1,1,1;1,1,1,1,1; 1217 1707 908 1.40 0.75
pulsar light=false:quads=5:texture=false 1298 1531 688 1.18 0.53
desktop blur-radius=5:effect=blur:passes=1:separable=true:windows=4 976 275 157 0.28 0.16
desktop effect=shadow:windows=4 1044 234 125 0.22 0.12
buffer columns=200:interleave=false:update-dispersion=0.9:update-fraction=0.5:update-method=map 330 unsupported unsupported - -
buffer columns=200:interleave=false:update-dispersion=0.9:update-fraction=0.5:update-method=subdata 441 573 482 1.30 1.09
buffer columns=200:interleave=true:update-dispersion=0.9:update-fraction=0.5:update-method=map 408 unsupported unsupported - -
ideas speed=duration 688 138 66 0.20 0.10
jellyfish 1185 1438 841 1.21 0.71
terrain 443 149 115 0.34 0.26
shadow 975 1139 644 1.17 0.66
refract 838 981 331 1.17 0.39
conditionals fragment-steps=0:vertex-steps=0 1291 1671 1023 1.29 0.79
conditionals fragment-steps=5:vertex-steps=0 1173 1649 886 1.41 0.76
conditionals fragment-steps=0:vertex-steps=5 1263 1512 1042 1.20 0.83
function fragment-complexity=low:fragment-steps=5 1280 1461 993 1.14 0.78
function fragment-complexity=medium:fragment-steps=5 1175 1745 1046 1.49 0.89
loop fragment-loop=false:fragment-steps=5:vertex-steps=5 1246 1647 985 1.32 0.79
loop fragment-steps=5:fragment-uniform=false:vertex-steps=5 1178 1697 976 1.44 0.83
loop fragment-steps=5:fragment-uniform=true:vertex-steps=5 1234 1531 1046 1.24 0.85

Per-scene FPS: glmark2-es2-wayland, fullscreen (median of runs)

Scene stock Espryt Magma Espryt/stock Magma/stock
build use-vbo=false 1355 366 322 0.27 0.24
build use-vbo=true 1518 798 491 0.53 0.32
texture texture-filter=nearest 1396 726 449 0.52 0.32
texture texture-filter=linear 1430 726 442 0.51 0.31
texture texture-filter=mipmap 1437 723 449 0.50 0.31
shading shading=gouraud 1438 679 423 0.47 0.29
shading shading=blinn-phong-inf 1495 687 259 0.46 0.17
shading shading=phong 1431 682 438 0.48 0.31
shading shading=cel 1478 679 435 0.46 0.29
bump bump-render=high-poly 1481 745 463 0.50 0.31
bump bump-render=normals 1434 780 465 0.54 0.32
bump bump-render=height 1467 790 475 0.54 0.32
effect2d kernel=0,1,0;1,-4,1;0,1,0; 1346 582 390 0.43 0.29
effect2d kernel=1,1,1,1,1;1,1,1,1,1;1,1,1,1,1; 698 468 354 0.67 0.51
pulsar light=false:quads=5:texture=false 1379 631 337 0.46 0.24
desktop blur-radius=5:effect=blur:passes=1:separable=true:windows=4 671 219 108 0.33 0.16
desktop effect=shadow:windows=4 1031 207 99 0.20 0.10
buffer columns=200:interleave=false:update-dispersion=0.9:update-fraction=0.5:update-method=map 266 unsupported unsupported - -
buffer columns=200:interleave=false:update-dispersion=0.9:update-fraction=0.5:update-method=subdata 410 474 281 1.15 0.68
buffer columns=200:interleave=true:update-dispersion=0.9:update-fraction=0.5:update-method=map 351 unsupported unsupported - -
ideas speed=duration 813 117 57 0.14 0.07
jellyfish 1346 616 277 0.46 0.21
terrain 141 113 92 0.80 0.65
shadow 796 656 401 0.82 0.50
refract 157 225 189 1.43 1.20
conditionals fragment-steps=0:vertex-steps=0 1457 649 421 0.45 0.29
conditionals fragment-steps=5:vertex-steps=0 1522 647 402 0.43 0.26
conditionals fragment-steps=0:vertex-steps=5 1445 649 430 0.45 0.30
function fragment-complexity=low:fragment-steps=5 1614 648 417 0.40 0.26
function fragment-complexity=medium:fragment-steps=5 1456 647 408 0.44 0.28
loop fragment-loop=false:fragment-steps=5:vertex-steps=5 1517 646 415 0.43 0.27
loop fragment-steps=5:fragment-uniform=false:vertex-steps=5 1561 651 416 0.42 0.27
loop fragment-steps=5:fragment-uniform=true:vertex-steps=5 1545 649 425 0.42 0.28

Per-scene FPS: glmark2-wayland (desktop GL), 800x600 window (median of runs)

Scene stock Espryt Magma Espryt/stock Magma/stock
build use-vbo=false 1216 457 474 0.38 0.39
build use-vbo=true 1327 1805 1043 1.36 0.79
texture texture-filter=nearest 1360 1557 923 1.14 0.68
texture texture-filter=linear 1366 1781 1019 1.30 0.75
texture texture-filter=mipmap 1372 1700 938 1.24 0.68
shading shading=gouraud 1413 1324 990 0.94 0.70
shading shading=blinn-phong-inf 1309 1768 930 1.35 0.71
shading shading=phong 1349 1558 1021 1.15 0.76
shading shading=cel 1268 1598 1068 1.26 0.84
bump bump-render=high-poly 1319 1851 923 1.40 0.70
bump bump-render=normals 1315 1451 1004 1.10 0.76
bump bump-render=height 1363 1839 986 1.35 0.72
effect2d kernel=0,1,0;1,-4,1;0,1,0; 1376 1533 663 1.11 0.48
effect2d kernel=1,1,1,1,1;1,1,1,1,1;1,1,1,1,1; 1320 1743 603 1.32 0.46
pulsar light=false:quads=5:texture=false 1302 1488 705 1.14 0.54
desktop blur-radius=5:effect=blur:passes=1:separable=true:windows=4 966 273 127 0.28 0.13
desktop effect=shadow:windows=4 1070 233 118 0.22 0.11
buffer columns=200:interleave=false:update-dispersion=0.9:update-fraction=0.5:update-method=map 355 462 437 1.30 1.23
buffer columns=200:interleave=false:update-dispersion=0.9:update-fraction=0.5:update-method=subdata 418 596 437 1.43 1.05
buffer columns=200:interleave=true:update-dispersion=0.9:update-fraction=0.5:update-method=map 413 501 522 1.21 1.26
ideas speed=duration 747 129 67 0.17 0.09
jellyfish 1218 1434 835 1.18 0.69
terrain 448 145 112 0.32 0.25
shadow 1056 1208 607 1.14 0.57
refract 831 928 310 1.12 0.37
conditionals fragment-steps=0:vertex-steps=0 1362 1773 1008 1.30 0.74
conditionals fragment-steps=5:vertex-steps=0 1353 1527 937 1.13 0.69
conditionals fragment-steps=0:vertex-steps=5 1317 1873 1047 1.42 0.80
function fragment-complexity=low:fragment-steps=5 1369 1545 961 1.13 0.70
function fragment-complexity=medium:fragment-steps=5 1322 1701 977 1.29 0.74
loop fragment-loop=false:fragment-steps=5:vertex-steps=5 1384 1779 863 1.28 0.62
loop fragment-steps=5:fragment-uniform=false:vertex-steps=5 1370 1556 1028 1.14 0.75
loop fragment-steps=5:fragment-uniform=true:vertex-steps=5 1376 1757 877 1.28 0.64

Per-scene FPS: glmark2-wayland (desktop GL), fullscreen (median of runs)

Scene stock Espryt Magma Espryt/stock Magma/stock
build use-vbo=false 1293 372 301 0.29 0.23
build use-vbo=true 1479 797 500 0.54 0.34
texture texture-filter=nearest 1368 725 440 0.53 0.32
texture texture-filter=linear 1387 721 449 0.52 0.32
texture texture-filter=mipmap 1411 726 446 0.51 0.32
shading shading=gouraud 1484 676 426 0.46 0.29
shading shading=blinn-phong-inf 1490 685 429 0.46 0.29
shading shading=phong 1382 677 431 0.49 0.31
shading shading=cel 1497 682 418 0.46 0.28
bump bump-render=high-poly 1533 751 458 0.49 0.30
bump bump-render=normals 1476 785 449 0.53 0.30
bump bump-render=height 1418 789 465 0.56 0.33
effect2d kernel=0,1,0;1,-4,1;0,1,0; 1340 582 389 0.43 0.29
effect2d kernel=1,1,1,1,1;1,1,1,1,1;1,1,1,1,1; 656 467 329 0.71 0.50
pulsar light=false:quads=5:texture=false 1404 627 353 0.45 0.25
desktop blur-radius=5:effect=blur:passes=1:separable=true:windows=4 673 232 108 0.34 0.16
desktop effect=shadow:windows=4 1027 196 95 0.19 0.09
buffer columns=200:interleave=false:update-dispersion=0.9:update-fraction=0.5:update-method=map 267 331 284 1.24 1.06
buffer columns=200:interleave=false:update-dispersion=0.9:update-fraction=0.5:update-method=subdata 423 494 263 1.17 0.62
buffer columns=200:interleave=true:update-dispersion=0.9:update-fraction=0.5:update-method=map 345 301 300 0.87 0.87
ideas speed=duration 818 120 67 0.15 0.08
jellyfish 1345 613 277 0.46 0.21
terrain 148 111 91 0.75 0.62
shadow 801 663 400 0.83 0.50
refract 157 223 189 1.42 1.20
conditionals fragment-steps=0:vertex-steps=0 1348 645 423 0.48 0.31
conditionals fragment-steps=5:vertex-steps=0 1480 645 417 0.44 0.28
conditionals fragment-steps=0:vertex-steps=5 1411 649 418 0.46 0.30
function fragment-complexity=low:fragment-steps=5 1425 650 431 0.46 0.30
function fragment-complexity=medium:fragment-steps=5 1398 647 411 0.46 0.29
loop fragment-loop=false:fragment-steps=5:vertex-steps=5 1446 645 418 0.45 0.29
loop fragment-steps=5:fragment-uniform=false:vertex-steps=5 1403 646 413 0.46 0.29
loop fragment-steps=5:fragment-uniform=true:vertex-steps=5 1546 648 395 0.42 0.26

Where the time goes

Per-frame profile of the scenes that lose (es2 window unless noted, pinned; client = the glmark2 thread, server = its mgl-srv-apply thread, % of one core over 8 s; waits = blocking client-to-server replies per frame; present RTT = wait for the shared-image present reply):

Espryt fps records/frame waits/frame (op) client CPU server apply thread GPU busy present RTT
texture 1394 11 1 (SharedImage present) 31% 44% 40% 0.30 ms
texture fullscreen 721 11 1 24% 35% 98% 0.60 ms
desktop blur 228 299 29 (28 ResourceCreate + present) 40% 51% 13% -
desktop shadow 173 395 53 (52 ResourceCreate + present) 40% 50% 7% -
ideas 118 587 (305 draws) 1 30% 45% 7% 5.6 ms
terrain 130 224 1 29% 45% 36% 0.63 ms
build use-vbo=false 448 21 3 (2 ResourceCreate + present) 33% 36% 15% -
Magma fps waits/frame client CPU server apply thread GPU busy present RTT
texture 916 1 28% 50% 37% 0.58 ms
texture fullscreen 418 1 17% 29% 75% 1.25 ms
desktop blur 118 29 35% 52% 9% -
desktop shadow 115 53 38% 53% 9% -
ideas 53 1 24% 60% 14% 15.2 ms
terrain 108 1 31% 49% 27% 0.91 ms
build use-vbo=false 498 3 42% 49% 26% -

The common shape: nothing is saturated. Outside fullscreen the GPU is 7-40% busy, and neither the client thread nor the server apply thread is near 100%. Both sides spend most of the frame waiting for each other, and each handoff costs a wake-up. Evidence that the handoffs, not the work, set the frame time:

  • With MOBILEGL_IPC_SPIN_US=1000 on the client only (Espryt; the server keeps its 50 us), the client spins through short replies instead of parking: ideas 94 -> 185 fps, terrain 132 -> 315, desktop shadow 187 -> 322, texture 1543 -> 1837 (8 s single-scene runs).
  • On Espryt's server in ideas the apply thread is 50% off-CPU, almost all of it parked in SocketDoorbell::Park waiting for records, and 18% of its on-CPU time is SocketDoorbell::Notify (a socket write that wakes the client). The client is 75% off-CPU in poll. The server's own frame work is ~2 ms (P65ServerFrame apply_ms for the ~600-record frames) of an 8.5 ms frame.

Ranked bottlenecks and proposed general fixes

Gains are estimates from the evidence above, at the pinned clocks; none of the fixes is glmark2-specific.

Shared: client library and pipe (both backends)

  1. Blocking ResourceCreate for client-memory vertex/index arrays. Every draw that sources a client array makes a brand-new owned buffer per array (MG_Impl/Pipe/OwnedDrawInputs.h MakeOwnedBuffer -> BufferObject ctor -> MGPipeEmitResourceCreate -> EmitCreateBlocking -> EmitAndWait): one round trip per array per draw, plus a respecify, a subdata and a destroy record. desktop blur pays 28 and shadow 52 of them per frame; 55% of the client's samples are the spin in SessionProducer::WaitForAppliedOrEventBacklog under EmitCreateBlocking. The fetch plan also sorts the referenced vertex list per draw (std::sort = 27% of the client in build use-vbo=false) even for a glDrawArrays range that is contiguous. Fix: a per-context streaming upload buffer for client arrays (one long-lived server resource, sub-allocated per draw, reused once the frames that read it retired; grows on overflow), or client-minted creates that need no reply; plus a contiguous-range fast path (one memcpy of [first, first+count) per attribute, no sort). Gain: blocking waits/frame -> ~1; desktop blur/shadow 2-3x, build use-vbo=false ~2x, on both backends; helps every GLES2-style app. Risk: sub-allocation lifetime vs in-flight draws on both backends; validate with the OwnedDrawInputs/Pipe unit tests and the retrace CI.
  2. Wake-up latency of every blocking wait and every ring handoff. Doorbells are one byte on a unix socket; a parked waiter needs a socket write plus a scheduler wake-up (hundreds of us to ms at these clocks), and the server rings the client on every applied-watermark advance while the client is parked, even if the client waits for a much later sequence (18% of Espryt's apply thread is SocketDoorbell::Notify). Fix: (a) the waiter publishes the sequence it waits for and the server rings only when applied >= target; (b) a bounded adaptive spin sized from the recently measured reply latency, only while a reply is expected (never when idle); (c) optionally futex wait/wake on the shared ring page instead of socket bytes (same kernel). Gain: spin alone gave 1.2x (light) to 2.4x (terrain); expect 1.3-2x on draw-heavy scenes, both backends. Risk: lost wake-ups (keep the doorbell's two-fence protocol), CPU and power at idle; RingTest, MultiSession tests, idle CPU of the desktop.
  3. Synchronous per-frame present. WindowSurface::Present -> PresentToSharedImage -> EmitSharedImage is EmitAndWait: the client waits until the server has applied the whole frame and queued the blit, and eglSwapBuffers' Present record also holds a present credit of 1. The client cannot record frame N+1 while the server finishes N (texture: 0.71 ms frame = 0.26 ms of client CPU + 0.30 ms of wait). Fix: make the shared-image present asynchronous: commit the wl_buffer from the reply on the event ring (or let the server order the compositor's acquire of that image after the producer's present record), and allow credit 2. Gain: 1.3-1.8x on light scenes. Risk: ordering with the compositor's import (Chrome flicker test, KWin, desktop-verify).
  4. Fullscreen pays an extra full-frame copy and full composition. Every frame the server copies the client's default framebuffer (a server pbuffer / Magma swapchain image) into the shared image (Espryt glBlitFramebuffer, Magma vkCmdCopyImage in a second submission), and KWin composites the whole screen (85 buffers/s latched during Espryt fullscreen). At 2560x1600 the GPU is 98% busy (Espryt) on texture for 726 fps against ~1430 on stock, whose fullscreen is even faster than its windowed run. Fix: let the client render straight into the window's shared images (default framebuffer = the current back image; the row order can be expressed with linux-dmabuf's Y_INVERT flag instead of the flipping blit); separately, direct scanout of fullscreen clients in the anland KWin backend. Gain: fullscreen 1.5-2x on both backends. Risk: buffer age and damage, resize, device loss; desktop-verify and Chrome.
  5. Per-record fixed cost on the server. The server is built for android-26, so every thread_local goes through emulated TLS: __emutls_get_address + pthread_getspecific are 7-12% of Espryt's and ~3% of Magma's apply thread. ServerLoop::DrainRing takes two steady_clock::now() per record for the P65ServerFrame statistics even when they are off (5% on Espryt), and MGPipeApplier() / ResolveThreadApplierKey / MGPipeApplierShareGroupKeyFor (TLS
    • mutex) run per record (~5%). Fix: resolve the applier/session once per drain and pass it down; time records only with the statistics on; consider ELF TLS (API 29+) for the anland server build. Gain: 10-15% of server CPU. Risk: low.
  6. Small: a one-draw frame sends 11 records; CreateVertexElements is re-sent per draw (ideas: 76 per frame) instead of being deduplicated by content on the client.

Espryt (DirectGLES)

  1. Per-draw state synchronization (DG::PrepareForDraw 28% of the apply thread in ideas: SyncCurrentProgramByHandle, BindCurrentProgramWithResources, SyncNeccessaryBuffers, VAO SyncToBackendFromApplier, ResolveVaoTwin / ResolveProgramTwin registry lookups, texture sync). The same per-draw redundancy as on Minecraft: version-gated early-outs keyed on monotonic lifetime ids. Gain: 10-20% on draw-heavy scenes once the pipe is fixed. Risk: stale caches; the retrace CI.
  2. Parity: GL_OES_mapbuffer, GL_OES_rgb8_rgba8, GL_OES_depth24 missing from ES contexts (two Unsupported scenes; es2 off-screen does not start).

Magma (DirectVulkan)

  1. A VkRenderPass per draw. VulkanRenderer::SetupWireDraw (WireDraw.inc) calls RetireWireDrawPass() unconditionally, which ends the active render pass, and begins a new one at the end of setup, for every draw. In ideas (305 draws/frame) SetupWireDraw is 41% of the apply thread and End/BeginRenderPass ~19%; on a tiler every pass also loads and stores its attachments. Fix: keep the pass open while the WireDrawPassKey (attachments, extent, epochs) is unchanged and nothing that must run outside a pass came in between (clear outside the pass, transfer, barrier, readback, layout change of an attachment, query); retire lazily. Gain: ideas, terrain, desktop 1.5-2x on Magma; less load/store bandwidth at full screen. Risk: attachment layout tracking and feedback loops; Magma unit tests (never the DirectVulkan integration tests), retrace CI.
  2. Per-frame submission overhead. Per frame: FlushPendingCommands for the frame, a second submission for the shared-image copy, and Present() of the client's own offscreen default surface (swapchain acquire + submit through libgui). GpuProgressMarkers::SubmitBracketed (hang watch) is 12% of texture fullscreen, and FreeRetiredCommandBuffersCompletedUpTo + EndCommandRecording ~25% of ideas (command buffers freed and reallocated rather than reset and reused). Fix: one submission per frame (append the copy), skip Present for a surface whose frames only go to shared images, recycle command buffers per frame slot, coalesce markers. Gain: light scenes 1.3-1.5x on Magma.
  3. Allocation churn. scudo malloc/free with lock contention (__aarch64_cas4_acq 20% + scudo ~15% of the ideas apply thread): per-draw vectors (BeginRenderPass clear values, vertex input state objects, retired wire objects). Fix: per-frame arenas / small vectors. Gain: 10-20%.
  4. A VkRenderPass + VkFramebuffer created per clear (WireFramebuffer.inc ~306-311): 7% of texture fullscreen, and the objects pile up when nothing retires them (C2).

Correctness found on the way

  • C1 glFinish does not wait for the GPU on the remote path: ClientSession::Finish only waits for the server to apply (WaitForApplyToCatchUp). Off-screen glmark2 then reports 6846 / 12117 and nothing bounds the GPU queue. It must reach the backend (driver finish / fence wait) and reply.
  • C2 Magma server crash under glmark2-wayland --off-screen: VK_ERROR_OUT_OF_HOST_MEMORY FATAL at WireFramebuffer.inc:311 (vkCreateFramebuffer in ClearWireFramebuffer), then SIGSEGV in vkCmdBeginRenderPass (two server crashes in a row); the per-clear objects accumulate when the client runs ahead with no backpressure (C1).
  • C3 ES extension parity (Espryt item 2).

Next steps

In this order, each measured on the affected scenes at the pinned clocks, frames shown, Plasma / Chrome / lock-unlock checked on both backends: C1-C3, shared 1 (client arrays), shared 2 (wake-ups), shared 3/4 (present), Magma 1 (render pass), shared 5 (per-record cost), then each backend's per-draw items.