app+docs: 真机验证——修实时全黑(ArrayStoreException)并补端到端测试;同场景 A/B 映射已对齐(平均4.5/255,逐计数差2级);复核确认 LUT 步进常数用反汇编公式即最优;剩余差异定位到计数噪声域

This commit is contained in:
ZXCLI
2026-10-06 15:45:19 +08:00
parent bf03e852ca
commit 2290fbbafb
4 changed files with 209 additions and 1 deletions
@@ -541,9 +541,10 @@ class RenderPipeline(
private fun lutRebuildAndroid() {
val lutRange = ((dev24 * 1000) shr dev4cShift).coerceAtLeast(0x80)
val fresh = androidLutOf(nuc, winLo, winHi, statMean, lutRange, lutBrightness)
// element copy on purpose: System.arraycopy cannot move ints into a ByteArray
for (j in 0 until 1023) lut[j] = fresh[j].toByte()
// lut[1023] is intentionally left untouched: the vendor loop stops at 1022,
// and MapTemperature can index 1023 for pixels at/above winHi.
System.arraycopy(fresh, 0, lut, 0, 1023)
}
private fun grayMap() {
@@ -0,0 +1,96 @@
package com.mag160c.thermal.core
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
/**
* End-to-end guard for the Android-LUT live configuration.
*
* `AndroidLutTest` pins the pure LUT function, but the *instance* path (stats ->
* androidLutOf -> the byte LUT -> gray -> upscale -> palette) can still break on
* its own — a `System.arraycopy` from the int LUT into the byte LUT threw
* ArrayStoreException on every frame and left the live view black while the pure
* test stayed green. This test therefore drives real frames through the pipeline
* with `androidLut = true` and asserts the output actually renders.
*/
class AndroidLutPipelineTest {
private fun res(name: String): ByteArray =
javaClass.classLoader.getResourceAsStream(name)!!.readBytes()
private fun frame(counter: Int, filler: Int, fpa: Int = 19000): ByteArray {
val f = ByteArray(0x38 + 38400)
MagProtocolBytes.putU32(f, 0, 0x1BB1B11B)
MagProtocolBytes.putU32(f, 4, counter)
MagProtocolBytes.putU32(f, 8, 38400)
MagProtocolBytes.putU32(f, 12, 0)
for (i in 0 until 38400) f[0x1C + i] = ((filler + i) and 0xFF).toByte()
MagProtocolBytes.putU32(f, 0x1C + 38400, 0x1BB1B11C)
MagProtocolBytes.putU32(f, 0x1C + 38400 + 4, counter)
MagProtocolBytes.putU32(f, 0x1C + 38400 + 8, fpa)
MagProtocolBytes.putU32(f, 0x1C + 38400 + 12, 0)
return f
}
private fun pipeline(androidLut: Boolean, fpaFix: Int = 1699): RenderPipeline =
RenderPipeline(
w = 160, h = 120, ffcPeriod = 1800, ffcDrift = 250, warmFrames = 2,
force75 = false, androidLut = androidLut, fpaTempFix = fpaFix, onFfc = {},
).also { assertTrue("DDT must load", it.loadDdt(res("mag160c_official.ddt"))) }
private fun renderOnce(p: RenderPipeline): IntArray {
val out = IntArray(320 * 240)
var counter = 0
while (counter < 200) {
if (p.frame(frame(counter, counter), true, out)) return out
counter++
}
throw AssertionError("pipeline never rendered")
}
@Test
fun androidLutPipelineRendersNonBlackFrames() {
val out = renderOnce(pipeline(androidLut = true))
val distinct = out.toSet().size
assertTrue("expected a mapped palette image, got $distinct distinct colours", distinct > 4)
// the warm-up frame is black on purpose; a broken LUT would leave every
// frame black, which is what this guards against
assertFalse("every pixel is black - LUT or palette path is broken",
out.all { (it and 0x00FFFFFF) == 0 })
}
@Test
fun androidLutAndPcLutBothRenderAndDiffer() {
val a = renderOnce(pipeline(androidLut = true))
val b = renderOnce(pipeline(androidLut = false))
assertFalse("the two LUT variants must not produce identical pixels", a.contentEquals(b))
}
@Test
fun fpaTempFixShiftsTheInterpolatedTables() {
// the fix moves the interpolation temperature, so with a DDT whose endpoint
// spacing is thousands of units the rendered pixels must change
val withFix = renderOnce(pipeline(androidLut = true, fpaFix = 1699))
val without = renderOnce(pipeline(androidLut = true, fpaFix = 0))
assertFalse("fpaTempFix had no effect on the per-pixel tables",
withFix.contentEquals(without))
}
@Test
fun androidLutRendersFramesAfterAnFfc() {
// a manual FFC restarts the reference windows and swaps the tables; the
// Android LUT must survive that transition
val p = pipeline(androidLut = true)
renderOnce(p)
p.requestFfc()
renderOnce(p)
val out = IntArray(320 * 240)
var counter = 100
var ok = false
while (counter < 400 && !ok) {
ok = p.frame(frame(counter, counter), true, out)
counter++
}
assertTrue("no frame rendered after the FFC", ok)
}
}