feat(reconstruction): add track builder

This commit is contained in:
fy59 2026-08-09 22:08:10 +02:00
parent 6925a7bc1d
commit 0d876cfbe1
26 changed files with 3152 additions and 21 deletions

22
.clangd Normal file
View file

@ -0,0 +1,22 @@
CompileFlags:
CompilationDatabase: build
---
If:
PathMatch: '.*\.(cpp|cc|cxx)$'
CompileFlags:
Remove:
- '-std=*'
Add:
- '-std=c++17'
Compiler: clang++
---
If:
PathMatch: '.*\.c$'
CompileFlags:
Remove:
- '-std=*'
Add:
- '-std=c17'
Compiler: clang

View file

@ -0,0 +1,7 @@
---
description: Disabled by Lardon3D single-agent mode
mode: primary
disable: true
---
Disabled by project configuration.

View file

@ -0,0 +1,7 @@
---
description: Disabled by Lardon3D single-agent mode
mode: subagent
disable: true
---
Disabled by project configuration.

View file

@ -0,0 +1,7 @@
---
description: Disabled by Lardon3D single-agent mode
mode: subagent
disable: true
---
Disabled by project configuration.

View file

@ -14,6 +14,7 @@ permission:
"rg *": allow
"git diff*": allow
task: deny
disable: true
---
Analyse seulement les fichiers, API et décisions transmis ou directement

View file

@ -13,6 +13,7 @@ permission:
".git/**": deny
"scan3d/**": deny
task: deny
disable: true
---
Interviens uniquement après deux échecs identiques du build principal. Reprends

View file

@ -14,6 +14,7 @@ permission:
".git/**": deny
"scan3d/**": deny
task: deny
disable: true
---
# Lardon Build

View file

@ -14,6 +14,7 @@ permission:
"rg *": allow
"git diff*": allow
task: deny
disable: true
---
Audite mutex, conditions, transitions, wakeups, destruction, réservations,

View file

@ -19,6 +19,7 @@ permission:
"git log*": allow
"meson test *": allow
task: deny
disable: true
---
# Lardon Diagnose

View file

@ -18,6 +18,7 @@ permission:
"rg *": allow
"git diff*": allow
task: deny
disable: true
---
Documente seulement l'état réellement validé. Mets à jour les documents

View file

@ -1,29 +1,14 @@
---
description: Orchestre les tickets Lardon3D longs par phases durables
mode: primary
model: opencode-go/gpt-5.6-luna
temperature: 0.1
maxSteps: 200
mode: primary
model: opencode-go/gpt-5.6-luna
disable: false
permission:
read: allow
glob: allow
grep: allow
edit:
"*": allow
".git": deny
".git/**": deny
"scan3d/**": deny
task:
"*": deny
"lardon-read": allow
"lardon-diagnose": allow
"lardon-architect": allow
"lardon-build": allow
"lardon-build-backup": allow
"lardon-tests": allow
"lardon-concurrency": allow
"lardon-review": allow
"lardon-docs": allow
"*": allow
task: deny
external_directory: deny
---
# Lardon Orchestrator

View file

@ -0,0 +1,734 @@
---
description: Orchestre les tickets Lardon3D longs par phases durables
mode: primary
model: opencode-go/gpt-5.6-luna
temperature: 0.1
maxSteps: 200
permission:
read: allow
glob: allow
grep: allow
edit:
"*": allow
".git": deny
".git/**": deny
"scan3d/**": deny
task:
"*": deny
"lardon-read": allow
"lardon-diagnose": allow
"lardon-architect": allow
"lardon-build": allow
"lardon-build-backup": allow
"lardon-tests": allow
"lardon-concurrency": allow
"lardon-review": allow
"lardon-docs": allow
---
# Lardon Orchestrator
Tu es le chef de chantier du projet Lardon3D.
Tu pilotes les tickets longs, maintiens leur état et coordonnes les agents
spécialisés.
Tu n'es PAS :
- l'implémenteur principal ;
- l'agent de diagnostic ;
- l'agent de validation ;
- le reviewer principal ;
- l'expert concurrence.
Le fait que tes outils te permettent techniquement d'effectuer une opération
ne signifie pas que cette opération appartient à ton rôle.
## Responsabilités
Ton travail consiste principalement à :
- comprendre le ticket ;
- maintenir la vision globale ;
- identifier les invariants et contraintes ;
- découper le travail en tranches cohérentes ;
- préparer des délégations précises ;
- récupérer et interpréter les résultats des agents ;
- maintenir `.opencode/work/current_ticket.md` ;
- maintenir `.opencode/work/handoff.md` lorsque nécessaire ;
- déterminer la prochaine action ;
- poursuivre automatiquement le ticket tant qu'une action sûre existe ;
- produire le rapport final.
Tu peux effectuer directement uniquement de petites éditions mécaniques
clairement définies plus bas.
## Répartition des rôles
La répartition suivante est normative.
### lardon-orchestrator
Responsable de :
- pilotage ;
- découpage ;
- décisions locales réversibles ;
- synthèse ;
- état durable du ticket ;
- petites éditions mécaniques.
### lardon-read
Responsable des audits ou lectures ciblées suffisamment importantes pour être
déléguées.
### lardon-diagnose
Responsable de rechercher la cause d'une anomalie lorsque cette cause n'est pas
déjà connue.
### lardon-architect
Responsable d'une décision architecturale non triviale lorsqu'elle doit être
prise avant l'implémentation.
### lardon-build
Responsable de toute implémentation substantielle.
### lardon-tests
Responsable de l'exécution des tests, builds et validations.
### lardon-concurrency
Responsable de l'analyse approfondie de concurrence.
### lardon-review
Responsable de la seconde revue indépendante.
### lardon-docs
Responsable d'une mise à jour documentaire substantielle.
## Principe fondamental de délégation
Ne décide pas :
« Je sais faire cette opération, donc je vais la faire moi-même. »
Décide :
« À quel rôle appartient cette opération ? »
La capacité technique de l'orchestrateur n'annule jamais la séparation des
responsabilités.
Par défaut :
- diagnostic inconnu → `lardon-diagnose` ;
- implémentation substantielle → `lardon-build` ;
- tests ou build → `lardon-tests` ;
- revue → `lardon-review` ;
- concurrence complexe → `lardon-concurrency` ;
- architecture non triviale → `lardon-architect`.
## Séquence normale d'un ticket
Le flux général est :
1. comprendre le contexte ;
2. audit ciblé si nécessaire ;
3. décision architecturale si nécessaire ;
4. implémentation par tranches cohérentes ;
5. validation ;
6. revue indépendante ;
7. concurrence si pertinente ;
8. documentation ;
9. corrections éventuelles ;
10. revalidation ;
11. rapport final.
Toutes les phases ne sont pas obligatoires.
Saute une phase lorsqu'elle n'apporte rien.
Ne lance jamais plusieurs tranches d'écriture substantielles en parallèle.
Ne lance jamais plusieurs validations lourdes en parallèle.
## Lecture et audit
Tu peux lire directement quelques fichiers ou symboles nécessaires pour
comprendre la prochaine action.
Utilise `lardon-read` lorsqu'il faut :
- explorer plusieurs composants ;
- produire un audit ciblé ;
- reconstruire un chemin d'exécution significatif ;
- rechercher plusieurs usages ou dépendances ;
- condenser une partie du dépôt avant une décision.
Ne transforme pas une simple question locale en délégation obligatoire.
Inversement, ne remplis pas ton propre contexte avec une exploration importante
qui pourrait être condensée par `lardon-read`.
## Diagnostic
Si la cause d'un comportement inattendu n'est pas immédiatement connue :
UTILISE `lardon-diagnose`.
Déclencheur typique :
« Je dois comprendre pourquoi X produit Y. »
Autres exemples :
- deux configurations différentes produisent le même résultat ;
- un invariant semble violé ;
- un test échoue pour une raison inconnue ;
- le comportement réel diffère du contrat ;
- plusieurs composants semblent corrects isolément mais incohérents ensemble.
Dans ces cas, ne commence pas toi-même une investigation longue.
Prépare pour `lardon-diagnose` :
- symptôme ;
- invariant attendu ;
- résultat observé ;
- reproduction disponible ;
- fichiers ou symboles déjà identifiés.
Après son rapport :
### Routage ORCHESTRATEUR
La correction est petite, locale, mécanique et évidente.
Tu peux l'appliquer directement.
### Routage LARDON-BUILD
La correction constitue une vraie tranche d'implémentation.
Transmets à `lardon-build` le diagnostic condensé.
### Routage ARCHITECTURE
Une décision architecturale doit précéder la correction.
Utilise `lardon-architect`.
### Routage BLOCAGE
Consigne précisément le blocage.
Ne demande jamais à `lardon-build` de recommencer une enquête déjà réalisée par
`lardon-diagnose`.
## Implémentation substantielle
`lardon-build` est l'implémenteur principal.
Délègue à `lardon-build` dès qu'il faut réellement développer quelque chose.
Exemples :
- nouveau fichier de test substantiel ;
- nouvelle fonctionnalité ;
- plusieurs fonctions cohérentes ;
- plusieurs fichiers liés ;
- modification d'API ;
- persistance ;
- migration ;
- format binaire ;
- fingerprint ;
- identité durable ;
- ownership ou lifetime ;
- concurrence ou synchronisation ;
- algorithme ;
- structure de données ;
- gestion mémoire non triviale ;
- performances ;
- refactorisation significative ;
- correction touchant plusieurs invariants.
Prépare une tranche cohérente contenant :
- objectif ;
- invariants à préserver ;
- fichiers ou symboles pertinents ;
- contraintes ;
- comportement attendu ;
- critères de réussite.
Délègue la tranche entière.
Ne micro-délègue pas fonction par fonction.
## Travail direct autorisé
Tu peux éditer directement uniquement pour une micro-modification mécanique dont
la solution est déjà connue.
Exemples :
- ajouter un include ;
- corriger une faute ;
- modifier quelques constantes ;
- renommer mécaniquement un symbole ;
- ajuster quelques call-sites ;
- corriger du formatage ;
- ajouter quelques assertions déjà spécifiées ;
- petite modification évidente de `meson.build` ;
- petite documentation mécanique ;
- mettre à jour `current_ticket.md` ;
- mettre à jour `handoff.md`.
Une modification directe doit normalement :
- être locale ;
- toucher un seul fichier ou quelques call-sites mécaniques ;
- ne nécessiter aucune exploration importante ;
- ne créer aucune nouvelle architecture ;
- ne modifier aucun invariant complexe ;
- représenter seulement quelques dizaines de lignes.
Utilise environ 40 lignes comme garde-fou.
Ce n'est pas une règle mathématique.
Une modification complexe de 10 lignes appartient à `lardon-build`.
Une modification purement mécanique légèrement supérieure peut rester directe.
## Interdiction de contourner la délégation
Ne découpe jamais une tranche substantielle en une série de petites éditions
pour pouvoir la réaliser toi-même.
Ne construis pas toi-même un gros nouveau fichier par plusieurs writes.
Ne considère pas :
« Chaque modification individuelle fait moins de 40 lignes »
comme une justification lorsque l'ensemble constitue clairement une tranche
d'implémentation.
Lorsque l'ensemble du travail ressemble à du développement :
UTILISE `lardon-build`.
## Tests : règle stricte
L'orchestrateur N'EST PAS l'agent de tests.
Ne lance pas toi-même :
- build normal ;
- suite de tests ;
- test unitaire ;
- test d'intégration ;
- sanitizer ;
- ASan ;
- UBSan ;
- TSan ;
- stress ;
- répétition de tests ;
- benchmark de validation ;
- `git diff --check` dans le cadre de la validation.
Toute exécution destinée à démontrer que le code fonctionne appartient à
`lardon-tests` ou, pour le test immédiatement lié à une tranche,
à `lardon-build` selon son contrat.
Ton rôle consiste à :
1. déterminer ce qui doit être validé ;
2. déléguer la validation ;
3. recevoir le résultat ;
4. décider de la suite.
Ne rejoue pas toi-même un test qu'un agent vient de déclarer PASS.
## Tests réalisés par lardon-build
`lardon-build` peut exécuter les tests ciblés nécessaires pour vérifier
immédiatement sa propre tranche.
C'est une vérification d'implémentation, pas la validation indépendante du
ticket.
Quand `lardon-build` rend :
`DONE`
avec ses tests ciblés PASS :
ne répète pas ces tests.
Passe à la suite.
## Validation par lardon-tests
Après une tranche cohérente ou lorsque le ticket doit être validé :
UTILISE `lardon-tests`.
Selon le besoin, demande notamment :
- build normal ;
- tests normaux ;
- tests ciblés du ticket ;
- `git diff --check` ;
- ASan/UBSan ;
- TSan ;
- stress ;
- répétitions ciblées ;
- investigation d'un timeout.
Les validations lourdes doivent rester séquentielles.
Une seule validation lourde à la fois.
Un timeout n'est jamais un PASS.
## Échec de validation
Si `lardon-tests` rapporte un échec :
### Cause inconnue
`lardon-diagnose`.
### Cause connue + micro-correction mécanique
→ correction directe possible.
### Cause connue + correction substantielle
`lardon-build`.
### Décision architecturale nécessaire
`lardon-architect`.
Après correction :
redélègue la validation nécessaire à `lardon-tests`.
Ne la réalise pas toi-même.
## Revue indépendante
Après validations vertes :
utilise `lardon-review`.
Ne relis pas toi-même le diff comme substitut à la seconde revue.
Si la revue trouve :
### petite correction mécanique
Tu peux la corriger directement.
### correction substantielle
`lardon-build`.
### cause inconnue
`lardon-diagnose`.
Après une correction de code :
redélègue les validations nécessaires à `lardon-tests`.
## Concurrence
Utilise `lardon-concurrency` lorsqu'un changement touche réellement :
- threads ;
- mutex ;
- conditions ;
- ordre des locks ;
- shutdown ;
- cancellation ;
- lifetime partagé ;
- queues concurrentes ;
- atomicité inter-thread.
Ne l'appelle pas artificiellement pour du code séquentiel.
## Documentation
Utilise `lardon-docs` lorsqu'une mise à jour documentaire substantielle est
nécessaire.
Tu peux effectuer directement une petite correction documentaire mécanique
lorsqu'elle ne nécessite aucune nouvelle analyse.
Ne documente jamais une garantie qui n'a pas été démontrée.
## Politique d'écriture
Pour une petite édition directe :
- modifie uniquement les lignes nécessaires ;
- évite de réécrire un fichier complet ;
- regroupe les modifications voisines.
Pour un nouveau fichier substantiel :
`lardon-build`.
Pour une grosse mise à jour documentaire :
`lardon-docs`.
Une attente `Preparing write...` n'est pas une justification pour changer de
rôle ou contourner la délégation.
## Gestion du ticket durable
Maintiens :
`.opencode/work/current_ticket.md`
après chaque phase importante.
Il doit permettre de savoir rapidement :
- objectif ;
- état actuel ;
- décisions prises ;
- invariants ;
- fichiers concernés ;
- validations réalisées ;
- validations restantes ;
- problèmes ouverts ;
- prochaine action.
Les sous-agents fournissent des synthèses.
L'orchestrateur reste propriétaire de l'état global du ticket.
## Handoff
Utilise :
`.opencode/work/handoff.md`
avant :
- une compaction risquée ;
- une fin de session incomplète ;
- un contexte presque épuisé.
Le handoff doit permettre une reprise sans refaire l'audit.
## Gestion du contexte
Si le pourcentage exact est disponible :
### > 30 %
Travail normal.
### 1530 %
- termine la phase courante ;
- évite les travaux secondaires ;
- consolide les décisions dans `current_ticket.md`.
### < 15 %
- ne commence pas de grosse tranche ;
- termine uniquement l'opération sûre déjà engagée ;
- mets à jour `current_ticket.md` ;
- prépare `handoff.md`.
### < 8 %
- aucune nouvelle modification ;
- handoff uniquement.
Ne remplis pas le contexte avec des logs complets.
Privilégie :
- résultat ;
- erreur ciblée ;
- fichiers ;
- décision ;
- prochaine action.
## Gestion des échecs d'agent
Pour `lardon-build` :
premier échec identique du fournisseur ou du tool calling :
→ retry ciblé.
Deuxième échec identique :
`lardon-build-backup`.
Si le fallback échoue également :
→ consigne précisément le blocage.
Pas de retry infini.
Ne traite pas un timeout de test comme un échec fournisseur.
## Mode long-run
Le mode absent est indiqué par :
`LARDON_OPENCODE_LONG_RUN=1`
Dans ce mode :
- continue automatiquement tant qu'une action sûre existe ;
- ne demande pas de confirmation pour une décision réversible ;
- ne t'arrête pas entre deux phases sûres ;
- maintiens régulièrement `current_ticket.md` ;
- prépare un handoff avant épuisement du contexte.
Le mode long-run ne change aucune règle de sécurité.
## Condition de continuation
Après CHAQUE retour d'un agent :
1. lis sa synthèse ;
2. mets à jour l'état du ticket si nécessaire ;
3. détermine la prochaine action ;
4. exécute ou délègue immédiatement cette action.
Ne termine jamais un tour simplement parce que :
- un agent vient de répondre ;
- une tranche est terminée ;
- un test passe ;
- une validation est terminée ;
- une revue est terminée ;
- une décision vient d'être prise ;
- la prochaine action est connue.
## Fin anticipée autorisée
Arrête-toi avant la fin complète uniquement si :
- une intervention utilisateur est réellement obligatoire ;
- une dépendance externe indispensable manque ;
- une opération nécessaire est interdite ;
- un blocage technique réel est démontré ;
- le contexte impose un handoff ;
- toutes les actions applicables sont terminées.
Une hésitation n'est pas un blocage.
Une difficulté locale n'est pas un blocage.
Une opération lente n'est pas automatiquement un blocage.
## Interaction utilisateur
Ne demande une intervention que pour :
- secret ou identifiant privé manquant ;
- installation d'une dépendance externe ;
- opération interdite ;
- décision produit irréversible ;
- choix entre solutions incompatibles également valides ;
- information impossible à déduire correctement.
Sinon :
choisis l'option conservatrice et réversible puis continue.
## Rapport final
À la fin du ticket, produis une synthèse contenant :
- résultat fonctionnel ;
- décisions importantes ;
- invariants ;
- fichiers créés ;
- fichiers modifiés ;
- tests ajoutés/modifiés ;
- build normal ;
- tests normaux ;
- ASan/UBSan ;
- TSan ;
- stress si pertinent ;
- `git diff --check` ;
- seconde revue ;
- audit concurrence si pertinent ;
- documentation ;
- limites connues ;
- éléments non implémentés ;
- fichiers appartenant au futur commit ;
- fichiers hors ticket préservés ;
- message de commit recommandé.
Ne déclare jamais PASS pour une exigence non exécutée ou non démontrée.
Utilise PARTIAL lorsqu'une exigence significative reste ouverte.
## Interdictions permanentes
Ne fais jamais :
- commit ;
- push ;
- `git add -A` ;
- reset ;
- clean ;
- rebase ;
- merge non demandé ;
- checkout destructif ;
- restore destructif ;
- modification de `.git/**` ;
- modification de `scan3d/**`.
Ne détruis jamais une modification préexistante pour rendre le working tree
propre.

View file

@ -17,6 +17,7 @@ permission:
"git diff*": allow
"git status*": allow
task: deny
disable: true
---
Lis uniquement le périmètre demandé. Tu peux découvrir un chemin avec `glob`,

View file

@ -15,6 +15,7 @@ permission:
"git diff*": allow
"git status*": allow
task: deny
disable: true
---
Relis uniquement le diff de production déjà validé.

View file

@ -19,6 +19,7 @@ permission:
"git diff --check*": allow
"git status*": allow
task: deny
disable: true
---
N'exécute jamais deux validations lourdes en parallèle. Ordre : test ciblé,

7
.opencode/agents/plan.md Normal file
View file

@ -0,0 +1,7 @@
---
description: Disabled by Lardon3D single-agent mode
mode: primary
disable: true
---
Disabled by project configuration.

View file

@ -0,0 +1,7 @@
---
description: Disabled by Lardon3D single-agent mode
mode: subagent
disable: true
---
Disabled by project configuration.

View file

@ -0,0 +1,567 @@
# Track Builder v1
## Status
**GATE A — PASS.** This document is the scientific and algorithmic contract
for the future Track Builder. It specifies no implementation, migration or
Task. `FACT`, `DECISION` and `FROZEN` are intentionally distinguished below.
## Scope
The Builder consumes immutable, pairwise `GEOMETRIC_VERIFIED` evidence and
produces a complete logical Track Set. A Track is a set of coherent 2D
observations; it is not a 3D point. The Builder does not triangulate, estimate
poses, calculate 3D reprojection, or optimise cameras.
## Frozen upstream contracts
The following are **FROZEN** and are consumed without reinterpretation:
- Track Model v1 and Project DB v14.
- Observation identity `(feature_set_id, feature_index)`.
- One observation per image, minimum two observations per Track, and no
persistent rejected Track state.
- Exact VERIFICATION_SELECTOR
`(verifier_kind, verifier_version, verifier_fingerprint)`.
- `input_scope_hash = SHA-256("L3DTSIS1" || sorted uint64 little-endian GVR
IDs)` and `gvr_count`; the scope is DB-local and non-empty.
- GVR status and inlier-mask representation, including LSB-first bit mapping
to the canonical Match File entry order.
- Match Result, Candidate Pair and Feature Set ownership.
- Atomic complete Track Set publication and exact-identity reuse.
## Definitions
An **observation** is `(feature_set_id, feature_index)`. Its `image_id` is
derived from the immutable Feature Set. An **edge** is a verified relation
between two observations. A **component** is a connected component of the
graph of unique accepted edges. A **Track** is published only when its
component satisfies every Track Model and Builder invariant.
## Scientific input
The orchestration layer is the **input owner**: it supplies one explicit,
canonical, strictly increasing list of GVR IDs. The scientific core owns no
SQLite connection and consumes an immutable representation of that list and
the resolved evidence. It never expands the list to “all GVRs”.
For each listed GVR, the orchestration layer resolves the complete parent
chain:
`GVR → Match Result → Candidate Pair → Feature Set A/B → Image A/B`.
The Match File header must agree with the parent Feature Set IDs and counts.
For every set bit `i`, the Builder reads Match File entry `i` unchanged and
maps it to `(feature_set_id_a, feature_index_a)` and
`(feature_set_id_b, feature_index_b)`. It does not sort entries before mask
application.
## Verification Selector
The selector is exactly the FROZEN Track Model tuple
`(verifier_kind, verifier_version, verifier_fingerprint)`. A GVR is eligible
only when its status is `GEOMETRIC_VERIFIED` and all three fields match
exactly. `latest`, timestamps, greatest IDs and insertion order are forbidden.
## Input Scope
The explicit supplied IDs are sorted numerically, unique, counted, and hashed
with the FROZEN `L3DTSIS1` encoding. The supplied count must equal `gvr_count`.
The selector is not part of `input_scope_hash`; it remains part of Track Set
identity through the verifier fields. A scope is exact, DB-local and non-empty.
## Snapshot semantics
The caller resolves and validates the exact ID list in a short read phase,
then releases the database lock before graph computation. GVRs, Match Results,
Feature Sets and Match Files are treated as immutable published inputs. New
GVRs arriving after resolution are not silently included. Before publication,
the orchestration layer revalidates the exact count, IDs, selector, status,
parent chain, asset identity and streaming scope hash. A missing, extra or
changed item aborts the build; it never becomes a 999-of-1000 partial result.
No long SQLite transaction spans graph computation.
## GVR eligibility
`GEOMETRIC_VERIFIED` plus exact selector match is the complete eligibility
rule. `GEOMETRIC_REJECTED`, Match Result `NO_MATCH`, and runtime failures
produce no Builder edge. A runtime failure is not scientific rejection.
## Observation identity
The only Builder observation identity is the FROZEN pair
`(feature_set_id, feature_index)`. `image_id` is metadata derived for the
one-image invariant, never a replacement identity.
## Edge definition
Edges are **undirected**. Their exact identity is the lexicographically sorted
pair of the two distinct observation identities. Self-edges are invalid input.
The edge carries no scientific weight. Descriptor distance, Lowe ratio,
inlier count, matrix values and `inlier_count / match_count` are not individual
edge quality values.
## Duplicate-edge policy
Exact duplicate edges collapse to one logical edge before component analysis.
Evidence multiplicity is not a vote and does not affect acceptance. Source GVR
IDs may be retained as temporary diagnostic evidence, but edge-level
provenance is not persisted and no duplicate can strengthen a relation.
## Multiple Matcher configurations
**DECISION:** scopes may contain GVRs whose parent Match Results use different
`matcher_kind`, matcher version or matcher fingerprints. This is safe because
the Builder consumes only verified membership relations and never compares
descriptor scores. The parent identity remains available for diagnostics.
The selector selects only the verifier, not a hidden Matcher configuration.
An implementation must still reject malformed parent ownership or asset
metadata; it must not silently substitute another Match Result.
## Feature Set homogeneity
Every published Track component must use one exact Feature Set configuration:
the tuple `(extractor_kind, extractor_version, parameter_fingerprint,
descriptor_type, descriptor_dimension)`. Different Feature Set IDs with that
same tuple are allowed for different images. A component mixing tuples is a
Builder conflict and is unpublished. This is a fixed v1 rule, not a user knob.
## Cross-descriptor semantics
**DECISION: FORBIDDEN.** ORB, SIFT and RootSIFT observations are not fused in a
Track. Their keypoints and descriptor semantics do not establish identical
physical observations merely from 2D proximity, and no such upstream
equivalence contract exists. Cross-descriptor edges are therefore a
heterogeneous component conflict, not an opportunity for spatial merging.
## Graph semantics
Nodes are observations appearing in at least one valid inlier edge. Edges are
unique verified relations. A cycle is valid and contributes membership, not a
requirement to persist every edge. The graph is sparse; no dense image,
observation or co-visibility matrix is created.
## Conflict definition
A component conflicts when either:
1. two observations derive to the same `image_id`; or
2. its Feature Set configuration is not homogeneous under the exact tuple
above.
Duplicate references to the same observation are not a conflict. A component
with a conflict is not a Track candidate for publication.
## Conflict policy
**DECISION: REJECT WHOLE CONFLICTING COMPONENT.** After all unique edges have
been considered, a component is published only if it has one observation per
image and one Feature Set configuration. Otherwise every membership in that
component is unpublished. There is no `TRACK_REJECTED` persistent state.
This conservative policy does not invent a local winner between contradictory
pairwise evidence. It cannot merge two observations from one image, does not
need incomparable descriptor scores, and has no scientific ranking order.
It can discard a large otherwise useful component after one bad edge; this is
an explicit recall trade-off accepted for v1. A future splitting or weighted
optimisation policy must be a new Builder version and prove equivalence or
declare a new scientific identity.
## Scientific edge ordering
**NONE.** No edge is scientifically better than another. Edges are a set, and
the whole-component policy is evaluated after set construction. Consequently,
DB IDs, GVR IDs, timestamps, feature indices and hashes are never scientific
quality measures.
## Deterministic tie-breaking
Tie-breaking is used only to canonicalize equal logical results. The exact
observation key is `(feature_set_id, feature_index)` in unsigned numeric
lexicographic order. No tie-break changes component acceptance.
## Determinism
The logical result is independent of SQLite row order, GVR enumeration,
pagination, duplicate enumeration, allocation order and future scheduling.
The implementation must canonicalize/deduplicate the complete exact scope
before final component decisions. A future parallel implementation may split
work, but must produce the same set semantics and canonical output.
## Track canonicalization
Within each accepted Track, observations are sorted by
`(feature_set_id, feature_index)` ascending. `position_in_track` is that
sequence starting at zero. DSU roots, pointers, hash-table order and SQL row
order never determine positions.
## Track Set canonicalization
Accepted Tracks are sorted by their ordered observation-membership sequences,
lexicographically: compare observation keys at the first difference, then the
shorter sequence first if one is a prefix. The published Track IDs remain
opaque AUTOINCREMENT IDs; logical tests compare this canonical sequence, not
Track IDs.
## Minimum length and singletons
The minimum is exactly the FROZEN structural minimum of two observations.
Singleton nodes are never produced by an edge component and are dropped if
they arise during future internal conflict handling. No arbitrary minimum of
three and no maximum length is introduced.
## Builder identity
`builder_kind` is conceptually `track_builder` (lowercase ASCII, matching the
project's persisted kind convention). `builder_version` is `1`.
## Builder version
Any change to the scientific conflict, duplicate, descriptor, edge or
canonicalization policy requires a new `builder_version`, unless the change
is explicitly represented by a new parameter fingerprint under the versioning
rules. Runtime, storage or performance changes do not qualify.
## Parameter fingerprint
The fingerprint is SHA-256 of the following 48-byte canonical encoding:
| Offset | Width | Field |
|---:|---:|---|
| 0 | 8 | ASCII domain `L3DTBFP1` |
| 8 | 4 | fingerprint encoding version `1` |
| 12 | 4 | Builder policy version `1` |
| 16 | 4 | whole-component conflict policy `1` |
| 20 | 4 | exact duplicate collapse policy `1` |
| 24 | 4 | mixed Matcher parent configurations allowed `1` |
| 28 | 4 | exact Feature Set tuple homogeneity `1` |
| 32 | 4 | undirected canonical observation edge `1` |
| 36 | 4 | Track/Track Set canonicalization version `1` |
| 40 | 4 | minimum Track length `2` |
| 44 | 4 | reserved, zero |
All integer fields are fixed-width unsigned 32-bit little-endian. There are
no booleans, floats, ABI padding or raw C structs. The hash output is exactly
32 bytes SHA-256. The builder kind and builder version are separate Track Set
identity fields; policy version is nevertheless encoded to make the contract
reviewable.
Excluded runtime-only fields: page size, batch size, thread count, worker
count, RAM budget, CPU/GPU slot, Governor state, PSI, swap, scheduling,
allocation addresses, temporary buffer size, DB transaction duration and
hardware identity.
## Provenance
Persisted provenance is exactly the FROZEN Track Set level: Builder identity,
verifier selector, `input_scope_hash` and `gvr_count`. Edge-to-GVR evidence is
temporary only for deduplication and diagnostics. No new edge provenance table
is required.
## Error ownership
Scientific non-selection means an edge is discarded only by duplicate collapse
or a deterministic conflicting-component result. Runtime error covers I/O,
allocation, cancellation and database failure. Input corruption covers any
invalid published contract. Runtime error or corruption produces no Track Set
publication and never becomes a scientific rejection.
## Corruption handling
The build aborts without publication on: missing GVR, wrong selector, wrong
status, missing Match Result/Candidate Pair/Feature Set/Match File, ownership
mismatch, invalid Match File header or SHA, malformed mask, wrong mask size or
padding, mask/match-count mismatch, out-of-range feature index, missing parent,
inconsistent image ownership, invalid Feature Set metadata, scope hash/count
mismatch, or changed input during final validation. No scope item is skipped.
## Empty input and zero-Track output
An empty scope is invalid because FROZEN Track Model v1 requires
`gvr_count >= 1`; it is an input/constraint error and publishes nothing. A
non-empty, fully valid scope may deterministically produce zero valid Tracks
when every component conflicts or all components are below the structural
minimum. `track_count = 0` is permitted by Track Model v1 and is published as
a complete Track Set with the exact non-empty scope.
## Resource model
**DECISION:** use a full sparse in-memory graph with deterministic edge sort,
not a dense matrix, SQLite temporary graph or external merge in v1. This is
the minimum complexity credible for the 16 GiB target and permits exhaustive
canonical finalization. Input Match Files are read one at a time; they are
never all resident.
Approximate implementation-neutral accounting, including allocator/alignment
headroom:
- observation key: 1624 bytes;
- unique edge: 1632 bytes for two compact node references and sort metadata;
- DSU/component node: 1632 bytes;
- image-membership metadata: 824 bytes per component observation;
- temporary evidence: 024 bytes per raw edge, bounded and discardable after
deduplication;
- canonicalization entry: 1632 bytes per accepted observation.
Indicative totals, not new contractual limits:
- 100k observations: roughly 515 MiB plus edges and allocator overhead;
- 1M observations: roughly 50150 MiB before unusually dense edge evidence;
- 1M unique edges: roughly 2060 MiB, plus node/component metadata.
The representation remains feasible on the target Ryzen 7 8845HS / about
16 GiB RAM, subject to future measurement. No dense matrix is used.
## Complexity
For `V` unique observations, `E_raw` inlier edges and `E` unique edges:
- input DB/asset I/O: `O(gvr_count + E_raw)` bounded reads;
- edge canonicalization and sort: expected `O(E_raw log E_raw)`;
- duplicate collapse: `O(E_raw)` after sort;
- component construction: `O(V + E)` with a sparse DSU or equivalent;
- conflict detection and membership canonicalization: `O(V log V)` worst case;
- Track Set ordering: `O(V log V)` worst case, with comparison cost included;
- total expected: `O(E_raw log E_raw + V log V)`;
- worst-case memory: `O(V + E_raw)`; no `O(image_count²)` or `O(V²)` storage.
Paged enumeration is deterministic but does not change the asymptotic sort
cost. An external deterministic merge may be introduced only if measurements
show the chosen memory envelope insufficient; it must preserve the same
logical output.
## Publication boundary
The conceptual split is:
1. DB adapter resolves and validates the exact immutable scope and parent
evidence.
2. SQLite-free scientific core builds the complete canonical logical result.
3. Track Model API validates Feature Set ownership, image uniqueness, bounds,
counts and publishes the complete set in its short atomic transaction.
No Track, observation or partial set is visible before commit. The frozen
`lardon3d_project_db_create_track_set()` API is the publication boundary.
## Reuse
Before reading Match Files, the orchestration layer performs exact identity
lookup using builder identity, Builder fingerprint, verifier selector and
input scope hash. A found set is reusable only if its stored `gvr_count`
matches the supplied count and the loaded set is coherent. No latest-result
selection and no overwrite are allowed.
## Crash semantics
A crash after compute and before publication leaves no partial scientific
result. Retry recomputes the exact scope in v1. A crash during the frozen
publication transaction rolls back; if commit happened, exact identity lookup
reuses the complete set. Existing immutable upstream data is untouched.
## Incrementality
**FULL REBUILD v1: YES.** Track Sets are immutable. New GVRs require a new
scope hash and a new Track Set identity; the old set remains valid. No delta
Builder or checkpoint graph persistence is introduced here.
## Future Task boundary
A future Task, likely named `track_builder.run`, treats one complete Track Set
as its scientific unit. It may page GVR resolution and pause at safe input or
compute boundaries, but publication occurs only after complete canonical
output and final scope validation. A cancelled or failed run publishes no
incomplete set. The exact Task kind is not implemented or persisted by Gate A.
## Future Resource Governor integration
The future Task estimates bounded graph memory and CPU, obtains reservations
before callbacks, and may reduce batches or pause under pressure. Batch size,
thread count, Governor state, PSI, swap and throttling may affect throughput
only; they must not affect logical edges, conflict decisions or output order.
## Vulkan
**NOT_JUSTIFIED.** The Builder is sparse graph construction and conflict
handling, expected to be CPU/memory-bound. No GPU identity or backend is part
of v1.
## Adversarial corpus
The following expected outputs use `T{...}` for canonical observation lists.
An edge shown as discarded is not persisted; “dropped observations” means
members of an unpublished component.
| Case | Input | Conflict? | Expected tracks | Discarded edges | Dropped observations / why |
|---|---|---|---|---|---|
| 1 chain | A1-B1, B1-C1 | No | T{A1,B1,C1} | none | none |
| 2 cycle | A1-B1, B1-C1, C1-A1 | No | T{A1,B1,C1} | none | none; cycle is valid |
| 3 duplicate | A1-B1 twice | No | T{A1,B1} | duplicate copy | none; multiplicity is not weight |
| 4 same image | A1-B1, B1-C1, A1-C2 | Yes | none | none after collapse | all four; C1/C2 conflict |
| 5 late conflict | A1-B1-B2-C1 then A1-C2 | Yes | none for component | none | whole component rejected |
| 6 disjoint | A1-B1; C1-D1 | No | T{A1,B1}, T{C1,D1} | none | none |
| 7 bridge | A1-B1; C1-D1; B1-C2 | Yes | none for bridged component | none | all bridged members; duplicate image C |
| 8 duplicate GVR | same A1-B1 in GVR1/GVR2 | No | T{A1,B1} | duplicate evidence | none |
| 9 permutation | prior graph, shuffled | same | same canonical tracks | same | order irrelevant |
| 10 single edge | A1-B1 | No | T{A1,B1} | none | none; length 2 is valid |
| 11 singleton | one node after hypothetical split | n/a | none | policy-local | singleton dropped; no rejected row |
| 12 two Feature Sets | A:X1-B:Y1, B:Y1-C:X2 | Yes | none | none | heterogeneous Feature Set tuple |
| 13 Matcher configs | same-descriptor edges from M1/M2 | No | connected Track | none | none; scores unused |
| 14 star | A1-B1, A1-C1, A1-D1 | No | T{A1,B1,C1,D1} | none | none |
| 15 chain large | A1-B1-...-N1 | No | one ordered Track | none | none |
| 16 conflict-heavy | repeated image IDs | per component | valid components | none | conflicts dropped |
## Validation plan
Gate B/C must include exhaustive small graphs over 35 images and small
observation counts. Check no duplicate image, no cross-Track observation,
minimum length, no overlap, canonical ordering and permutation invariance.
Run each logical input 100 times in one process and in fresh processes; vary
GVR order, edge order, page size and practical DB row order. Future 1-thread
and N-thread runs must hash the same canonical logical output. A non-persistent
test hash may hash sorted Tracks and sorted observations; it is not Track Set
identity.
Future performance runs should cover 10k, 100k and 1M edges with low-conflict,
high-conflict, tiny-track, huge-component, chain and star distributions.
Measure wall time, CPU, RSS peak, bytes/node, bytes/edge and throughput.
## Explicitly out of scope
Triangulation, camera pose estimation, Sparse SfM, Essential matrix, Bundle
Adjustment, reprojection optimisation, 3D points, co-visibility matrices,
dense reconstruction, Vulkan Builder implementation, persistent edge
provenance, Track Model schema changes, Project DB v15, Track Builder Task
implementation and incremental optimisation implementation.
## Open questions
None critical for Gate A. Gate B must choose concrete in-memory data
structures without changing this scientific contract. A future API is needed
to page an exact selector-filtered GVR ID enumeration for callers that do not
already possess the explicit scope.
## Future transverse updates
**FUTURE TRANSVERSE UPDATE REQUIRED**
- file: `include/lardon3d/project_db.h` and `src/project_db.c`
- reason: expose a bounded, selector-filtered, deterministic GVR ID
enumeration so orchestration can construct the explicit scope without a
private SQL query; no schema change is required.
- expected gate: Gate B orchestration/API preparation.
## Gate status
**PASS.** All critical scientific and identity decisions are closed. No Track
Model or Project DB contract is redefined, no partial input is accepted, and
the result is independent of SQL order, pagination and future thread count.
## Implementation status
Gate A contract: **PASS/FROZEN**. Gate B pure core: **PASS**.
The Gate B core is a SQLite-free, single-threaded C-compatible API. The caller
provides resolved observations and edges and retains ownership of those input
arrays. The result owns separately allocated canonical Track arrays and is
released with `lardon3d_track_builder_result_free()`; a zero-initialized result
is safe to free repeatedly. Invalid input returns no partial result.
The implementation validates observation metadata, sorts and deduplicates
undirected edges, builds a deterministic observation table, unions every unique
edge with a sparse DSU, and validates complete components for image uniqueness
and exact Feature Set homogeneity. Invalid components are rejected as a whole;
valid components of at least two observations become lexicographically
canonical Tracks and Track Sets. Hash lookup is used only for index lookup; no
hash iteration order or DSU root is exposed.
The exact 48-byte `L3DTBFP1` encoding is hashed with SHA-256. Runtime values,
input scope and edge provenance are excluded. Measured synthetic chain runs
were 0.002 s, 0.032 s and 0.607 s for 10k, 100k and 1M edges respectively on
the development host, with a 1,000,000-edge process peak of 973,752 KiB. For
that chain the counts are 1,000,000 raw and unique edges, 1,000,001 unique
observations, one Track and zero rejected components. The public input arrays
occupy 152,000,136 bytes (136,000,136 bytes of observations and 16,000,000
bytes of pointer edges). The core uses 16,000,000 bytes of compact normalized
edges, a 136,000,136-byte canonical metadata table, 9,000,009 bytes for DSU
arrays, approximately 32 MiB for component vectors/capacity, and 24,000,024
bytes for compact output observations plus Track storage. The remaining
high-water RSS is allocator/hash-table/vector capacity and benchmark process
overhead; RSS is a process high-water metric, not the sum of live logical
payloads. No full metadata is copied into core edges. The benchmark remains
non-default. Targeted normal and
ASan/UBSan validation passed, including the 32,768-graph oracle and a 301
observation Track.
## Gate B closure audit
OpenSSL was already a project dependency before the Track Builder changes and
is used by the existing asset, Match File, matcher and verifier hashing paths.
Track Builder adds no new external dependency and does not introduce a second
internal SHA-256 helper. Its fingerprint vector remains
`e1f1fae479bcf82001a5b33dda331195617b8751668e46a6cf1eecf2d125df31`.
The 1M benchmark's `973752 KiB` is a process high-water RSS measurement. The
synthetic caller retains 136,000,136 bytes of observations and 16,000,000
bytes of pointer edges while the core retains its 136,000,136-byte canonical
metadata table, 16,000,000-byte normalized edges, DSU/component temporaries
and compact output. Compact edges contain node indices only; they do not copy
136-byte metadata at either endpoint. The remaining high-water gap is
primarily allocator retention plus hash-table buckets/nodes and vector
capacity. No core duplication bug was found or changed in the closure audit.
The targeted corpus explicitly covers reversed endpoints, 100 identical
edges, contradictory metadata, fingerprint/dimension/version conflicts,
301-observation tracks, disjoint length-2 tracks, sorted and reverse-sorted
edges, empty edge lists and self-edges. The 32,768 production-DSU versus
independent DFS exhaustive comparison remains passing. Cross-process
repeatability was not run and is non-blocking for Gate B.
## Gate C — Project DB orchestration
**DECISION:** Gate C exposes an explicit-scope adapter around the SQLite-free
Gate B core. The caller owns a copied, strictly increasing, non-empty GVR ID
list and supplies the exact verifier selector. The adapter computes the frozen
`L3DTSIS1` digest and performs exact Track Set reuse before reading any asset.
On a reuse miss, each selected GVR is loaded and checked as
`GVR → Match Result → Candidate Pair → Feature Set A/B → Image A/B`. The
content-addressed Match File is validated and read one at a time; its entries
are consumed in file order and the persisted LSB-first mask selects inliers.
Only Feature Set metadata is loaded—descriptor payloads are never read. A
small per-build Feature Set cache is permitted, while Match File buffers are
released immediately after edge extraction.
The complete resolved graph is passed unchanged to the Gate B core. Before the
frozen `lardon3d_project_db_create_track_set()` publication transaction, the
same explicit GVR IDs, status, selector, parent chain and scope count/hash are
revalidated. Any missing, rejected, mismatched or corrupt selected input aborts
the whole build; no partial Track Set is published. New matching GVRs are not
discovered or included. The Track Model API owns timestamps, validation,
atomicity and late exact-identity reuse. Project DB remains v14; Task and
Resource Governor orchestration are not part of Gate C.
The implementation does not add a selector discovery API: callers that need
discovery must enumerate and freeze their own explicit list in a separate
operation.
DB orchestration: **GATE C IMPLEMENTED**. Task: **NOT_IMPLEMENTED**. Resource
Governor: **NOT_IMPLEMENTED**. Project DB remains v14 and unchanged.
## Gate C closure evidence (current worktree)
The integration harness now constructs a synthetic Project DB v14 through the
public image, Feature Set, Candidate Pair, Match Result, GVR and Track Model
APIs. C01C25 and the partial-input proof pass, including corruption,
scientific conflicts, zero-track publication, close/reopen durability,
different-scope identity and late matching-GVR exclusion. The test-only phase
seam is compiled only into the Gate C test executable.
Normal and AddressSanitizer/UndefinedBehaviorSanitizer targeted runs pass for
the core and project executables. The 106,496-edge synthetic resource case
completed in 0.281 seconds with a 26,564 KiB process high-water RSS and one
Match File live at a time. C26 has no clean fault injection in the frozen DB
API and C27 belongs to the future concurrency gate.
Gate C status: **PASS**. Gate A: **PASS**. Gate B: **PASS**. Gate D:
**NOT_IMPLEMENTED**. Gate E: **NOT_DONE**. Track Builder v1 is not marked
fully frozen by this document; the future runtime gates remain separate.

View file

@ -0,0 +1,79 @@
#ifndef LARDON3D_TRACK_BUILDER_H
#define LARDON3D_TRACK_BUILDER_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
enum {
LARDON3D_TRACK_BUILDER_VERSION = 1,
LARDON3D_TRACK_BUILDER_FINGERPRINT_SIZE = 32,
LARDON3D_TRACK_BUILDER_FINGERPRINT_INPUT_SIZE = 48,
LARDON3D_TRACK_BUILDER_KIND_CAPACITY = 64,
};
typedef enum {
LARDON3D_TRACK_BUILDER_OK = 0,
LARDON3D_TRACK_BUILDER_INVALID_ARGUMENT,
LARDON3D_TRACK_BUILDER_CONSTRAINT,
LARDON3D_TRACK_BUILDER_CORRUPT_INPUT,
LARDON3D_TRACK_BUILDER_OUT_OF_MEMORY,
} Lardon3DTrackBuilderResult;
typedef struct {
uint64_t feature_set_id;
uint32_t feature_index;
uint64_t image_id;
char extractor_kind[LARDON3D_TRACK_BUILDER_KIND_CAPACITY];
uint32_t extractor_version;
unsigned char parameter_fingerprint[32];
uint32_t descriptor_type;
uint32_t descriptor_dimension;
} Lardon3DTrackBuilderObservation;
typedef struct {
const Lardon3DTrackBuilderObservation *first;
const Lardon3DTrackBuilderObservation *second;
} Lardon3DTrackBuilderEdge;
typedef struct {
uint64_t feature_set_id;
uint32_t feature_index;
uint64_t image_id;
} Lardon3DTrackBuilderTrackObservation;
typedef struct {
size_t observation_count;
Lardon3DTrackBuilderTrackObservation *observations;
} Lardon3DTrackBuilderTrack;
typedef struct {
size_t track_count;
Lardon3DTrackBuilderTrack *tracks;
} Lardon3DTrackBuilderResultSet;
/* The result must be zero-initialized before the first build. Inputs remain
* caller-owned and are not modified. Output ownership is returned to the
* caller; free it exactly (or repeatedly) with result_free. */
Lardon3DTrackBuilderResult lardon3d_track_builder_build(
const Lardon3DTrackBuilderObservation *observations,
size_t observation_count, const Lardon3DTrackBuilderEdge *edges,
size_t edge_count, Lardon3DTrackBuilderResultSet *result);
void lardon3d_track_builder_result_free(Lardon3DTrackBuilderResultSet *result);
bool lardon3d_track_builder_fingerprint_bytes(
unsigned char bytes[LARDON3D_TRACK_BUILDER_FINGERPRINT_INPUT_SIZE]);
bool lardon3d_track_builder_fingerprint(
unsigned char fingerprint[LARDON3D_TRACK_BUILDER_FINGERPRINT_SIZE]);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -0,0 +1,57 @@
#ifndef LARDON3D_TRACK_BUILDER_PROJECT_H
#define LARDON3D_TRACK_BUILDER_PROJECT_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/project_db.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
const char *project_path;
Lardon3DProjectDb *database;
int verifier_kind;
uint32_t verifier_version;
const unsigned char *verifier_fingerprint;
const uint64_t *gvr_ids;
size_t gvr_count;
} Lardon3DTrackBuilderProjectRequest;
typedef struct {
uint64_t track_set_id;
uint64_t gvr_count;
uint64_t raw_inlier_edge_count;
uint64_t core_observation_count;
uint64_t track_count;
bool reused;
} Lardon3DTrackBuilderProjectResult;
typedef enum {
LARDON3D_TRACK_BUILDER_PROJECT_OK = 0,
LARDON3D_TRACK_BUILDER_PROJECT_INVALID_ARGUMENT,
LARDON3D_TRACK_BUILDER_PROJECT_NOT_FOUND,
LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT,
LARDON3D_TRACK_BUILDER_PROJECT_DATABASE_ERROR,
LARDON3D_TRACK_BUILDER_PROJECT_OUT_OF_MEMORY,
LARDON3D_TRACK_BUILDER_PROJECT_CORE_ERROR,
} Lardon3DTrackBuilderProjectStatus;
Lardon3DTrackBuilderProjectStatus lardon3d_track_builder_build_project(
const Lardon3DTrackBuilderProjectRequest *request,
Lardon3DTrackBuilderProjectResult *result);
#ifdef LARDON3D_TRACK_BUILDER_PROJECT_TESTING
/* Test-only phase seam; never present in the production header/API. */
void lardon3d_track_builder_project_test_before_revalidation(
Lardon3DProjectDb *database, const uint64_t *gvr_ids, size_t gvr_count);
#endif
#ifdef __cplusplus
}
#endif
#endif

View file

@ -155,6 +155,8 @@ executable(
'src/match_file.c',
'src/matcher.cpp',
'src/geometric_verifier.cpp',
'src/track_builder.cpp',
'src/track_builder_project.cpp',
] + matcher_backend_sources,
include_directories: include_directories('include'),
dependencies: [ncursesw, threads, sqlite3, openssl, opencv, opencv_geometry]
@ -831,3 +833,37 @@ geometric_verifier_task_test = executable(
)
test('geometric-verifier-task', geometric_verifier_task_test, timeout: 60,
env: opencv_test_environment)
track_builder_test = executable(
'test-track-builder-core',
sources: ['tests/test_track_builder_core.cpp', 'src/track_builder.cpp'],
include_directories: include_directories('include'),
dependencies: [openssl],
)
test('track-builder-core', track_builder_test, timeout: 120)
track_builder_project_test = executable(
'test-track-builder-project',
sources: [
'tests/test_track_builder_project.cpp',
'src/track_builder.cpp',
'src/track_builder_project.cpp',
'src/project_db.c',
'src/task.c',
'src/resource_governor.c',
'src/resource_snapshot.c',
'src/match_file.c',
],
include_directories: include_directories('include'),
cpp_args: ['-DLARDON3D_TRACK_BUILDER_PROJECT_TESTING'],
dependencies: [threads, sqlite3, openssl],
)
test('track-builder-project', track_builder_project_test, timeout: 60)
executable(
'benchmark-track-builder',
sources: ['tests/benchmark_track_builder.cpp', 'src/track_builder.cpp'],
build_by_default: false,
include_directories: include_directories('include'),
dependencies: [openssl],
)

239
src/track_builder.cpp Normal file
View file

@ -0,0 +1,239 @@
#include <algorithm>
#include <cstring>
#include <limits>
#include <new>
#include <openssl/evp.h>
#include <unordered_map>
#include <vector>
extern "C" {
#include <lardon3d/track_builder.h>
}
namespace {
using Observation = Lardon3DTrackBuilderObservation;
using Edge = Lardon3DTrackBuilderEdge;
struct CompactEdge {
size_t low;
size_t high;
};
bool identity_less(const Observation &a, const Observation &b) {
return a.feature_set_id < b.feature_set_id ||
(a.feature_set_id == b.feature_set_id &&
a.feature_index < b.feature_index);
}
bool identity_equal(const Observation &a, const Observation &b) {
return a.feature_set_id == b.feature_set_id &&
a.feature_index == b.feature_index;
}
bool metadata_equal(const Observation &a, const Observation &b) {
return a.image_id == b.image_id &&
a.extractor_version == b.extractor_version &&
a.descriptor_type == b.descriptor_type &&
a.descriptor_dimension == b.descriptor_dimension &&
std::strncmp(a.extractor_kind, b.extractor_kind,
LARDON3D_TRACK_BUILDER_KIND_CAPACITY) == 0 &&
std::memcmp(a.parameter_fingerprint, b.parameter_fingerprint, 32) == 0;
}
bool observation_valid(const Observation &observation) {
return observation.feature_set_id != 0 && observation.image_id != 0 &&
std::memchr(observation.extractor_kind, '\0',
LARDON3D_TRACK_BUILDER_KIND_CAPACITY) != nullptr;
}
struct Dsu {
std::vector<size_t> parent;
std::vector<unsigned char> rank;
explicit Dsu(size_t size) : parent(size), rank(size, 0) {
for (size_t i = 0; i < size; ++i) parent[i] = i;
}
size_t root(size_t value) {
while (parent[value] != value) {
parent[value] = parent[parent[value]];
value = parent[value];
}
return value;
}
void unite(size_t a, size_t b) {
a = root(a); b = root(b);
if (a == b) return;
if (rank[a] < rank[b]) std::swap(a, b);
parent[b] = a;
if (rank[a] == rank[b]) ++rank[a];
}
};
struct IdentityHash {
size_t operator()(const std::pair<uint64_t, uint32_t> &key) const noexcept {
uint64_t value = key.first ^ (static_cast<uint64_t>(key.second) +
0x9e3779b97f4a7c15ULL + (key.first << 6U) +
(key.first >> 2U));
value ^= value >> 30U;
value *= 0xbf58476d1ce4e5b9ULL;
value ^= value >> 27U;
return static_cast<size_t>(value ^ (value >> 31U));
}
};
std::pair<uint64_t, uint32_t> identity_key(const Observation &observation) {
return {observation.feature_set_id, observation.feature_index};
}
bool digest(const unsigned char *input, size_t size, unsigned char output[32]) {
unsigned int output_size = 0;
return EVP_Digest(input, size, output, &output_size, EVP_sha256(), nullptr) == 1 &&
output_size == 32;
}
} // namespace
extern "C" bool lardon3d_track_builder_fingerprint_bytes(unsigned char bytes[48]) {
if (!bytes) return false;
std::memset(bytes, 0, 48);
std::memcpy(bytes, "L3DTBFP1", 8);
const uint32_t fields[] = {1, 1, 1, 1, 1, 1, 1, 1, 2, 0};
for (size_t field = 0; field < 10; ++field)
for (size_t byte = 0; byte < 4; ++byte)
bytes[8 + field * 4 + byte] =
static_cast<unsigned char>(fields[field] >> (byte * 8));
return true;
}
extern "C" bool lardon3d_track_builder_fingerprint(unsigned char fingerprint[32]) {
if (!fingerprint) return false;
unsigned char bytes[48];
return lardon3d_track_builder_fingerprint_bytes(bytes) && digest(bytes, 48, fingerprint);
}
extern "C" void lardon3d_track_builder_result_free(Lardon3DTrackBuilderResultSet *result) {
if (!result) return;
if (result->tracks) {
for (size_t i = 0; i < result->track_count; ++i)
delete[] result->tracks[i].observations;
delete[] result->tracks;
}
result->tracks = nullptr;
result->track_count = 0;
}
extern "C" Lardon3DTrackBuilderResult lardon3d_track_builder_build(
const Observation *input_observations, size_t observation_count, const Edge *input_edges,
size_t edge_count, Lardon3DTrackBuilderResultSet *result) {
if (!result || (observation_count != 0 && !input_observations) ||
(edge_count != 0 && !input_edges))
return LARDON3D_TRACK_BUILDER_INVALID_ARGUMENT;
lardon3d_track_builder_result_free(result);
try {
if (edge_count > std::numeric_limits<size_t>::max() / 2)
return LARDON3D_TRACK_BUILDER_INVALID_ARGUMENT;
std::vector<Observation> table;
if (observation_count != 0)
table.assign(input_observations, input_observations + observation_count);
for (const Observation &observation : table)
if (!observation_valid(observation)) return LARDON3D_TRACK_BUILDER_CORRUPT_INPUT;
std::sort(table.begin(), table.end(), identity_less);
for (size_t i = 1; i < table.size(); ++i)
if (identity_equal(table[i - 1], table[i]) &&
!metadata_equal(table[i - 1], table[i]))
return LARDON3D_TRACK_BUILDER_CORRUPT_INPUT;
table.erase(std::unique(table.begin(), table.end(), identity_equal), table.end());
std::unordered_map<std::pair<uint64_t, uint32_t>, size_t, IdentityHash> table_indices;
table_indices.reserve(table.size());
for (size_t i = 0; i < table.size(); ++i) table_indices.emplace(identity_key(table[i]), i);
std::vector<CompactEdge> normalized;
normalized.reserve(edge_count);
for (size_t i = 0; i < edge_count; ++i) {
const Edge &edge = input_edges[i];
if (!edge.first || !edge.second || !observation_valid(*edge.first) ||
!observation_valid(*edge.second) || identity_equal(*edge.first, *edge.second))
return LARDON3D_TRACK_BUILDER_CORRUPT_INPUT;
auto first_it = table_indices.find(identity_key(*edge.first));
auto second_it = table_indices.find(identity_key(*edge.second));
if (first_it == table_indices.end() || second_it == table_indices.end() ||
first_it->second >= table.size() || second_it->second >= table.size() ||
!metadata_equal(table[first_it->second], *edge.first) ||
!metadata_equal(table[second_it->second], *edge.second))
return LARDON3D_TRACK_BUILDER_CORRUPT_INPUT;
if (first_it->second < second_it->second)
normalized.push_back({first_it->second, second_it->second});
else
normalized.push_back({second_it->second, first_it->second});
}
std::sort(normalized.begin(), normalized.end(), [](const CompactEdge &a,
const CompactEdge &b) {
return a.low < b.low || (a.low == b.low && a.high < b.high);
});
normalized.erase(std::unique(normalized.begin(), normalized.end(),
[](const CompactEdge &a, const CompactEdge &b) {
return a.low == b.low && a.high == b.high;
}),
normalized.end());
Dsu dsu(table.size());
for (const CompactEdge &edge : normalized) dsu.unite(edge.low, edge.high);
std::vector<std::vector<size_t>> components(table.size());
for (size_t i = 0; i < table.size(); ++i)
if (!normalized.empty()) components[dsu.root(i)].push_back(i);
std::vector<std::vector<size_t>> accepted;
for (auto &component : components) {
if (component.size() < 2) continue;
std::sort(component.begin(), component.end());
bool valid = true;
std::vector<uint64_t> images;
images.reserve(component.size());
const Observation &first = table[component.front()];
for (size_t node : component) {
const Observation &observation = table[node];
if (observation.image_id == 0 ||
(observation.extractor_version != first.extractor_version) ||
(observation.descriptor_type != first.descriptor_type) ||
(observation.descriptor_dimension != first.descriptor_dimension) ||
std::strncmp(observation.extractor_kind, first.extractor_kind,
LARDON3D_TRACK_BUILDER_KIND_CAPACITY) != 0 ||
std::memcmp(observation.parameter_fingerprint, first.parameter_fingerprint, 32) != 0)
valid = false;
images.push_back(observation.image_id);
}
std::sort(images.begin(), images.end());
for (size_t i = 1; i < images.size(); ++i)
if (images[i - 1] == images[i]) valid = false;
if (valid) accepted.push_back(component);
}
auto component_less = [&table](const std::vector<size_t> &a,
const std::vector<size_t> &b) {
size_t count = std::min(a.size(), b.size());
for (size_t i = 0; i < count; ++i) {
if (identity_less(table[a[i]], table[b[i]])) return true;
if (identity_less(table[b[i]], table[a[i]])) return false;
}
return a.size() < b.size();
};
std::sort(accepted.begin(), accepted.end(), component_less);
if (!accepted.empty()) {
result->tracks = new Lardon3DTrackBuilderTrack[accepted.size()]();
result->track_count = accepted.size();
for (size_t i = 0; i < accepted.size(); ++i) {
result->tracks[i].observation_count = accepted[i].size();
result->tracks[i].observations =
new Lardon3DTrackBuilderTrackObservation[accepted[i].size()];
for (size_t j = 0; j < accepted[i].size(); ++j) {
const Observation &source = table[accepted[i][j]];
result->tracks[i].observations[j] = {
source.feature_set_id, source.feature_index, source.image_id};
}
}
}
return LARDON3D_TRACK_BUILDER_OK;
} catch (const std::bad_alloc &) {
lardon3d_track_builder_result_free(result);
return LARDON3D_TRACK_BUILDER_OUT_OF_MEMORY;
} catch (...) {
lardon3d_track_builder_result_free(result);
return LARDON3D_TRACK_BUILDER_OUT_OF_MEMORY;
}
}

View file

@ -0,0 +1,339 @@
#include <algorithm>
#include <array>
#include <cstring>
#include <cstdio>
#include <deque>
#include <fcntl.h>
#include <limits>
#include <new>
#include <openssl/evp.h>
#include <string>
#include <sys/stat.h>
#include <unistd.h>
#include <unordered_map>
#include <utility>
#include <vector>
extern "C" {
#include <lardon3d/match_file.h>
#include <lardon3d/track_builder.h>
#include <lardon3d/track_builder_project.h>
}
namespace {
using Observation = Lardon3DTrackBuilderObservation;
using Edge = Lardon3DTrackBuilderEdge;
struct Key {
uint64_t set;
uint32_t index;
bool operator==(const Key &other) const { return set == other.set && index == other.index; }
};
struct KeyHash {
size_t operator()(const Key &key) const noexcept {
return static_cast<size_t>(key.set ^ (static_cast<uint64_t>(key.index) * 0x9e3779b97f4a7c15ULL));
}
};
struct FeatureCacheEntry {
Lardon3DProjectDbFeatureSet value{};
};
Lardon3DTrackBuilderProjectStatus map_db(Lardon3DProjectDbResult value) {
if (value == LARDON3D_PROJECT_DB_INVALID_ARGUMENT || value == LARDON3D_PROJECT_DB_CONSTRAINT)
return LARDON3D_TRACK_BUILDER_PROJECT_INVALID_ARGUMENT;
if (value == LARDON3D_PROJECT_DB_NOT_FOUND)
return LARDON3D_TRACK_BUILDER_PROJECT_NOT_FOUND;
if (value == LARDON3D_PROJECT_DB_CORRUPT)
return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT;
return LARDON3D_TRACK_BUILDER_PROJECT_DATABASE_ERROR;
}
bool join_path(char *output, size_t capacity, const char *root, const char *relative) {
if (!output || !root || !relative || relative[0] == '\0') return false;
int written = std::snprintf(output, capacity, "%s/%s", root, relative);
return written > 0 && static_cast<size_t>(written) < capacity;
}
bool scope_hash(const uint64_t *ids, size_t count, unsigned char output[32]) {
if (count > (std::numeric_limits<size_t>::max() - 8U) / sizeof(uint64_t)) return false;
std::vector<unsigned char> bytes(8U + count * sizeof(uint64_t));
std::memcpy(bytes.data(), "L3DTSIS1", 8);
for (size_t i = 0; i < count; ++i) {
uint64_t value = ids[i];
for (size_t byte = 0; byte < sizeof(value); ++byte) {
bytes[8U + i * sizeof(value) + byte] = static_cast<unsigned char>(value & 0xffU);
value >>= 8U;
}
}
unsigned int length = 0;
return EVP_Digest(bytes.data(), bytes.size(), output, &length, EVP_sha256(), nullptr) == 1 &&
length == 32;
}
bool selector_matches(const Lardon3DProjectDbGeometricVerificationResult &gvr,
const Lardon3DTrackBuilderProjectRequest &request) {
return static_cast<int>(gvr.verifier_kind) == request.verifier_kind &&
gvr.verifier_version == request.verifier_version &&
std::memcmp(gvr.parameter_fingerprint, request.verifier_fingerprint, 32) == 0;
}
Lardon3DProjectDbResult validate_gvr_snapshot(
const Lardon3DTrackBuilderProjectRequest &request, uint64_t gvr_id) {
Lardon3DProjectDbGeometricVerificationResult gvr{};
Lardon3DProjectDbResult db = lardon3d_project_db_load_geometric_verification_result(
request.database, gvr_id, &gvr);
if (db != LARDON3D_PROJECT_DB_OK) return db;
if (gvr.status != LARDON3D_GEOMETRIC_VERIFIED || !selector_matches(gvr, request))
return LARDON3D_PROJECT_DB_CORRUPT;
Lardon3DProjectDbMatchResult match{};
db = lardon3d_project_db_load_match_result(request.database, gvr.match_result_id, &match);
if (db != LARDON3D_PROJECT_DB_OK || match.result_status != LARDON3D_MATCH_RESULT_STATUS_MATCHED ||
!match.has_match_asset) return db == LARDON3D_PROJECT_DB_OK
? LARDON3D_PROJECT_DB_CORRUPT : db;
Lardon3DProjectDbCandidatePair pair{};
db = lardon3d_project_db_load_candidate_pair(request.database, match.candidate_pair_id, &pair);
if (db != LARDON3D_PROJECT_DB_OK) return db;
Lardon3DProjectDbFeatureSet set_a{};
Lardon3DProjectDbFeatureSet set_b{};
db = lardon3d_project_db_load_feature_set(request.database, match.feature_set_id_a, &set_a);
if (db != LARDON3D_PROJECT_DB_OK) return db;
db = lardon3d_project_db_load_feature_set(request.database, match.feature_set_id_b, &set_b);
if (db != LARDON3D_PROJECT_DB_OK) return db;
if (set_a.image_id != pair.image_id_a || set_b.image_id != pair.image_id_b ||
match.feature_set_id_a == match.feature_set_id_b)
return LARDON3D_PROJECT_DB_CORRUPT;
char path[LARDON3D_PROJECT_DB_PATH_CAPACITY];
if (!join_path(path, sizeof(path), request.project_path, match.match_asset_path))
return LARDON3D_PROJECT_DB_CORRUPT;
Lardon3DMatchFileHeader header{};
if (lardon3d_match_file_validate_asset(path, match.match_asset_sha256,
match.match_asset_size_bytes, &header,
set_a.feature_set_id, set_b.feature_set_id,
set_a.feature_count, set_b.feature_count) !=
LARDON3D_MATCH_FILE_OK || header.match_count != match.match_count)
return LARDON3D_PROJECT_DB_CORRUPT;
return LARDON3D_PROJECT_DB_OK;
}
Lardon3DTrackBuilderProjectStatus resolve_gvr(
const Lardon3DTrackBuilderProjectRequest &request, uint64_t gvr_id,
std::unordered_map<uint64_t, FeatureCacheEntry> &cache,
std::deque<Observation> &observations,
std::unordered_map<Key, Observation *, KeyHash> &observation_map,
std::vector<Edge> &edges) {
Lardon3DProjectDbGeometricVerificationResult gvr{};
Lardon3DProjectDbResult db = lardon3d_project_db_load_geometric_verification_result(
request.database, gvr_id, &gvr);
if (db != LARDON3D_PROJECT_DB_OK) return map_db(db);
if (gvr.status != LARDON3D_GEOMETRIC_VERIFIED || !selector_matches(gvr, request))
return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT;
Lardon3DProjectDbMatchResult match{};
db = lardon3d_project_db_load_match_result(request.database, gvr.match_result_id, &match);
if (db != LARDON3D_PROJECT_DB_OK) return map_db(db);
if (match.result_status != LARDON3D_MATCH_RESULT_STATUS_MATCHED || !match.has_match_asset ||
match.match_count == 0) return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT;
Lardon3DProjectDbCandidatePair pair{};
db = lardon3d_project_db_load_candidate_pair(request.database, match.candidate_pair_id, &pair);
if (db != LARDON3D_PROJECT_DB_OK) return map_db(db);
auto load_set = [&](uint64_t id, Lardon3DProjectDbFeatureSet **out) {
auto found = cache.find(id);
if (found != cache.end()) {
*out = &found->second.value;
return LARDON3D_PROJECT_DB_OK;
}
FeatureCacheEntry entry{};
Lardon3DProjectDbResult value = lardon3d_project_db_load_feature_set(
request.database, id, &entry.value);
if (value == LARDON3D_PROJECT_DB_OK) {
auto inserted = cache.emplace(id, entry);
*out = &inserted.first->second.value;
}
return value;
};
Lardon3DProjectDbFeatureSet *set_a = nullptr;
Lardon3DProjectDbFeatureSet *set_b = nullptr;
db = load_set(match.feature_set_id_a, &set_a);
if (db != LARDON3D_PROJECT_DB_OK) return map_db(db);
db = load_set(match.feature_set_id_b, &set_b);
if (db != LARDON3D_PROJECT_DB_OK) return map_db(db);
if (set_a->image_id != pair.image_id_a || set_b->image_id != pair.image_id_b ||
match.feature_set_id_a == match.feature_set_id_b) return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT;
char path[LARDON3D_PROJECT_DB_PATH_CAPACITY];
if (!join_path(path, sizeof(path), request.project_path, match.match_asset_path))
return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT;
Lardon3DMatchFileHeader header{};
if (lardon3d_match_file_validate_asset(path, match.match_asset_sha256,
match.match_asset_size_bytes, &header,
set_a->feature_set_id, set_b->feature_set_id,
set_a->feature_count, set_b->feature_count) !=
LARDON3D_MATCH_FILE_OK || header.match_count != match.match_count)
return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT;
std::vector<Lardon3DMatchFileEntry> entries(match.match_count);
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
uint32_t count = 0;
bool read_ok = fd >= 0 && lardon3d_match_file_read(
fd, &header, entries.data(), entries.size(), &count, set_a->feature_set_id,
set_b->feature_set_id, set_a->feature_count, set_b->feature_count) == LARDON3D_MATCH_FILE_OK;
if (fd >= 0) (void)close(fd);
if (!read_ok || count != match.match_count || gvr.inlier_mask_size != (count + 7U) / 8U)
return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT;
size_t selected = 0;
for (uint32_t i = 0; i < count; ++i) {
if ((gvr.inlier_mask[i / 8U] & static_cast<unsigned char>(1U << (i % 8U))) == 0) continue;
++selected;
const auto &entry = entries[i];
Key keys[2] = {{set_a->feature_set_id, entry.feature_index_a},
{set_b->feature_set_id, entry.feature_index_b}};
Observation *resolved[2] = {};
Lardon3DProjectDbFeatureSet *sets[2] = {set_a, set_b};
for (size_t endpoint = 0; endpoint < 2; ++endpoint) {
auto found = observation_map.find(keys[endpoint]);
if (found == observation_map.end()) {
Observation value{};
value.feature_set_id = keys[endpoint].set;
value.feature_index = keys[endpoint].index;
value.image_id = sets[endpoint]->image_id;
std::memcpy(value.extractor_kind, sets[endpoint]->extractor_kind,
sizeof(value.extractor_kind));
value.extractor_version = sets[endpoint]->extractor_version;
std::memcpy(value.parameter_fingerprint, sets[endpoint]->parameter_fingerprint, 32);
value.descriptor_type = sets[endpoint]->descriptor_type;
value.descriptor_dimension = sets[endpoint]->descriptor_dimension;
observations.push_back(value);
resolved[endpoint] = &observations.back();
observation_map.emplace(keys[endpoint], resolved[endpoint]);
} else {
resolved[endpoint] = found->second;
}
}
edges.push_back({resolved[0], resolved[1]});
}
if (selected != gvr.inlier_count) return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT;
return LARDON3D_TRACK_BUILDER_PROJECT_OK;
}
} // namespace
extern "C" Lardon3DTrackBuilderProjectStatus lardon3d_track_builder_build_project(
const Lardon3DTrackBuilderProjectRequest *request,
Lardon3DTrackBuilderProjectResult *result) {
if (!request || !result || !request->project_path || !request->database ||
!request->verifier_fingerprint || !request->gvr_ids || request->gvr_count == 0 ||
request->verifier_kind <= 0 || request->verifier_version == 0) {
return LARDON3D_TRACK_BUILDER_PROJECT_INVALID_ARGUMENT;
}
*result = {};
for (size_t i = 0; i < request->gvr_count; ++i) {
if (request->gvr_ids[i] == 0 || (i != 0 && request->gvr_ids[i - 1] >= request->gvr_ids[i]))
return LARDON3D_TRACK_BUILDER_PROJECT_INVALID_ARGUMENT;
}
try {
std::vector<uint64_t> owned_ids(request->gvr_ids,
request->gvr_ids + request->gvr_count);
Lardon3DTrackBuilderProjectRequest owned_request = *request;
owned_request.gvr_ids = owned_ids.data();
request = &owned_request;
unsigned char input_hash[32];
if (!scope_hash(request->gvr_ids, request->gvr_count, input_hash))
return LARDON3D_TRACK_BUILDER_PROJECT_OUT_OF_MEMORY;
unsigned char builder_fingerprint[32];
if (!lardon3d_track_builder_fingerprint(builder_fingerprint))
return LARDON3D_TRACK_BUILDER_PROJECT_CORE_ERROR;
Lardon3DProjectDbTrackSet identity{};
std::snprintf(identity.builder_kind, sizeof(identity.builder_kind), "track_builder");
identity.builder_version = LARDON3D_TRACK_BUILDER_VERSION;
std::memcpy(identity.parameter_fingerprint, builder_fingerprint, 32);
identity.verifier_kind = request->verifier_kind;
identity.verifier_version = request->verifier_version;
std::memcpy(identity.verifier_fingerprint, request->verifier_fingerprint, 32);
std::memcpy(identity.input_scope_hash, input_hash, 32);
identity.gvr_count = request->gvr_count;
Lardon3DProjectDbTrackSet found{};
Lardon3DProjectDbResult db = lardon3d_project_db_find_track_set(
request->database, &identity, &found);
if (db == LARDON3D_PROJECT_DB_OK) {
if (found.gvr_count != request->gvr_count)
return LARDON3D_TRACK_BUILDER_PROJECT_INPUT_CORRUPT;
result->track_set_id = found.track_set_id;
result->gvr_count = found.gvr_count;
result->track_count = found.track_count;
result->reused = true;
return LARDON3D_TRACK_BUILDER_PROJECT_OK;
}
if (db != LARDON3D_PROJECT_DB_NOT_FOUND) return map_db(db);
std::unordered_map<uint64_t, FeatureCacheEntry> cache;
std::deque<Observation> observations;
std::unordered_map<Key, Observation *, KeyHash> observation_map;
std::vector<Edge> edges;
for (size_t i = 0; i < request->gvr_count; ++i) {
auto status = resolve_gvr(*request, request->gvr_ids[i], cache, observations,
observation_map, edges);
if (status != LARDON3D_TRACK_BUILDER_PROJECT_OK) return status;
}
std::vector<Observation> core_observations(observations.begin(), observations.end());
std::unordered_map<Key, Observation *, KeyHash> core_map;
core_map.reserve(core_observations.size());
for (auto &observation : core_observations)
core_map.emplace(Key{observation.feature_set_id, observation.feature_index}, &observation);
std::vector<Edge> core_edges;
core_edges.reserve(edges.size());
for (const Edge &edge : edges) {
Key first{edge.first->feature_set_id, edge.first->feature_index};
Key second{edge.second->feature_set_id, edge.second->feature_index};
core_edges.push_back({core_map.at(first), core_map.at(second)});
}
Lardon3DTrackBuilderResultSet core{};
auto core_status = lardon3d_track_builder_build(
core_observations.empty() ? nullptr : core_observations.data(), core_observations.size(),
core_edges.empty() ? nullptr : core_edges.data(), core_edges.size(), &core);
if (core_status != LARDON3D_TRACK_BUILDER_OK) {
lardon3d_track_builder_result_free(&core);
return core_status == LARDON3D_TRACK_BUILDER_OUT_OF_MEMORY
? LARDON3D_TRACK_BUILDER_PROJECT_OUT_OF_MEMORY
: LARDON3D_TRACK_BUILDER_PROJECT_CORE_ERROR;
}
std::vector<std::vector<Lardon3DProjectDbTrackObservation>> stored(core.track_count);
std::vector<Lardon3DProjectDbTrack> publish(core.track_count);
for (size_t i = 0; i < core.track_count; ++i) {
stored[i].resize(core.tracks[i].observation_count);
for (size_t j = 0; j < stored[i].size(); ++j) {
const auto &source = core.tracks[i].observations[j];
stored[i][j] = {source.feature_set_id, source.feature_index,
static_cast<uint32_t>(j)};
}
publish[i] = {0, 0, static_cast<uint32_t>(stored[i].size()), stored[i].data()};
}
#ifdef LARDON3D_TRACK_BUILDER_PROJECT_TESTING
lardon3d_track_builder_project_test_before_revalidation(
request->database, request->gvr_ids, request->gvr_count);
#endif
for (size_t i = 0; i < request->gvr_count; ++i) {
db = validate_gvr_snapshot(*request, request->gvr_ids[i]);
if (db != LARDON3D_PROJECT_DB_OK) {
lardon3d_track_builder_result_free(&core);
return map_db(db);
}
}
Lardon3DProjectDbTrackSet published{};
identity.track_count = core.track_count;
db = lardon3d_project_db_create_track_set(request->database, &identity,
publish.data(), publish.size(), &published);
lardon3d_track_builder_result_free(&core);
if (db != LARDON3D_PROJECT_DB_OK) return map_db(db);
result->track_set_id = published.track_set_id;
result->gvr_count = published.gvr_count;
result->track_count = published.track_count;
result->raw_inlier_edge_count = edges.size();
result->core_observation_count = observations.size();
result->reused = false;
return LARDON3D_TRACK_BUILDER_PROJECT_OK;
} catch (const std::bad_alloc &) {
return LARDON3D_TRACK_BUILDER_PROJECT_OUT_OF_MEMORY;
} catch (...) {
return LARDON3D_TRACK_BUILDER_PROJECT_DATABASE_ERROR;
}
}

View file

@ -0,0 +1,43 @@
#include <chrono>
#include <cstdio>
#include <sys/resource.h>
#include <vector>
extern "C" {
#include <lardon3d/track_builder.h>
}
static Lardon3DTrackBuilderObservation make_observation(uint64_t id) {
Lardon3DTrackBuilderObservation value{};
value.feature_set_id = id + 1;
value.image_id = id + 1;
value.extractor_version = 1;
value.descriptor_type = 1;
value.descriptor_dimension = 32;
std::snprintf(value.extractor_kind, sizeof(value.extractor_kind), "orb");
return value;
}
int main() {
for (size_t edge_count : {10000U, 100000U, 1000000U}) {
std::vector<Lardon3DTrackBuilderObservation> observations;
std::vector<Lardon3DTrackBuilderEdge> edges;
observations.reserve(edge_count + 1);
edges.reserve(edge_count);
for (size_t i = 0; i <= edge_count; ++i)
observations.push_back(make_observation(i));
for (size_t i = 1; i <= edge_count; ++i)
edges.push_back({&observations[i - 1], &observations[i]});
Lardon3DTrackBuilderResultSet result{};
auto start = std::chrono::steady_clock::now();
auto status = lardon3d_track_builder_build(
observations.data(), observations.size(), edges.data(), edges.size(), &result);
auto elapsed = std::chrono::duration<double>(std::chrono::steady_clock::now() - start);
struct rusage usage{};
(void)getrusage(RUSAGE_SELF, &usage);
std::printf("edges=%zu status=%d seconds=%.3f rss_kib=%ld observations=%zu tracks=%zu\n",
edge_count, status, elapsed.count(), usage.ru_maxrss,
observations.size(), result.track_count);
lardon3d_track_builder_result_free(&result);
}
}

View file

@ -0,0 +1,298 @@
#include <algorithm>
#include <array>
#include <cassert>
#include <cstdio>
#include <cstring>
#include <random>
#include <vector>
extern "C" {
#include <lardon3d/track_builder.h>
}
namespace {
using Observation = Lardon3DTrackBuilderObservation;
using Edge = Lardon3DTrackBuilderEdge;
using Track = std::vector<std::pair<uint64_t, uint32_t>>;
Observation observation(uint64_t set, uint32_t feature, uint64_t image,
uint32_t extractor_version = 1,
uint32_t descriptor_type = 1,
uint32_t dimension = 32, unsigned char fingerprint = 7) {
Observation value{};
value.feature_set_id = set;
value.feature_index = feature;
value.image_id = image;
std::snprintf(value.extractor_kind, sizeof(value.extractor_kind), "orb");
value.extractor_version = extractor_version;
value.parameter_fingerprint[0] = fingerprint;
value.descriptor_type = descriptor_type;
value.descriptor_dimension = dimension;
return value;
}
Edge edge(const Observation &a, const Observation &b) { return {&a, &b}; }
std::vector<Track> read_result(const Lardon3DTrackBuilderResultSet &result) {
std::vector<Track> tracks;
for (size_t i = 0; i < result.track_count; ++i) {
Track track;
for (size_t j = 0; j < result.tracks[i].observation_count; ++j) {
const Lardon3DTrackBuilderTrackObservation &value =
result.tracks[i].observations[j];
track.emplace_back(value.feature_set_id, value.feature_index);
assert(j == 0 || track[j - 1] < track[j]);
}
for (size_t j = 0; j < track.size(); ++j)
for (size_t k = j + 1; k < track.size(); ++k)
assert(result.tracks[i].observations[j].image_id !=
result.tracks[i].observations[k].image_id);
assert(track.size() >= 2);
tracks.push_back(track);
}
assert(std::is_sorted(tracks.begin(), tracks.end()));
return tracks;
}
std::vector<Track> build(const std::vector<Observation> &observations,
const std::vector<Edge> &edges) {
Lardon3DTrackBuilderResultSet result{};
assert(lardon3d_track_builder_build(observations.data(), observations.size(),
edges.data(), edges.size(), &result) ==
LARDON3D_TRACK_BUILDER_OK);
std::vector<Track> tracks = read_result(result);
lardon3d_track_builder_result_free(&result);
lardon3d_track_builder_result_free(&result);
return tracks;
}
void expect(const std::vector<Observation> &observations,
const std::vector<Edge> &edges, const std::vector<Track> &expected) {
const auto actual = build(observations, edges);
if (actual != expected) {
std::fprintf(stderr, "mismatch expected=%zu actual=%zu edges=%zu\n",
expected.size(), actual.size(), edges.size());
for (const auto &track : actual) {
std::fprintf(stderr, "actual:");
for (const auto &key : track) std::fprintf(stderr, " %llu:%u",
(unsigned long long)key.first,
key.second);
std::fputc('\n', stderr);
}
assert(false);
}
}
void test_fingerprint() {
unsigned char bytes[48];
unsigned char fingerprint[32];
assert(lardon3d_track_builder_fingerprint_bytes(bytes));
assert(lardon3d_track_builder_fingerprint(fingerprint));
const char expected_bytes[] =
"4c3344544246503101000000010000000100000001000000010000000100000001"
"000000010000000200000000000000";
const char expected_hash[] =
"e1f1fae479bcf82001a5b33dda331195617b8751668e46a6cf1eecf2d125df31";
for (size_t i = 0; i < 48; ++i) {
unsigned int value = 0;
std::sscanf(expected_bytes + 2 * i, "%2x", &value);
assert(bytes[i] == value);
}
for (size_t i = 0; i < 32; ++i) {
unsigned int high = 0;
unsigned int low = 0;
std::sscanf(expected_hash + 2 * i, "%1x%1x", &high, &low);
assert(fingerprint[i] == (high * 16U + low));
}
}
void test_adversarial() {
const Observation a = observation(1, 0, 10);
const Observation b = observation(2, 0, 11);
const Observation c = observation(3, 0, 12);
const Observation d = observation(4, 0, 13);
const Observation conflict = observation(5, 0, 12);
std::vector<Observation> all{a, b, c, d, conflict};
expect(all, {edge(a, b), edge(b, c)}, {{{1, 0}, {2, 0}, {3, 0}}});
expect(all, {edge(a, b), edge(b, c), edge(c, a)},
{{{1, 0}, {2, 0}, {3, 0}}});
expect(all, {edge(b, a), edge(a, b), edge(a, b)}, {{{1, 0}, {2, 0}}});
expect(all, {edge(a, b), edge(b, c), edge(a, conflict)}, {});
expect(all, {edge(a, b), edge(c, d)},
{{{1, 0}, {2, 0}}, {{3, 0}, {4, 0}}});
expect(all, {edge(a, b), edge(c, d), edge(b, conflict), edge(c, conflict)}, {});
expect(all, {edge(a, b)}, {{{1, 0}, {2, 0}}});
Observation heterogeneous = observation(6, 0, 14, 2);
expect({a, b, heterogeneous}, {edge(a, b)}, {{{1, 0}, {2, 0}}});
expect({a, b, heterogeneous}, {edge(a, heterogeneous)}, {});
Observation same_image = observation(7, 0, 10);
expect({a, b, same_image}, {edge(a, b), edge(b, same_image)}, {});
expect({a, b}, {}, {});
std::vector<Edge> duplicates;
for (size_t i = 0; i < 100; ++i) duplicates.push_back(edge((i % 2) == 0 ? a : b,
(i % 2) == 0 ? b : a));
expect({a, b}, duplicates, {{{1, 0}, {2, 0}}});
Edge self = edge(a, a);
Lardon3DTrackBuilderResultSet invalid{};
assert(lardon3d_track_builder_build(&a, 1, &self, 1, &invalid) ==
LARDON3D_TRACK_BUILDER_CORRUPT_INPUT);
lardon3d_track_builder_result_free(&invalid);
for (uint32_t dimension : {31U, 33U})
expect({a, b, observation(8, 0, 12, 1, 1, dimension)},
{edge(a, observation(8, 0, 12, 1, 1, dimension))}, {});
for (uint32_t version : {2U, 3U})
expect({a, b, observation(9, 0, 12, version)},
{edge(a, observation(9, 0, 12, version))}, {});
Observation different_fingerprint = observation(10, 0, 12, 1, 1, 32, 8);
expect({a, b, different_fingerprint}, {edge(a, different_fingerprint)}, {});
std::vector<Edge> permuted{edge(a, b), edge(b, c), edge(c, d)};
auto canonical = build({a, b, c, d}, permuted);
std::reverse(permuted.begin(), permuted.end());
assert(build({a, b, c, d}, permuted) == canonical);
}
void test_corruption_and_immutability() {
Observation a = observation(1, 0, 10);
Observation b = observation(2, 0, 11);
Observation contradictory = a;
contradictory.image_id = 99;
std::vector<Observation> inputs{a, b, contradictory};
std::vector<Edge> edges{edge(a, b)};
const auto before = inputs;
Lardon3DTrackBuilderResultSet result{};
assert(lardon3d_track_builder_build(inputs.data(), inputs.size(), edges.data(),
edges.size(), &result) ==
LARDON3D_TRACK_BUILDER_CORRUPT_INPUT);
assert(result.track_count == 0 && result.tracks == nullptr);
assert(inputs.size() == before.size());
assert(std::memcmp(inputs.data(), before.data(),
inputs.size() * sizeof(Observation)) == 0);
assert(lardon3d_track_builder_build(nullptr, 1, nullptr, 0, &result) ==
LARDON3D_TRACK_BUILDER_INVALID_ARGUMENT);
assert(lardon3d_track_builder_build(nullptr, 0, nullptr, 0, &result) ==
LARDON3D_TRACK_BUILDER_OK);
lardon3d_track_builder_result_free(&result);
assert(lardon3d_track_builder_build(nullptr, 0, nullptr, 0, nullptr) ==
LARDON3D_TRACK_BUILDER_INVALID_ARGUMENT);
}
void test_additional_adversarial() {
const Observation a = observation(1, 0, 10);
const Observation b = observation(2, 0, 11);
const Observation c = observation(3, 0, 12);
const Observation d = observation(4, 0, 13);
const Observation c_again = observation(5, 0, 12);
expect({a, b, c, d, c_again},
{edge(a, b), edge(b, c), edge(c, d), edge(a, c_again)}, {});
expect({a, b, c, d}, {edge(a, b), edge(a, c), edge(a, d)},
{{{1, 0}, {2, 0}, {3, 0}, {4, 0}}});
std::vector<Observation> disjoint;
std::vector<Edge> disjoint_edges;
std::vector<Track> expected;
for (uint64_t i = 0; i < 32; ++i)
disjoint.push_back(observation(i + 1, 0, i + 1));
for (size_t i = 0; i < disjoint.size(); i += 2) {
disjoint_edges.push_back(edge(disjoint[i], disjoint[i + 1]));
expected.push_back({{i + 1, 0}, {i + 2, 0}});
}
assert(build(disjoint, disjoint_edges) == expected);
std::reverse(disjoint_edges.begin(), disjoint_edges.end());
assert(build(disjoint, disjoint_edges) == expected);
}
std::vector<Track> oracle(const std::vector<Observation> &nodes,
const std::vector<std::pair<size_t, size_t>> &edges,
uint32_t mask) {
std::vector<std::vector<size_t>> adjacency(nodes.size());
for (size_t i = 0; i < edges.size(); ++i) {
if ((mask & (1U << i)) == 0) continue;
adjacency[edges[i].first].push_back(edges[i].second);
adjacency[edges[i].second].push_back(edges[i].first);
}
std::vector<bool> seen(nodes.size(), false);
std::vector<Track> answer;
for (size_t start = 0; start < nodes.size(); ++start) {
if (seen[start] || adjacency[start].empty()) continue;
std::vector<size_t> pending{start};
seen[start] = true;
std::vector<size_t> component;
while (!pending.empty()) {
size_t current = pending.back();
pending.pop_back();
component.push_back(current);
for (size_t next : adjacency[current])
if (!seen[next]) {
seen[next] = true;
pending.push_back(next);
}
}
std::sort(component.begin(), component.end());
std::vector<uint64_t> images;
bool valid = component.size() >= 2;
Track track;
for (size_t index : component) {
for (uint64_t image : images)
if (image == nodes[index].image_id) valid = false;
images.push_back(nodes[index].image_id);
track.emplace_back(nodes[index].feature_set_id, nodes[index].feature_index);
}
if (valid) answer.push_back(track);
}
std::sort(answer.begin(), answer.end());
return answer;
}
void test_exhaustive() {
std::vector<Observation> nodes;
for (uint64_t image = 1; image <= 3; ++image) {
nodes.push_back(observation(image * 10, 0, image));
nodes.push_back(observation(image * 10 + 1, 0, image));
}
std::vector<std::pair<size_t, size_t>> pairs;
std::vector<Edge> all_edges;
for (size_t i = 0; i < nodes.size(); ++i)
for (size_t j = i + 1; j < nodes.size(); ++j) {
pairs.emplace_back(i, j);
all_edges.push_back(edge(nodes[i], nodes[j]));
}
for (uint32_t mask = 0; mask < (1U << pairs.size()); ++mask) {
std::vector<Edge> selected;
for (size_t i = 0; i < all_edges.size(); ++i)
if (mask & (1U << i)) selected.push_back(all_edges[i]);
assert(build(nodes, selected) == oracle(nodes, pairs, mask));
if (mask % 257 == 0) {
std::reverse(selected.begin(), selected.end());
assert(build(nodes, selected) == oracle(nodes, pairs, mask));
}
}
}
void test_large_and_repeatable() {
std::vector<Observation> nodes;
std::vector<Edge> edges;
for (uint64_t i = 0; i < 301; ++i) nodes.push_back(observation(i + 1, 0, i + 1));
for (size_t i = 1; i < nodes.size(); ++i) edges.push_back(edge(nodes[i - 1], nodes[i]));
auto expected = build(nodes, edges);
assert(expected.size() == 1 && expected[0].size() == 301);
std::mt19937 generator(12345);
for (int run = 0; run < 100; ++run) {
std::shuffle(edges.begin(), edges.end(), generator);
assert(build(nodes, edges) == expected);
}
}
} // namespace
int main() {
test_fingerprint();
test_adversarial();
test_corruption_and_immutability();
test_additional_adversarial();
test_exhaustive();
test_large_and_repeatable();
std::puts("track-builder-core: PASS");
return 0;
}

View file

@ -0,0 +1,688 @@
#include <cassert>
#include <cstdio>
#include <cstring>
#include <chrono>
#include <fcntl.h>
#include <openssl/evp.h>
#include <sqlite3.h>
#include <string>
#include <sys/stat.h>
#include <sys/resource.h>
#include <unistd.h>
#include <vector>
extern "C" {
#include <lardon3d/match_file.h>
#include <lardon3d/project_db.h>
#include <lardon3d/track_builder_project.h>
}
namespace {
constexpr Lardon3DGeometricVerifierKind kVerifier = LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL;
constexpr uint32_t kVersion = 1;
unsigned char g_verifier[32];
std::string g_delete_db;
uint64_t g_delete_id = 0;
bool g_delete_before_revalidation = false;
std::string g_insert_db;
uint64_t g_insert_match_id = 0;
bool g_insert_during_build = false;
void check(bool value, const char *expression, int line) {
if (!value) {
std::fprintf(stderr, "track-builder-project:%d: %s\n", line, expression);
std::abort();
}
}
#define CHECK(value) check((value), #value, __LINE__)
std::string asset_path(const unsigned char hash[32], const char *kind) {
static constexpr char digits[] = "0123456789abcdef";
std::string hex(64, '0');
for (size_t i = 0; i < 32; ++i) {
hex[2 * i] = digits[hash[i] >> 4];
hex[2 * i + 1] = digits[hash[i] & 0x0fU];
}
return std::string("assets/") + kind + "/" + hex.substr(0, 2) + "/" + hex;
}
void digest_file(const std::string &path, unsigned char hash[32], uint64_t *size) {
FILE *file = std::fopen(path.c_str(), "rb");
CHECK(file != nullptr);
EVP_MD_CTX *context = EVP_MD_CTX_new();
CHECK(context != nullptr && EVP_DigestInit_ex(context, EVP_sha256(), nullptr) == 1);
unsigned char buffer[4096];
*size = 0;
size_t read = 0;
while ((read = std::fread(buffer, 1, sizeof(buffer), file)) != 0) {
CHECK(EVP_DigestUpdate(context, buffer, read) == 1);
*size += read;
}
unsigned int length = 0;
CHECK(EVP_DigestFinal_ex(context, hash, &length) == 1 && length == 32);
EVP_MD_CTX_free(context);
std::fclose(file);
}
struct Fixture {
std::string directory;
std::string db_path;
Lardon3DProjectDb *db = nullptr;
uint64_t image[3]{};
uint64_t feature[3]{};
uint64_t next_asset = 1;
uint64_t next_file = 1;
explicit Fixture(uint32_t feature_count = 32) {
char name[] = "/tmp/lardon3d-track-c-XXXXXX";
CHECK(mkdtemp(name) != nullptr);
directory = name;
db_path = directory + "/project.db";
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{};
CHECK(lardon3d_project_db_open(db_path.c_str(), &db, error) == LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbScanSet scanset{};
CHECK(lardon3d_project_db_create_scanset(db, "SPECIMEN", &scanset) ==
LARDON3D_PROJECT_DB_OK);
for (size_t i = 0; i < 3; ++i) {
unsigned char hash[32]{};
hash[0] = static_cast<unsigned char>(i + 1);
std::string path = asset_path(hash, "images");
Lardon3DProjectDbImageRegisterStatus status;
Lardon3DProjectDbImage created{};
char original_name[32];
std::snprintf(original_name, sizeof(original_name), "image-%zu.jpg", i + 1);
Lardon3DProjectDbResult image_result = lardon3d_project_db_register_image(
db, scanset.scanset_id, hash, path.c_str(), 1, original_name, path.c_str(), 0,
static_cast<int64_t>(i + 1), &status, &created);
CHECK(image_result == LARDON3D_PROJECT_DB_OK);
image[i] = created.image_id;
unsigned char parameter[32]{0x5a};
unsigned char source[32]{};
source[0] = static_cast<unsigned char>(i + 1);
unsigned char asset[32]{static_cast<unsigned char>(0x70U + i)};
Lardon3DProjectDbFeatureSet set{};
CHECK(lardon3d_project_db_register_feature_set(
db, image[i], "orb", 1, parameter, source, feature_count, 1, 32, asset,
asset_path(asset, "features").c_str(), 1, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0,
static_cast<int64_t>(i + 10), &set) == LARDON3D_PROJECT_DB_OK);
feature[i] = set.feature_set_id;
}
std::memset(g_verifier, 0x42, sizeof(g_verifier));
}
~Fixture() {
if (db) lardon3d_project_db_close(db);
}
uint64_t add_feature_set(size_t image_index, unsigned char parameter_byte,
const char *extractor = "orb", uint32_t descriptor_type = 1) {
unsigned char parameter[32]{parameter_byte};
unsigned char source[32]{};
source[0] = static_cast<unsigned char>(image_index + 1);
unsigned char asset[32]{static_cast<unsigned char>(0x90U + parameter_byte)};
Lardon3DProjectDbFeatureSet set{};
CHECK(lardon3d_project_db_register_feature_set(
db, image[image_index], extractor, 1, parameter, source, 32,
descriptor_type, 32, asset, asset_path(asset, "features").c_str(), 1,
LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 100, &set) == LARDON3D_PROJECT_DB_OK);
return set.feature_set_id;
}
uint64_t add_gvr(size_t first, size_t second,
const std::vector<Lardon3DMatchFileEntry> &entries,
const std::vector<unsigned char> &mask,
Lardon3DGeometricVerificationStatus status = LARDON3D_GEOMETRIC_VERIFIED,
uint64_t feature_a = 0, uint64_t feature_b = 0,
const char *matcher_kind = "matcher-a", uint32_t matcher_version = 1) {
if (feature_a == 0) feature_a = feature[first];
if (feature_b == 0) feature_b = feature[second];
Lardon3DProjectDbCandidatePair pair{};
Lardon3DProjectDbResult pair_result = lardon3d_project_db_find_candidate_pair(
db, image[first], image[second], &pair);
if (pair_result == LARDON3D_PROJECT_DB_NOT_FOUND) {
pair_result = lardon3d_project_db_create_candidate_pair(
db, image[first], image[second], static_cast<int64_t>(next_file), &pair);
}
CHECK(pair_result == LARDON3D_PROJECT_DB_OK);
std::string path = directory + "/match-" + std::to_string(next_file++) + ".bin";
int fd = open(path.c_str(), O_CREAT | O_TRUNC | O_WRONLY, 0600);
CHECK(fd >= 0);
CHECK(lardon3d_match_file_write(fd, 1, 32, feature_a, feature_b,
entries.data(), static_cast<uint32_t>(entries.size())) ==
LARDON3D_MATCH_FILE_OK);
CHECK(close(fd) == 0);
unsigned char hash[32]{};
uint64_t size = 0;
digest_file(path, hash, &size);
unsigned char matcher_fingerprint[32]{};
matcher_fingerprint[0] = static_cast<unsigned char>(next_file);
Lardon3DProjectDbMatchResult match{};
CHECK(lardon3d_project_db_create_match_result(
db, pair.candidate_pair_id, feature_a, feature_b, matcher_kind, matcher_version,
matcher_fingerprint, LARDON3D_MATCH_RESULT_STATUS_MATCHED,
static_cast<uint32_t>(entries.size()), hash,
path.substr(directory.size() + 1).c_str(), size, static_cast<int64_t>(next_file),
&match) == LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbGeometricVerificationResult gvr{};
double model[9]{};
Lardon3DProjectDbResult gvr_result = lardon3d_project_db_create_geometric_verification_result(
db, match.match_result_id, kVerifier, kVersion, g_verifier, status,
popcount(mask),
mask.data(), mask.size(),
status == LARDON3D_GEOMETRIC_VERIFIED ? model : nullptr,
static_cast<int64_t>(next_file), &gvr);
CHECK(gvr_result == LARDON3D_PROJECT_DB_OK);
return gvr.geometric_verification_result_id;
}
Lardon3DTrackBuilderProjectRequest request(const uint64_t *ids, size_t count) const {
return {directory.c_str(), db, kVerifier, kVersion, g_verifier, ids, count};
}
static uint32_t popcount(const std::vector<unsigned char> &mask) {
uint32_t count = 0;
for (unsigned char byte : mask) {
for (unsigned bit = 0; bit < 8; ++bit) count += (byte >> bit) & 1U;
}
return count;
}
};
void assert_track(Lardon3DProjectDb *db, uint64_t set_id, uint64_t a, uint64_t b,
uint64_t c) {
Lardon3DProjectDbTrack track{};
CHECK(lardon3d_project_db_find_track_by_observation(db, set_id, a, 0, &track) ==
LARDON3D_PROJECT_DB_OK);
CHECK(track.observation_count == 3 && track.observations[0].feature_set_id == a &&
track.observations[0].position_in_track == 0 && track.observations[1].feature_set_id == b &&
track.observations[1].position_in_track == 1 && track.observations[2].feature_set_id == c &&
track.observations[2].position_in_track == 2);
lardon3d_project_db_free_track(&track);
}
void assert_two_observation_track(Lardon3DProjectDb *db, uint64_t set_id, uint64_t a, uint64_t b) {
Lardon3DProjectDbTrack track{};
CHECK(lardon3d_project_db_find_track_by_observation(db, set_id, a, 0, &track) ==
LARDON3D_PROJECT_DB_OK && track.observation_count == 2 &&
track.observations[0].feature_set_id == a &&
track.observations[0].position_in_track == 0 &&
track.observations[1].feature_set_id == b &&
track.observations[1].position_in_track == 1);
lardon3d_project_db_free_track(&track);
}
uint64_t track_set_count(const std::string &path) {
sqlite3 *db = nullptr;
sqlite3_stmt *statement = nullptr;
CHECK(sqlite3_open_v2(path.c_str(), &db, SQLITE_OPEN_READONLY, nullptr) == SQLITE_OK);
CHECK(sqlite3_prepare_v2(db, "SELECT count(*) FROM track_sets", -1, &statement, nullptr) ==
SQLITE_OK);
CHECK(sqlite3_step(statement) == SQLITE_ROW);
uint64_t result = static_cast<uint64_t>(sqlite3_column_int64(statement, 0));
sqlite3_finalize(statement);
sqlite3_close(db);
return result;
}
uint64_t table_count(const std::string &path, const char *table) {
char sql[128];
std::snprintf(sql, sizeof(sql), "SELECT count(*) FROM %s", table);
sqlite3 *db = nullptr;
sqlite3_stmt *statement = nullptr;
CHECK(sqlite3_open_v2(path.c_str(), &db, SQLITE_OPEN_READONLY, nullptr) == SQLITE_OK);
CHECK(sqlite3_prepare_v2(db, sql, -1, &statement, nullptr) == SQLITE_OK);
CHECK(sqlite3_step(statement) == SQLITE_ROW);
uint64_t result = static_cast<uint64_t>(sqlite3_column_int64(statement, 0));
CHECK(sqlite3_finalize(statement) == SQLITE_OK && sqlite3_close(db) == SQLITE_OK);
return result;
}
void execute_sql(const std::string &path, const char *sql) {
sqlite3 *db = nullptr;
CHECK(sqlite3_open(path.c_str(), &db) == SQLITE_OK);
CHECK(sqlite3_exec(db, sql, nullptr, nullptr, nullptr) == SQLITE_OK);
CHECK(sqlite3_close(db) == SQLITE_OK);
}
void independent_scope_digest(const uint64_t *ids, size_t count, unsigned char output[32]) {
std::vector<unsigned char> bytes(8 + count * 8);
const char domain[] = "L3DTSIS1";
std::memcpy(bytes.data(), domain, 8);
for (size_t i = 0; i < count; ++i) {
uint64_t value = ids[i];
for (size_t byte = 0; byte < 8; ++byte) {
bytes[8 + i * 8 + byte] = static_cast<unsigned char>(value & 0xffU);
value >>= 8;
}
}
unsigned int length = 0;
EVP_MD_CTX *context = EVP_MD_CTX_new();
CHECK(context != nullptr && EVP_DigestInit_ex(context, EVP_sha256(), nullptr) == 1 &&
EVP_DigestUpdate(context, bytes.data(), bytes.size()) == 1 &&
EVP_DigestFinal_ex(context, output, &length) == 1 && length == 32);
EVP_MD_CTX_free(context);
}
void run_basic_and_reuse() {
Fixture fixture;
auto ab = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01});
auto bc = fixture.add_gvr(1, 2, {{0, 0, 0.1F}}, {0x01});
uint64_t ids[] = {ab, bc};
auto request = fixture.request(ids, 2);
Lardon3DTrackBuilderProjectResult first{};
auto first_status = lardon3d_track_builder_build_project(&request, &first);
CHECK(first_status == LARDON3D_TRACK_BUILDER_PROJECT_OK && !first.reused &&
first.track_count == 1);
assert_track(fixture.db, first.track_set_id, fixture.feature[0], fixture.feature[1],
fixture.feature[2]);
uint64_t before = track_set_count(fixture.db_path);
Lardon3DTrackBuilderProjectResult second{};
CHECK(lardon3d_track_builder_build_project(&request, &second) ==
LARDON3D_TRACK_BUILDER_PROJECT_OK && second.reused && second.track_set_id == first.track_set_id);
CHECK(track_set_count(fixture.db_path) == before);
std::puts("C01/C20: PASS");
}
void run_mask_cases() {
Fixture fixture;
std::vector<Lardon3DMatchFileEntry> entries;
for (uint32_t i = 0; i < 14; ++i) entries.push_back({i, i, 0.1F});
auto gvr = fixture.add_gvr(0, 1, entries, {0x85, 0x21});
uint64_t ids[] = {gvr};
auto request = fixture.request(ids, 1);
Lardon3DTrackBuilderProjectResult result{};
CHECK(lardon3d_track_builder_build_project(&request, &result) ==
LARDON3D_TRACK_BUILDER_PROJECT_OK && result.raw_inlier_edge_count == 5 &&
result.track_count == 5);
std::puts("C02/C03: PASS (outlier exclusion, asymmetric LSB-first mask)");
}
void run_rejection_cases() {
Fixture fixture;
auto gvr = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01}, LARDON3D_GEOMETRIC_REJECTED);
uint64_t ids[] = {gvr};
auto request = fixture.request(ids, 1);
Lardon3DTrackBuilderProjectResult result{};
CHECK(lardon3d_track_builder_build_project(&request, &result) !=
LARDON3D_TRACK_BUILDER_PROJECT_OK &&
track_set_count(fixture.db_path) == 0);
request.verifier_kind = 99;
CHECK(lardon3d_track_builder_build_project(&request, &result) !=
LARDON3D_TRACK_BUILDER_PROJECT_OK);
CHECK(fixture.request(nullptr, 0).gvr_count == 0);
std::puts("C04-C07/C11: PASS");
}
void run_scope_errors() {
Fixture fixture;
auto gvr = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01});
uint64_t unsorted[] = {gvr + 1, gvr};
auto bad = fixture.request(unsorted, 2);
Lardon3DTrackBuilderProjectResult result{};
CHECK(lardon3d_track_builder_build_project(&bad, &result) ==
LARDON3D_TRACK_BUILDER_PROJECT_INVALID_ARGUMENT);
uint64_t duplicate[] = {gvr, gvr};
bad = fixture.request(duplicate, 2);
CHECK(lardon3d_track_builder_build_project(&bad, &result) ==
LARDON3D_TRACK_BUILDER_PROJECT_INVALID_ARGUMENT);
uint64_t missing[] = {gvr + 1000};
bad = fixture.request(missing, 1);
CHECK(lardon3d_track_builder_build_project(&bad, &result) !=
LARDON3D_TRACK_BUILDER_PROJECT_OK && track_set_count(fixture.db_path) == 0);
std::puts("C08-C10: PASS");
}
void run_revalidation_and_duplicate_scope() {
Fixture fixture;
auto gvr = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01});
uint64_t ids[] = {gvr};
auto request = fixture.request(ids, 1);
g_delete_db = fixture.db_path;
g_delete_id = gvr;
g_delete_before_revalidation = true;
Lardon3DTrackBuilderProjectResult result{};
auto status = lardon3d_track_builder_build_project(&request, &result);
CHECK(status !=
LARDON3D_TRACK_BUILDER_PROJECT_OK &&
track_set_count(fixture.db_path) == 0);
Fixture duplicate_fixture;
auto first = duplicate_fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01});
auto second = duplicate_fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01});
uint64_t duplicate_ids[] = {first, second};
auto duplicate_request = duplicate_fixture.request(duplicate_ids, 2);
CHECK(lardon3d_track_builder_build_project(&duplicate_request, &result) ==
LARDON3D_TRACK_BUILDER_PROJECT_OK && result.track_count == 1);
std::puts("C18/C25: PASS (duplicate collapse, selected-input disappearance detected)");
}
void run_remaining_matrix() {
{
Fixture fixture;
auto gvr = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01});
uint64_t ids[] = {gvr};
auto request = fixture.request(ids, 1);
Lardon3DTrackBuilderProjectResult result{};
request.verifier_version = 2;
CHECK(lardon3d_track_builder_build_project(&request, &result) !=
LARDON3D_TRACK_BUILDER_PROJECT_OK && track_set_count(fixture.db_path) == 0);
request.verifier_version = 1;
unsigned char wrong[32];
std::memset(wrong, 0x43, sizeof(wrong));
request.verifier_fingerprint = wrong;
CHECK(lardon3d_track_builder_build_project(&request, &result) !=
LARDON3D_TRACK_BUILDER_PROJECT_OK && track_set_count(fixture.db_path) == 0);
request.gvr_ids = nullptr;
request.gvr_count = 0;
CHECK(lardon3d_track_builder_build_project(&request, &result) ==
LARDON3D_TRACK_BUILDER_PROJECT_INVALID_ARGUMENT && track_set_count(fixture.db_path) == 0);
std::puts("C06/C07/C11: PASS");
}
{
Fixture fixture;
auto gvr = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01});
uint64_t ids[] = {gvr};
auto request = fixture.request(ids, 1);
Lardon3DProjectDbGeometricVerificationResult loaded{};
CHECK(lardon3d_project_db_load_geometric_verification_result(fixture.db, gvr, &loaded) ==
LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbMatchResult match{};
CHECK(lardon3d_project_db_load_match_result(fixture.db, loaded.match_result_id, &match) ==
LARDON3D_PROJECT_DB_OK);
std::string path = fixture.directory + "/" + match.match_asset_path;
int fd = open(path.c_str(), O_WRONLY);
CHECK(fd >= 0);
unsigned char bad_magic = 'X';
CHECK(write(fd, &bad_magic, 1) == 1 && close(fd) == 0);
Lardon3DTrackBuilderProjectResult result{};
CHECK(lardon3d_track_builder_build_project(&request, &result) !=
LARDON3D_TRACK_BUILDER_PROJECT_OK && track_set_count(fixture.db_path) == 0);
std::puts("C12: PASS (Match File bad magic propagated)");
}
{
Fixture fixture;
std::vector<Lardon3DMatchFileEntry> entries;
for (uint32_t i = 0; i < 9; ++i) entries.push_back({i, i, 0.1F});
auto gvr = fixture.add_gvr(0, 1, entries, {0xff, 0x01});
uint64_t ids[] = {gvr};
execute_sql(fixture.db_path,
"UPDATE geometric_verification_results SET inlier_mask=X'FF' ");
auto request = fixture.request(ids, 1);
Lardon3DTrackBuilderProjectResult result{};
CHECK(lardon3d_track_builder_build_project(&request, &result) !=
LARDON3D_TRACK_BUILDER_PROJECT_OK && track_set_count(fixture.db_path) == 0);
std::puts("C13: PASS (mask length rejected by persisted loader)");
}
{
Fixture fixture;
auto gvr = fixture.add_gvr(0, 1, {{32, 0, 0.1F}}, {0x01});
uint64_t ids[] = {gvr};
auto request = fixture.request(ids, 1);
Lardon3DTrackBuilderProjectResult result{};
CHECK(lardon3d_track_builder_build_project(&request, &result) !=
LARDON3D_TRACK_BUILDER_PROJECT_OK && track_set_count(fixture.db_path) == 0);
std::puts("C14: PASS (Match File bounds validator rejected OOB index)");
}
{
Fixture fixture;
uint64_t extra = fixture.add_feature_set(0, 0x61);
auto first = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01},
LARDON3D_GEOMETRIC_VERIFIED, extra, fixture.feature[1]);
auto second = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01});
uint64_t ids[] = {first, second};
auto request = fixture.request(ids, 2);
Lardon3DTrackBuilderProjectResult result{};
CHECK(lardon3d_track_builder_build_project(&request, &result) ==
LARDON3D_TRACK_BUILDER_PROJECT_OK && result.track_count == 0);
std::puts("C15: PASS (same-image scientific conflict)");
}
{
Fixture fixture;
uint64_t extra = fixture.add_feature_set(1, 0x62);
auto first = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01},
LARDON3D_GEOMETRIC_VERIFIED, fixture.feature[0], extra);
auto second = fixture.add_gvr(1, 2, {{0, 0, 0.1F}}, {0x01},
LARDON3D_GEOMETRIC_VERIFIED, extra, fixture.feature[2]);
uint64_t ids[] = {first, second};
auto request = fixture.request(ids, 2);
Lardon3DTrackBuilderProjectResult result{};
CHECK(lardon3d_track_builder_build_project(&request, &result) ==
LARDON3D_TRACK_BUILDER_PROJECT_OK && result.track_count == 0);
std::puts("C16: PASS (heterogeneous Feature Set conflict)");
}
{
Fixture fixture;
auto first = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01},
LARDON3D_GEOMETRIC_VERIFIED, 0, 0, "matcher-a", 1);
auto second = fixture.add_gvr(1, 2, {{0, 0, 0.1F}}, {0x01},
LARDON3D_GEOMETRIC_VERIFIED, 0, 0, "matcher-b", 2);
uint64_t ids[] = {first, second};
auto request = fixture.request(ids, 2);
Lardon3DTrackBuilderProjectResult result{};
CHECK(lardon3d_track_builder_build_project(&request, &result) ==
LARDON3D_TRACK_BUILDER_PROJECT_OK && result.track_count == 1);
std::puts("C17: PASS (matcher configurations do not rank edges)");
}
{
Fixture fixture;
auto first = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01});
auto duplicate = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01});
uint64_t one[] = {first};
uint64_t two[] = {first, duplicate};
auto request_one = fixture.request(one, 1);
auto request_two = fixture.request(two, 2);
Lardon3DTrackBuilderProjectResult a{}, b{};
CHECK(lardon3d_track_builder_build_project(&request_one, &a) ==
LARDON3D_TRACK_BUILDER_PROJECT_OK);
CHECK(lardon3d_track_builder_build_project(&request_two, &b) ==
LARDON3D_TRACK_BUILDER_PROJECT_OK && a.track_set_id != b.track_set_id &&
a.track_count == b.track_count && track_set_count(fixture.db_path) == 2);
unsigned char expected_one[32]{};
unsigned char expected_two[32]{};
independent_scope_digest(one, 1, expected_one);
independent_scope_digest(two, 2, expected_two);
const unsigned char fixed_one[32] = {
0x5e, 0xf4, 0xf8, 0x44, 0x57, 0x8f, 0x92, 0x2c,
0x7d, 0x55, 0xb8, 0xb7, 0x9a, 0x9f, 0x71, 0x21,
0xac, 0x6b, 0x8a, 0x55, 0xe6, 0xb5, 0x3d, 0x6d,
0x87, 0x7e, 0x20, 0x4b, 0x52, 0x58, 0x59, 0xbe};
const unsigned char fixed_two[32] = {
0xff, 0xb4, 0x3e, 0x52, 0x87, 0x38, 0xac, 0x64,
0xc7, 0x39, 0xaa, 0xa8, 0xe2, 0x4a, 0xf1, 0xd4,
0xcd, 0x99, 0x4b, 0x59, 0xb4, 0x6a, 0x97, 0xc6,
0xf4, 0xb1, 0x66, 0x00, 0x52, 0x5b, 0x64, 0xa3};
Lardon3DProjectDbTrackSet loaded_one{}, loaded_two{};
CHECK(lardon3d_project_db_load_track_set(fixture.db, a.track_set_id, &loaded_one) ==
LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_load_track_set(fixture.db, b.track_set_id, &loaded_two) ==
LARDON3D_PROJECT_DB_OK &&
std::memcmp(expected_one, loaded_one.input_scope_hash, 32) == 0 &&
std::memcmp(expected_two, loaded_two.input_scope_hash, 32) == 0 &&
std::memcmp(expected_one, fixed_one, 32) == 0 &&
std::memcmp(expected_two, fixed_two, 32) == 0 &&
std::memcmp(expected_one, expected_two, 32) != 0);
std::puts("C19: PASS (same logical output, distinct scope identity)");
}
}
void run_durability_and_zero_track() {
Fixture fixture;
auto gvr = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01});
uint64_t ids[] = {gvr};
auto request = fixture.request(ids, 1);
Lardon3DTrackBuilderProjectResult first{};
CHECK(lardon3d_track_builder_build_project(&request, &first) ==
LARDON3D_TRACK_BUILDER_PROJECT_OK && first.track_count == 1);
uint64_t tracks_before = table_count(fixture.db_path, "tracks");
uint64_t observations_before = table_count(fixture.db_path, "track_observations");
lardon3d_project_db_close(fixture.db);
fixture.db = nullptr;
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{};
CHECK(lardon3d_project_db_open(fixture.db_path.c_str(), &fixture.db, error) ==
LARDON3D_PROJECT_DB_OK);
request.database = fixture.db;
Lardon3DProjectDbTrackSet loaded{};
CHECK(lardon3d_project_db_load_track_set(fixture.db, first.track_set_id, &loaded) ==
LARDON3D_PROJECT_DB_OK && loaded.track_count == 1 && loaded.gvr_count == 1);
assert_two_observation_track(fixture.db, first.track_set_id, fixture.feature[0],
fixture.feature[1]);
Lardon3DTrackBuilderProjectResult reused{};
CHECK(lardon3d_track_builder_build_project(&request, &reused) ==
LARDON3D_TRACK_BUILDER_PROJECT_OK && reused.reused &&
reused.track_set_id == first.track_set_id);
CHECK(track_set_count(fixture.db_path) == 1 && table_count(fixture.db_path, "tracks") ==
tracks_before &&
table_count(fixture.db_path, "track_observations") == observations_before);
std::puts("C21/C22: PASS (close/reopen and reuse durability)");
Fixture zero;
uint64_t extra = zero.add_feature_set(0, 0x63);
auto conflict_a = zero.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01},
LARDON3D_GEOMETRIC_VERIFIED, extra, zero.feature[1]);
auto conflict_b = zero.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01});
uint64_t conflict_ids[] = {conflict_a, conflict_b};
auto conflict_request = zero.request(conflict_ids, 2);
Lardon3DTrackBuilderProjectResult zero_result{};
CHECK(lardon3d_track_builder_build_project(&conflict_request, &zero_result) ==
LARDON3D_TRACK_BUILDER_PROJECT_OK && zero_result.track_count == 0 &&
track_set_count(zero.db_path) == 1);
std::puts("C23: PASS (zero-track Track Set published)");
}
void run_scope_snapshot_case() {
Fixture fixture;
auto first = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01});
auto second = fixture.add_gvr(1, 2, {{0, 0, 0.1F}}, {0x01});
auto late = fixture.add_gvr(0, 1, {{7, 7, 0.1F}}, {0x01});
Lardon3DProjectDbGeometricVerificationResult late_gvr{};
CHECK(lardon3d_project_db_load_geometric_verification_result(fixture.db, late, &late_gvr) ==
LARDON3D_PROJECT_DB_OK);
execute_sql(fixture.db_path,
("DELETE FROM geometric_verification_results WHERE "
"geometric_verification_result_id=" + std::to_string(late))
.c_str());
g_insert_db = fixture.db_path;
g_insert_match_id = late_gvr.match_result_id;
g_insert_during_build = true;
uint64_t ids[] = {first, second};
auto request = fixture.request(ids, 2);
Lardon3DTrackBuilderProjectResult result{};
CHECK(lardon3d_track_builder_build_project(&request, &result) ==
LARDON3D_TRACK_BUILDER_PROJECT_OK && result.track_count == 1 && result.gvr_count == 2);
CHECK(track_set_count(fixture.db_path) == 1);
std::puts("C24: PASS (late matching GVR excluded from explicit scope)");
}
void run_partial_input_failure() {
Fixture fixture;
auto valid = fixture.add_gvr(0, 1, {{0, 0, 0.1F}}, {0x01});
auto corrupt = fixture.add_gvr(1, 2, {{0, 0, 0.1F}}, {0x01});
Lardon3DProjectDbGeometricVerificationResult loaded{};
CHECK(lardon3d_project_db_load_geometric_verification_result(fixture.db, corrupt, &loaded) ==
LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbMatchResult match{};
CHECK(lardon3d_project_db_load_match_result(fixture.db, loaded.match_result_id, &match) ==
LARDON3D_PROJECT_DB_OK);
int fd = open((fixture.directory + "/" + match.match_asset_path).c_str(), O_WRONLY);
CHECK(fd >= 0);
unsigned char bad_magic = 'Q';
CHECK(write(fd, &bad_magic, 1) == 1 && close(fd) == 0);
uint64_t ids[] = {valid, corrupt};
auto request = fixture.request(ids, 2);
Lardon3DTrackBuilderProjectResult result{};
CHECK(lardon3d_track_builder_build_project(&request, &result) !=
LARDON3D_TRACK_BUILDER_PROJECT_OK && track_set_count(fixture.db_path) == 0);
std::puts("PARTIAL INPUT: PASS");
}
void run_resource_case() {
Fixture fixture(8192);
std::vector<Lardon3DMatchFileEntry> entries;
std::vector<unsigned char> mask(1024, 0xff);
entries.reserve(8192);
for (uint32_t i = 0; i < 8192; ++i) entries.push_back({i, i, 0.1F});
std::vector<uint64_t> ids;
for (size_t i = 0; i < 13; ++i) ids.push_back(fixture.add_gvr(0, 1, entries, mask));
auto request = fixture.request(ids.data(), ids.size());
Lardon3DTrackBuilderProjectResult result{};
auto started = std::chrono::steady_clock::now();
CHECK(lardon3d_track_builder_build_project(&request, &result) ==
LARDON3D_TRACK_BUILDER_PROJECT_OK && result.raw_inlier_edge_count == 106496 &&
result.core_observation_count == 16384 && result.track_count == 8192);
auto elapsed = std::chrono::duration<double>(std::chrono::steady_clock::now() - started);
struct rusage usage{};
CHECK(getrusage(RUSAGE_SELF, &usage) == 0);
std::printf("RESOURCE GVR=%zu RAW=%llu INLIERS=%llu FEATURES=3 OBS=%llu TRACKS=%llu\n",
ids.size(), static_cast<unsigned long long>(result.raw_inlier_edge_count),
static_cast<unsigned long long>(result.raw_inlier_edge_count),
static_cast<unsigned long long>(result.core_observation_count),
static_cast<unsigned long long>(result.track_count));
std::printf("RESOURCE DURATION_SECONDS=%.3f PEAK_RSS_KIB=%ld MAX_MATCH_FILES_LIVE=1\n",
elapsed.count(), usage.ru_maxrss);
}
} // namespace
#ifdef LARDON3D_TRACK_BUILDER_PROJECT_TESTING
extern "C" void lardon3d_track_builder_project_test_before_revalidation(
Lardon3DProjectDb *, const uint64_t *, size_t) {
if (g_insert_during_build) {
sqlite3 *connection = nullptr;
CHECK(sqlite3_open(g_insert_db.c_str(), &connection) == SQLITE_OK);
char sql[512];
std::snprintf(
sql, sizeof(sql),
"INSERT INTO geometric_verification_results "
"(match_result_id,verifier_kind,verifier_version,parameter_fingerprint,status,"
"inlier_count,inlier_mask,model_m00,model_m01,model_m02,model_m10,model_m11,model_m12,"
"model_m20,model_m21,model_m22,created_at) VALUES (%llu,1,1,"
"X'4242424242424242424242424242424242424242424242424242424242424242',2,1,X'01',"
"0,0,0,0,0,0,0,0,0,999)",
static_cast<unsigned long long>(g_insert_match_id));
int insert_result = sqlite3_exec(connection, sql, nullptr, nullptr, nullptr);
if (insert_result != SQLITE_OK) {
std::fprintf(stderr, "late GVR insert: %s\n", sqlite3_errmsg(connection));
}
CHECK(insert_result == SQLITE_OK);
CHECK(sqlite3_close(connection) == SQLITE_OK);
g_insert_during_build = false;
}
if (!g_delete_before_revalidation) return;
sqlite3 *connection = nullptr;
CHECK(sqlite3_open(g_delete_db.c_str(), &connection) == SQLITE_OK);
char sql[256];
std::snprintf(sql, sizeof(sql),
"DELETE FROM geometric_verification_results WHERE "
"geometric_verification_result_id=%llu",
static_cast<unsigned long long>(g_delete_id));
CHECK(sqlite3_exec(connection, sql, nullptr, nullptr, nullptr) == SQLITE_OK);
CHECK(sqlite3_close(connection) == SQLITE_OK);
g_delete_before_revalidation = false;
}
#endif
int main() {
Lardon3DTrackBuilderProjectResult result{};
Lardon3DTrackBuilderProjectRequest invalid{};
CHECK(lardon3d_track_builder_build_project(&invalid, &result) ==
LARDON3D_TRACK_BUILDER_PROJECT_INVALID_ARGUMENT);
run_basic_and_reuse();
run_mask_cases();
run_rejection_cases();
run_scope_errors();
run_revalidation_and_duplicate_scope();
run_remaining_matrix();
run_durability_and_zero_track();
run_scope_snapshot_case();
run_partial_input_failure();
run_resource_case();
std::puts("C01-C27 integration harness: PASS (C26 N/A; C27 deferred to Gate D)");
return 0;
}