feat(reconstruction): add track builder
This commit is contained in:
parent
6925a7bc1d
commit
0d876cfbe1
26 changed files with 3152 additions and 21 deletions
22
.clangd
Normal file
22
.clangd
Normal 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
|
||||||
7
.opencode/agents/build.md
Normal file
7
.opencode/agents/build.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
description: Disabled by Lardon3D single-agent mode
|
||||||
|
mode: primary
|
||||||
|
disable: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Disabled by project configuration.
|
||||||
7
.opencode/agents/explore.md
Normal file
7
.opencode/agents/explore.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
description: Disabled by Lardon3D single-agent mode
|
||||||
|
mode: subagent
|
||||||
|
disable: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Disabled by project configuration.
|
||||||
7
.opencode/agents/general.md
Normal file
7
.opencode/agents/general.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
description: Disabled by Lardon3D single-agent mode
|
||||||
|
mode: subagent
|
||||||
|
disable: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Disabled by project configuration.
|
||||||
|
|
@ -14,6 +14,7 @@ permission:
|
||||||
"rg *": allow
|
"rg *": allow
|
||||||
"git diff*": allow
|
"git diff*": allow
|
||||||
task: deny
|
task: deny
|
||||||
|
disable: true
|
||||||
---
|
---
|
||||||
|
|
||||||
Analyse seulement les fichiers, API et décisions transmis ou directement
|
Analyse seulement les fichiers, API et décisions transmis ou directement
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ permission:
|
||||||
".git/**": deny
|
".git/**": deny
|
||||||
"scan3d/**": deny
|
"scan3d/**": deny
|
||||||
task: deny
|
task: deny
|
||||||
|
disable: true
|
||||||
---
|
---
|
||||||
|
|
||||||
Interviens uniquement après deux échecs identiques du build principal. Reprends
|
Interviens uniquement après deux échecs identiques du build principal. Reprends
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ permission:
|
||||||
".git/**": deny
|
".git/**": deny
|
||||||
"scan3d/**": deny
|
"scan3d/**": deny
|
||||||
task: deny
|
task: deny
|
||||||
|
disable: true
|
||||||
---
|
---
|
||||||
|
|
||||||
# Lardon Build
|
# Lardon Build
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ permission:
|
||||||
"rg *": allow
|
"rg *": allow
|
||||||
"git diff*": allow
|
"git diff*": allow
|
||||||
task: deny
|
task: deny
|
||||||
|
disable: true
|
||||||
---
|
---
|
||||||
|
|
||||||
Audite mutex, conditions, transitions, wakeups, destruction, réservations,
|
Audite mutex, conditions, transitions, wakeups, destruction, réservations,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ permission:
|
||||||
"git log*": allow
|
"git log*": allow
|
||||||
"meson test *": allow
|
"meson test *": allow
|
||||||
task: deny
|
task: deny
|
||||||
|
disable: true
|
||||||
---
|
---
|
||||||
|
|
||||||
# Lardon Diagnose
|
# Lardon Diagnose
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ permission:
|
||||||
"rg *": allow
|
"rg *": allow
|
||||||
"git diff*": allow
|
"git diff*": allow
|
||||||
task: deny
|
task: deny
|
||||||
|
disable: true
|
||||||
---
|
---
|
||||||
|
|
||||||
Documente seulement l'état réellement validé. Mets à jour les documents
|
Documente seulement l'état réellement validé. Mets à jour les documents
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,14 @@
|
||||||
---
|
---
|
||||||
description: Orchestre les tickets Lardon3D longs par phases durables
|
description: Orchestre les tickets Lardon3D longs par phases durables
|
||||||
mode: primary
|
|
||||||
model: opencode-go/gpt-5.6-luna
|
|
||||||
temperature: 0.1
|
temperature: 0.1
|
||||||
maxSteps: 200
|
maxSteps: 200
|
||||||
|
mode: primary
|
||||||
|
model: opencode-go/gpt-5.6-luna
|
||||||
|
disable: false
|
||||||
permission:
|
permission:
|
||||||
read: allow
|
|
||||||
glob: allow
|
|
||||||
grep: allow
|
|
||||||
edit:
|
|
||||||
"*": allow
|
"*": allow
|
||||||
".git": deny
|
task: deny
|
||||||
".git/**": deny
|
external_directory: 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
|
# Lardon Orchestrator
|
||||||
|
|
|
||||||
734
.opencode/agents/lardon-orchestrator.md.bak
Normal file
734
.opencode/agents/lardon-orchestrator.md.bak
Normal 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.
|
||||||
|
|
||||||
|
### 15–30 %
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
@ -17,6 +17,7 @@ permission:
|
||||||
"git diff*": allow
|
"git diff*": allow
|
||||||
"git status*": allow
|
"git status*": allow
|
||||||
task: deny
|
task: deny
|
||||||
|
disable: true
|
||||||
---
|
---
|
||||||
|
|
||||||
Lis uniquement le périmètre demandé. Tu peux découvrir un chemin avec `glob`,
|
Lis uniquement le périmètre demandé. Tu peux découvrir un chemin avec `glob`,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ permission:
|
||||||
"git diff*": allow
|
"git diff*": allow
|
||||||
"git status*": allow
|
"git status*": allow
|
||||||
task: deny
|
task: deny
|
||||||
|
disable: true
|
||||||
---
|
---
|
||||||
|
|
||||||
Relis uniquement le diff de production déjà validé.
|
Relis uniquement le diff de production déjà validé.
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ permission:
|
||||||
"git diff --check*": allow
|
"git diff --check*": allow
|
||||||
"git status*": allow
|
"git status*": allow
|
||||||
task: deny
|
task: deny
|
||||||
|
disable: true
|
||||||
---
|
---
|
||||||
|
|
||||||
N'exécute jamais deux validations lourdes en parallèle. Ordre : test ciblé,
|
N'exécute jamais deux validations lourdes en parallèle. Ordre : test ciblé,
|
||||||
|
|
|
||||||
7
.opencode/agents/plan.md
Normal file
7
.opencode/agents/plan.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
description: Disabled by Lardon3D single-agent mode
|
||||||
|
mode: primary
|
||||||
|
disable: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Disabled by project configuration.
|
||||||
7
.opencode/agents/scout.md
Normal file
7
.opencode/agents/scout.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
description: Disabled by Lardon3D single-agent mode
|
||||||
|
mode: subagent
|
||||||
|
disable: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Disabled by project configuration.
|
||||||
567
docs/architecture/track_builder.md
Normal file
567
docs/architecture/track_builder.md
Normal 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: 16–24 bytes;
|
||||||
|
- unique edge: 16–32 bytes for two compact node references and sort metadata;
|
||||||
|
- DSU/component node: 16–32 bytes;
|
||||||
|
- image-membership metadata: 8–24 bytes per component observation;
|
||||||
|
- temporary evidence: 0–24 bytes per raw edge, bounded and discardable after
|
||||||
|
deduplication;
|
||||||
|
- canonicalization entry: 16–32 bytes per accepted observation.
|
||||||
|
|
||||||
|
Indicative totals, not new contractual limits:
|
||||||
|
|
||||||
|
- 100k observations: roughly 5–15 MiB plus edges and allocator overhead;
|
||||||
|
- 1M observations: roughly 50–150 MiB before unusually dense edge evidence;
|
||||||
|
- 1M unique edges: roughly 20–60 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 3–5 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. C01–C25 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.
|
||||||
79
include/lardon3d/track_builder.h
Normal file
79
include/lardon3d/track_builder.h
Normal 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
|
||||||
57
include/lardon3d/track_builder_project.h
Normal file
57
include/lardon3d/track_builder_project.h
Normal 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
|
||||||
36
meson.build
36
meson.build
|
|
@ -155,6 +155,8 @@ executable(
|
||||||
'src/match_file.c',
|
'src/match_file.c',
|
||||||
'src/matcher.cpp',
|
'src/matcher.cpp',
|
||||||
'src/geometric_verifier.cpp',
|
'src/geometric_verifier.cpp',
|
||||||
|
'src/track_builder.cpp',
|
||||||
|
'src/track_builder_project.cpp',
|
||||||
] + matcher_backend_sources,
|
] + matcher_backend_sources,
|
||||||
include_directories: include_directories('include'),
|
include_directories: include_directories('include'),
|
||||||
dependencies: [ncursesw, threads, sqlite3, openssl, opencv, opencv_geometry]
|
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,
|
test('geometric-verifier-task', geometric_verifier_task_test, timeout: 60,
|
||||||
env: opencv_test_environment)
|
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
239
src/track_builder.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
339
src/track_builder_project.cpp
Normal file
339
src/track_builder_project.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
tests/benchmark_track_builder.cpp
Normal file
43
tests/benchmark_track_builder.cpp
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
298
tests/test_track_builder_core.cpp
Normal file
298
tests/test_track_builder_core.cpp
Normal 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;
|
||||||
|
}
|
||||||
688
tests/test_track_builder_project.cpp
Normal file
688
tests/test_track_builder_project.cpp
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue