new scheduling for scopes

This commit is contained in:
Boof2015
2026-07-25 20:16:33 -04:00
parent 6dd82420c8
commit 629be4d9ef
7 changed files with 283 additions and 23 deletions
@@ -5,6 +5,7 @@ import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.roundToLong
internal enum class ScopeMode { internal enum class ScopeMode {
SPECTRUM, SPECTRUM,
@@ -34,6 +35,7 @@ internal object AstraScopeProjection {
const val OSCILLOSCOPE_POINTS = 256 const val OSCILLOSCOPE_POINTS = 256
const val DECAY_PER_FRAME = 0.72f const val DECAY_PER_FRAME = 0.72f
const val REST_EPSILON = 0.004f const val REST_EPSILON = 0.004f
const val MAX_ADAPTIVE_OSCILLOSCOPE_FPS = 90f
private const val MIN_FREQUENCY = 20.0 private const val MIN_FREQUENCY = 20.0
private const val MAX_FREQUENCY = 20_000.0 private const val MAX_FREQUENCY = 20_000.0
@@ -42,10 +44,27 @@ internal object AstraScopeProjection {
fun cadenceMs(requestedMs: Double, refreshRate: Float): Long { fun cadenceMs(requestedMs: Double, refreshRate: Float): Long {
if (requestedMs > 0.0) return max(1L, requestedMs.roundToInt().toLong()) if (requestedMs > 0.0) return max(1L, requestedMs.roundToInt().toLong())
val safeRate = if (refreshRate.isFinite() && refreshRate >= 30f) refreshRate else 60f val safeRate = safeRefreshRate(refreshRate)
return max(1L, (1_000.0 / safeRate).roundToInt().toLong()) return max(1L, (1_000.0 / safeRate).roundToInt().toLong())
} }
fun cadenceNanos(requestedMs: Double, refreshRate: Float): Long {
if (requestedMs > 0.0) {
return max(1L, (requestedMs * NANOS_PER_MILLISECOND).roundToLong())
}
return max(1L, (NANOS_PER_SECOND / safeRefreshRate(refreshRate)).roundToLong())
}
/**
* Follows 60 Hz displays directly and uses the measured 90 fps scope budget
* on faster panels. Power or thermal pressure constrains rendering to 60 fps.
*/
fun adaptiveOscilloscopeFps(refreshRate: Float, constrained: Boolean): Float {
val safeRate = safeRefreshRate(refreshRate)
val limit = if (constrained) 60f else MAX_ADAPTIVE_OSCILLOSCOPE_FPS
return min(safeRate, limit)
}
fun clamp01(value: Float): Float = when { fun clamp01(value: Float): Float = when {
!value.isFinite() || value <= 0f -> 0f !value.isFinite() || value <= 0f -> 0f
value >= 1f -> 1f value >= 1f -> 1f
@@ -115,6 +134,9 @@ internal object AstraScopeProjection {
private fun frequencyAt(t: Double, minFrequency: Double, maxFrequency: Double): Double = private fun frequencyAt(t: Double, minFrequency: Double, maxFrequency: Double): Double =
minFrequency * (maxFrequency / minFrequency).pow(t) minFrequency * (maxFrequency / minFrequency).pow(t)
private fun safeRefreshRate(refreshRate: Float): Float =
if (refreshRate.isFinite() && refreshRate >= 30f) refreshRate else 60f
private fun interpolated( private fun interpolated(
values: java.nio.FloatBuffer, values: java.nio.FloatBuffer,
count: Int, count: Int,
@@ -138,6 +160,9 @@ internal object AstraScopeProjection {
for (index in (start + 1)..end) result = max(result, values.get(index)) for (index in (start + 1)..end) result = max(result, values.get(index))
return result return result
} }
private const val NANOS_PER_MILLISECOND = 1_000_000.0
private const val NANOS_PER_SECOND = 1_000_000_000.0
} }
/** /**
@@ -163,3 +188,29 @@ internal class ScopeRenderGate {
fun currentGeneration(): Int = generation fun currentGeneration(): Int = generation
} }
/**
* Keeps fractional target cadences phase-locked to vsync instead of resetting
* the deadline after each rendered frame (which would turn 90-on-120 into 60).
*/
internal class AdaptiveFrameDeadline {
private var nextFrameAtNanos = Long.MIN_VALUE
fun reset() {
nextFrameAtNanos = Long.MIN_VALUE
}
fun isDue(frameTimeNanos: Long, cadenceNanos: Long, toleranceNanos: Long = 0L): Boolean {
val cadence = max(1L, cadenceNanos)
if (nextFrameAtNanos == Long.MIN_VALUE) {
nextFrameAtNanos = frameTimeNanos + cadence
return true
}
if (frameTimeNanos + max(0L, toleranceNanos) < nextFrameAtNanos) return false
do {
nextFrameAtNanos += cadence
} while (nextFrameAtNanos <= frameTimeNanos)
return true
}
}
@@ -10,9 +10,13 @@ import android.graphics.Path
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.graphics.Shader import android.graphics.Shader
import android.graphics.SurfaceTexture import android.graphics.SurfaceTexture
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.HandlerThread import android.os.HandlerThread
import android.os.PowerManager
import android.os.SystemClock import android.os.SystemClock
import android.view.Choreographer
import android.view.Surface
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.TextureView import android.view.TextureView
@@ -29,12 +33,24 @@ import kotlin.math.roundToInt
private object ScopeRenderDispatcher { private object ScopeRenderDispatcher {
private val thread = HandlerThread("AstraScopeRender").apply { start() } private val thread = HandlerThread("AstraScopeRender").apply { start() }
val handler = Handler(thread.looper) val handler = Handler(thread.looper)
fun postFrameCallback(callback: Choreographer.FrameCallback) {
handler.post {
Choreographer.getInstance().postFrameCallback(callback)
}
}
fun removeFrameCallback(callback: Choreographer.FrameCallback) {
handler.post {
Choreographer.getInstance().removeFrameCallback(callback)
}
}
} }
/** /**
* Android-native spectrum/oscilloscope surface. The shared worker owns every * Android-native spectrum/oscilloscope surface. The shared worker owns every
* FFT read and path mutation; the UI thread only paints the most recently * FFT read, path mutation, and hardware-surface draw; no audio frame crosses
* prepared front path. * React or the JavaScript thread.
*/ */
internal class AstraScopeView( internal class AstraScopeView(
context: Context, context: Context,
@@ -84,16 +100,66 @@ internal class AstraScopeView(
private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG) private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val textureView = TextureView(context) private val textureView = TextureView(context)
private val renderGate = ScopeRenderGate() private val renderGate = ScopeRenderGate()
private val powerManager =
context.getSystemService(Context.POWER_SERVICE) as PowerManager
private var attached = false private var attached = false
private var windowVisible = false private var windowVisible = false
@Volatile @Volatile
private var surfaceAvailable = false private var surfaceAvailable = false
@Volatile
private var renderSurface: Surface? = null
private var scheduledToken = 0 private var scheduledToken = 0
private var lastAnalysisAt = 0L private var lastAnalysisAt = 0L
private var lastDrawAt = 0L private var lastDrawAt = 0L
private var lastAnalysisAtNanos = 0L
private var lastAdaptivePolicyAt = 0L
private var appliedFrameRate = Float.NaN
private var hasNewFrame = false private var hasNewFrame = false
private val adaptiveFrameDeadline = AdaptiveFrameDeadline()
@Volatile @Volatile
private var displayRefreshRate = 60f private var displayRefreshRate = 60f
@Volatile
private var cadenceConstrained = false
private val adaptiveFrameCallback = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
val token = scheduledToken
if (!renderGate.isCurrent(token)) return
refreshAdaptivePolicy(SystemClock.uptimeMillis())
val targetFps = AstraScopeProjection.adaptiveOscilloscopeFps(
displayRefreshRate,
cadenceConstrained
)
val analysisCadence =
AstraScopeProjection.cadenceNanos(analysisFrameMs, targetFps)
val renderThisVsync = adaptiveFrameDeadline.isDue(
frameTimeNanos,
AstraScopeProjection.cadenceNanos(frameMs, targetFps),
VSYNC_TOLERANCE_NANOS
)
val analyzeThisVsync = if (analysisFrameMs <= 0.0) {
renderThisVsync
} else {
isFrameDue(frameTimeNanos, lastAnalysisAtNanos, analysisCadence)
}
if (analyzeThisVsync) {
lastAnalysisAtNanos = frameTimeNanos
hasNewFrame = readScopeFrame()
}
if (hasNewFrame && renderThisVsync) {
if (!renderGate.isCurrent(token)) return
preparePaths()
hasNewFrame = false
publishFrame()
}
if (renderGate.isCurrent(token)) {
Choreographer.getInstance().postFrameCallback(this)
}
}
}
private val renderRunnable = object : Runnable { private val renderRunnable = object : Runnable {
override fun run() { override fun run() {
@@ -134,6 +200,9 @@ internal class AstraScopeView(
textureView.isOpaque = false textureView.isOpaque = false
textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener { textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
renderSurface?.release()
renderSurface = Surface(surface)
appliedFrameRate = Float.NaN
surfaceAvailable = true surfaceAvailable = true
restartRendering() restartRendering()
} }
@@ -146,6 +215,8 @@ internal class AstraScopeView(
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
surfaceAvailable = false surfaceAvailable = false
cancelRendering() cancelRendering()
renderSurface?.release()
renderSurface = null
return true return true
} }
@@ -195,19 +266,33 @@ internal class AstraScopeView(
private fun restartRendering() { private fun restartRendering() {
ScopeRenderDispatcher.handler.removeCallbacks(renderRunnable) ScopeRenderDispatcher.handler.removeCallbacks(renderRunnable)
ScopeRenderDispatcher.removeFrameCallback(adaptiveFrameCallback)
lastAnalysisAt = 0L lastAnalysisAt = 0L
lastDrawAt = 0L lastDrawAt = 0L
lastAnalysisAtNanos = 0L
adaptiveFrameDeadline.reset()
hasNewFrame = false hasNewFrame = false
val eligible = attached && windowVisible && surfaceAvailable && width > 0 && height > 0 val eligible = attached && windowVisible && surfaceAvailable && width > 0 && height > 0
displayRefreshRate = display?.refreshRate ?: 60f refreshAdaptivePolicy(SystemClock.uptimeMillis(), force = true)
scheduledToken = renderGate.update(eligible) scheduledToken = renderGate.update(eligible)
if (eligible) ScopeRenderDispatcher.handler.post(renderRunnable) applyFrameRateVote(
if (eligible && usesAdaptiveVsync()) adaptiveOscilloscopeFps() else 0f
)
if (eligible) {
if (usesAdaptiveVsync()) {
ScopeRenderDispatcher.postFrameCallback(adaptiveFrameCallback)
} else {
ScopeRenderDispatcher.handler.post(renderRunnable)
}
}
} }
private fun cancelRendering() { private fun cancelRendering() {
scheduledToken = renderGate.update(false) scheduledToken = renderGate.update(false)
applyFrameRateVote(0f)
ScopeRenderDispatcher.handler.removeCallbacks(renderRunnable) ScopeRenderDispatcher.handler.removeCallbacks(renderRunnable)
ScopeRenderDispatcher.removeFrameCallback(adaptiveFrameCallback)
} }
private fun readScopeFrame(): Boolean { private fun readScopeFrame(): Boolean {
@@ -295,11 +380,83 @@ internal class AstraScopeView(
publishFrame() publishFrame()
if (peak >= AstraScopeProjection.REST_EPSILON && renderGate.isCurrent(token)) { if (peak >= AstraScopeProjection.REST_EPSILON && renderGate.isCurrent(token)) {
val cadence = AstraScopeProjection.cadenceMs(frameMs, displayRefreshRate) val decayFrameMs = if (frameMs > 0.0) frameMs else FALLBACK_FRAME_MS
val cadence = AstraScopeProjection.cadenceMs(decayFrameMs, displayRefreshRate)
ScopeRenderDispatcher.handler.postDelayed(renderRunnable, cadence) ScopeRenderDispatcher.handler.postDelayed(renderRunnable, cadence)
} }
} }
private fun usesAdaptiveVsync(): Boolean =
requestedActive && mode == ScopeMode.OSCILLOSCOPE && frameMs <= 0.0
private fun refreshAdaptivePolicy(now: Long, force: Boolean = false) {
if (!force && now - lastAdaptivePolicyAt < ADAPTIVE_POLICY_POLL_MS) return
val previousTarget = adaptiveOscilloscopeFps()
lastAdaptivePolicyAt = now
displayRefreshRate = display?.refreshRate ?: 60f
cadenceConstrained = powerManager.isPowerSaveMode ||
(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
powerManager.currentThermalStatus >= PowerManager.THERMAL_STATUS_MODERATE
)
val nextTarget = adaptiveOscilloscopeFps()
if (!force && abs(nextTarget - previousTarget) >= FRAME_RATE_CHANGE_EPSILON) {
textureView.post {
applyFrameRateVote(
if (renderGate.eligible && usesAdaptiveVsync()) nextTarget else 0f
)
}
}
}
private fun adaptiveOscilloscopeFps(): Float =
AstraScopeProjection.adaptiveOscilloscopeFps(
displayRefreshRate,
cadenceConstrained
)
private fun applyFrameRateVote(requestedRate: Float) {
if (
appliedFrameRate.isFinite() &&
abs(appliedFrameRate - requestedRate) < FRAME_RATE_CHANGE_EPSILON
) {
return
}
appliedFrameRate = requestedRate
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
textureView.setRequestedFrameRate(
if (requestedRate > 0f) {
requestedRate
} else {
View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE
}
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val surface = renderSurface ?: return
if (!surface.isValid) return
try {
if (requestedRate > 0f) {
surface.setFrameRate(
requestedRate,
Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE
)
} else {
surface.clearFrameRate()
}
} catch (_: IllegalArgumentException) {
// The TextureView may detach while a frame-rate vote is being updated.
} catch (_: IllegalStateException) {
// The TextureView may detach while a frame-rate vote is being updated.
}
}
}
private fun isFrameDue(now: Long, previous: Long, cadence: Long): Boolean =
previous == 0L || now - previous >= max(1L, cadence - VSYNC_TOLERANCE_NANOS)
private fun preparePaths() { private fun preparePaths() {
val count = renderedPointCount val count = renderedPointCount
val canvasWidth = width.toFloat() val canvasWidth = width.toFloat()
@@ -361,13 +518,21 @@ internal class AstraScopeView(
} }
/** /**
* SurfaceTexture publication does not invalidate the React/Android view tree. * The serialized scope worker prepares and hardware-rasterizes this small
* The serialized scope worker prepares and rasterizes this small transparent * transparent layer. TextureView publication may schedule a platform frame,
* layer; the platform compositor presents it with the retained scene. * but it never schedules React work or rebuilds the surrounding scene.
*/ */
private fun publishFrame() { private fun publishFrame() {
if (!surfaceAvailable) return if (!surfaceAvailable) return
val canvas = textureView.lockCanvas() ?: return val surface = renderSurface ?: return
if (!surface.isValid) return
val canvas = try {
surface.lockHardwareCanvas()
} catch (_: IllegalArgumentException) {
return
} catch (_: IllegalStateException) {
return
}
try { try {
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
synchronized(pathLock) { synchronized(pathLock) {
@@ -378,7 +543,13 @@ internal class AstraScopeView(
canvas.drawPath(linePaths[frontPath], strokePaint) canvas.drawPath(linePaths[frontPath], strokePaint)
} }
} finally { } finally {
textureView.unlockCanvasAndPost(canvas) try {
surface.unlockCanvasAndPost(canvas)
} catch (_: IllegalArgumentException) {
// The TextureView may detach between lock and post.
} catch (_: IllegalStateException) {
// The TextureView may detach between lock and post.
}
} }
} }
@@ -447,5 +618,9 @@ internal class AstraScopeView(
companion object { companion object {
private const val MAX_RENDER_POINTS = 512 private const val MAX_RENDER_POINTS = 512
private const val ADAPTIVE_POLICY_POLL_MS = 1_000L
private const val VSYNC_TOLERANCE_NANOS = 1_000_000L
private const val FALLBACK_FRAME_MS = 1_000.0 / 60.0
private const val FRAME_RATE_CHANGE_EPSILON = 0.5f
} }
} }
@@ -46,6 +46,37 @@ class AstraScopeProjectionTest {
assertEquals(16L, AstraScopeProjection.cadenceMs(16.0, 120f)) assertEquals(16L, AstraScopeProjection.cadenceMs(16.0, 120f))
assertEquals(8L, AstraScopeProjection.cadenceMs(0.0, 120f)) assertEquals(8L, AstraScopeProjection.cadenceMs(0.0, 120f))
assertEquals(17L, AstraScopeProjection.cadenceMs(0.0, 60f)) assertEquals(17L, AstraScopeProjection.cadenceMs(0.0, 60f))
assertEquals(16_000_000L, AstraScopeProjection.cadenceNanos(16.0, 120f))
assertEquals(8_333_333L, AstraScopeProjection.cadenceNanos(0.0, 120f))
assertEquals(11_111_111L, AstraScopeProjection.cadenceNanos(0.0, 90f))
}
@Test
fun adaptiveOscilloscopeUsesNinetyFpsOnFastDisplaysAndSixtyWhenConstrained() {
assertEquals(60f, AstraScopeProjection.adaptiveOscilloscopeFps(60f, false), 0f)
assertEquals(90f, AstraScopeProjection.adaptiveOscilloscopeFps(90f, false), 0f)
assertEquals(90f, AstraScopeProjection.adaptiveOscilloscopeFps(120f, false), 0f)
assertEquals(90f, AstraScopeProjection.adaptiveOscilloscopeFps(144f, false), 0f)
assertEquals(60f, AstraScopeProjection.adaptiveOscilloscopeFps(120f, true), 0f)
assertEquals(60f, AstraScopeProjection.adaptiveOscilloscopeFps(90f, true), 0f)
assertEquals(60f, AstraScopeProjection.adaptiveOscilloscopeFps(Float.NaN, false), 0f)
}
@Test
fun fractionalDeadlineProducesNineFramesAcrossTwelve120HzVsyncs() {
val deadline = AdaptiveFrameDeadline()
val vsyncCadence = AstraScopeProjection.cadenceNanos(0.0, 120f)
val renderCadence = AstraScopeProjection.cadenceNanos(0.0, 90f)
var rendered = 0
repeat(12) { frame ->
if (deadline.isDue(frame * vsyncCadence, renderCadence, 500_000L)) rendered += 1
}
assertEquals(9, rendered)
deadline.reset()
assertTrue(deadline.isDue(200_000_000L, renderCadence))
} }
@Test @Test
+1
View File
@@ -82,6 +82,7 @@ export interface AstraScopeViewProps extends ViewProps {
source?: 'pre' | 'post'; source?: 'pre' | 'post';
active: boolean; active: boolean;
reducedMotion?: boolean; reducedMotion?: boolean;
/** Positive milliseconds for a fixed cadence; 0 enables native display sync. */
frameMs: number; frameMs: number;
analysisFrameMs?: number; analysisFrameMs?: number;
smoothing?: number; smoothing?: number;
+2 -2
View File
@@ -8,7 +8,7 @@ interface OscilloscopeWaveProps {
active: boolean; active: boolean;
width: number; width: number;
height: number; height: number;
/** Live render cadence; 0 means display-sync. */ /** Live render cadence; 0 uses native adaptive display synchronization. */
frameMs?: number; frameMs?: number;
color?: string; color?: string;
lineWidth?: number; lineWidth?: number;
@@ -27,7 +27,7 @@ export function OscilloscopeWave({
active, active,
width, width,
height, height,
frameMs = 16, frameMs = 0,
color: colorProp, color: colorProp,
lineWidth = 2, lineWidth = 2,
glow = false, glow = false,
+6 -5
View File
@@ -12,9 +12,10 @@ import { createThemedStyles, useColors } from '@/theme/themed';
import { useScopeActive } from '@/scope/scopeStore'; import { useScopeActive } from '@/scope/scopeStore';
const CANVAS_HEIGHT = 96; const CANVAS_HEIGHT = 96;
// 60fps cap: display-sync (0) pinned the JS thread at 120Hz on high-refresh // Spectrum remains intentionally capped; native oscilloscope rendering follows
// devices and starved every other animation. // the display up to 90 Hz and falls back to at most 60 Hz under system pressure.
const STAGE_FRAME_MS = 16; const SPECTRUM_FRAME_MS = 16;
const OSCILLOSCOPE_FRAME_MS = 0;
type Mode = 'spectrum' | 'scope'; type Mode = 'spectrum' | 'scope';
@@ -73,7 +74,7 @@ export function Visualizer({
{mode === 'spectrum' ? ( {mode === 'spectrum' ? (
<SpectrumCurve <SpectrumCurve
active={spectrumActive} active={spectrumActive}
frameMs={STAGE_FRAME_MS} frameMs={SPECTRUM_FRAME_MS}
smoothing={spectrumSmoothing} smoothing={spectrumSmoothing}
width={width} width={width}
height={height} height={height}
@@ -83,7 +84,7 @@ export function Visualizer({
) : ( ) : (
<OscilloscopeWave <OscilloscopeWave
active={scopeWaveActive} active={scopeWaveActive}
frameMs={STAGE_FRAME_MS} frameMs={OSCILLOSCOPE_FRAME_MS}
width={width} width={width}
height={height} height={height}
glow glow
+6 -5
View File
@@ -16,9 +16,10 @@ import { useScopeActive } from '@/scope/scopeStore';
import { spacing } from '@/theme'; import { spacing } from '@/theme';
import { createThemedStyles } from '@/theme/themed'; import { createThemedStyles } from '@/theme/themed';
// 60fps cap, matching Visualizer — display-sync starved the JS thread on // Spectrum stays capped at 60 fps. The native oscilloscope follows the display
// high-refresh devices. // up to 90 Hz and falls back to at most 60 Hz under system pressure.
const STAGE_FRAME_MS = 16; const SPECTRUM_FRAME_MS = 16;
const OSCILLOSCOPE_FRAME_MS = 0;
// The rack artwork is atmosphere, not a second cover card. It bleeds beyond // The rack artwork is atmosphere, not a second cover card. It bleeds beyond
// the old frame, stays softly defocused, and contributes restrained color. // the old frame, stays softly defocused, and contributes restrained color.
const BACKDROP_BLUR_RADIUS = 10; const BACKDROP_BLUR_RADIUS = 10;
@@ -111,7 +112,7 @@ export function ScopeRack({
<View style={[styles.strips, { width, left: (size - width) / 2 }]}> <View style={[styles.strips, { width, left: (size - width) / 2 }]}>
<OscilloscopeWave <OscilloscopeWave
active={active} active={active}
frameMs={STAGE_FRAME_MS} frameMs={OSCILLOSCOPE_FRAME_MS}
width={width} width={width}
height={stripHeight} height={stripHeight}
glow glow
@@ -120,7 +121,7 @@ export function ScopeRack({
/> />
<SpectrumCurve <SpectrumCurve
active={active} active={active}
frameMs={STAGE_FRAME_MS} frameMs={SPECTRUM_FRAME_MS}
smoothing={spectrumSmoothing} smoothing={spectrumSmoothing}
width={width} width={width}
height={stripHeight} height={stripHeight}