mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
Remove the Performance mode switch. The session always runs the way Performance mode did: replaced buffers go back to the game at once (except while mirroring to glasses), gamescope's WSI layer and winex11 reuse their per-present window checks, and games get eight swapchain images while uncapped. Steam's Frame Limit in the Quick Access menu is the only limit. gamescope 3.16.29-p15 (p14 plus two patches): - 0119: with the Frame Limit on, gamescope forces FIFO while the WSI layer hands the driver MAILBOX, so a game with eight images could queue up to seven frames ahead of the screen. While the limiter forces FIFO the layer uses its default of 3 images; DXVK and VKD3D-Proton recreate their swapchain when the limiter changes, other apps are asked to with VK_SUBOPTIMAL_KHR. - 0120: -r fixed gamescope's pacing and limiter rate for the whole session. With GAMESCOPE_NESTED_REFRESH_FOLLOWS_HOST=1 a rate the host has reported for 300 ms replaces it (a switch from 144 to 120 Hz passes through 72 Hz for a few frames), and Steam gets the display information again. The session sets it whenever it passes -r. Steam keeps its Frame Limit as the client setting gamescope_app_target_framerate and offers the values gamescope's display state lists. After a refresh change, a limit the new rate does not offer (72 at 120 Hz) left the slider on its first value while the game kept the old limit. The Steam helper watches the offered values and, once they settle, keeps the limit when it is still offered and moves it to the highest value otherwise, through Steam's own setting so the slider shows it. Steam's webpack does not expose its module cache, so the gamescope service is found by the calls its module makes, and the helper retries the install every 10 s until Steam is ready. The session window asks for the fastest mode the peak refresh setting allows and chose again only when the display changed, so after it had asked for 60 Hz, raising Android's refresh setting left the display, gamescope and the limit at 60. It now also chooses again when peak_refresh_rate or min_refresh_rate changes.
173 lines
8.1 KiB
Python
173 lines
8.1 KiB
Python
import json
|
|
from pathlib import Path
|
|
import runpy
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
BIN = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin"
|
|
LIVE = runpy.run_path(str(BIN / "droiddeck-steam-compat"))
|
|
|
|
|
|
class DeviceControlsTest(unittest.TestCase):
|
|
def test_sync_controls_queued_notifications_are_processed_without_another_frame(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
Path(directory, "state.json").write_text(json.dumps({"brightness": 0.5, "revision": 1}))
|
|
helper = LIVE["Helper"]("/steam", 0)
|
|
helper.controls_installed = True
|
|
page = mock.Mock(events=[])
|
|
page.ws.pending.return_value = False
|
|
helper.page = page
|
|
payload = json.dumps({"id": 1, "options": {"strTitle": "Pick a file"}})
|
|
def evaluate(expression):
|
|
for name, value in [(LIVE["BRIGHTNESS_BINDING"], "0.4"), (LIVE["FILE_BINDING"], payload)]:
|
|
page.events.append({"method": "Runtime.bindingCalled", "params": {"name": name, "payload": value}})
|
|
return True
|
|
def drain():
|
|
events, page.events = page.events, []
|
|
return events
|
|
page.evaluate.side_effect = evaluate
|
|
page.drain.side_effect = drain
|
|
helper.reconcile = mock.Mock()
|
|
helper.sync_frame_limit = mock.Mock()
|
|
helper.ensure_tools = mock.Mock(side_effect=[None, StopIteration])
|
|
helper.open_chooser = mock.Mock()
|
|
watcher = mock.Mock()
|
|
watcher.fileno.return_value = -1
|
|
watcher.changed.return_value = False
|
|
globals_ = LIVE["Helper"].serve.__globals__
|
|
with mock.patch.dict(globals_, {"CONTROLS_DIR": directory,
|
|
"brightness_request": lambda value: LIVE["brightness_request"](value, directory)}), \
|
|
mock.patch.dict(globals_["COMPAT"], {"state_path": lambda name: str(Path(directory, name))}), \
|
|
mock.patch.object(globals_["select"], "select", return_value=([], [], [])) as select_:
|
|
with self.assertRaises(StopIteration):
|
|
helper.serve(watcher)
|
|
self.assertEqual(float(Path(directory, "brightness.request").read_text()), 0.4)
|
|
helper.open_chooser.assert_called_once_with(payload)
|
|
self.assertEqual(page.events, [])
|
|
select_.assert_not_called()
|
|
|
|
def test_native_brightness_notifications_do_not_echo_android_readback(self):
|
|
harness = r"""
|
|
const assert = require('assert');
|
|
globalThis.window = globalThis;
|
|
const requests = [], callbacks = [];
|
|
const timers = [];
|
|
globalThis.setTimeout = callback => timers.push(callback);
|
|
let current = 1;
|
|
window.droiddeckBrightnessChanged = value => requests.push(Number(value));
|
|
globalThis.SteamClient = { System: { Display: {
|
|
RegisterForBrightnessChanges(cb) { callbacks.push(cb); cb({ flBrightness: 1 }); return {}; },
|
|
SetBrightness(value) {
|
|
if (current === value) return;
|
|
current = value;
|
|
callbacks.forEach(cb => cb({ flBrightness: value }));
|
|
}
|
|
} } };
|
|
const script = require('fs').readFileSync(process.argv[2], 'utf8');
|
|
assert.strictEqual(eval(script), 'installed');
|
|
assert.strictEqual(eval(script), 'present');
|
|
const bridge = window.__droiddeckBrightness;
|
|
bridge.set(0.5);
|
|
bridge.set(0.5);
|
|
assert.deepStrictEqual(requests, []);
|
|
callbacks.forEach(cb => cb({ flBrightness: 0.8 }));
|
|
assert.deepStrictEqual(requests, [0.8]);
|
|
bridge.set(0.7992126);
|
|
assert.deepStrictEqual(requests, [0.8]);
|
|
for (const value of [NaN, Infinity, -1, 2, '0.5']) callbacks.forEach(cb => cb({ flBrightness: value }));
|
|
assert.deepStrictEqual(requests, [0.8]);
|
|
assert.strictEqual(callbacks.length, 1);
|
|
SteamClient.System.Display.SetBrightness = value => callbacks.forEach(cb => cb({ flBrightness: value + 0.1 }));
|
|
bridge.set(0.3);
|
|
assert.deepStrictEqual(requests, [0.8]);
|
|
callbacks.forEach(cb => cb({ flBrightness: 0.6 }));
|
|
assert.deepStrictEqual(requests, [0.8, 0.6]);
|
|
SteamClient.System.Display.SetBrightness = () => {};
|
|
bridge.set(0.65);
|
|
timers.forEach(callback => callback());
|
|
callbacks.forEach(cb => cb({ flBrightness: 0.7 }));
|
|
assert.deepStrictEqual(requests, [0.8, 0.6, 0.7]);
|
|
"""
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
script = Path(directory) / "bridge.js"
|
|
script.write_text(LIVE["BRIGHTNESS_JS"])
|
|
test = Path(directory) / "test.js"
|
|
test.write_text(harness)
|
|
result = subprocess.run(["node", str(test), str(script)], capture_output=True, text=True, timeout=20)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
|
|
def test_requests_reject_nonfinite_and_out_of_range_values(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / "brightness.request"
|
|
for value in ["NaN", "Infinity", "-0.1", "1.1", "invalid", None]:
|
|
LIVE["brightness_request"](value, directory)
|
|
self.assertFalse(path.exists())
|
|
LIVE["brightness_request"]("0.4", directory)
|
|
self.assertEqual(float(path.read_text()), 0.4)
|
|
|
|
def test_failed_request_revision_republishes_the_actual_brightness(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
globals_ = LIVE["Helper"].sync_controls.__globals__
|
|
previous = globals_["CONTROLS_DIR"]
|
|
try:
|
|
globals_["CONTROLS_DIR"] = directory
|
|
helper = LIVE["Helper"]("/steam", 0)
|
|
calls = []
|
|
helper.controls_installed = True
|
|
helper.page = type("Page", (), {"evaluate": lambda self, expression: calls.append(expression) or True})()
|
|
path = Path(directory) / "state.json"
|
|
path.write_text(json.dumps({"brightness": 0.5, "revision": 1}))
|
|
helper.sync_controls()
|
|
helper.sync_controls()
|
|
self.assertEqual(len(calls), 1)
|
|
path.write_text(json.dumps({"brightness": 0.5, "revision": 2}))
|
|
helper.sync_controls()
|
|
self.assertEqual(len(calls), 2)
|
|
finally:
|
|
globals_["CONTROLS_DIR"] = previous
|
|
|
|
def test_display_bridge_retries_binding_and_install_before_caching_state(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
globals_ = LIVE["Helper"].sync_controls.__globals__
|
|
previous = globals_["CONTROLS_DIR"]
|
|
try:
|
|
globals_["CONTROLS_DIR"] = directory
|
|
Path(directory, "state.json").write_text(json.dumps({"brightness": 0.5, "revision": 1}))
|
|
calls, installs, writes = [], ["unavailable", "installed", "present"], []
|
|
class Page:
|
|
present = True
|
|
def call(self, method, params):
|
|
calls.append((method, params))
|
|
def evaluate(self, expression):
|
|
if expression == LIVE["BRIGHTNESS_JS"]:
|
|
return installs.pop(0)
|
|
writes.append(expression)
|
|
return self.present
|
|
helper = LIVE["Helper"]("/steam", 0)
|
|
helper.page = Page()
|
|
helper.sync_controls()
|
|
self.assertIsNone(helper.controls_state)
|
|
self.assertFalse(writes)
|
|
helper.sync_controls()
|
|
helper.sync_controls()
|
|
self.assertEqual(len(calls), 2)
|
|
self.assertEqual(len(writes), 1)
|
|
self.assertEqual(calls[1], ("Runtime.addBinding", {"name": LIVE["BRIGHTNESS_BINDING"]}))
|
|
helper.page.present = False
|
|
Path(directory, "state.json").write_text(json.dumps({"brightness": 0.7, "revision": 2}))
|
|
helper.sync_controls()
|
|
self.assertIsNone(helper.controls_state)
|
|
self.assertFalse(helper.controls_installed)
|
|
helper.page.present = True
|
|
helper.sync_controls()
|
|
self.assertEqual(len(calls), 3)
|
|
self.assertEqual(helper.controls_state["brightness"], 0.7)
|
|
finally:
|
|
globals_["CONTROLS_DIR"] = previous
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|