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
|
||||
"git diff*": allow
|
||||
task: deny
|
||||
disable: true
|
||||
---
|
||||
|
||||
Analyse seulement les fichiers, API et décisions transmis ou directement
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ permission:
|
|||
".git/**": deny
|
||||
"scan3d/**": deny
|
||||
task: deny
|
||||
disable: true
|
||||
---
|
||||
|
||||
Interviens uniquement après deux échecs identiques du build principal. Reprends
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ permission:
|
|||
".git/**": deny
|
||||
"scan3d/**": deny
|
||||
task: deny
|
||||
disable: true
|
||||
---
|
||||
|
||||
# Lardon Build
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ permission:
|
|||
"rg *": allow
|
||||
"git diff*": allow
|
||||
task: deny
|
||||
disable: true
|
||||
---
|
||||
|
||||
Audite mutex, conditions, transitions, wakeups, destruction, réservations,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ permission:
|
|||
"git log*": allow
|
||||
"meson test *": allow
|
||||
task: deny
|
||||
disable: true
|
||||
---
|
||||
|
||||
# Lardon Diagnose
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ permission:
|
|||
"rg *": allow
|
||||
"git diff*": allow
|
||||
task: deny
|
||||
disable: true
|
||||
---
|
||||
|
||||
Documente seulement l'état réellement validé. Mets à jour les documents
|
||||
|
|
|
|||
|
|
@ -1,29 +1,14 @@
|
|||
---
|
||||
description: Orchestre les tickets Lardon3D longs par phases durables
|
||||
mode: primary
|
||||
model: opencode-go/gpt-5.6-luna
|
||||
temperature: 0.1
|
||||
maxSteps: 200
|
||||
mode: primary
|
||||
model: opencode-go/gpt-5.6-luna
|
||||
disable: false
|
||||
permission:
|
||||
read: allow
|
||||
glob: allow
|
||||
grep: allow
|
||||
edit:
|
||||
"*": allow
|
||||
".git": deny
|
||||
".git/**": deny
|
||||
"scan3d/**": deny
|
||||
task:
|
||||
"*": deny
|
||||
"lardon-read": allow
|
||||
"lardon-diagnose": allow
|
||||
"lardon-architect": allow
|
||||
"lardon-build": allow
|
||||
"lardon-build-backup": allow
|
||||
"lardon-tests": allow
|
||||
"lardon-concurrency": allow
|
||||
"lardon-review": allow
|
||||
"lardon-docs": allow
|
||||
task: deny
|
||||
external_directory: deny
|
||||
---
|
||||
|
||||
# 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 status*": allow
|
||||
task: deny
|
||||
disable: true
|
||||
---
|
||||
|
||||
Lis uniquement le périmètre demandé. Tu peux découvrir un chemin avec `glob`,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ permission:
|
|||
"git diff*": allow
|
||||
"git status*": allow
|
||||
task: deny
|
||||
disable: true
|
||||
---
|
||||
|
||||
Relis uniquement le diff de production déjà validé.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ permission:
|
|||
"git diff --check*": allow
|
||||
"git status*": allow
|
||||
task: deny
|
||||
disable: true
|
||||
---
|
||||
|
||||
N'exécute jamais deux validations lourdes en parallèle. Ordre : test ciblé,
|
||||
|
|
|
|||
7
.opencode/agents/plan.md
Normal file
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/matcher.cpp',
|
||||
'src/geometric_verifier.cpp',
|
||||
'src/track_builder.cpp',
|
||||
'src/track_builder_project.cpp',
|
||||
] + matcher_backend_sources,
|
||||
include_directories: include_directories('include'),
|
||||
dependencies: [ncursesw, threads, sqlite3, openssl, opencv, opencv_geometry]
|
||||
|
|
@ -831,3 +833,37 @@ geometric_verifier_task_test = executable(
|
|||
)
|
||||
test('geometric-verifier-task', geometric_verifier_task_test, timeout: 60,
|
||||
env: opencv_test_environment)
|
||||
|
||||
track_builder_test = executable(
|
||||
'test-track-builder-core',
|
||||
sources: ['tests/test_track_builder_core.cpp', 'src/track_builder.cpp'],
|
||||
include_directories: include_directories('include'),
|
||||
dependencies: [openssl],
|
||||
)
|
||||
test('track-builder-core', track_builder_test, timeout: 120)
|
||||
|
||||
track_builder_project_test = executable(
|
||||
'test-track-builder-project',
|
||||
sources: [
|
||||
'tests/test_track_builder_project.cpp',
|
||||
'src/track_builder.cpp',
|
||||
'src/track_builder_project.cpp',
|
||||
'src/project_db.c',
|
||||
'src/task.c',
|
||||
'src/resource_governor.c',
|
||||
'src/resource_snapshot.c',
|
||||
'src/match_file.c',
|
||||
],
|
||||
include_directories: include_directories('include'),
|
||||
cpp_args: ['-DLARDON3D_TRACK_BUILDER_PROJECT_TESTING'],
|
||||
dependencies: [threads, sqlite3, openssl],
|
||||
)
|
||||
test('track-builder-project', track_builder_project_test, timeout: 60)
|
||||
|
||||
executable(
|
||||
'benchmark-track-builder',
|
||||
sources: ['tests/benchmark_track_builder.cpp', 'src/track_builder.cpp'],
|
||||
build_by_default: false,
|
||||
include_directories: include_directories('include'),
|
||||
dependencies: [openssl],
|
||||
)
|
||||
|
|
|
|||
239
src/track_builder.cpp
Normal file
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