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
import android.app.Activity
import android.content.Context
import android.content.pm.ActivityInfo
import android.graphics.Canvas
import android.graphics.Paint
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 androidx.activity.ComponentActivity
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.semantics
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.layout.ContentScale
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
@@ -592,12 +601,6 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry:
brightnessPercent = ui.settings.immersiveBrightnessPercent,
)
val flashFrame by rememberSplitFlashFrame(
isRunning = ui.runtimeState == RuntimeState.RUNNING,
flashOnMs = ui.runtimeParams.flashOnMs,
flashOffMs = ui.runtimeParams.flashOffMs,
)
val progress = sessionProgressFraction(
durationSec = ui.timeline.durationSec,
remainingSec = ui.remainingSec,
@@ -619,20 +622,19 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry:
}
) {
if (isVisualMode) {
Row(Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.background(flashFrame.left.toComposeColor())
)
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.background(flashFrame.right.toComposeColor())
)
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { ctx ->
SplitFlashRenderView(ctx).apply {
setRunning(ui.runtimeState == RuntimeState.RUNNING)
updateIntervals(ui.runtimeParams.flashOnMs, ui.runtimeParams.flashOffMs)
}
},
update = { view ->
view.updateIntervals(ui.runtimeParams.flashOnMs, ui.runtimeParams.flashOffMs)
view.setRunning(ui.runtimeState == RuntimeState.RUNNING)
}
)
}
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) {
FlashColor.RED -> Color.Red
FlashColor.GREEN -> Color.Green
@@ -815,6 +961,8 @@ private fun rememberSplitFlashFrame(
(1_000_000_000.0 / refreshRateHz.coerceAtLeast(1f)).roundToLong().coerceAtLeast(1L)
}
val timingLogTag = "MindMachineTiming"
return produceState(initialValue = SplitFlashFrame(FlashColor.BLACK, FlashColor.BLACK), isRunning, framePeriodNanos) {
if (!isRunning) {
value = SplitFlashFrame(FlashColor.BLACK, FlashColor.BLACK)
@@ -832,6 +980,20 @@ private fun rememberSplitFlashFrame(
var nextToggleNanos = 0L
var onNanos = 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) {
0 -> SplitFlashFrame(left = FlashColor.RED, right = FlashColor.GREEN)
@@ -849,6 +1011,19 @@ private fun rememberSplitFlashFrame(
onNanos = onFrames * 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) {
@@ -859,6 +1034,13 @@ private fun rememberSplitFlashFrame(
nextToggleNanos = now + onNanos
value = frameForPhase(phase)
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
}
@@ -866,9 +1048,35 @@ private fun rememberSplitFlashFrame(
// Catch up if we missed one or more phase boundaries due to frame delays.
while (now >= nextToggleNanos) {
val scheduledToggleNanos = nextToggleNanos
val previousPhase = phase
phase = (phase + 1) % 4
val latenessNanos = now - scheduledToggleNanos
val actualDeltaNanos = now - lastToggleAtNanos
val dur = if (phase == 0 || phase == 2) onNanos else offNanos
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)