Every artwork budget in the image pipeline was tuned for 1080p surfaces: the transcode request clamp (1920x1080), the per-type decode caps (poster 720x1080, thumb 960x540, heroLogo 1000x500, ...) and the TV image-cache bytes. Those numbers are exact on phones and on the many TV boxes that composite the app at 1080p, but a TV compositing at 4K renders every capped image below its slot and GPU-upscales the result: hero backdrops by 2x, hero logos by ~1.8x, wide episode thumbs by ~1.3x, shelf posters by ~1.13x - the softness reported against the official Plex client in #1697, and the class #860's min-2x-DPR fix could not reach. DevicePerformance now latches a display budget factor - the display's shortest physical axis over 1080, capped at 2x - whenever the image cache budget is applied (startup, post-mount, effects-setting changes). The transcode clamp, the full-tier decode caps and the TV cache bytes all scale by it, so a 4K surface fetches and decodes 4K backdrops and proportionally larger cards. The reduced tier stays pinned to 1.0, and sub-2.5GiB hardware holds the factor at 1.5 so full-budget 4K art (~33MB per decode) cannot starve mid-RAM boxes; latching once per session keeps transcode URLs - and with them the disk cache keys - stable across rotation and rebuilds. Whether a given TV composites at 1080p or 4K decides whether any of this can help, and logs never recorded it: the startup banner and the log-upload header now carry a display line (physical, logical, DPR, latched budget) so uploaded logs answer that question directly. The two pre-existing Windows-host test failures (automotive auto-PiP gate, backdrop temp-dir teardown lock) reproduce unchanged on the base commit.
103 lines
3.4 KiB
Dart
103 lines
3.4 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:plezy/services/device_performance.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
setUp(() {
|
|
DevicePerformance.debugReset();
|
|
addTearDown(DevicePerformance.debugReset);
|
|
});
|
|
|
|
test('concurrent callers wait for hardware detection', () async {
|
|
final detection = Completer<void>();
|
|
DevicePerformance.debugDetectionGate = detection.future;
|
|
|
|
final first = DevicePerformance.getInstance(override: VisualEffectsSetting.reduced);
|
|
var secondCompleted = false;
|
|
final second = DevicePerformance.getInstance();
|
|
unawaited(second.then((_) => secondCompleted = true));
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
expect(secondCompleted, isFalse);
|
|
detection.complete();
|
|
|
|
final instances = await Future.wait([first, second]);
|
|
expect(identical(instances.first, instances.last), isTrue);
|
|
expect(DevicePerformance.isReduced, isTrue);
|
|
});
|
|
|
|
test('failed hardware detection can be retried', () async {
|
|
DevicePerformance.debugDetectionGate = Future<void>.error(StateError('detection failed'));
|
|
|
|
await expectLater(DevicePerformance.getInstance(), throwsStateError);
|
|
|
|
DevicePerformance.debugDetectionGate = null;
|
|
final recovered = await DevicePerformance.getInstance();
|
|
expect(recovered, isNotNull);
|
|
});
|
|
|
|
group('displayBudgetFactor', () {
|
|
void detectAt(double shortestSide) {
|
|
DevicePerformance.debugDisplayShortestSideOverride = shortestSide;
|
|
DevicePerformance.debugDetectDisplayBudget();
|
|
}
|
|
|
|
test('stays 1.0 until a latch runs', () {
|
|
DevicePerformance.debugReset(autoReduced: false, override: VisualEffectsSetting.auto);
|
|
expect(DevicePerformance.displayBudgetFactor(), 1.0);
|
|
});
|
|
|
|
test('scales with the display shortest side up to 2x', () {
|
|
DevicePerformance.debugReset(autoReduced: false, override: VisualEffectsSetting.auto);
|
|
|
|
detectAt(1080);
|
|
expect(DevicePerformance.displayBudgetFactor(), 1.0);
|
|
|
|
detectAt(1440);
|
|
expect(DevicePerformance.displayBudgetFactor(), closeTo(1440 / 1080, 0.001));
|
|
|
|
detectAt(2160);
|
|
expect(DevicePerformance.displayBudgetFactor(), 2.0);
|
|
|
|
// 8K stays at the 2x ceiling.
|
|
detectAt(4320);
|
|
expect(DevicePerformance.displayBudgetFactor(), 2.0);
|
|
});
|
|
|
|
test('sub-1080p displays never shrink the budget below 1.0', () {
|
|
DevicePerformance.debugReset(autoReduced: false, override: VisualEffectsSetting.auto);
|
|
detectAt(720);
|
|
expect(DevicePerformance.displayBudgetFactor(), 1.0);
|
|
});
|
|
|
|
test('mid-RAM hardware holds a 4K budget at 1.5x', () {
|
|
DevicePerformance.debugReset(
|
|
autoReduced: false,
|
|
override: VisualEffectsSetting.auto,
|
|
totalMemBytes: 2400 << 20,
|
|
);
|
|
detectAt(2160);
|
|
expect(DevicePerformance.displayBudgetFactor(), 1.5);
|
|
});
|
|
|
|
test('high-RAM hardware keeps the full 4K budget', () {
|
|
DevicePerformance.debugReset(
|
|
autoReduced: false,
|
|
override: VisualEffectsSetting.auto,
|
|
totalMemBytes: 2870 << 20,
|
|
);
|
|
detectAt(2160);
|
|
expect(DevicePerformance.displayBudgetFactor(), 2.0);
|
|
});
|
|
|
|
test('reduced tier pins the budget to 1.0 even after a 4K latch', () {
|
|
DevicePerformance.debugReset(autoReduced: true, override: VisualEffectsSetting.auto);
|
|
detectAt(2160);
|
|
expect(DevicePerformance.displayBudgetFactor(), 1.0);
|
|
});
|
|
});
|
|
}
|