Commit 1b9515bc authored by Andrey Filippov's avatar Andrey Filippov

CODEX: Run LWIR conditioning on resident GPU images

Co-authored-by: 's avatarCodex <codex@elphel.com>
parent 65c4e35c
......@@ -49,6 +49,31 @@ import com.elphel.imagej.tileprocessor.QuadCLT;
* dust) is the future improvement, hence the 2-image (scale, offset) framing.
*/
public class CuasConditioning {
/** One resident calibration record per native GPU instance. The FPN object is
* shared by all spawned scenes; scales/offsets are cloned per scene, so their
* small value arrays are compared rather than their identities. */
private static final java.util.WeakHashMap<GpuQuad, ConditioningCache> GPU_CONDITIONING =
new java.util.WeakHashMap<GpuQuad, ConditioningCache>();
private static class ConditioningCache {
final Object fpn_key;
final double [] scales;
final double [] offsets;
final long dc_bits;
final int num_pixels;
ConditioningCache(Object fpn_key, double [] scales, double [] offsets, double dc, int num_pixels) {
this.fpn_key = fpn_key;
this.scales = scales;
this.offsets = offsets;
this.dc_bits = Double.doubleToRawLongBits(dc);
this.num_pixels = num_pixels;
}
boolean matches(Object fpn_key, double [] scales, double [] offsets, double dc, int num_pixels) {
return (this.fpn_key == fpn_key) && (this.num_pixels == num_pixels) &&
(this.dc_bits == Double.doubleToRawLongBits(dc)) &&
java.util.Arrays.equals(this.scales, scales) &&
java.util.Arrays.equals(this.offsets, offsets);
}
}
public static class Config {
public boolean rowcol_enable = true; // subtract per-row and per-col noise (runs AFTER photometric+FPN since 07/07/2026 - see condition())
......@@ -91,6 +116,66 @@ public class CuasConditioning {
return cfg;
}
/**
* Prepare the native production two-map calibration once. This path is valid
* only for the intended camera contract: no software Row/Col, fixed (or
* disabled) DC, and no quadratic term. The CPU implementation remains the
* fallback/oracle for every other configuration and for pose_corr capture.
* By Codex on 07/15/2026.
*/
private static synchronized boolean prepareGpuConditioning(
final GpuQuad gpuQuad,
final double [][][] image_data,
final double [] scales_in,
final double [] offsets_in,
final double [] scales2,
final double [][][] fpn,
final Config cfg) {
if (!gpuQuad.supportsConditioningMaps() || cfg.rowcol_enable ||
com.elphel.imagej.gpu.testdata.PoseCorrExport.isArmed()) return false;
if (cfg.zero_dc && Double.isNaN(cfg.dc_level)) return false; // AUTO median stays in the CPU oracle
if (cfg.photometric && (scales2 != null)) for (double a : scales2) if (a != 0.0) return false;
final int num_sens = image_data.length;
if ((num_sens == 0) || (image_data[0] == null) || (image_data[0][0] == null)) return false;
if (((scales_in != null) && (scales_in.length < num_sens)) ||
((offsets_in != null) && (offsets_in.length < num_sens)) ||
((scales2 != null) && (scales2.length < num_sens)) ||
(cfg.apply_fpn && (fpn != null) && (fpn.length < num_sens))) return false;
final int num_pixels = image_data[0][0].length;
final double dc = cfg.zero_dc ? cfg.dc_level : 0.0;
final double [] scales = new double [num_sens];
final double [] offsets = new double [num_sens];
for (int s = 0; s < num_sens; s++) {
if ((image_data[s] == null) || (image_data[s].length != 1) || (image_data[s][0] == null) ||
(image_data[s][0].length != num_pixels)) return false;
scales[s] = cfg.photometric && (scales_in != null) ? scales_in[s] : 1.0;
offsets[s] = cfg.photometric && (offsets_in != null) ? offsets_in[s] : 0.0;
if (cfg.apply_fpn && (fpn != null) && ((fpn[s] == null) || (fpn[s][0] == null) ||
(fpn[s][0].length != num_pixels))) return false;
}
final Object fpn_key = cfg.apply_fpn ? fpn : null;
final ConditioningCache cached = GPU_CONDITIONING.get(gpuQuad);
if ((cached != null) && cached.matches(fpn_key, scales, offsets, dc, num_pixels)) return true;
final float [] scale_maps = new float [num_sens * num_pixels];
final float [] offset_maps = new float [scale_maps.length];
for (int s = 0; s < num_sens; s++) {
final double scale = scales[s];
final double base = scale * offsets[s];
final double [] fp = (cfg.apply_fpn && (fpn != null)) ? fpn[s][0] : null;
final int base_index = s * num_pixels;
for (int i = 0; i < num_pixels; i++) {
scale_maps[base_index + i] = (float) scale;
offset_maps[base_index + i] = (float) (base + ((fp != null) ? fp[i] : 0.0) + dc);
}
}
if (!gpuQuad.setConditioningMaps(scale_maps, offset_maps)) return false;
GPU_CONDITIONING.put(gpuQuad, new ConditioningCache(fpn_key, scales, offsets, dc, num_pixels));
System.out.println("CuasConditioning: GPU two-map conditioning active (Row/Col OFF, resident calibration, dc="+
dc+")");
return true;
}
/**
* Condition all sensors in place.
* @param image_data [sensor][0][pixel] mono LWIR, row-major (idx = y*width + x). Modified in place.
......@@ -269,21 +354,37 @@ public class CuasConditioning {
return false;
}
final int width = scene.getGeometryCorrection().getSensorWH()[0];
condition(
data, // double [][][] image_data (in place)
width, // int width
scene.getLwirScales(), // double [] scales
scene.getLwirOffsets(), // double [] offsets
scene.getLwirScales2(), // double [] scales2
scene.getFPN(), // double [][][] fpn (null - none)
cfg); // Config (null - defaults)
final Config use_cfg = (cfg != null) ? cfg : new Config();
final GpuQuad gpuQuad = scene.getGPUQuad();
boolean gpu_condition = prepareGpuConditioning(
gpuQuad, data, scene.getLwirScales(), scene.getLwirOffsets(),
scene.getLwirScales2(), scene.getFPN(), use_cfg);
if (!gpu_condition) {
condition(
data, // double [][][] image_data (in place)
width, // int width
scene.getLwirScales(), // double [] scales
scene.getLwirOffsets(), // double [] offsets
scene.getLwirScales2(), // double [] scales2
scene.getFPN(), // double [][][] fpn (null - none)
use_cfg); // Config
}
scene.saveQuadClt(); // bind this scene (conditional; resets bayer guard on switch)
scene.setHasNewImageData(false); // the guard's other re-pull trigger - must be clear
gpuQuad.setBayerImages(data, true); // force H2D; sets bayer_set -> pipeline skips its pull
gpuQuad.setBayerImages(data, true); // CPU path: conditioned; GPU path: raw
if (gpu_condition && !gpuQuad.execConditioning()) {
// Fail safe: rebuild the known CPU result and replace the raw device image.
condition(data, width, scene.getLwirScales(), scene.getLwirOffsets(),
scene.getLwirScales2(), scene.getFPN(), use_cfg);
gpuQuad.setBayerImages(data, true);
gpu_condition = false;
}
// pose_corr kernel-test capture: the conditioned data is uploaded directly (never
// stored in the QuadCLT), so the exporter takes its bit-exact copy here. No-op
// unless the curt.kernel_test=pose_corr run armed it. By Claude on 07/13/2026.
// An armed pose_corr capture deliberately forced the CPU path above, so data
// is conditioned here. Normal GPU runs leave data raw and the no-op exporter
// returns before looking at it.
com.elphel.imagej.gpu.testdata.PoseCorrExport.offerImages(scene, data);
return true;
}
......
......@@ -1291,6 +1291,24 @@ public class GpuQuad{ // quad camera description
copyD2H.Height = img_height; // /4;
cuMemcpy2D(copyD2H); // run copy
}
/** @return true when this backend can condition all resident source images in place. */
public boolean supportsConditioningMaps() {
return false;
}
/**
* Upload resident per-pixel conditioning maps, contiguous
* {@code [sensor][height][width]}. Base/JCuda does not carry this kernel.
*/
public boolean setConditioningMaps(float [] scale_maps, float [] offset_maps) {
return false;
}
/** Run {@code raw*scale_map-offset_map} on every resident source image. */
public boolean execConditioning() {
return false;
}
public int [] getWH(boolean use_ref) {
return use_ref ? gpu_clt_ref_wh : gpu_clt_wh;
}
......
/**
** ConditioningJna.java - Java/JNA correctness and timing gate for LWIR conditioning
**
** Copyright (C) 2026 Elphel, Inc.
**
** -----------------------------------------------------------------------------**
**
** ConditioningJna.java is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 3 of the License, or
** (at your option) any later version.
**
** ConditioningJna.java is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with ConditioningJna.java. If not, see <http://www.gnu.org/licenses/>.
** -----------------------------------------------------------------------------**
**
*/
package com.elphel.imagej.gpu.jna;
import java.util.Arrays;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
/**
* Full Java -> JNA -> NVRTC gate for the LWIR conditioning component. It runs
* the production dimensions (16 x 640 x 512), verifies the double kernel
* bit-for-bit against the current {@code CuasConditioning.condition()} linear
* expression, verifies the float kernel bit-for-bit against {@link Math#fma},
* characterizes float-map error, and measures both synchronized kernel calls
* and the current 16-array JNA upload plus kernel path.
*
* Run after building libtileproc.so:
* <pre>
* java -Xmx2g -Djna.library.path=/home/elphel/git/tile_processor_gpu/jna \
* -cp target/classes:~/.m2/repository/net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar \
* com.elphel.imagej.gpu.jna.ConditioningJna
* </pre>
* By Codex on 07/15/2026.
*/
public class ConditioningJna {
private static void require(final TpJna lib, final int rc, final String what) {
if (rc != 0) throw new IllegalStateException(what+" rc="+rc+": "+lib.tp_last_error());
}
private static long floatUlpDistance(final float a, final float b) {
if (Float.isNaN(a) || Float.isNaN(b)) return (Float.isNaN(a) && Float.isNaN(b)) ? 0 : Long.MAX_VALUE;
int ia = Float.floatToRawIntBits(a);
int ib = Float.floatToRawIntBits(b);
if (ia < 0) ia = 0x80000000 - ia;
if (ib < 0) ib = 0x80000000 - ib;
return Math.abs((long) ia - (long) ib);
}
private static void printTiming(final String name, final long [] ns) {
final long [] sorted = ns.clone();
Arrays.sort(sorted);
final double p50 = sorted[(int) Math.floor(0.50 * (sorted.length - 1))] * 1.0e-6;
final double p95 = sorted[(int) Math.floor(0.95 * (sorted.length - 1))] * 1.0e-6;
final double p99 = sorted[(int) Math.floor(0.99 * (sorted.length - 1))] * 1.0e-6;
final double max = sorted[sorted.length - 1] * 1.0e-6;
System.out.printf("%s: p50=%.3f ms p95=%.3f ms p99=%.3f ms max=%.3f ms (%d frames)%n",
name, p50, p95, p99, max, sorted.length);
}
public static void main(final String [] args) {
final String srcdir = (args.length > 0) ? args[0] : "/home/elphel/git/tile_processor_gpu/src";
final String devrt = (args.length > 1) ? args[1] :
"/usr/local/cuda-12.8/targets/x86_64-linux/lib/libcudadevrt.a";
final int cams = 16;
final int width = 640;
final int height = 512;
final int pixels = width * height;
final int total = cams * pixels;
final double dc = -3512.375;
final float [][] raw = new float [cams][pixels];
final float [][] expectedDouble = new float [cams][pixels];
final float [][] expectedFloat = new float [cams][pixels];
final double [] scales = new double [cams];
final double [] offsets = new double [cams];
final double [] scales2 = new double [cams];
final double [] fpn = new double [total];
final float [] scaleMaps = new float [total];
final float [] offsetMaps = new float [total];
for (int c = 0; c < cams; c++) {
scales[c] = 0.9785 + 0.0029 * c;
offsets[c] = 20117.25 + 2.75 * c;
for (int i = 0; i < pixels; i++) {
final int k = c * pixels + i;
raw[c][i] = (float) (20750.0 + 0.125 * (i % 97) - 3.5 * c + 0.03125 * ((i / width) % 13));
fpn[k] = 0.03125 * ((i * 17 + c * 29) % 257 - 128) + 0.00031 * (i % 19);
final double d = (double) raw[c][i] - offsets[c];
expectedDouble[c][i] = (float) (scales2[c] * d * d + scales[c] * d - fpn[k] - dc);
scaleMaps[k] = (float) scales[c];
offsetMaps[k] = (float) (scales[c] * offsets[c] + fpn[k] + dc);
expectedFloat[c][i] = Math.fma(raw[c][i], scaleMaps[k], -offsetMaps[k]);
}
}
final TpJna lib = Native.load("tileproc", TpJna.class);
final Pointer module = lib.tp_create_module(srcdir, devrt);
if (module == null) throw new IllegalStateException("tp_create_module: "+lib.tp_last_error());
Pointer proc = null;
int badDouble = 0;
int badFloat = 0;
double maxModelDiff = 0.0;
long maxModelUlp = 0;
try {
proc = lib.tp_proc_create(module);
if (proc == null) throw new IllegalStateException("tp_proc_create: "+lib.tp_last_error());
require(lib, lib.tp_proc_setup(proc, cams, 1, width, height, 0, 0), "tp_proc_setup");
final long mapStart = System.nanoTime();
require(lib, lib.tp_proc_set_conditioning_maps(proc, scaleMaps, offsetMaps, pixels),
"tp_proc_set_conditioning_maps");
final double mapMs = (System.nanoTime() - mapStart) * 1.0e-6;
require(lib, lib.tp_proc_set_conditioning_oracle(proc, scales, offsets, scales2, fpn, pixels),
"tp_proc_set_conditioning_oracle");
for (int c = 0; c < cams; c++) require(lib, lib.tp_proc_set_image(proc, c, raw[c]), "set raw "+c);
require(lib, lib.tp_proc_exec_conditioning(proc, 1, dc), "condition_lwir_d");
final float [] got = new float [pixels];
for (int c = 0; c < cams; c++) {
require(lib, lib.tp_proc_get_image(proc, c, got), "get double "+c);
for (int i = 0; i < pixels; i++) if (
Float.floatToRawIntBits(got[i]) != Float.floatToRawIntBits(expectedDouble[c][i])) badDouble++;
}
for (int c = 0; c < cams; c++) require(lib, lib.tp_proc_set_image(proc, c, raw[c]), "set raw "+c);
require(lib, lib.tp_proc_exec_conditioning(proc, 0, dc), "condition_lwir_f");
for (int c = 0; c < cams; c++) {
require(lib, lib.tp_proc_get_image(proc, c, got), "get float "+c);
for (int i = 0; i < pixels; i++) {
if (Float.floatToRawIntBits(got[i]) != Float.floatToRawIntBits(expectedFloat[c][i])) badFloat++;
final double d = Math.abs((double) got[i] - expectedDouble[c][i]);
if (d > maxModelDiff) maxModelDiff = d;
final long ulp = floatUlpDistance(got[i], expectedDouble[c][i]);
if (ulp > maxModelUlp) maxModelUlp = ulp;
}
}
System.out.println("condition_lwir_d Java expression: "+total+" pixels, bit mismatches="+
badDouble+" -> "+((badDouble == 0) ? "PASS" : "FAIL"));
System.out.println("condition_lwir_f two-map fma: "+total+" pixels, bit mismatches="+
badFloat+" -> "+((badFloat == 0) ? "PASS" : "FAIL"));
System.out.printf("float two-map vs current double model: max|diff|=%.9g counts, max ULP=%d%n",
maxModelDiff, maxModelUlp);
System.out.printf("resident float-map upload (40 MiB through JNA): %.3f ms%n", mapMs);
// Identity maps keep repeated kernel-only timing numerically stable while
// preserving the exact memory traffic of the production calculation.
Arrays.fill(scaleMaps, 1.0f);
Arrays.fill(offsetMaps, 0.0f);
require(lib, lib.tp_proc_set_conditioning_maps(proc, scaleMaps, offsetMaps, pixels),
"set identity maps");
for (int c = 0; c < cams; c++) require(lib, lib.tp_proc_set_image(proc, c, raw[c]), "timing raw "+c);
for (int i = 0; i < 20; i++) require(lib, lib.tp_proc_exec_conditioning(proc, 0, dc), "kernel warmup");
final long [] kernelNs = new long [200];
for (int i = 0; i < kernelNs.length; i++) {
final long start = System.nanoTime();
require(lib, lib.tp_proc_exec_conditioning(proc, 0, dc), "kernel timing");
kernelNs[i] = System.nanoTime() - start;
}
for (int i = 0; i < 10; i++) {
for (int c = 0; c < cams; c++) require(lib, lib.tp_proc_set_image(proc, c, raw[c]), "upload warmup");
require(lib, lib.tp_proc_exec_conditioning(proc, 0, dc), "upload warmup kernel");
}
final long [] uploadKernelNs = new long [100];
for (int i = 0; i < uploadKernelNs.length; i++) {
final long start = System.nanoTime();
for (int c = 0; c < cams; c++) require(lib, lib.tp_proc_set_image(proc, c, raw[c]), "timing upload");
require(lib, lib.tp_proc_exec_conditioning(proc, 0, dc), "timing upload kernel");
uploadKernelNs[i] = System.nanoTime() - start;
}
printTiming("synchronized conditioning kernel", kernelNs);
printTiming("16 JNA uploads + conditioning", uploadKernelNs);
System.out.println("60 FPS frame budget: 16.667 ms");
} finally {
if (proc != null) lib.tp_proc_destroy(proc);
lib.tp_destroy_module(module);
}
if ((badDouble != 0) || (badFloat != 0)) System.exit(1);
}
}
......@@ -315,6 +315,35 @@ public class GpuQuadJna extends GpuQuad {
@Override public void setBayerImage(float[] bayer_image, int ncam) {
lib.tp_proc_set_image(proc, ncam, bayer_image);
}
@Override public void getBayerImage(float[] bayer_image, int ncam) {
int rc = lib.tp_proc_get_image(proc, ncam, bayer_image);
if (rc != 0) throw new IllegalStateException("tp_proc_get_image rc="+rc+": "+lib.tp_last_error());
}
// Resident LWIR two-map conditioning. Calibration upload is deliberately
// separate from execution so it is paid once per calibration, never once
// per scene. By Codex on 07/15/2026.
@Override public boolean supportsConditioningMaps() {
return !rectilinear && !"cpu".equalsIgnoreCase(System.getProperty("tp.conditioning", "gpu"));
}
@Override public boolean setConditioningMaps(float[] scale_maps, float[] offset_maps) {
if ((scale_maps == null) || (offset_maps == null) ||
(scale_maps.length != num_cams * img_width * img_height) ||
(offset_maps.length != scale_maps.length)) return false;
int rc = lib.tp_proc_set_conditioning_maps(proc, scale_maps, offset_maps, img_width * img_height);
if (rc != 0) {
System.out.println("GpuQuadJna.setConditioningMaps(): native rc="+rc+" ("+lib.tp_last_error()+")");
return false;
}
return true;
}
@Override public boolean execConditioning() {
int rc = lib.tp_proc_exec_conditioning(proc, 0, 0.0); // production maps already contain fixed DC
if (rc != 0) {
System.out.println("GpuQuadJna.execConditioning(): native rc="+rc+" ("+lib.tp_last_error()+")");
return false;
}
return true;
}
// CLT buffers (main + ref) are pre-allocated full-frame in tp_proc_setup, so there is nothing to (re)allocate
// per call. Base allocs JCuda gpu_clt[_ref] here; native is fixed-size -> no-op (nothing newly allocated).
@Override public boolean reAllocateClt(int[] wh, boolean ref_scene) {
......
......@@ -12,8 +12,8 @@ public interface TpJna extends Library {
/** NVRTC-compile the kernels in srcdir (+ getTpDefines), cuLink(libcudadevrt), load the module.
* Returns an opaque module handle, or null on failure (see tp_last_error()). */
Pointer tp_create_module(String srcdir, String libcudadevrt);
/** Number of kernel functions resolved in the module (26 expected through rung 3:
* 19 base + 4 consolidation + 2 peak + 1 LMA-products), or -1 if handle is null. */
/** Number of kernel functions resolved in the module (28 expected through LWIR conditioning:
* 19 base + 2 conditioning + 4 consolidation + 2 peak + 1 LMA-products), or -1 if handle is null. */
int tp_module_num_functions(Pointer module);
/** Last error message (NVRTC/CUDA log), empty if none. */
String tp_last_error();
......@@ -72,6 +72,15 @@ public interface TpJna extends Library {
int tp_proc_set_kernels(Pointer proc, int cam, float[] d, int n);
int tp_proc_set_kernel_offsets(Pointer proc, int cam, float[] d, int n);
int tp_proc_set_image(Pointer proc, int cam, float[] d);
/** De-pitch one resident source image into out[width*height]. */
int tp_proc_get_image(Pointer proc, int cam, float[] out);
/** Resident production calibration maps, contiguous [camera][height][width]. */
int tp_proc_set_conditioning_maps(Pointer proc, float[] scales, float[] offsets, int numPixels);
/** Java-expression validation calibration; FPN is contiguous [camera][height][width]. */
int tp_proc_set_conditioning_oracle(Pointer proc, double[] scales, double[] offsets,
double[] scales2, double[] fpn, int numPixels);
/** Condition all resident source images in place; useDouble=0 selects production two-map float. */
int tp_proc_exec_conditioning(Pointer proc, int useDouble, double dcLevel);
/** use_center_image: broadcast one center image to all sensors (FPN back-prop mode). */
int tp_proc_set_center_image(Pointer proc, float[] d);
int tp_proc_set_tasks(Pointer proc, float[] ftasks, int ntiles, int totalFloats);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment