Auto commit (MindMachine) Mon Apr 13 06:01:01 PM CDT 2026

This commit is contained in:
Tretzi
2026-04-13 18:01:01 -05:00
parent adbfa0754b
commit de6e9d6230

View File

@@ -1,8 +1,16 @@
package solutions.tretter.mindmachine package solutions.tretter.mindmachine
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.graphics.Canvas
import android.graphics.Paint
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.util.Log
import android.view.View
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
@@ -84,6 +92,7 @@ import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
@@ -592,12 +601,6 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry:
brightnessPercent = ui.settings.immersiveBrightnessPercent, brightnessPercent = ui.settings.immersiveBrightnessPercent,
) )
val flashFrame by rememberSplitFlashFrame(
isRunning = ui.runtimeState == RuntimeState.RUNNING,
flashOnMs = ui.runtimeParams.flashOnMs,
flashOffMs = ui.runtimeParams.flashOffMs,
)
val progress = sessionProgressFraction( val progress = sessionProgressFraction(
durationSec = ui.timeline.durationSec, durationSec = ui.timeline.durationSec,
remainingSec = ui.remainingSec, remainingSec = ui.remainingSec,
@@ -619,20 +622,19 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry:
} }
) { ) {
if (isVisualMode) { if (isVisualMode) {
Row(Modifier.fillMaxSize()) { AndroidView(
Box( modifier = Modifier.fillMaxSize(),
modifier = Modifier factory = { ctx ->
.weight(1f) SplitFlashRenderView(ctx).apply {
.fillMaxHeight() setRunning(ui.runtimeState == RuntimeState.RUNNING)
.background(flashFrame.left.toComposeColor()) updateIntervals(ui.runtimeParams.flashOnMs, ui.runtimeParams.flashOffMs)
)
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.background(flashFrame.right.toComposeColor())
)
} }
},
update = { view ->
view.updateIntervals(ui.runtimeParams.flashOnMs, ui.runtimeParams.flashOffMs)
view.setRunning(ui.runtimeState == RuntimeState.RUNNING)
}
)
} }
if (ui.runtimeState == RuntimeState.COUNTDOWN) { if (ui.runtimeState == RuntimeState.COUNTDOWN) {
@@ -792,6 +794,150 @@ private fun ApplyImmersiveMode(enabled: Boolean, revealSignal: Int) {
} }
} }
private class SplitFlashRenderView(context: Context) : View(context) {
private val timingLogTag = "MindMachineTiming"
private val handler = Handler(Looper.getMainLooper())
private val leftPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val rightPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private var isRunning = false
private var phase = 1 // start black
private var rawOnMs = 167L
private var rawOffMs = 167L
private var onMs = 167L
private var offMs = 167L
private var started = false
private var startUptimeMs = 0L
private var nextToggleUptimeMs = 0L
private var toggleCount = 0L
private var lateToggleCount = 0L
private var maxLatenessMs = 0L
private val toggleRunnable = object : Runnable {
override fun run() {
if (!isRunning) return
val now = SystemClock.uptimeMillis()
val latenessMs = (now - nextToggleUptimeMs).coerceAtLeast(0L)
val previousPhase = phase
if (latenessMs > 0L) {
lateToggleCount += 1
maxLatenessMs = maxOf(maxLatenessMs, latenessMs)
}
phase = (phase + 1) % 4
toggleCount += 1
invalidate()
val nextDuration = if (phase == 0 || phase == 2) onMs else offMs
nextToggleUptimeMs += nextDuration
val shouldLogToggle = toggleCount <= 20 || latenessMs > framePeriodMs() || toggleCount % 60L == 0L
if (shouldLogToggle) {
Log.d(
timingLogTag,
"toggle#$toggleCount prev=${phaseName(previousPhase)} next=${phaseName(phase)} " +
"scheduledMs=${nextToggleUptimeMs - startUptimeMs - nextDuration} " +
"actualMs=${now - startUptimeMs} latenessMs=$latenessMs " +
"lateCount=$lateToggleCount maxLateMs=$maxLatenessMs"
)
}
scheduleNext()
}
}
fun updateIntervals(flashOnMs: Int, flashOffMs: Int) {
rawOnMs = flashOnMs.coerceIn(50, 2000).toLong()
rawOffMs = flashOffMs.coerceIn(50, 2000).toLong()
val frameMs = framePeriodMs().coerceAtLeast(1L)
onMs = snapToFrameMs(rawOnMs, frameMs)
offMs = snapToFrameMs(rawOffMs, frameMs)
Log.d(
timingLogTag,
"config onMs=$rawOnMs offMs=$rawOffMs refreshHz=${"%.2f".format(refreshRate())} frameMs=$frameMs snappedOnMs=$onMs snappedOffMs=$offMs"
)
if (isRunning && started) {
scheduleFromNow()
}
}
fun setRunning(running: Boolean) {
if (isRunning == running) return
isRunning = running
handler.removeCallbacks(toggleRunnable)
if (!running) {
started = false
phase = 1
invalidate()
return
}
started = true
phase = 0
startUptimeMs = SystemClock.uptimeMillis()
nextToggleUptimeMs = startUptimeMs + onMs
toggleCount = 0L
lateToggleCount = 0L
maxLatenessMs = 0L
Log.d(
timingLogTag,
"session-start phase=${phaseName(phase)} refreshHz=${"%.2f".format(refreshRate())} frameMs=${framePeriodMs()} nowMs=$startUptimeMs"
)
invalidate()
scheduleNext()
}
override fun onDetachedFromWindow() {
handler.removeCallbacks(toggleRunnable)
super.onDetachedFromWindow()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val mid = width / 2f
val (leftColor, rightColor) = colorsForPhase(phase)
leftPaint.color = leftColor
rightPaint.color = rightColor
canvas.drawRect(0f, 0f, mid, height.toFloat(), leftPaint)
canvas.drawRect(mid, 0f, width.toFloat(), height.toFloat(), rightPaint)
}
private fun scheduleNext() {
handler.removeCallbacks(toggleRunnable)
handler.postAtTime(toggleRunnable, nextToggleUptimeMs)
}
private fun scheduleFromNow() {
val now = SystemClock.uptimeMillis()
nextToggleUptimeMs = now + if (phase == 0 || phase == 2) onMs else offMs
scheduleNext()
}
private fun colorsForPhase(phase: Int): Pair<Int, Int> = when (phase) {
0 -> Pair(android.graphics.Color.RED, android.graphics.Color.GREEN)
2 -> Pair(android.graphics.Color.GREEN, android.graphics.Color.RED)
else -> Pair(android.graphics.Color.BLACK, android.graphics.Color.BLACK)
}
private fun phaseName(phase: Int): String = when (phase) {
0 -> "RED_GREEN"
1 -> "BLACK"
2 -> "GREEN_RED"
else -> "BLACK"
}
private fun refreshRate(): Float = display?.refreshRate?.takeIf { it > 0f } ?: 60f
private fun framePeriodMs(): Long = (1000f / refreshRate()).roundToLong().coerceAtLeast(1L)
private fun snapToFrameMs(valueMs: Long, frameMs: Long): Long = ((valueMs + frameMs / 2) / frameMs).coerceAtLeast(1L) * frameMs
}
private fun FlashColor.toComposeColor(): Color = when (this) { private fun FlashColor.toComposeColor(): Color = when (this) {
FlashColor.RED -> Color.Red FlashColor.RED -> Color.Red
FlashColor.GREEN -> Color.Green FlashColor.GREEN -> Color.Green
@@ -815,6 +961,8 @@ private fun rememberSplitFlashFrame(
(1_000_000_000.0 / refreshRateHz.coerceAtLeast(1f)).roundToLong().coerceAtLeast(1L) (1_000_000_000.0 / refreshRateHz.coerceAtLeast(1f)).roundToLong().coerceAtLeast(1L)
} }
val timingLogTag = "MindMachineTiming"
return produceState(initialValue = SplitFlashFrame(FlashColor.BLACK, FlashColor.BLACK), isRunning, framePeriodNanos) { return produceState(initialValue = SplitFlashFrame(FlashColor.BLACK, FlashColor.BLACK), isRunning, framePeriodNanos) {
if (!isRunning) { if (!isRunning) {
value = SplitFlashFrame(FlashColor.BLACK, FlashColor.BLACK) value = SplitFlashFrame(FlashColor.BLACK, FlashColor.BLACK)
@@ -832,6 +980,20 @@ private fun rememberSplitFlashFrame(
var nextToggleNanos = 0L var nextToggleNanos = 0L
var onNanos = 0L var onNanos = 0L
var offNanos = 0L var offNanos = 0L
var lastLoggedFlashOnMs = -1
var lastLoggedFlashOffMs = -1
var toggleCount = 0L
var lateToggleCount = 0L
var maxLatenessNanos = 0L
var lastToggleAtNanos = 0L
var sessionStartNanos = 0L
fun phaseName(p: Int): String = when (p) {
0 -> "RED_GREEN"
1 -> "BLACK"
2 -> "GREEN_RED"
else -> "BLACK"
}
fun frameForPhase(p: Int): SplitFlashFrame = when (p) { fun frameForPhase(p: Int): SplitFlashFrame = when (p) {
0 -> SplitFlashFrame(left = FlashColor.RED, right = FlashColor.GREEN) 0 -> SplitFlashFrame(left = FlashColor.RED, right = FlashColor.GREEN)
@@ -849,6 +1011,19 @@ private fun rememberSplitFlashFrame(
onNanos = onFrames * framePeriodNanos onNanos = onFrames * framePeriodNanos
offNanos = offFrames * framePeriodNanos offNanos = offFrames * framePeriodNanos
val currentFlashOnMs = latestFlashOnMs.value.coerceIn(50, 2000)
val currentFlashOffMs = latestFlashOffMs.value.coerceIn(50, 2000)
if (currentFlashOnMs != lastLoggedFlashOnMs || currentFlashOffMs != lastLoggedFlashOffMs) {
Log.d(
timingLogTag,
"config onMs=$currentFlashOnMs offMs=$currentFlashOffMs refreshHz=${"%.2f".format(refreshRateHz)} " +
"frameMs=${"%.3f".format(framePeriodNanos / 1_000_000.0)} snappedOnMs=${"%.3f".format(onNanos / 1_000_000.0)} " +
"snappedOffMs=${"%.3f".format(offNanos / 1_000_000.0)}"
)
lastLoggedFlashOnMs = currentFlashOnMs
lastLoggedFlashOffMs = currentFlashOffMs
}
} }
while (true) { while (true) {
@@ -859,6 +1034,13 @@ private fun rememberSplitFlashFrame(
nextToggleNanos = now + onNanos nextToggleNanos = now + onNanos
value = frameForPhase(phase) value = frameForPhase(phase)
started = true started = true
sessionStartNanos = now
lastToggleAtNanos = now
Log.d(
timingLogTag,
"session-start phase=${phaseName(phase)} refreshHz=${"%.2f".format(refreshRateHz)} " +
"frameMs=${"%.3f".format(framePeriodNanos / 1_000_000.0)} nowNs=$now"
)
return@withFrameNanos return@withFrameNanos
} }
@@ -866,9 +1048,35 @@ private fun rememberSplitFlashFrame(
// Catch up if we missed one or more phase boundaries due to frame delays. // Catch up if we missed one or more phase boundaries due to frame delays.
while (now >= nextToggleNanos) { while (now >= nextToggleNanos) {
val scheduledToggleNanos = nextToggleNanos
val previousPhase = phase
phase = (phase + 1) % 4 phase = (phase + 1) % 4
val latenessNanos = now - scheduledToggleNanos
val actualDeltaNanos = now - lastToggleAtNanos
val dur = if (phase == 0 || phase == 2) onNanos else offNanos val dur = if (phase == 0 || phase == 2) onNanos else offNanos
nextToggleNanos += dur nextToggleNanos += dur
toggleCount += 1
lastToggleAtNanos = now
if (latenessNanos > 0L) {
lateToggleCount += 1
if (latenessNanos > maxLatenessNanos) {
maxLatenessNanos = latenessNanos
}
}
val shouldLogToggle = toggleCount <= 20 || latenessNanos > framePeriodNanos || toggleCount % 60L == 0L
if (shouldLogToggle) {
Log.d(
timingLogTag,
"toggle#$toggleCount prev=${phaseName(previousPhase)} next=${phaseName(phase)} " +
"scheduledMs=${"%.3f".format((scheduledToggleNanos - sessionStartNanos) / 1_000_000.0)} " +
"actualMs=${"%.3f".format((now - sessionStartNanos) / 1_000_000.0)} " +
"actualDeltaMs=${"%.3f".format(actualDeltaNanos / 1_000_000.0)} " +
"latenessMs=${"%.3f".format(latenessNanos / 1_000_000.0)} " +
"lateCount=$lateToggleCount maxLateMs=${"%.3f".format(maxLatenessNanos / 1_000_000.0)}"
)
}
} }
value = frameForPhase(phase) value = frameForPhase(phase)