feat(clients): render-scale setting on every client — shared punktfunk_core::render_scale

Client-side supersampling/downscaling: the client asks the host to render
and encode at chosen-resolution × scale (the host does no scaling) and the
presenter rescales the decoded frame to the display. >1 supersamples for
sharpness; <1 lightens the host GPU and the link. Default 1.0 = Native, the
prior behavior.

The geometry lives once in punktfunk_core::render_scale (multiply, preserve
aspect ratio, floor to even, clamp to the codec's per-axis ceiling — 4096
for H.264, 8192 otherwise), the Rust twin of the Apple client's
RenderScale.swift, consumed by the native session client, the presenter's
match-window path, the Windows/Linux settings UIs, Decky, and Android
(settings + host connect + unit test).

Implemented and platform-verified by the Apple-client-features session
(Linux+Android+Apple green there); the punktfunk-core wiring
(pub mod render_scale) is restored here after being lost in a working-tree
reconciliation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 17:14:57 +02:00
parent 600693914f
commit 871ebb31ce
15 changed files with 450 additions and 13 deletions
+2 -2
View File
@@ -896,8 +896,8 @@ class Plugin:
except (OSError, json.JSONDecodeError):
# The client's own defaults (native display, host-default bitrate, auto pad).
return {
"width": 0, "height": 0, "refresh_hz": 0, "bitrate_kbps": 0,
"codec": "auto", "gamepad": "auto", "compositor": "auto",
"width": 0, "height": 0, "refresh_hz": 0, "render_scale": 1.0,
"bitrate_kbps": 0, "codec": "auto", "gamepad": "auto", "compositor": "auto",
"inhibit_shortcuts": True, "mic_enabled": False,
}
+1
View File
@@ -99,6 +99,7 @@ export interface StreamSettings {
width: number; // 0 = native
height: number; // 0 = native
refresh_hz: number; // 0 = native
render_scale?: number; // render-resolution multiplier; 1.0 = native (absent in pre-scale files)
bitrate_kbps: number; // 0 = host default
codec?: string; // "auto" | "hevc" | "h264" | "av1" — soft preference (absent in pre-codec files)
gamepad: string; // "auto" | "xbox360" | "xboxone" | "dualsense" | "dualshock4" | "steamdeck"
+22
View File
@@ -25,6 +25,10 @@ const RESOLUTIONS: [number, number, string][] = [
[2560, 1440, "2560 × 1440"],
];
const REFRESH = [0, 30, 60, 90, 120];
// Render-resolution multipliers (mirrors punktfunk_core::render_scale::PRESETS). 1.0 = native.
const RENDER_SCALES = [0.5, 0.67, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0];
const renderScaleLabel = (x: number): string =>
x === 1 ? "Native (1×)" : x > 1 ? `${x}× · supersample` : `${x}×`;
const GAMEPADS = ["auto", "xbox360", "xboxone", "dualsense", "dualshock4", "steamdeck"];
const GAMEPAD_LABELS: Record<string, string> = {
auto: "Automatic",
@@ -106,6 +110,24 @@ export const SettingsSection: FC = () => {
</div>
</RowActions>
</Field>
<Field
label="Render scale"
description="Supersample for sharpness (> 1×, more bandwidth) or render below native (< 1×) — the Deck resamples to its screen"
childrenContainerWidth="max"
>
<RowActions>
<div style={selectShell}>
<Dropdown
rgOptions={RENDER_SCALES.map((x) => ({ data: x, label: renderScaleLabel(x) }))}
// Snap the stored value to the nearest preset so the dropdown always shows a match.
selectedOption={RENDER_SCALES.reduce((best, x) =>
Math.abs(x - (s.render_scale ?? 1)) < Math.abs(best - (s.render_scale ?? 1)) ? x : best,
)}
onChange={(o) => patch({ render_scale: o.data as number })}
/>
</div>
</RowActions>
</Field>
<SliderField
label="Bitrate"
description="Mbit/s · 0 = host default"