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.
This commit is contained in:
Jorijn van der Graaf 2026-09-04 23:28:06 +02:00
commit 3e06003fd0

View file

@ -1402,6 +1402,7 @@ public:
if (g_edgeWake) sensor_.DrainEdges(); if (g_edgeWake) sensor_.DrainEdges();
en::TouchTracker tracker; en::TouchTracker tracker;
harvested_ = 0; // no fold has happened in this session yet
bool inPress = false, pressMatched = false, pressRejected = false; bool inPress = false, pressMatched = false, pressRejected = false;
int pressFrames = 0, rescans = 0; int pressFrames = 0, rescans = 0;
std::uint32_t pressFid = 0; std::uint32_t pressFid = 0;
@ -1457,7 +1458,22 @@ public:
v = ta::Verdict::Rejected; v = ta::Verdict::Rejected;
} }
switch (v) { 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::Rejected: pressRejected = true; note += " rej"; break;
case ta::Verdict::NotIdentifiedYet: rescans++; note += " -11"; break; case ta::Verdict::NotIdentifiedYet: rescans++; note += " -11"; break;
case ta::Verdict::MatcherNeverRan: break; case ta::Verdict::MatcherNeverRan: break;
@ -1512,7 +1528,8 @@ public:
SendCommand(app_, ta::Cmd::Cancel, {}); SendCommand(app_, ta::Cmd::Cancel, {});
out.cancelled = true; 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); if (out.matched && g_learn) HarvestTemplate(g_learnMaxFrames);
return out; return out;
} }
@ -1532,10 +1549,27 @@ public:
// Which means the frames learned from are exactly the frames a real // Which means the frames learned from are exactly the frames a real
// unlock produces -- the "enrolment must resemble verification" problem, // unlock produces -- the "enrolment must resemble verification" problem,
// solved by the algorithm instead of by coaching the user. // 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<std::byte> up(ta::UpdateTemplatePayloadSize);
ta::BuildUpdateTemplate(up, static_cast<std::uint32_t>(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<int>(u.result));
return false;
}
templateDirty_ = true;
return true;
}
int HarvestTemplate(int maxFrames) { int HarvestTemplate(int maxFrames) {
namespace ta = fingerprintd::ta; namespace ta = fingerprintd::ta;
int folded = 0; int folded = harvested_;
for (int i = 0; i < maxFrames; i++) { harvested_ = 0;
for (int i = folded; i < maxFrames; i++) {
std::vector<std::byte> cap(ta::CaptureDeclaredLen); std::vector<std::byte> cap(ta::CaptureDeclaredLen);
ta::BuildCapturePayload(cap); ta::BuildCapturePayload(cap);
auto c = SendCommand(app_, ta::Cmd::CaptureImage, cap); auto c = SendCommand(app_, ta::Cmd::CaptureImage, cap);
@ -1546,22 +1580,15 @@ public:
c.metric, folded); c.metric, folded);
break; break;
} }
std::vector<std::byte> up(ta::UpdateTemplatePayloadSize); // Bit 6 only on the very first fold of the press, mirroring
// Bit 6 on the first frame, mirroring stock: it marks the frame // stock: it marks the frame whose reported event was
// whose reported event was FingerTouched, and it selects which of // FingerTouched, and it selects which of the algorithm's two
// the algorithm's two update entries runs. // update entries runs.
ta::BuildUpdateTemplate(up, static_cast<std::uint32_t>(folded), folded == 0); if (!FoldFrame(folded, folded == 0)) break;
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<int>(u.result));
break;
}
folded++; folded++;
if (g_verbose) if (g_verbose)
std::println(" learn: frame {} folded in (metric={})", folded, c.metric); 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, std::println(" learn: {} frame(s) folded into the template{}", folded,
templateDirty_ ? ", save pending" : ""); templateDirty_ ? ", save pending" : "");
return folded; return folded;
@ -1701,8 +1728,11 @@ private:
Sensor sensor_; Sensor sensor_;
fingerprintd::engine::Baseline baseline_; fingerprintd::engine::Baseline baseline_;
std::uint32_t gid_ = 0xFFFFFFFF; 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; 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<bool> fingerPresent_{false}; std::atomic<bool> fingerPresent_{false};
std::thread irqThread_; std::thread irqThread_;
std::atomic<bool> irqQuit_{false}; std::atomic<bool> irqQuit_{false};