Files
DroidDeck/tools/tests/test_render_on_commit.py
T
The412Banner 8549ac9ce8 perf(compositor): draw a new frame when it arrives (#1707)
A new game frame waited for the next screen tick before the compositor
drew it: 1.9-3.5 ms on average at 144 Hz, measured with the pacing
counters. With frames going straight back to the game (no compositor
limit), the compositor now draws as soon as the clients' requests are
handled, at most once per refresh, and presentation times stay on the
tick.

gamescope delivers about one frame per refresh, so the tick leaves a
frame younger than one refresh for the next arrival to draw, unless the
tick itself changed the window, effects or headset pose; a frame no
other frame follows is drawn at the tick after. Once a held frame is
still waiting at the next tick (a client paced by frame callbacks), the
tick draws every frame for a second before holding one again. The
queued draw checks again that drawing on arrival is still on.

Paced output (glasses), streaming and frame generation keep drawing on
the tick. The hidden pref renderOnCommit (default on) turns it off. The
pacing log line counts the scenes drawn on arrival. On the device new
frames wait 0.12 ms instead of ~2 ms.
2026-10-28 09:21:07 +00:00

224 lines
8.2 KiB
Python

import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
from test_output_refresh import SOURCE, extract_function
PREAMBLE = r'''
#include <assert.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
struct wl_event_source { int armed; };
static struct wl_event_source idle_source, timer_source;
static void (*idle_func)(void *);
static int idles_added, timers_armed, renders, window_requests, effects_changes, pose_changes;
static int64_t clock_ns;
static int g_display_storage;
static void *g_display = &g_display_storage;
static struct wl_event_source *g_render_idle, *g_fallback_timer;
static int g_fallback_armed, g_dirty, g_hud_fresh, framegen;
static void *g_hud_surface;
static int64_t g_dirty_since_ns, g_last_vsync_ns, g_tick_vsync_ns, g_hud_last_ns, g_rendered_tick_ns;
static int g_tick_held;
static int64_t g_hold_missed_ns;
#define HOLD_RETRY_NS 1000000000LL
static int64_t g_refresh_ns = 16666667;
volatile int g_render_on_commit = 1;
volatile int g_release_unpaced = 1;
static atomic_int g_stream_on;
static struct { unsigned ticks, ticks_late_2ms, commit_renders; int64_t tick_late_ns, tick_late_max_ns; } g_pace;
static struct { unsigned ticks; } g_perf;
static int64_t now_ns(void) { return clock_ns; }
static void *wl_display_get_event_loop(void *display) { return display; }
static struct wl_event_source *wl_event_loop_add_idle(void *loop, void (*func)(void *), void *data) {
idles_added++; idle_func = func; return &idle_source;
}
static struct wl_event_source *wl_event_loop_add_timer(void *loop, int (*func)(void *), void *data) { return &timer_source; }
static int wl_event_source_timer_update(struct wl_event_source *source, int ms) { timers_armed++; return 0; }
static int vkp_framegen_active(void) { return framegen; }
static int vkp_apply_window_request(void) { return window_requests; }
static int vkp_effects_sync(void) { return effects_changes; }
static int vkp_cardboard_pose_changed(void) { return pose_changes; }
static void render_scene(void) { renders++; g_dirty = 0; g_dirty_since_ns = 0; g_rendered_tick_ns = g_tick_vsync_ns; g_tick_held = 0; }
static int on_fallback_timer(void *data) { return 0; }
static void on_commit_idle(void *data);
'''
CHECKS = r'''
static void run_idle(void) {
void (*func)(void *) = idle_func;
idle_func = NULL;
if (func) func(NULL);
}
static void tick(int64_t t) { clock_ns = t; on_vsync(t); }
static void arrive(int64_t after_ns) { clock_ns += after_ns; schedule_render(); }
int main(void) {
const int64_t refresh = 6944444, ms = 1000000;
int64_t t = 1000000000LL;
int r0;
unsigned c0;
g_refresh_ns = refresh;
tick(t);
assert(renders == 0);
/* a new frame is drawn when it arrives */
arrive(2 * ms);
assert(idles_added == 1 && renders == 0);
run_idle();
assert(renders == 1 && g_pace.commit_renders == 1 && g_render_idle == NULL);
/* a second frame in the same refresh is left for the next arrival, which is drawn when it arrives */
arrive(1 * ms);
assert(idles_added == 1);
tick(t += refresh);
assert(renders == 1 && g_tick_held == 1);
arrive(2 * ms);
assert(idles_added == 2);
run_idle();
assert(renders == 2 && g_pace.commit_renders == 2 && g_tick_held == 0);
/* a frame no other frame follows is drawn at the tick after next */
arrive(1 * ms);
tick(t += refresh);
assert(renders == 2);
tick(t += refresh);
assert(renders == 3 && g_pace.commit_renders == 2);
/* after that, the tick draws every frame for a second, then holds one again */
arrive(1 * ms);
tick(t += refresh);
assert(renders == 4 && g_tick_held == 0);
t += HOLD_RETRY_NS;
tick(t);
arrive(1 * ms);
run_idle();
arrive(1 * ms);
tick(t += refresh);
assert(renders == 5 && g_tick_held == 1);
arrive(2 * ms);
run_idle();
assert(renders == 6 && g_pace.commit_renders == 4);
/* one frame per refresh, starting right after a tick draw, settles on drawing every frame when it arrives */
window_requests = 1;
tick(t += refresh);
window_requests = 0;
arrive(2 * ms);
r0 = renders; c0 = g_pace.commit_renders;
for (int i = 0; i < 10; i++) {
tick(t += refresh);
arrive(2 * ms);
run_idle();
}
assert(renders - r0 == 10 && g_pace.commit_renders - c0 == 10);
/* a client that draws again only after its frame callback (sent when the scene is drawn) keeps one frame
per refresh: two held frames in 200 refreshes, about 1.4 s */
r0 = renders; c0 = g_pace.commit_renders;
{
int seen = renders - 1;
for (int i = 0; i < 200; i++) {
if (renders != seen) { seen = renders; arrive(1 * ms); run_idle(); }
tick(t += refresh);
}
}
assert(renders - r0 == 198 && g_pace.commit_renders == c0);
/* a window, effect or headset change at the tick draws the waiting frame now */
t += HOLD_RETRY_NS;
tick(t);
arrive(1 * ms);
run_idle();
r0 = renders;
arrive(1 * ms);
window_requests = 1;
tick(t += refresh);
window_requests = 0;
assert(renders - r0 == 1 && g_tick_held == 0);
/* a draw queued before the tick still draws the frame once */
tick(t += refresh);
r0 = renders; c0 = g_pace.commit_renders;
arrive(1 * ms);
tick(t += refresh);
assert(renders == r0);
run_idle();
assert(renders - r0 == 1 && g_pace.commit_renders - c0 == 1);
tick(t += refresh);
assert(renders - r0 == 1);
/* streaming switched on after the draw was queued leaves the frame for the tick */
tick(t += refresh);
r0 = renders; c0 = g_pace.commit_renders;
arrive(1 * ms);
atomic_store(&g_stream_on, 1);
run_idle();
assert(renders == r0);
tick(t += refresh);
atomic_store(&g_stream_on, 0);
assert(renders - r0 == 1 && g_pace.commit_renders == c0);
/* paced output, frame generation, the switch off and streaming keep drawing on the tick */
const int idles = idles_added;
r0 = renders;
for (int c = 0; c < 4; c++) {
tick(t += refresh);
if (c == 0) g_release_unpaced = 0;
if (c == 1) framegen = 1;
if (c == 2) g_render_on_commit = 0;
if (c == 3) atomic_store(&g_stream_on, 1);
arrive(1 * ms);
assert(idles_added == idles);
tick(t += refresh);
g_release_unpaced = 1; framegen = 0; g_render_on_commit = 1; atomic_store(&g_stream_on, 0);
assert(renders - r0 == c + 1);
}
/* without screen ticks the fallback timer still draws */
clock_ns += 200000000LL;
schedule_render();
assert(idles_added == idles && g_fallback_armed == 1 && timers_armed == 1);
puts("PASS");
return 0;
}
'''
@unittest.skipUnless(sys.platform.startswith('linux'), 'needs a Linux host compiler')
class RenderOnCommitTest(unittest.TestCase):
def test_a_new_frame_is_drawn_on_arrival_at_most_once_per_refresh(self):
compiler = shutil.which(os.environ.get('CC', 'cc'))
if compiler is None:
self.skipTest('A host C compiler is required')
source = SOURCE.read_text()
render = extract_function(source, 'render_scene')
self.assertIn('g_rendered_tick_ns = g_tick_vsync_ns;', render)
self.assertIn('g_tick_held = 0;', render)
functions = ('commit_renders_on', 'render_on_commit', 'schedule_render', 'scene_waits_for_game', 'on_commit_idle', 'on_vsync')
harness = PREAMBLE + '\n'.join(extract_function(source, name) for name in functions) + CHECKS
with tempfile.TemporaryDirectory(prefix='droiddeck-render-on-commit-') as folder:
path = Path(folder)
(path / 'commit.c').write_text(harness)
binary = path / 'commit-test'
build = subprocess.run([compiler, '-std=c11', '-Wall', '-Wextra', '-Werror',
'-Wno-unused-parameter', '-Wno-unused-function', '-O2',
str(path / 'commit.c'), '-o', str(binary)], capture_output=True, text=True)
self.assertEqual(0, build.returncode, build.stderr)
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual('PASS', result.stdout.strip())
if __name__ == '__main__':
unittest.main()