feat(kiosk): graphics tiers + Settings knob; network map kiosk mode
Demo images / Build & push demo images (push) Successful in 3m51s
Demo images / Build & push demo images (push) Successful in 3m51s
The animated federation map froze the framework-pt 4K TV: the launcher held every machine to the HD 5500-era choppy-audio flags (single raster thread, GpuRasterization banned) while the map wrote SVG attrs at 60fps. - Launcher: two flag tiers. legacy = the proven conservative set; modern (Intel gen8+, 'NNth Gen' models, AMD Ryzen) = default raster threads + GPU rasterization. Classified from /proc/cpuinfo (11 model strings covered by tests in-session); KIOSK_GRAPHICS=performance|quality in kiosk-display.conf overrides; headless unchanged. Reaches deployed kiosks via the include_str! self-heal, same as the vsync fix. - system.kiosk-display.get/set: carries a 'graphics' field alongside 'preset'; setting one no longer clobbers the other. - Settings → Display: Graphics picker (Auto / Compatibility / Quality). - NetworkMap3D: kiosks default to the 2D projection (remembered toggle still works) and tick at half rate with carried-over deltas — same spin speed, half the paint cost. - Changelog: curated Unreleased notes for all of the above + the gate frame-embedding fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6672d978f7
commit
9243babcdb
@@ -853,7 +853,25 @@ function render() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Kiosk TVs run the kiosk image's conservative rasterizer (single raster
|
||||
* thread, GPU raster off — the June choppy-audio tuning), so a 60fps
|
||||
* SVG-attribute animation at 4K froze the browser outright (framework-pt,
|
||||
* 2026-08-12). On kiosks the ticker renders every OTHER frame (~30fps at a
|
||||
* 60Hz panel) with the skipped frame's delta carried over so spin speed is
|
||||
* unchanged — at couch distance the half rate is invisible, the paint cost
|
||||
* halves. */
|
||||
const kioskLowPower =
|
||||
typeof document !== 'undefined' && document.documentElement.classList.contains('kiosk-mode')
|
||||
let lowPowerPhase = 0
|
||||
let carriedDeltaMS = 0
|
||||
|
||||
function tick(_time: number, deltaMS: number) {
|
||||
if (kioskLowPower) {
|
||||
carriedDeltaMS += deltaMS
|
||||
if ((lowPowerPhase++ & 1) === 1) return
|
||||
deltaMS = carriedDeltaMS
|
||||
carriedDeltaMS = 0
|
||||
}
|
||||
elapsed += deltaMS / 1000
|
||||
if (!dragging) cam.rotY += cam.spin * (deltaMS / 1000)
|
||||
render()
|
||||
@@ -1169,11 +1187,14 @@ function initialBuild() {
|
||||
applyGraph()
|
||||
|
||||
// First measurement decides the default projection: saved preference wins,
|
||||
// otherwise portrait containers (phone/companion) open in the flat 2D view.
|
||||
// otherwise portrait containers (phone/companion) AND kiosk TVs open in the
|
||||
// flat 2D view — the depth view stays one tap away and is remembered. The
|
||||
// kiosk default exists for the same reason as the half-rate ticker above.
|
||||
modeInitialized = true
|
||||
let saved: string | null = null
|
||||
try { saved = localStorage.getItem('federation-map-projection') } catch { /* private mode */ }
|
||||
viewMode.value = saved === '2d' || saved === '3d' ? saved : (height > width ? '2d' : '3d')
|
||||
viewMode.value =
|
||||
saved === '2d' || saved === '3d' ? saved : (kioskLowPower || height > width ? '2d' : '3d')
|
||||
applyMode(viewMode.value, false)
|
||||
|
||||
if (staticMode) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { rpcClient } from '@/api/rpc-client'
|
||||
// have a kiosk display (has_kiosk from the backend).
|
||||
const hasKiosk = ref(false)
|
||||
const preset = ref('auto')
|
||||
const graphics = ref('auto')
|
||||
const applying = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
@@ -32,11 +33,35 @@ const presets = [
|
||||
},
|
||||
]
|
||||
|
||||
// Graphics tier: which browser rendering flags the on-screen display runs
|
||||
// with. Auto classifies the hardware (older kiosk boxes keep the proven
|
||||
// conservative flags; modern chips get GPU rendering); the two overrides
|
||||
// exist for troubleshooting and unclassified hardware.
|
||||
const graphicsModes = [
|
||||
{
|
||||
id: 'auto',
|
||||
label: 'Auto (recommended)',
|
||||
description: 'Detect this machine’s graphics hardware and pick the right rendering mode for it.',
|
||||
},
|
||||
{
|
||||
id: 'performance',
|
||||
label: 'Compatibility',
|
||||
description: 'Most conservative rendering — use if the screen stutters, tears, or the audio crackles.',
|
||||
},
|
||||
{
|
||||
id: 'quality',
|
||||
label: 'Quality',
|
||||
description: 'Full GPU rendering — smoothest animations on capable hardware. If unsure, use Auto.',
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await rpcClient.call<{ has_kiosk: boolean; preset: string }>({ method: 'system.kiosk-display.get' })
|
||||
const res = await rpcClient.call<{ has_kiosk: boolean; preset: string; graphics?: string }>({ method: 'system.kiosk-display.get' })
|
||||
hasKiosk.value = res.has_kiosk
|
||||
preset.value = res.preset
|
||||
// Older backend without the graphics field: hide nothing, default Auto.
|
||||
graphics.value = res.graphics ?? 'auto'
|
||||
} catch { /* backend without the RPC — leave the section hidden */ }
|
||||
})
|
||||
|
||||
@@ -55,6 +80,22 @@ async function apply(id: string) {
|
||||
applying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function applyGraphics(id: string) {
|
||||
if (applying.value || id === graphics.value) return
|
||||
applying.value = true
|
||||
error.value = ''
|
||||
const prev = graphics.value
|
||||
graphics.value = id
|
||||
try {
|
||||
await rpcClient.call({ method: 'system.kiosk-display.set', params: { graphics: id }, timeout: 20000 })
|
||||
} catch (e: unknown) {
|
||||
graphics.value = prev
|
||||
error.value = e instanceof Error ? e.message : 'Failed to apply graphics setting'
|
||||
} finally {
|
||||
applying.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -77,6 +118,25 @@ async function apply(id: string) {
|
||||
<p class="text-sm text-white/60">{{ p.description }}</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-white/96 mt-8 mb-2">Graphics</h3>
|
||||
<p class="text-sm text-white/60 mb-6">
|
||||
How the on-screen display uses this machine’s graphics hardware. Changing this restarts the on-screen display.
|
||||
</p>
|
||||
<div data-controller-container tabindex="0" class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<button
|
||||
v-for="g in graphicsModes"
|
||||
:key="g.id"
|
||||
:disabled="applying"
|
||||
@click="applyGraphics(g.id)"
|
||||
class="path-option-card text-left p-5 disabled:opacity-60"
|
||||
:class="{ 'path-option-card--selected': graphics === g.id }"
|
||||
>
|
||||
<div class="font-medium text-white/90 mb-1">{{ g.label }}</div>
|
||||
<p class="text-sm text-white/60">{{ g.description }}</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="mt-4 alert-error text-sm">{{ error }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user