mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +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
106 lines
4.2 KiB
Python
106 lines
4.2 KiB
Python
import os
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
SOURCE = ROOT / 'app/src/main/cpp/waylandcomp/src/compositor.c'
|
|
|
|
|
|
def extract_function(source, name):
|
|
match = re.search(r'^static int(?:64_t)? ' + re.escape(name) + r'\([^;]*?\) \{', source, re.MULTILINE)
|
|
if match is None:
|
|
raise AssertionError(f'Missing compositor function: {name}')
|
|
cursor, depth = match.end(), 1
|
|
while depth:
|
|
depth += (source[cursor] == '{') - (source[cursor] == '}')
|
|
cursor += 1
|
|
return source[match.start():cursor]
|
|
|
|
|
|
HARNESS = r'''
|
|
#include <assert.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
|
|
@FUNCTIONS@
|
|
|
|
#define MS 1000000LL
|
|
|
|
static int run(int64_t refresh, int limit, int64_t jitter, int seconds) {
|
|
int renders = 0;
|
|
int64_t due = 0;
|
|
for (int64_t tick = 0; tick * refresh < seconds * 1000 * MS; tick++) {
|
|
int64_t now = 1000 * MS + tick * refresh + (tick % 2 ? jitter : -jitter);
|
|
if (scene_held_for_limit(now, due, limit, 0)) continue;
|
|
renders++;
|
|
due = scene_next_due(now, due, limit);
|
|
}
|
|
return renders;
|
|
}
|
|
|
|
int main(void) {
|
|
assert(!scene_held_for_limit(100 * MS, 0, 30, 0));
|
|
assert(!scene_held_for_limit(100 * MS, 130 * MS, 0, 0));
|
|
assert(!scene_held_for_limit(100 * MS, 130 * MS, 30, 1));
|
|
assert(scene_held_for_limit(100 * MS, 130 * MS, 30, 0));
|
|
assert(scene_held_for_limit(100 * MS, 102 * MS, 30, 0));
|
|
assert(!scene_held_for_limit(100 * MS, 101 * MS, 30, 0));
|
|
assert(!scene_held_for_limit(100 * MS, 90 * MS, 30, 0));
|
|
assert(scene_next_due(100 * MS, 0, 0) == 0);
|
|
assert(scene_next_due(100 * MS, 0, 25) == 140 * MS);
|
|
assert(scene_next_due(101 * MS, 100 * MS, 25) == 140 * MS);
|
|
assert(scene_next_due(200 * MS, 100 * MS, 25) == 240 * MS);
|
|
const struct { int64_t refresh; int limit; int expected; } cases[] = {
|
|
{8333333, 30, 300}, {8333333, 60, 600}, {16666667, 25, 250}, {16666667, 30, 300},
|
|
{10000000, 30, 300}, {11111111, 30, 300}, {6944444, 40, 400}, {16666667, 120, 600},
|
|
};
|
|
for (unsigned i = 0; i < sizeof cases / sizeof cases[0]; i++) {
|
|
int got = run(cases[i].refresh, cases[i].limit, 0, 10);
|
|
if (got > cases[i].expected + 1 || got < cases[i].expected - 2) {
|
|
fprintf(stderr, "refresh %lld limit %d: %d renders, expected %d\n",
|
|
(long long)cases[i].refresh, cases[i].limit, got, cases[i].expected);
|
|
return 1;
|
|
}
|
|
got = run(cases[i].refresh, cases[i].limit, 300000, 10);
|
|
if (got > cases[i].expected + 1 || got < cases[i].expected - 2) {
|
|
fprintf(stderr, "jittered refresh %lld limit %d: %d renders, expected %d\n",
|
|
(long long)cases[i].refresh, cases[i].limit, got, cases[i].expected);
|
|
return 1;
|
|
}
|
|
}
|
|
int fallback = run(8 * MS, 30, 0, 10);
|
|
assert(fallback <= 301 && fallback >= 290);
|
|
puts("PASS");
|
|
return 0;
|
|
}
|
|
'''
|
|
|
|
|
|
class SceneLimitHoldTest(unittest.TestCase):
|
|
def test_a_frame_limit_holds_scene_renders_to_its_rate_without_slowing_paced_frames(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()
|
|
harness = HARNESS.replace('@FUNCTIONS@', '\n'.join(extract_function(source, name) for name in ('scene_held_for_limit', 'scene_next_due')))
|
|
with tempfile.TemporaryDirectory(prefix='droiddeck-scene-hold-') as folder:
|
|
path = Path(folder)
|
|
(path / 'hold.c').write_text(harness)
|
|
binary = path / 'hold-test'
|
|
build = subprocess.run([compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-O2',
|
|
str(path / 'hold.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()
|