Auto commit (MindMachine) Mon Apr 13 06:01:01 PM CDT 2026
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user