app+docs: 真机验证——修实时全黑(ArrayStoreException)并补端到端测试;同场景 A/B 映射已对齐(平均4.5/255,逐计数差2级);复核确认 LUT 步进常数用反汇编公式即最优;剩余差异定位到计数噪声域
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user