P1 native color matrix + crop (processColorAsync): parity verified on Xiaomi (classic-neg on black -> rgb(13,5,0), translate*255)

This commit is contained in:
2026-09-09 15:18:30 +07:00
parent dfcf0f3a28
commit d412f40861
4 changed files with 161 additions and 9 deletions
+42 -8
View File
@@ -1,22 +1,51 @@
import RecipescamExport from '../../modules/recipescam-export';
import { applyExposureGain, getSkiaColorMatrix } from '../utils/colorUtils';
import { BaseFilter, ColorAdjustments } from '../types';
const RUNS = 3;
const NEUTRAL_ADJ: ColorAdjustments = {
exposure: 0,
contrast: 0,
saturation: 0,
temperature: 5500,
tint: 0,
highlight: 0,
shadow: 0,
denoise: 0,
clarity: 0,
grain: 0,
colorChrome: 'none',
colorChromeBlue: 'none',
dynamicRange: 100,
sharpening: 0,
exposureCompensation: 0,
};
/**
* P0 spike harness (temporary — removed at P7). Runs native decode+encode on
* the bundled wallframe artwork (3117x4000, representative of a 12MP export)
* and logs per-stage ms proving the JS thread is not blocked while native work
* runs. Gated by EXPO_PUBLIC_BENCH=1.
*
* Release-build visibility is via native Log.i (RecipesCamExport tag) because
* Hermes release builds strip console.* entirely. JS-side dispatch gaps appear
* as the logcat deltas between consecutive decode "start" lines.
* P0/P1 probe harness (temporary — removed at P7).
* P0: native decode+encode on the bundled wallframe artwork (3117x4000).
* P1: native color matrix + crop path. Parity trick: classic-neg matrix applied
* to a pure-black image must yield translate*255 per channel in the native
* filter (Skia 0..1 space -> Android 0..255 space), i.e. ~(13, 5, 0); the
* center pixel is logged natively (RecipesCamExport tag) since release Hermes
* strips console.*.
*/
export async function nativeBenchProbe(): Promise<void> {
const m = RecipescamExport as any;
try {
const pong = await m.ping();
const src: string = await m.materializeBenchAsset('wallframe');
// P1 parity: classic-neg matrix on black -> expect center ~(13,5,0).
const matrix = applyExposureGain(getSkiaColorMatrix('classic-neg' as BaseFilter, NEUTRAL_ADJ), 0);
const black = await m.materializeBlackAsset();
const blackOut = black.replace(/\.png$/, `-neg-${Date.now()}.jpg`);
await m.processColorAsync(black, blackOut, matrix, null, 95);
console.error('[BENCH] classic-neg-on-black matrix[0..4]=' + matrix.slice(0, 5).join(','));
// P0 runs (decode+encode) + P1 identity-matrix run on the real artwork.
const identity = new Array(20).fill(0);
identity[0] = identity[6] = identity[12] = identity[18] = 1;
for (let i = 0; i < RUNS; i++) {
const t0 = Date.now();
const dst = src.replace(/\.png$/, `-out-${Date.now()}-${i}.jpg`);
@@ -26,6 +55,11 @@ export async function nativeBenchProbe(): Promise<void> {
' decodeMs=' + timings.decodeMs + ' encodeWriteMs=' + timings.encodeWriteMs +
' totalMs=' + timings.totalMs + ' pong=' + pong
);
// P1 crop: center 2000x2000 window of the 3117x4000 artwork.
const crop = { x: Math.round((3117 - 2000) / 2), y: Math.round((4000 - 2000) / 2), width: 2000, height: 2000 };
const colorDst = src.replace(/\.png$/, `-color-${Date.now()}-${i}.jpg`);
const ct = await m.processColorAsync(src, colorDst, identity, crop, 95);
console.error('[BENCH] colorRun=' + i + ' decodeMs=' + ct.decodeMs + ' colorMs=' + ct.colorMs + ' encodeWriteMs=' + ct.encodeWriteMs + ' totalMs=' + ct.totalMs);
}
} catch (e) {
console.error('[BENCH] failed', e);