P6 parity probe: same photo through legacy Skia + native engines, 3-panel composite+diff on Xiaomi

This commit is contained in:
2026-09-09 17:06:54 +07:00
parent a51b28f32a
commit 1fdf765d54
3 changed files with 199 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
// P6 parity probe (temporary — removed at P7). Runs ONE source image through
// the legacy Skia engine (processAndExportPhoto) and the native Kotlin engine
// (processAndExportPhotoNative) with identical recipe/options, measures wall
// time per engine plus a JS-tick counter (legacy renders sync JSI → JS thread
// stalls; native runs off-thread → ticks keep firing), then asks the module to
// downscale-diff the two outputs and write a 3-panel composite
// [legacy|native|diff*8] JPEG into cache + gallery for on-device QA.
import RecipescamExport from '../../modules/recipescam-export';
import * as FileSystem from 'expo-file-system/legacy';
import * as MediaLibrary from 'expo-media-library/legacy';
import { processAndExportPhoto } from '../utils/exportEngine';
import { processAndExportPhotoNative } from '../utils/nativeExport';
import { DEFAULT_RECIPES } from '../utils/defaultRecipes';
const stripScheme = (uri: string) => uri.replace(/^file:\/\//, '');
const toUri = (p: string) => (/^(file|content|ph):\/\//.test(p) ? p : `file://${p}`);
interface ParityCase {
tag: string;
recipeId: string;
}
const CASES: ParityCase[] = [
// matrix + tone + CC + enhance (denoise/clarity/grain) — full core pipeline.
{ tag: 'classic-neg', recipeId: 'classic-neg-default' },
// matrix + mist (clarity<0) + classic-white frame + grain.
{ tag: 'retro-amber-frame', recipeId: 'retro-amber-default' },
// cinema seasonal grade + tone + enhance.
{ tag: 'cinema-summer', recipeId: 'cinema-summer-default' },
];
// Library-style export (evFromCamera falsy → 2^EV matrix gain applies, as it
// does for gallery stills); aspect full so the whole frame is compared.
const OPTIONS = { dpi: 300, sharpen: true };
async function newestGalleryPhoto(): Promise<string | null> {
try {
const perm = await MediaLibrary.requestPermissionsAsync();
if (!perm.granted) return null;
const page = await MediaLibrary.getAssetsAsync({
first: 1,
// @ts-ignore legacy SortByValue tuple shape
sortBy: [['creationTime', false]],
});
const a = page.assets?.[0];
return a && a.uri ? a.uri : null;
} catch (e) {
console.error('[PARITY] gallery read failed', e);
return null;
}
}
export async function parityProbe(): Promise<void> {
const m = RecipescamExport as any;
try {
const src = (await newestGalleryPhoto()) ?? toUri(await m.materializeBenchAsset('wallframe'));
console.error(`[PARITY] source=${src}`);
for (const c of CASES) {
const recipe = DEFAULT_RECIPES.find((r) => r.id === c.recipeId);
if (!recipe) {
console.error(`[PARITY] ${c.tag}: recipe ${c.recipeId} missing`);
continue;
}
const frameId = recipe.frameId ?? 'none';
let ticks = 0;
const iv = setInterval(() => {
ticks += 1;
}, 40);
const t0 = Date.now();
const legacy = await processAndExportPhoto(src, recipe, frameId, false, null, OPTIONS);
const legacyMs = Date.now() - t0;
const legacyTicks = ticks;
ticks = 0;
const t1 = Date.now();
const native = await processAndExportPhotoNative(src, recipe, frameId, false, null, OPTIONS);
const nativeMs = Date.now() - t1;
const nativeTicks = ticks;
clearInterval(iv);
console.error(
`[PARITY] ${c.tag}: legacyMs=${legacyMs} ticks=${legacyTicks} uri=${legacy?.uri} | nativeMs=${nativeMs} ticks=${nativeTicks} uri=${native?.uri}`
);
if (!legacy || !native) {
console.error(`[PARITY] ${c.tag}: missing engine output`);
continue;
}
const out = `${FileSystem.cacheDirectory}parity_${c.tag}_${Date.now()}.jpg`;
const res = await m.parityCompareAsync(
stripScheme(legacy.uri),
stripScheme(native.uri),
stripScheme(out),
1280
);
let saved = 'not-saved';
try {
const perm = await MediaLibrary.requestPermissionsAsync();
if (perm.granted) {
const asset = await MediaLibrary.createAssetAsync(out);
saved = `asset=${asset.id}`;
}
} catch (e) {
console.warn('[PARITY] save composite failed', e);
}
console.error(`[PARITY] ${c.tag}: composite=${saved} stats=${JSON.stringify(res)}`);
}
console.error('[PARITY] done');
} catch (e) {
console.error('[PARITY] failed', e);
}
}