mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 11:01:07 +00:00
* fix: a session in picture-in-picture draws at 30 fps instead of the full refresh Since auto picture-in-picture became the default (#1727), pressing Home leaves the session running in a small window that takes no input. A game kept drawing at its full rate there: on a REDMAGIC Astra 2 Among Us in picture-in-picture drew 3.31 W, the same as playing it and about 1.05 W more than a suspended session. While in picture-in-picture the compositor now paces the session at 30 fps (or a lower limit already set). Mirroring to glasses keeps the glasses rate, and the display mode request ignores the picture-in-picture limit so the app in front is not slowed. * fix: keep the no-argument output limit the glasses pacing tests read * fix: hold scene renders to the output limit and keep frame generation off in picture-in-picture A frame limit only paced dmabuf buffer releases, so a shared-memory client was still drawn on every vsync with its frame callbacks sent at the panel's rate. The vsync tick now waits until the limit's interval, less half a refresh, has passed since the last scene. Frame generation is disarmed while picture-in-picture is shown and armed again when it ends; frame generation requests are applied in order. * Give the commit render harness the frame limit hold * Hold scene renders to a frame limit deadline, also on the fallback timer, and keep frame generation for glasses mirroring in picture in picture
226 lines
8.3 KiB
Python
226 lines
8.3 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;
|
|
volatile int g_fps_limit;
|
|
static int64_t g_scene_due_ns;
|
|
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 = ('scene_held_for_limit', '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()
|