mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-13 03:51:10 +00:00
On the device, render-on-commit drew only 4-12 % of frames on arrival while a game ran uncapped: gamescope delivers about one frame per refresh, so once the tick drew a frame, the next one arrived in the same refresh, was held back by the once-per-refresh limit and was drawn by the next tick, which held back the one after it. Waits stayed at ~2 ms. The tick now 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.
170 lines
6.5 KiB
Python
170 lines
6.5 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 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; }
|
|
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;
|
|
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);
|
|
arrive(2 * ms);
|
|
assert(idles_added == 2);
|
|
run_idle();
|
|
assert(renders == 2 && g_pace.commit_renders == 2);
|
|
|
|
/* 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);
|
|
|
|
/* one frame per refresh, starting right after a tick draw, settles on drawing every frame when it arrives */
|
|
arrive(2 * ms);
|
|
assert(idles_added == 2);
|
|
for (int i = 0; i < 10; i++) {
|
|
tick(t += refresh);
|
|
arrive(2 * ms);
|
|
run_idle();
|
|
}
|
|
assert(renders == 13 && g_pace.commit_renders == 12);
|
|
|
|
/* a window, effect or headset change at the tick draws the waiting frame now */
|
|
arrive(1 * ms);
|
|
window_requests = 1;
|
|
tick(t += refresh);
|
|
window_requests = 0;
|
|
assert(renders == 14 && g_pace.commit_renders == 12);
|
|
|
|
/* a draw queued before the tick still draws the frame once */
|
|
tick(t += refresh);
|
|
arrive(1 * ms);
|
|
tick(t += refresh);
|
|
assert(renders == 14);
|
|
run_idle();
|
|
assert(renders == 15 && g_pace.commit_renders == 13);
|
|
tick(t += refresh);
|
|
assert(renders == 15);
|
|
|
|
/* paced output, frame generation, the switch off and streaming keep drawing on the tick */
|
|
const int idles = idles_added;
|
|
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 == 16 + c);
|
|
}
|
|
|
|
/* 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)
|
|
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()
|