import RecipescamExport, { PhotoAdjust } from '../../modules/recipescam-export'; import { applyExposureGain, getSkiaColorMatrix } from '../utils/colorUtils'; import { getCinemaUniforms } from '../utils/cinemaShader'; import { BaseFilter, ColorAdjustments } from '../types'; const RUNS = 2; const IDENTITY = (() => { const m = new Array(20).fill(0); m[0] = m[6] = m[12] = m[18] = 1; return m; })(); 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-P3 probe harness (temporary — removed at P7). Reads the native * RecipesCamExport "centerPixel"/"finalPixel" log lines (probe gates compare * those against the node reference sim). */ export async function nativeBenchProbe(): Promise { const m = RecipescamExport as any; try { const pong = await m.ping(); const run = async (tag: string, src: string, o: PhotoAdjust & { matrix?: number[] } = {}) => { const { matrix, ...adj } = o; const dst = src.replace(/\.png$/, `-${tag}-${Date.now()}.jpg`); const t = await m.processPhotoAsync(src, dst, matrix ?? IDENTITY, null, adj); console.error(`[BENCH] ${tag} totalMs=${t.totalMs} decodeMs=${t.decodeMs} toneMs=${t.toneMs ?? 0} cinemaMs=${t.cinemaMs ?? 0} colorMs=${t.colorMs} clarityMs=${t.clarityMs ?? 0} grainMs=${t.grainMs ?? 0} frameMs=${t.frameMs ?? 0} encodeWriteMs=${t.encodeWriteMs}`); }; 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.materializeSolidAsset(0, 0, 0); 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(',')); // P2 tone parity on solid ramps (expected values computed from TONE_SKSL): // white(255) hl=-1 -> 199; gray64 sh=+1 -> 113; white dr=1 -> 209; // gray26 sh=+1 -> 105; gray26 hl=+1 sh=-1 -> 0. const toneGates: Array<{ rgb: [number, number, number]; tone: { dr: number; hl: number; sh: number }; tag: string }> = [ { rgb: [255, 255, 255], tone: { dr: 0, hl: -1, sh: 0 }, tag: 'white-hl-1' }, { rgb: [64, 64, 64], tone: { dr: 0, hl: 0, sh: 1 }, tag: 'gray64-sh+1' }, { rgb: [255, 255, 255], tone: { dr: 1, hl: 0, sh: 0 }, tag: 'white-dr1' }, { rgb: [26, 26, 26], tone: { dr: 0, hl: 0, sh: 1 }, tag: 'gray26-sh+1' }, { rgb: [26, 26, 26], tone: { dr: 0, hl: 1, sh: -1 }, tag: 'gray26-hl+1-sh-1' }, ]; for (const g of toneGates) { const solid = await m.materializeSolidAsset(g.rgb[0], g.rgb[1], g.rgb[2]); await run(`tone-${g.tag}`, solid, { toneDr: g.tone.dr, toneHl: g.tone.hl, toneSh: g.tone.sh }); } // P3 cinema parity (CINEMA_SKSL): lit+warm+glow path, shade+cool+fog path, // and a saturated pixel to exercise the saturation mix. const summer = getCinemaUniforms('summer')!.flat; const winter = getCinemaUniforms('winter')!.flat; const s192 = await m.materializeSolidAsset(192, 192, 192); await run('cinema-summer-192', s192, { cinema: summer }); const s32 = await m.materializeSolidAsset(32, 32, 32); await run('cinema-winter-32', s32, { cinema: winter }); const sat = await m.materializeSolidAsset(200, 120, 60); await run('cinema-summer-sat', sat, { cinema: summer }); // P3 enhance gates on the gray ramp (gradient makes blur/conv non-trivial). const ramp = await m.materializeRampAsset(); await run('denoise10-ramp', ramp, { denoise: 10 }); await run('clarity10-ramp', ramp, { clarity: 10 }); await run('mist-ramp', ramp, { clarity: -10 }); // Step gates: rows 0..31=0, 32..63=255. A ramp is invariant under box // blur, so the step makes heavy blur non-trivial (expected center row32: // denoise r=1 -> 170, mist r=4 -> 142 per the padded-boxBlur reference). const step = await m.materializeStepAsset(); await run('denoise10-step', step, { denoise: 10 }); await run('mist-step', step, { clarity: -10 }); // Grain is structurally-gated only (float32 hash noise vs node doubles // differ) — the run must complete and the center must stay plausible. const gray100 = await m.materializeSolidAsset(100, 100, 100); await run('grain10-100', gray100, { grain: 10 }); // P4 frame parity on solid gray100 64x64 — border/bar/card colors and the // composed output geometry are fully predictable, so probeSamples assert // exact pixels. Expected (offline verifier re-checks): // classic-white: (1,1)=white border, (32,32)=100 photo, size 64x64. // cinematic: (1,1)=black bar, (32,32)=100, size 64x64. // polaroid: outH=round(64*(SIDE+DECK)+64*WIN_W)=78; (1,1)=card #FAF9F6, // (32,32)=100 photo, (32,70)=card deck. await run('frame-classic', gray100, { frameId: 'classic-white', png: true, probeSamples: [{ x: 1, y: 1 }, { x: 32, y: 32 }] }); await run('frame-cinematic', gray100, { frameId: 'cinematic', png: true, probeSamples: [{ x: 1, y: 1 }, { x: 32, y: 32 }] }); await run('frame-polaroid', gray100, { frameId: 'polaroid', png: true, probeSamples: [{ x: 1, y: 1 }, { x: 32, y: 32 }, { x: 32, y: 70 }] }); // wallframe: export becomes the fixed 3117x4000 rotated artwork; center is // the cover-cropped photo (100), mat samples compare against wallframe.png // pixels offline (display->source: (dx,dy)->(dy, 3117-dx)). Art + fonts are // resolved natively from bundled resources — embedded metro assets expose // no readable file URI to JS (readAsStringAsync rejects 'wallframe'). await run('frame-wallframe', gray100, { frameId: 'wallframe', png: true, probeSamples: [{ x: 100, y: 1500 }, { x: 2900, y: 200 }, { x: 1559, y: 1998 }] }); // P4 GPS watermark (no frame): 400x400 gray base, stamp bottom-left. // probeStampDiff logs stampPixelDiff/amber/white after drawing — counts > 0 // prove the emoji + amber text actually rendered (vs a silent no-op). const gray400 = await m.materializeSolidSized(400, 400, 100, 100, 100); await run('geo-stamp', gray400, { geo: { locality: 'HANOI TEST', timestamp: Date.now() }, png: true, probeStampDiff: true }); // Full-artwork run: provia + tone + cinema + all enhance passes, 12MP. const provia = applyExposureGain(getSkiaColorMatrix('provia' as BaseFilter, NEUTRAL_ADJ), 0); for (let i = 0; i < RUNS; i++) { await run(`fullRun-${i}`, src, { matrix: provia, toneDr: 0.33, toneHl: 0.3, toneSh: 0.3, cinema: summer, denoise: 10, clarity: 10, grain: 10 }); } console.error('[BENCH] done pong=' + pong); } catch (e) { console.error('[BENCH] failed', e); } }