From 3e06003fd038f4e969c40eef2d32913a383a5b78 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Fri, 4 Sep 2026 23:28:06 +0200 Subject: [PATCH] Fold the matched frame at the moment it matches, not after the verdict Template learning was doing nothing. Across three measured trials, six presses matched and five of them folded zero frames: the harvest captured a NEW frame before folding, and by the time it ran the finger was gone, so the capture read the idle floor and the loop exited. Every press in those trials was exactly one frame long -- a tap lasts 400 to 600 ms and a frame costs 200 to 300, so there is never a second frame to harvest. Stock does not capture first. In the reference trace UPDATE_TEMPLATE follows do_authenticate directly -- auth success score, authenticated result is updated, CANCEL, checking the template, UPDATE_TEMPLATE -- with no CAPTURE_IMAGE between them. The trustlet still holds the image it just matched against, so the frame that produced the verdict is the frame to fold, and it costs one command with nothing on the sensor. So the fold happens at the match site now, and the harvest continues from slot one for as long as the finger actually stays down. A quick tap folds exactly one frame instead of none, which is the case that matters: a quick tap is what a user does. The touch-frame flag follows the event that carried the verdict, so a match on the rising edge sets bit 6 exactly as stock does. --- implementations/main.cpp | 64 +++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/implementations/main.cpp b/implementations/main.cpp index fbe640e..b260a6f 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -1402,6 +1402,7 @@ public: if (g_edgeWake) sensor_.DrainEdges(); en::TouchTracker tracker; + harvested_ = 0; // no fold has happened in this session yet bool inPress = false, pressMatched = false, pressRejected = false; int pressFrames = 0, rescans = 0; std::uint32_t pressFid = 0; @@ -1457,7 +1458,22 @@ public: v = ta::Verdict::Rejected; } switch (v) { - case ta::Verdict::Match: pressMatched = true; pressFid = r.fid; note += " MATCH"; break; + case ta::Verdict::Match: + pressMatched = true; pressFid = r.fid; note += " MATCH"; + // FOLD THE FRAME THAT JUST MATCHED, HERE, WITHOUT + // CAPTURING A NEW ONE. This is the only fold a quick tap + // will ever get, and stock does exactly this: in the + // reference trace UPDATE_TEMPLATE follows do_authenticate + // directly -- `auth success score` -> `authenticated + // result is updated` -> CANCEL -> `checking the + // template...` -> UPDATE_TEMPLATE -- with NO CAPTURE_IMAGE + // between them. The trustlet still holds the image it just + // matched against. + if (g_learn && FoldFrame(0, ev == ta::Event::FingerTouched)) { + harvested_ = 1; + note += " folded"; + } + break; case ta::Verdict::Rejected: pressRejected = true; note += " rej"; break; case ta::Verdict::NotIdentifiedYet: rescans++; note += " -11"; break; case ta::Verdict::MatcherNeverRan: break; @@ -1512,7 +1528,8 @@ public: SendCommand(app_, ta::Cmd::Cancel, {}); out.cancelled = true; } - // A press that matched is the only material template learning gets. + // The matching frame was already folded, at the moment it matched. + // This picks up any further frames the finger stayed down for. if (out.matched && g_learn) HarvestTemplate(g_learnMaxFrames); return out; } @@ -1532,10 +1549,27 @@ public: // Which means the frames learned from are exactly the frames a real // unlock produces -- the "enrolment must resemble verification" problem, // solved by the algorithm instead of by coaching the user. + // Fold one frame the trustlet already holds. No capture: the caller has + // just had a verdict out of it, so the image is the one that produced it. + bool FoldFrame(int slot, bool touchFrame) { + namespace ta = fingerprintd::ta; + std::vector up(ta::UpdateTemplatePayloadSize); + ta::BuildUpdateTemplate(up, static_cast(slot), touchFrame); + auto u = SendCommand(app_, ta::Cmd::UpdateTemplate, up); + if (!u.Ok()) { + std::println(" learn: UPDATE_TEMPLATE rc={} ({}) result={}", u.rc, + ta::StrError(u.rc), static_cast(u.result)); + return false; + } + templateDirty_ = true; + return true; + } + int HarvestTemplate(int maxFrames) { namespace ta = fingerprintd::ta; - int folded = 0; - for (int i = 0; i < maxFrames; i++) { + int folded = harvested_; + harvested_ = 0; + for (int i = folded; i < maxFrames; i++) { std::vector cap(ta::CaptureDeclaredLen); ta::BuildCapturePayload(cap); auto c = SendCommand(app_, ta::Cmd::CaptureImage, cap); @@ -1546,22 +1580,15 @@ public: c.metric, folded); break; } - std::vector up(ta::UpdateTemplatePayloadSize); - // Bit 6 on the first frame, mirroring stock: it marks the frame - // whose reported event was FingerTouched, and it selects which of - // the algorithm's two update entries runs. - ta::BuildUpdateTemplate(up, static_cast(folded), folded == 0); - auto u = SendCommand(app_, ta::Cmd::UpdateTemplate, up); - if (!u.Ok()) { - std::println(" learn: UPDATE_TEMPLATE rc={} ({}) result={} -- stopping", - u.rc, ta::StrError(u.rc), static_cast(u.result)); - break; - } + // Bit 6 only on the very first fold of the press, mirroring + // stock: it marks the frame whose reported event was + // FingerTouched, and it selects which of the algorithm's two + // update entries runs. + if (!FoldFrame(folded, folded == 0)) break; folded++; if (g_verbose) std::println(" learn: frame {} folded in (metric={})", folded, c.metric); } - if (folded > 0) templateDirty_ = true; std::println(" learn: {} frame(s) folded into the template{}", folded, templateDirty_ ? ", save pending" : ""); return folded; @@ -1701,8 +1728,11 @@ private: Sensor sensor_; fingerprintd::engine::Baseline baseline_; std::uint32_t gid_ = 0xFFFFFFFF; - // Set when a harvest folded at least one frame in; cleared by the save. + // Set when a fold succeeded; cleared by the save. bool templateDirty_ = false; + // Frames folded during the verify loop itself, carried into the harvest so + // the slot index keeps counting up across the two. + int harvested_ = 0; std::atomic fingerPresent_{false}; std::thread irqThread_; std::atomic irqQuit_{false};