fps controls

This commit is contained in:
Boof2015
2026-03-28 13:25:44 -04:00
parent 60e8b921c0
commit 0d5e487385
10 changed files with 475 additions and 18 deletions
+42
View File
@@ -1,8 +1,10 @@
import { useEffect, useLayoutEffect, useRef, type CSSProperties, type JSX } from 'react'
import { useAudioStore } from '../stores/audioStore'
import { usePerformanceStore } from '../stores/performanceStore'
import { useSettingsStore } from '../stores/settingsStore'
import { useThemeStore, PRESETS, PRESET_IDS } from '../stores/themeStore'
import type { ScopeKind } from '../../types/scope'
import { VISUALIZER_FRAME_TARGETS, type VisualizerFrameTarget } from '../../types/performance'
import { SCOPE_KINDS } from '../../types/scope'
const SCOPE_LABELS: Record<ScopeKind, string> = {
@@ -20,11 +22,23 @@ interface BottomBarProps {
onHeightChange?: (height: number) => void
}
const FRAME_TARGET_LABELS: Record<VisualizerFrameTarget, string> = {
10: '10',
30: '30',
60: '60',
120: '120',
144: '144',
'display-sync': 'Sync',
}
export default function BottomBar({ onClose, onHeightChange }: BottomBarProps): JSX.Element {
const rootRef = useRef<HTMLDivElement | null>(null)
const hiddenScopes = useSettingsStore((s) => s.hiddenScopes)
const toggleScope = useSettingsStore((s) => s.toggleScope)
const frameTarget = usePerformanceStore((s) => s.frameTarget)
const dockedRenderFps = usePerformanceStore((s) => s.dockedRenderFps)
const setFrameTarget = usePerformanceStore((s) => s.setFrameTarget)
const { presetId, accent, setPreset, setCustomAccent, customAccent } = useThemeStore()
const {
@@ -113,6 +127,7 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
: 'Idle'
const trimPercent = Math.min(100, Math.max(0, ((inputGainDb + 12) / 24) * 100))
const roundedDockedRenderFps = Math.max(0, Math.round(dockedRenderFps))
return (
<div className="bottom-bar" ref={rootRef}>
@@ -226,6 +241,33 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
<div className="bottom-bar__divider" />
<section className="bottom-bar__section bottom-bar__section--performance">
<div className="bottom-bar__section-title">Performance</div>
<div className="bottom-bar__section-body">
<div className="bottom-bar__inline bottom-bar__inline--performance">
<div className="bottom-bar__inline bottom-bar__inline--chips">
{VISUALIZER_FRAME_TARGETS.map((target) => (
<button
key={String(target)}
type="button"
className={`settings-chip ${frameTarget === target ? 'is-active' : ''}`.trim()}
onClick={() => setFrameTarget(target)}
title={target === 'display-sync' ? 'Display Sync' : `Cap visualizers at ${target} FPS`}
>
{FRAME_TARGET_LABELS[target]}
</button>
))}
</div>
<div className="settings-status-pill bottom-bar__fps-pill" title="Docked visualizer render FPS">
<span>{roundedDockedRenderFps} FPS</span>
</div>
</div>
</div>
</section>
<div className="bottom-bar__divider" />
<section className="bottom-bar__section bottom-bar__section--trim">
<div className="bottom-bar__section-title">Trim</div>
<div className="bottom-bar__section-body">
+17 -12
View File
@@ -1,7 +1,9 @@
import { useEffect, useMemo, useRef } from 'react'
import { audioRouter } from '../audio/AudioRouter'
import { usePerformanceStore } from '../stores/performanceStore'
import { useSettingsStore } from '../stores/settingsStore'
import { useThemeStore } from '../stores/themeStore'
import { FrameScheduler } from '../visualizers/frameScheduler'
import type {
ScopePopoutAudioBatch,
ScopePopoutSessionState,
@@ -59,14 +61,20 @@ export default function ScopePopoutBridge(): null {
const updatePopoutBounds = useSettingsStore((s) => s.updatePopoutBounds)
const updateScopeSettings = useSettingsStore((s) => s.updateScopeSettings)
const accent = useThemeStore((s) => s.accent)
const frameTarget = usePerformanceStore((s) => s.frameTarget)
const activePopoutKinds = useMemo(
() => SCOPE_KINDS.filter((kind) => scopePopouts[kind]?.poppedOut && !hiddenScopes.has(kind)),
[hiddenScopes, scopePopouts],
)
const flushScheduler = useMemo(() => new FrameScheduler({ frameTarget }), [])
const activePopoutKindsRef = useRef<ScopeKind[]>(activePopoutKinds)
const sessionStateRef = useRef(audioRouter.getSessionState())
useEffect(() => {
flushScheduler.setFrameTarget(frameTarget)
}, [flushScheduler, frameTarget])
useEffect(() => {
activePopoutKindsRef.current = activePopoutKinds
}, [activePopoutKinds])
@@ -153,10 +161,9 @@ export default function ScopePopoutBridge(): null {
}, [activePopoutKinds])
useEffect(() => {
let frameId = 0
let unsubscribeFlush: (() => void) | null = null
const flushFrame = (): void => {
frameId = 0
if (!sessionStateRef.current.capturing || activePopoutKindsRef.current.length === 0) {
return
}
@@ -167,22 +174,20 @@ export default function ScopePopoutBridge(): null {
window.electronAPI.sendScopePopoutAudio(kind, batch)
}
}
frameId = window.requestAnimationFrame(flushFrame)
}
const syncFlushLoop = (): void => {
const shouldRun = sessionStateRef.current.capturing && activePopoutKindsRef.current.length > 0
if (!shouldRun) {
if (frameId) {
window.cancelAnimationFrame(frameId)
frameId = 0
if (unsubscribeFlush) {
unsubscribeFlush()
unsubscribeFlush = null
}
return
}
if (!frameId) {
frameId = window.requestAnimationFrame(flushFrame)
if (!unsubscribeFlush) {
unsubscribeFlush = flushScheduler.subscribe(flushFrame)
}
}
@@ -194,12 +199,12 @@ export default function ScopePopoutBridge(): null {
syncFlushLoop()
return () => {
if (frameId) {
window.cancelAnimationFrame(frameId)
if (unsubscribeFlush) {
unsubscribeFlush()
}
unsubscribeSession()
}
}, [activePopoutKinds])
}, [activePopoutKinds, flushScheduler])
useEffect(() => {
return audioRouter.subscribeToSessionChanges((state) => {
+19 -1
View File
@@ -6,6 +6,7 @@ import type { WindowBounds } from '../../types/popout'
import ScopeModule from './ScopeModule'
import { buildAnalyzerGridTemplateColumns } from '../analyzerLayout'
import { audioRouter } from '../audio/AudioRouter'
import { usePerformanceStore } from '../stores/performanceStore'
import { FrameScheduler } from '../visualizers/frameScheduler'
export default function Strip(): JSX.Element {
@@ -17,7 +18,9 @@ export default function Strip(): JSX.Element {
const setScopeWidthWeight = useSettingsStore((s) => s.setScopeWidthWeight)
const popOutScope = useSettingsStore((s) => s.popOutScope)
const accent = useThemeStore((s) => s.accent)
const frameScheduler = useMemo(() => new FrameScheduler(), [])
const frameTarget = usePerformanceStore((s) => s.frameTarget)
const setDockedRenderFps = usePerformanceStore((s) => s.setDockedRenderFps)
const frameScheduler = useMemo(() => new FrameScheduler({ frameTarget }), [])
const stripRef = useRef<HTMLDivElement>(null)
const gridRef = useRef<HTMLDivElement>(null)
const scopeRefs = useRef<Partial<Record<ScopeKind, HTMLDivElement | null>>>({})
@@ -35,6 +38,21 @@ export default function Strip(): JSX.Element {
return { gridTemplateColumns } as CSSProperties
}, [gridTemplateColumns])
useEffect(() => {
frameScheduler.setFrameTarget(frameTarget)
}, [frameScheduler, frameTarget])
useEffect(() => {
const unsubscribe = frameScheduler.subscribeToActualFps((fps) => {
setDockedRenderFps(fps)
})
return () => {
unsubscribe()
setDockedRenderFps(0)
}
}, [frameScheduler, setDockedRenderFps])
const updateHandleOffsets = useCallback((): void => {
if (dockedScopes.length < 2) {
setHandleOffsets([])