refactor(core): freeze global maintenance baseline

Complete the A-to-Z Lardon3D maintenance and coherence pass.

Generalize host resource policy, remove the global CPU12 ceiling, preserve
host CPU/RAM reserves, scale Task capabilities through the Resource Governor,
and validate deterministic parallel GV execution.

Migrate Project DB to v23 with data-driven camera, lens, optical configuration
and calibration profiles, including manual lenses without EXIF.

Integrate safe optional LARDON SSD swap/scratch control with Governor and F10
drain/safe-to-unplug semantics.

Refactor the ncurses TUI into a runtime observatory with durable progress,
elapsed time, smoothed ETA, throughput, resource telemetry, Governor state,
optics workflow, colors and compact/no-color fallbacks.

Reconcile Queue lifetime, persistence, concurrency, comments, tests,
README, AGENTS and canonical documentation.

GLOBAL_MAINTENANCE_AUDIT=PASS/FROZEN
This commit is contained in:
fy59 2026-09-01 08:00:47 +02:00
parent 96d0791ffb
commit 6b894a24df
126 changed files with 27499 additions and 1732 deletions

View file

@ -34,7 +34,7 @@ through an explicitly authorized, explicitly scoped human ticket:
- F0 — PASS/FROZEN
- Phase H v1 — PASS/FROZEN
- MVS-M1 — PASS/FROZEN
- Project DB v22 — PASS/FROZEN
- Project DB v22 scientific/persistence foundation — PASS/FROZEN
- Calibration Bootstrap v1 — PASS/FROZEN
- Selected Scientific Execution — PASS/FROZEN
- Photo Quality Triage / Acquisition Selection — PASS/FROZEN
@ -42,14 +42,26 @@ through an explicitly authorized, explicitly scoped human ticket:
- S2 Capture-safe Standard Ingestion — PASS/FROZEN
- S3 Capture / Acquisition Ingestion — PASS/FROZEN
- Durable Acquisition-Campaign Execution — PASS/FROZEN
- Global Maintenance Audit — PASS/FROZEN
Detailed subcontracts remain defined by their canonical documents. This file
does not duplicate every S3 substage, scientific threshold, migration detail,
or persistence format.
Historical references to older Project DB versions remain valid when they
describe the actual historical contract or migration path. Do not rewrite
legitimate v16/v17/v18/v19 history merely because v22 is current.
Project DB v23 is the current additive optical-context overlay. It preserves
the v22 scientific/persistence foundation and must not infer or backfill optical
identity from historical data. Historical references to older Project DB
versions remain valid when they describe the actual historical contract or
migration path. Do not rewrite legitimate v16v22 history merely because v23
is current.
The global maintenance implementation, fresh portable/Vulkan/sanitizer/
concurrency validation and independent final review are acquired. Its lifecycle
is `GLOBAL_MAINTENANCE_AUDIT=PASS/FROZEN`. The review independently passed the
portable build, 64/64 complete suite, 15/15 focused matrix, 76/76 strict header
probes across 19 modified/new public headers, ABI and production-seam checks,
retained-manifest verification and diff validation, with zero blocking findings.
Do not reopen this boundary or infer a new scientific policy from the freeze.
When a ticket declares `NO_NEW_SUBSYSTEM`, do not introduce an unrelated:
@ -130,6 +142,11 @@ require a clean worktree unless the ticket explicitly requires one.
termination semantics.
- If a public function may be retried, its idempotency or conflict behavior
must be explicit where non-obvious.
- Sparse SfM relative-pose and PnP `max_iterations`/`minimum_inliers` remain
fixed-width `uint32_t` scientific fields, but their public OpenCV boundary is
operationally limited to `INT_MAX`. Reject a larger value before narrowing,
allocation, solver execution or output mutation; do not alter the FROZEN
defaults, encodings or fingerprints to accommodate an unsafe cast.
Compiler success alone is not proof of API, ABI, persistence, or scientific
contract correctness.
@ -173,6 +190,21 @@ explicitly defines such an identity.
to canonical architecture.
- Keep ncurses on its designated/main thread wherever the canonical contract
requires it.
- The current TUI is a validated operational observatory/control center. Keep
Queue/Task/host observation coalesced and bounded, keep durable scientific
progress distinct from generic runtime percentage, and keep unknown
provenance visibly UNKNOWN. Full layout starts at 100x30, compact is
supported through 60x15 (72x20 is the reference compact boundary), and only
the bounded "Terminal trop petit" fallback is allowed below that minimum.
- Preserve the contextual key contract. `F10 SSD` remains literally visible at
60 columns in idle, text-input and import-running modes; text input owns only
Enter/Escape/F10 and a running import owns only cancel (`X`) and F10 while
quit/Escape are visibly disabled. ncurses input and rendering remain on the
main thread.
- Opening, closing, or switching a project is a Queue/DB lifetime boundary:
destroy and join the sole Queue, including finished callbacks, before Project
DB close; then recreate one empty Queue and rebind observers. Never sample
running/pending counts as a substitute for that boundary.
- Extend validated abstractions rather than rewriting validated modules.
- Reuse the existing Task / Queue / Scheduler / Resource Governor ownership
model rather than creating parallel runtime infrastructure.
@ -203,9 +235,29 @@ Resource-sensitive work must identify, where relevant:
- cancellation cleanup;
- whether the bound is operational or scientific.
External USB SSD scratch/swap support remains a planned resource-management
capability. Do not implement ad-hoc mounting, formatting, `swapon`, scratch
ownership, or device cleanup outside an explicitly authorized ticket.
The reviewed external USB SSD controller is the authorized physical-lifecycle
boundary for the exact UDisks Drive/label/UUID contract. Its validated snapshot
is registered with the Resource Governor; the Governor wrappers are the sole
production orchestrator for scratch-lease acquire/release. The controller does
not replace the Governor or invent Task scratch eligibility, and swap/scratch
never become RAM. The application lifetime order is strict: destroy/join the
Queue so every Task lease is released, checked-join/unregister the SSD binding,
destroy the controller, then destroy the Governor. The current fourteen Task
kinds have no scratch consumer, so availability is capability, not fabricated
usage. Do not add ad-hoc discovery, mounting, formatting, `swapon`, cleanup,
force-drain, shell commands, or a second resource/scheduling subsystem outside
the reviewed controller/Governor APIs.
Snapshot conversion is fail-closed by physical state. Any pairing or authority
requires current detection of the Drive and both UUID-bearing partitions,
positive known partition extents, and coherent mount/activity/capability facts;
partial `DETECTED` state is observable but non-actionable. A disconnected sticky
hazard may retain identity only as non-allocating `ERROR`, and drain authority
requires the exact reconnected original tuple. Controller generation
`UINT64_MAX` is legal saturation: arbitrary equal-generation public updates
remain stale and cannot regrant authority, while only the serialized Governor
lease wrapper may reconcile its own exact completion and address-backed lease
count at that watermark.
## 7. Code quality and readability
@ -552,8 +604,8 @@ Persistence changes require explicit attention to:
For Project DB:
- preserve current v22 semantics unless a ticket explicitly authorizes a schema
change;
- preserve the FROZEN v22 semantics and the current additive v23 optical
overlay unless a ticket explicitly authorizes a later schema change;
- schema-version changes require explicit human authorization;
- migrations must be additive unless a different migration is explicitly
authorized;
@ -578,9 +630,11 @@ make retries convenient.
behavior.
- Roadmap documents may describe future behavior, but future capabilities must
be clearly marked as planned/later/exploratory.
- Never describe future viewer, Capture Guidance, video/keyframe, SSD scratch,
or camera-control capabilities as implemented before they are actually
validated.
- Never describe future viewer, Capture Guidance, video/keyframe, Task scratch
consumption, or camera-control capabilities as implemented before they are
actually validated. The current TUI/F10 and controller-to-Governor registry
are validated operationally, but no current Task kind consumes scratch and
those interfaces do not make dense/scratch-consuming workflows complete.
- Statuses such as `PLANNED`, `IMPLEMENTED`, `VALIDATION PENDING`, and
`PASS/FROZEN` are authoritative lifecycle statements.
- Update lifecycle state only when implementation, validation, and review

View file

@ -1,6 +1,7 @@
# Lardon3D
Moteur de reconstruction géométrique persistante et incrémentale, piloté par une TUI ncursesw.
Moteur de photogrammétrie générique, persistant, incrémental et sensible aux
ressources, piloté par une TUI ncursesw.
## Vision
@ -35,9 +36,10 @@ persistante, enrichissable et versionnable.
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences
- **Task Checkpoint v1** : snapshot durable, protocole `.chk.next` → SQLite →
`.chk` sous verrou par tâche, et reprise sûre
- **Project Database v22** : fondations v20/v21 préservées, avec snapshot
d'exécution sélectionnée, publication bornée des représentations, attache de
scope de calibration et tâche durable mince `raw.develop` — PASS / FROZEN
- **Project Database v23** : overlay optique additif au-dessus de la fondation
v22 PASS / FROZEN ; profils de boîtier et d'objectif, configurations
body+lens+focale, affectations campagne/Capture et calibrations exactement
compatibles, sans inférence ni backfill — IMPLEMENTED / VALIDATED / REVIEWED
- **[Photo Quality Triage](docs/architecture/photo_quality_triage.md)** : métriques JPEG
- **[Calibration Bootstrap v1](docs/architecture/calibration_bootstrap.md)** :
import borné d'une calibration optimisée et traçable avant le Sparse SfM à
@ -73,9 +75,21 @@ persistante, enrichissable et versionnable.
- **Hardware Profile** : détection des capacités matérielles
- **Resource Snapshot** : capture instantanée des ressources
- **Resource Governor** : arbitrage centralisé des budgets et réservations
- **TUI observatoire / centre de contrôle** : modèle de vue pur et borné,
observation coalescée, progression durable/ETA honnête, écrans Tasks,
Resources, Optique et SSD, avec ncurses exclusivement sur le thread principal
— CURRENT / VALIDATED OPERATIONAL
- **Contrôleur SSD externe optionnel** : frontière physique UDisks2/GDBus,
identité Drive+labels+UUIDs, drain sûr et capacités de contrôle exactes ; son
état physique est enregistré auprès du Governor, seul orchestrateur des
leases scratch de production — CURRENT / VALIDATED OPERATIONAL
### Intégration réelle validée
Sony A6000 et Samsung S21 FE sont des preuves de validation de la chaîne
générique. Ils ne définissent ni l'identité produit, ni un profil caméra
hardcodé, ni une limite de CPU ou de dataset.
- **Intégration multi-campagne A6000 + S21 FE Engine Bay** : PASS — les plans
réels A6000 (953 paires confirmées `CALLER_EXPLICIT`) et Samsung SM-G990B
(3544 JPEG singleton) ont été validés dans deux ScanSets d'un même projet
@ -89,8 +103,8 @@ persistante, enrichissable et versionnable.
### Plus tard / différé
- publication durable dense/mesh et scratch SSD externe optionnel gouverné ;
- workflow TUI de confirmation/progression ;
- publication durable dense/mesh et consommation Task explicite du scratch
SSD optionnel ;
- vidéo/keyframes et **Capture Guidance / Live Coverage** : analyse et viewer de
couverture, suggestions de prises de vue puis assistance live, après
reconstruction mature ;
@ -102,11 +116,11 @@ persistante, enrichissable et versionnable.
```text
TUI / Projet
Scheduler
Task Queue bornée (un worker, ordre/backpressure)
Resource Governor
Resource Governor (admission et réservation)
Workers
Task callback admis (parallélisme interne borné si prouvé)
Résultats atomiques / persistants
@ -116,12 +130,37 @@ Viewer (consommation passive de snapshots)
### Invariants fondamentaux
- Aucun callback de tâche sans réservation active validée
- Le scheduler ne décide jamais des ressources
- La Queue/runtime ne décide jamais des ressources
- Le Resource Governor est l'unique propriétaire des budgets
- ncurses appartient exclusivement au thread principal
- Les estimations de ressources sont immuables
- Les buffers et files sont strictement bornés
### TUI opérationnelle
La TUI sépare le modèle de vue pur du rendu ncurses. Son observateur copie au
plus 129 entrées Queue (64 pending, une active, 64 historiques) et coalesce les
captures hôte autour d'une seconde ; aucun scan DB ou `/proc` volumineux n'a
lieu par frame. La progression scientifique exacte provient seulement des
compteurs durables typés. Le taux EWMA et l'ETA restent « calcul » jusqu'à deux
intervalles positifs, excluent le préfixe repris et deviennent explicitement
indéterminés, stalled ou throttled lorsque l'évidence l'exige.
Les tailles supportées sont 100×30 et plus en vue complète, 72×20 en compacte
de référence, et jusqu'au minimum 60×15 ; en dessous, seul « Terminal trop
petit » est affiché. Les couleurs ont toujours un équivalent textuel/bold/dim.
`F1` à `F7` ouvrent aide, projets, import, viewer futur, tâches, ressources et
optique. Le segment littéral `F10 SSD` reste visible à 60 colonnes et déclenche
uniquement l'action autorisée par le contrôleur. Pendant une saisie, seules
Enter, Échap et F10 sont actives ; pendant un import, seules `X` et F10 le sont,
et quitter/retour accueil sont explicitement désactivés.
Ouvrir, fermer ou changer de projet détruit et joint d'abord l'unique Queue,
callbacks terminaux inclus, puis ferme Project DB et recrée une Queue vide. Le
workflow optique utilise les alias metadata exacts, accepte normalement les
objectifs manuels sans EXIF, crée des profils/configurations immuables et exige
une affectation/sélection de calibration explicite et exactement compatible.
## Pipeline cible
```text
@ -150,7 +189,7 @@ Acquisitions
- [Resource Governor](docs/architecture/resource_governor.md)
- [Parallélisme interne borné](docs/architecture/internal_parallelism.md)
- [Pipeline sensible aux ressources](docs/architecture/resource_aware_pipeline.md)
- [Intégration Scheduler ↔ Governor](docs/architecture/scheduler_resource_integration.md)
- [Intégration Queue/runtime ↔ Governor](docs/architecture/scheduler_resource_integration.md)
- [Pipeline de reconstruction](docs/architecture/reconstruction_pipeline.md)
- [Persistance](docs/architecture/persistence.md)
- [Base de données projet](docs/architecture/project_database.md)
@ -167,7 +206,8 @@ Acquisitions
- [Backend Vulkan ORB](docs/architecture/vulkan_matcher.md)
- [Viewer](docs/architecture/viewer.md)
- [Resource Boundary — No New Resource Subsystem](docs/architecture/resource_boundary.md)
- [Revue des fondations](docs/architecture/foundation_review.md)
- [Audit global de maintenance — état consolidé](docs/architecture/global_maintenance_audit.md)
- [Revue historique des fondations](docs/architecture/foundation_review.md)
### Concepts
- [Scan Sets](docs/concepts/scan_sets.md)
@ -223,17 +263,32 @@ les paramètres dans `L3DMDID2` v2 (220 octets). Chaque appel utilise un espace
travail privé neuf sous le staging appelant, sans réemploi d'une scène,
profondeur, cache ou sortie antérieure. Le DAG, le viewer et les autres étapes
denses restent des tickets séparés planifiés.
Project DB v22, `raw.develop` et Calibration Bootstrap v1 sont **PASS /
FROZEN** : la suite normale 53/53, les contrôles syntaxiques C17, `git diff
--check`, la validation ciblée ASan/UBSan et l'audit final ont passé. La suite
ASan/UBSan complète demeure qualifiée par le comportement LSan du pilote tiers
RADV ; elle n'est pas présentée comme un PASS complet du dépôt. Les campagnes
La fondation Project DB v22, `raw.develop` et Calibration Bootstrap v1 reste
**PASS / FROZEN**. La tête courante v23 ajoute seulement le contexte optique
générique : neuf tables, migration transactionnelle sans backfill, objectifs
manuels sans EXIF, configurations multiples par campagne et sélection de
calibration exactement compatible. Les migrations de copies réelles S21/A6000
ont conservé leurs lignes scientifiques et laissé l'overlay vide. Les campagnes
réelles S21 et A6000 Engine Bay sont
`CALIBRATION_UNAVAILABLE` par non-identifiabilité scientifique des données de
calibration connues ; le Sparse SfM réel reste
`BLOCKED_BY_KNOWN_CALIBRATION_DATA`, sans pseudo-calibration ni import inféré.
Le Resource Governor ne constitue pas un Resource System générique : voir la décision
darchitecture.
Le Resource Governor ne constitue pas un Resource System générique : il reste
l'unique propriétaire des budgets et le seul orchestrateur des leases scratch
de production. Le contrôleur SSD UDisks2 est une frontière physique séparée,
jamais un second scheduler ou Governor. Les quatorze Task kinds actuels ne
consomment encore aucun scratch : l'espace disponible est une capacité
observable, pas un usage fabriqué, et scratch/swap ne deviennent jamais de la
RAM. La TUI/F10 et cette intégration sont validées opérationnellement. L'audit
global est désormais `GLOBAL_MAINTENANCE_AUDIT=PASS/FROZEN` : après les builds
Clang frais portable/Vulkan, les suites normales 64/64 et 65/65, les sanitizers
applicables, TSan et les contrôles ABI, l'unique revue finale indépendante
GPT-5.6 SOL/ULTRA a conclu PASS sans finding bloquant. Elle a indépendamment
rejoué le build portable, la suite 64/64, une matrice focalisée 15/15 et les
76/76 probes strictes C17/C++17 couvrant 19 headers publics modifiés/nouveaux,
ainsi que l'ABI, les négatifs de seams production, le SHA du manifest GV et le
diff-check. Ce gel clôt la gate de maintenance ; il n'exécute pas à lui seul la
tranche scientifique suivante.
## Licence

View file

@ -238,18 +238,21 @@ registry production et resoumise à la queue.
Les anciens snapshots v1 produits avec l'estimation opérationnelle exacte
128 Kio fixes, 64 Kio par item, lot 164, CPU 1, IO 1 et GPU 0 sont normalisés
éphémèrement par la registry à la forme courante CPU12 avant admission. Le
éphémèrement par la registry à la forme courante CPU64 avant admission. La
forme historique immédiatement précédente CPU12/256 Kio fixes/64 Kio par item
est elle aussi reconnue exactement. Le
snapshot durable original reste la source du reconstructeur ; aucun checkpoint
d'estimation seule n'est stagé, promu ou publié sous le même résumé. Une panne
pré-terminale répète donc cette normalisation exacte. Aucun autre snapshot,
curseur ou paramètre scientifique n'est réinterprété.
### Intégration scheduler
### Intégration Task/Queue/Governor
La tâche utilise le scheduler générique via le pattern standard :
- Estimation opérationnelle (256 Kio fixes, 64 Kio par item, lot 164).
La Queue conserve un callback ; jusqu'à douze participants CPU admis peuvent
calculer une fenêtre interne bornée sans modifier l'identité scientifique.
La tâche utilise le runtime générique via le pattern standard :
- Estimation opérationnelle (256 Kio fixes, 8 Mio par item, lot 164).
La Queue conserve un callback ; jusqu'à soixante-quatre participants CPU
admis peuvent calculer une fenêtre interne bornée sans modifier l'identité
scientifique.
- Réservation CPU + IO avant exécution
- `lardon3d_task_sequence_break()` entre chaque lot pour réadmission Governor
- Callback terminal checkpoint après `COMPLETED`/`FAILED`/`CANCELLED`
@ -365,7 +368,7 @@ canonicalisation, idempotence, batch projet, fingerprint et
réutilisation/invalidation.
**IMPLEMENTED** — tâche durable `candidate_pair.generate` v1
via le scheduler générique, avec estimation immuable,
via le runtime/Queue générique, avec estimation immuable,
checkpoint par curseur, reprise idempotente et intégration
dans la registry production.

View file

@ -28,13 +28,12 @@ porte sur les 24 octets canoniques `L3DORBP1`, version et trois entiers
little-endian. Il ne dépend ni du padding, ni de la locale. Le contrat ORB v1
produit 32 octets binaires par point.
Au démarrage, avant la création des workers, Lardon3D configure la limite de
threads interne process-wide d'OpenCV à
`min(12, logical_cpu_count - system_cpu_reserve)`. La borne douze est
opérationnelle et correspond au plafond audité. `features.extract` demande
jusqu'à douze threads au Resource Governor ; celui-ci réduit l'admission au
budget hôte, donc à la limite OpenCV configurée. La Queue conserve un callback actif ; aucun pool Lardon3D
supplémentaire n'est créé pour ORB.
Au démarrage, avant la création du worker Queue, Lardon3D configure la baseline
OpenCV process-wide depuis le compute-pool réel. `features.extract` publie la
borne positive `int` acceptée par OpenCV ; le Resource Governor réduit toujours
l'admission au budget hôte. Douze threads demeure une cohorte de validation,
pas un plafond portable. La Queue conserve un callback actif ; aucun pool
Lardon3D supplémentaire n'est créé pour ORB.
Cette limite est une configuration opérationnelle, jamais un paramètre
scientifique : elle n'entre ni dans le fingerprint ORB v1, ni dans l'identité

View file

@ -1,4 +1,11 @@
# Revue des fondations Lardon3D
# Revue historique des fondations Lardon3D
Ce document conserve l'état de la revue de fondation à son époque. Les limites
« absence de persistance », tombstones et tickets recommandés ci-dessous ne
décrivent plus l'état courant. Pour l'architecture active, voir
[Task](task_system.md), [Queue](task_queue.md),
[Resource Governor](resource_governor.md) et le
[registre de maintenance](global_maintenance_audit.md).
## Objectif
@ -51,7 +58,7 @@ Documenter la revue technique de la phase de fondation : task, task_queue, hardw
6. Protection mutex unique du gouverneur
7. Séparation stricte des rôles (scheduler ne décide pas des ressources)
## Limites connues
## Limites connues lors de cette revue historique
- File à worker unique avec FIFO strict
- Absence de notification automatique de libération externe
@ -69,7 +76,7 @@ Documenter la revue technique de la phase de fondation : task, task_queue, hardw
- Bornage de la mémoire
- Indépendance de la publication atomique
## Feuille de route
## Feuille de route historique (désormais supersédée)
### Prochains tickets recommandés
1. Sélectionner une tâche admissible sans blocage par la tête de file ✓
@ -87,4 +94,4 @@ Documenter la revue technique de la phase de fondation : task, task_queue, hardw
- TSan passé
- git diff --check propre
## Statut : DOCUMENTATION DE L'IMPLÉMENTATION ACTUELLE
## Statut : ARCHIVE D'ÉVIDENCE HISTORIQUE

View file

@ -13,7 +13,8 @@ les paramètres, l'ordre et l'acceptance v1, mais ajoute avant USAC le support m
observations canoniques distinctes décrit ci-dessous. V3 est la policy de production courante :
après les mêmes validations intégrales, elle compose ce support v2 avec la preuve exacte de
faisabilité d'acceptation `match_count >= min_inlier_count`. Chaque policy possède sa version et son
fingerprint distincts ; Project DB v22 stocke déjà ces champs et ne nécessite aucune migration.
fingerprint distincts ; le schéma les stocke déjà depuis v12 et la tête courante
Project DB v23 ne nécessite aucune migration GV.
## Inputs
@ -233,14 +234,24 @@ environ 761 Kio hors petits objets et scratch OpenCV. Les cartes appartiennent
libérées au retour ; leur borne Feature Store est opérationnelle et n'ajoute aucune limite
scientifique. Aucun descriptor ni matrice A×B n'est lu. Massif mesure 2,445 Mio de heap au pic
du test E2E complet, incluant SQLite, OpenCV, fixtures Feature Store et toutes les séquences de test.
Une réservation conservatrice de 4 Mio par job couvre ce profil mesuré.
La forme sérielle historique réservait 4 Mio fixes. La Task outer-parallel
courante réserve 8 Mio par item admis afin de couvrir aussi la pile enfant
bornée de 4 Mio, l'objet préparé, les readers et le scratch opaque. Avec un lot
maximal 16, cette charge reste bornée à 128 Mio et ne limite pas la cardinalité
du dataset.
## CPU policy
Le parallélisme OpenCV reste configuré process-wide. Le benchmark mesurera les threads réellement
consommés ; le verifier ne change pas `cv::setNumThreads()` par paire.
Le parallélisme scientifique interne OpenCV reste explicitement désactivé :
`UsacParams::isParallel=false` appartient au fingerprint FROZEN. Le verifier ne
change pas `cv::setNumThreads()` par paire.
La campagne a consommé environ 99 % d'un CPU logique : réservation v1 d'un thread et un worker.
Le parallélisme courant porte uniquement sur des Match Results indépendants.
Le Governor peut admettre CPU1..8 pour une fenêtre/lot d'au plus 16 ; le
callback Queue compte comme participant, crée au plus `cpu_threads-1` enfants,
les joint, puis publie seul dans l'ordre. La fenêtre participant a été validée
sûre jusqu'à 16, mais CPU8 est le maximum utile mesuré. Ce choix opérationnel
n'entre ni dans le fingerprint, ni dans le GVR.
## GPU policy
@ -270,9 +281,12 @@ sélectionne v3 ; le payload et l'ABI publique de configuration restent inchang
démarre avec une identité neuve et ne reprend ni ne ré-étiquette une tâche v1/v2.
Le Task Kind production est `geometric_verifier.run` version 1. Il pagine les Match Results par ID
strictement croissant avec une page de `batch + 1`, et traite des lots Governor 1/2/4/8. Les
parents autres que `MATCHED` avec `match_count > 0` sont seulement traversés par le curseur. Une
unité éligible appelle le core, qui reuse l'identité exacte avant toute lecture d'asset.
strictement croissant avec une page de `batch + 1`, et traite des lots Governor
1..16 avec CPU utile 1..8. Les préparations éligibles peuvent s'exécuter en
parallèle, mais la publication, l'avancement du curseur contigu et le checkpoint
restent owner-only et ordonnés. Les parents autres que `MATCHED` avec
`match_count > 0` sont seulement traversés par le curseur. Une unité éligible
appelle le core, qui reuse l'identité exacte avant toute lecture d'asset.
WHY GENERIC TASK PERSISTENCE IS INSUFFICIENT: aucun champ de payload métier dans le snapshot v1.
@ -388,7 +402,8 @@ Le dernier contrat est GREEN, CPU 1, GPU 0, I/O 1, batch 8, hôte 4 Mio et GPU 0
compute `0-5,8-13` et reserve `6,7,14,15`. Sur l'échantillonnage coalescé de 21 590 changements,
le minimum `MemAvailable` vaut 8 907 714 560 octets, le RSS/HWM processus maximal 45 690 880
octets, le PSI mémoire maximal 0,90 %, le PSI I/O maximal 50,17 % et les deltas swap-in/out sont
0/0. Les réserves 3 Gio/2 Gio restent respectées ; aucune voie GPU GV n'est créée.
0/0. Les réserves 3 Gio/2 Gio alors en vigueur pour ce run historique restent
respectées ; aucune voie GPU GV n'est créée.
La seconde reprise complète crée la Task 2833, traverse le même curseur et crée zéro GVR. Les
172 275 lignes avant/après sont égales sur toutes leurs colonnes par `EXCEPT` dans les deux sens,
@ -406,6 +421,47 @@ test runner ciblé passe aussi sous ASan/UBSan. `REAL_S21_GV_V3`, `RESTART_IDEMP
la preuve réelle de la policy v3 déjà gelée ; il ne rouvre ni algorithme, seuil, RNG, fingerprint,
Project DB v22, Matcher/Governor v2, Track Builder ou Sparse SfM.
## Maintenance outer-parallel — preuve représentative réelle
**IMPLEMENTED / VALIDATED / REVIEWED.** La maintenance sépare la préparation
scientifique de la publication sans modifier la policy v3. Une préparation
valide tout l'input immuable et produit un objet opaque borné ; elle n'écrit
jamais Project DB. Après jointure, le callback propriétaire publie ces objets
en ordre de `match_result_id`, détruit chacun exactement une fois et checkpoint
le seul préfixe contigu. Les pannes de création partielle, calcul, publication,
annulation et reprise ne peuvent donc ni publier un suffixe devant un trou, ni
laisser un enfant/handle vivant à la libération de réservation.
Le corpus représentatif réel contient 4 113 parents, dont 4 102 applicables,
578 `GEOMETRIC_VERIFIED` et 3 524 `GEOMETRIC_REJECTED`. CPU1/2/4/8/12
conservent les mêmes IDs 1..4102, toutes les colonnes scientifiques et le digest
`9401ef6168804b6f1d51f4cdf64cd6b33cbebd2934e5294c8feacc87f9c8ce86`.
Les walls Task complets sont 67,521078032/48,859141912/39,158236068/
35,170176868/34,251675780 s, soit 60,7514/83,9556/104,7545/116,6329/
119,7606 parents/s. Le gain CPU8→CPU12 vaut seulement 2,68 %, sous le seuil de
5 %. La capacité production est donc CPU utile 8, batch 16 et fenêtre sûre 16.
Les tests focalisés finaux passent 8/8, les répétitions de stress 60/60,
ASan/UBSan 3/3 et TSan 3/3, avec contrôles C17 GCC/Clang. La preuve S21
historique ci-dessus reste le run complet CPU1/batch8 acquis ; aucun rerun
complet de 3 221 s n'est revendiqué pour cette maintenance bornée. Le manifest
retenu de cette preuve a le SHA-256
`52a4412299c74050a66d5690122a793c9451c79faf47e32b6e65a5958f804856`.
Il compare littéralement les 4 102 GVR applicables — ordre/IDs 1..4102,
statut, compteur/masque d'inliers, présence et octets binary64 du modèle — et
vérifie intégrité DB, clés étrangères, absence de replay amont et absence de
travail Tracks/Sparse. Le run S21 complet acquis couvre déjà la policy v3 et
sa persistance FROZEN ; la maintenance ne change que la préparation externe et
la publication owner-only. Cette combinaison réelle bornée + tests ciblés de
panne/checkpoint/reprise discrimine donc le changement sans payer un second run
scientifique intégral ni prétendre l'avoir exécuté.
La validation globale fraîche qui englobe ce delta passe aussi dans le graphe
Clang portable Vulkan-off 931/931 et sa suite 64/64, puis le graphe Vulkan-on
939/939 et sa suite 65/65. Le TSan global reste volontairement Vulkan-disabled
et couvre les deux cibles GV dans sa matrice 14/14 plus répétitions ; il utilise
uniquement les suppressions externes OpenCV/TBB documentées par le projet.
## Out of scope
Tracks, model competition, classification planaire ou faible parallaxe, Essential, calibration,

View file

@ -0,0 +1,490 @@
# Global maintenance audit — état consolidé
## Lifecycle et portée
`GLOBAL_MAINTENANCE_AUDIT=PASS/FROZEN`.
Les tranches décrites ci-dessous, y compris l'observatoire TUI, F10 et le
raccordement SSD/Governor, sont implémentées, validées opérationnellement et
relues. Les builds Clang frais portable et Vulkan, les suites 64/64 et 65/65,
les sanitizers applicables, TSan, les probes de headers publics et d'ABI sont
acquis. L'unique revue finale indépendante GPT-5.6 SOL/ULTRA du dépôt consolidé
a ensuite conclu PASS sans finding bloquant. La gate de maintenance est donc
fermée et gelée ; ce statut ne réinterprète aucune science FROZEN et n'exécute
pas la tranche scientifique suivante.
Ce record réconcilie l'état courant sans rouvrir les contrats scientifiques
FROZEN. Les détails normatifs restent dans les documents de domaine :
- [Project Database](project_database.md) pour les migrations, identités et
transactions ;
- [Resource Governor](resource_governor.md) pour l'admission et la matrice des
quatorze Task kinds ;
- [Task](task_system.md), [Queue](task_queue.md) et
[runtime](runtime.md) pour ownership, reprise et concurrence ;
- [parallélisme interne](internal_parallelism.md) et
[Geometric Verifier](geometric_verifier.md) pour les bornes CPU et la science ;
- [Resource Boundary](resource_boundary.md) pour la séparation des systèmes ;
- [roadmap](../roadmap/roadmap.md) pour l'ordre des tranches.
## Positionnement produit et identité des preuves
Lardon3D est un moteur de photogrammétrie générique, persistant et sensible aux
ressources. Sony A6000, Samsung S21 FE, Ryzen 7 8845HS et Radeon 780M sont des
fixtures, corpus ou hôtes de validation. Ils ne sont ni une identité produit,
ni une branche de code métier, ni une limite portable de caméra, CPU ou GPU.
Les identités restent celles des contrats existants : Capture, Asset,
`image_id`, Task ID, groupe de campagne, profil de boîtier, profil d'objectif,
configuration optique et calibration sont distincts. Un chemin, basename,
SHA-256, champ EXIF ou modèle d'appareil n'est jamais promu implicitement en une
autre identité.
## Project Database v23 et contexte optique générique
La tête de schéma courante est **Project DB v23**. La v23 est une migration
additive au-dessus de la fondation scientifique et de persistance v22 gelée.
Elle ne réécrit aucune ligne scientifique v16v22 et crée neuf tables vides :
| Table v23 | Concept possédé |
| --- | --- |
| `camera_body_profiles` | profil de boîtier explicite |
| `camera_body_aliases` | alias metadata make/model exact, binaire |
| `lens_profiles` | profil d'objectif manuel, électronique ou intégré |
| `lens_profile_aliases` | alias metadata d'objectif exact et optionnel |
| `optical_configurations` | boîtier + objectif + focale exacte optionnelle |
| `acquisition_campaign_group_optics` | configuration explicite d'un groupe de campagne |
| `capture_optical_configurations` | configuration retenue pour un Capture et sa provenance |
| `optical_calibration_profiles` | calibration existante compatible avec une configuration exacte |
| `capture_calibration_selections` | sélection explicite de calibration pour un Capture |
La migration v22→v23 s'exécute dans la transaction de migration existante. Le
marqueur de version n'est publié qu'après création de toutes les tables ; tout
échec rollbacke l'ensemble. Elle n'infère et ne backfill aucun profil depuis
EXIF, chemin, nom, SHA-256, dimensions, calibration historique ou marque
d'appareil. Une base v22 migre donc avec les neuf tables optiques vides.
Un objectif manuel sans EXIF est un cas normal : un profil explicite peut ne
posséder aucun alias metadata. Le Meike manuel utilisé dans les tests est une
preuve de ce chemin générique, pas un modèle hardcodé. Une campagne peut
assigner des configurations différentes à ses groupes ; lors de la rétention
du Capture, le binding campagne/configuration et l'avancement du curseur sont
atomiques. Un Capture importé hors campagne peut recevoir une affectation
explicite distincte, sans valeur « inconnue » fabriquée.
Une calibration optique référence une calibration sparse immuable existante et
une seule configuration exacte. Il n'existe ni interpolation, ni fallback par
focale proche, ni sélection automatique. Le Capture et le profil doivent
référencer la même configuration ; plusieurs profils compatibles restent
ambigus jusqu'à une sélection explicite. Les retries exacts convergent, les
conflits valides sont des contraintes et les dépendances durables malformées
sont de la corruption.
Les migrations exercées via l'API production sur des copies des projets S21 et
A6000 ont atteint v23 avec `integrity_check` et clés étrangères propres, comptes
scientifiques inchangés et tables optiques vides. Les SHA-256 des projets source
sont restés inchangés. La validation v23 retenue comprend la suite normale
57/57, dix tests focalisés, cinq cibles ASan/UBSan, les contrôles de headers C17
et C++, les scénarios de rollback/retry et une revue indépendante après
correction. Cette preuve ne réinterprète aucune calibration réelle des deux
campagnes.
## Resource Governor portable
Le Resource Governor demeure l'unique propriétaire de l'admission RAM, CPU,
GPU et I/O et le seul orchestrateur des leases scratch de production. Le
contrôleur SSD décrit plus bas est une frontière physique ; il n'est ni un
scheduler, ni un second Governor, ni une source de capacité RAM.
La politique hôte courante réserve d'abord le système :
- quatre CPU logiques lorsque l'hôte le permet, tout en conservant au moins un
CPU de calcul sur un petit hôte ;
- quand la topologie est fiable, un ensemble déterministe de coeurs physiques
complets, frères SMT inclus, avec le dépassement minimal de la cible logique ;
- les CPU déjà exclus par une affinité externe comptent dans la réserve hôte,
afin de ne pas soustraire deux fois la même capacité ;
- sans topologie ou masque fiable, le fallback ne fabrique aucun ID ni masque :
il conserve seulement un budget de compte portable ;
- environ 3 Gio de `MemAvailable` comme réserve dure sur un hôte capable ; la
bande 34 Gio est une prudence YELLOW qui bloque la croissance mais ne
soustrait pas 4 Gio à la capacité ;
- sur un petit hôte RAM, une réserve fractionnaire déterministe permet une
dégradation sûre sans prétendre conserver 3 Gio inexistants ;
- PSI CPU/mémoire/I/O et deltas actifs de swap pilotent la pression ;
l'occupation historique du swap ne suffit pas ;
- la mémoire UMA est débitée exactement une fois et swap/zram/SSD ne deviennent
jamais un objectif de travail ni une extension de la RAM admise.
Il n'existe plus de plafond CPU global à 12. Le Governor borne chaque contrat
par le compute-pool réel et par la capacité intrinsèque du Task kind. Les IDs
`0-5,8-13` et la réserve `6,7,14,15` restent la preuve historique correcte de
l'hôte 16-CPU validé, pas une politique portable.
## Capacités des quatorze Task kinds
La matrice chiffrée complète appartient au
[Resource Governor](resource_governor.md#audit-des-14-kinds-de-production).
La classification courante est :
| Kind v1 | CPU et lot courants | Nature de la borne |
| --- | --- | --- |
| `raw.develop` | CPU 1, lot 1 | opération atomique ; OpenCV global appliqué/restauré, allowance RAW 2 Gio |
| `photo_quality.triage` | CPU 1, lot 1 | un groupe borné ; aucune preuve de scaling utile |
| `acquisition_campaign.run` | CPU 1, lot 1 | orchestration d'un groupe ; coût JPEG/RAW exact, aucun Task imbriqué |
| `import.images` | CPU 1, lot 1..32 | copie/hash I/O-bound, réadmission par lot |
| `features.extract` | CPU 1..compute-pool, lot 1 | API OpenCV positive-`int`, plafond hôte Governor |
| `features.extract.sift` | CPU 1..compute-pool, lot 1 | même contrat OpenCV ; historique CPU12/CPU1 accepté exactement |
| `features.extract.rootsift` | CPU 1..compute-pool, lot 1 | même contrat OpenCV ; aucune voie GPU validée |
| `visual_index.update` | CPU 1..16, lot 1..16 | maximum algorithmique du segment de 16 Feature Sets |
| `candidate_pair.generate` | CPU 1..64, lot 1..64 | maximum algorithmique/ressource d'un batch de 64 sources |
| `matcher.run` | CPU sûr 1..12 ; lot AUTO utile 1..8, sûr 1..12 | batch Matcher mesuré ; Vulkan ORB exact, SIFT/RootSIFT CPU |
| `geometric_verifier.run` | CPU utile 1..8, fenêtre CPU sûre 16, lot 1..16 | outer-parallelism opérationnel ; USAC interne reste `isParallel=false` |
| `track_builder.run` | CPU 1, lot 1 | rebuild DSU complet, publication atomique owner-only |
| `sparse_sfm.run` | CPU 1, lot 1 | contrat scientifique/atomique FROZEN, BA à un thread |
| `incremental_reconstruction.run` | CPU 1, lot 1 | recomputation atomique FROZEN depuis entrées immuables |
Candidate utilise au plus `cpu_threads-1` enfants et un handle DB privé par
participant, le callback Queue étant lui-même participant. Ses piles et états
sont facturés à 8 Mio par item ; `batch=1` ne crée pas de travail vide.
Visual Index conserve ses buffers de segment fixes et publie seul après toutes
les jointures. ORB/SIFT/RootSIFT utilisent `INT_MAX` seulement comme borne de
l'API OpenCV : l'admission réelle reste le compute-pool. Les signatures CPU12
historiques sont des formats de reprise exacts et ne réintroduisent aucun
plafond portable.
La même discipline vaut à la frontière géométrique Sparse SfM : les champs
publics `uint32_t` `max_iterations` et `minimum_inliers` de relative pose et PnP
doivent être représentables par le `int` OpenCV, donc être au plus `INT_MAX`
(`max_iterations` reste strictement positif). Une valeur supérieure est
refusée avant narrowing, allocation, appel solveur ou mutation des sorties.
L'encodage F0 reste `u32`, et tous les paramètres FROZEN représentables,
valeurs de référence, fingerprints et résultats scientifiques restent
inchangés : cette borne est opérationnelle/ABI externe, pas une nouvelle
politique scientifique.
Le Geometric Verifier prépare désormais des Match Results indépendants en
parallèle, puis le propriétaire unique publie dans l'ordre et checkpointe un
préfixe contigu. Sur le corpus représentatif réel de 4 113 parents, les sorties,
IDs canoniques et le digest scientifique
`9401ef6168804b6f1d51f4cdf64cd6b33cbebd2934e5294c8feacc87f9c8ce86`
restent identiques. Les débits Task complets CPU1/2/4/8/12 sont
60,7514/83,9556/104,7545/116,6329/119,7606 parents/s. CPU12 n'ajoute que
2,68 % sur CPU8, sous le seuil d'acceptation 5 % : le plafond utile est 8,
la fenêtre sûre et le lot maximal restent 16. La preuve S21 complète reste une
preuve historique CPU1/batch8 valide : 172 741 parents traversés, 172 275
applicables, 24 065 vérifiés et 148 210 rejetés en 3 221,758 s, sans doublon,
avec reprise/SIGKILL validée. Elle n'est pas présentée comme un rerun du
nouveau chemin. Le manifest retenu de la preuve outer-parallel a le SHA-256
`52a4412299c74050a66d5690122a793c9451c79faf47e32b6e65a5958f804856` :
il prouve littéralement les 4 102 lignes applicables, leurs IDs 1..4102,
statuts, modèles, masques et compteurs, pas seulement un wall. Un second run
complet S21 de 3 221 s n'est pas requis pour accepter cette maintenance : le
run S21 acquis porte déjà la science/persistance v3 gelée, tandis que la preuve
réelle bornée et les tests de publication/reprise discriminent exactement le
seul changement, l'ordonnancement externe de préparation.
## Task, Queue, persistance et concurrence
Une seule Queue possède un worker et un callback actif. Le Governor admet la
séquence ; la Task possède son état d'exécution ; la Queue possède seulement
ordre/backpressure et, après retour du callback terminal, une histoire de
64 snapshots maximum. Task et userdata sont détruits hors mutex Queue et
seulement après la fin du callback.
La fermeture Queue ferme d'abord l'ingress, attend le worker et tous les appels
déjà enregistrés, puis détruit exactement une fois. Comme pour tout pointeur C
brut, le propriétaire doit empêcher le début de nouveaux appels après le début
de la destruction. Les callbacks terminaux peuvent observer les API read-only,
mais ne doivent pas retirer synchroniquement leur propre entrée, détruire leur
Queue ou attendre une opération dépendant de leur propre retour.
Les IDs Queue générés sont monotones et leur épuisement à `UINT64_MAX` est
sticky ; ni un ID restauré plus petit, ni l'éviction de l'histoire ne réarme la
génération. La reprise restaure état métier et estimation durable, jamais
thread, callback, pointeur, réservation ou contrat actif. Les signatures de
ressources historiques explicitement reconnues sont normalisées seulement en
mémoire ; aucun checkpoint « estimate-only » n'est publié. Les curseurs métier
et la publication scientifique restent les seules frontières de progression.
Ouvrir, fermer ou changer de projet constitue une frontière de session exacte.
Les vues libèrent leurs borrows, puis la Queue est annulée, jointe et détruite
avant Project DB ; une unique Queue vide est ensuite recréée et les observateurs
sont rebondés. Les callbacks terminaux peuvent donc finir avec leur DB encore
vivante, et histoire/namespace Queue d'un projet ne fuient pas dans le suivant.
## Observatoire et centre de contrôle TUI
**CURRENT / VALIDATED OPERATIONAL.** ncurses, l'entrée et le rendu appartiennent
uniquement au thread principal. Un modèle de vue pur reçoit des copies bornées
et ne mute ni Queue, ni Governor, ni DB, ni science. L'observateur coalesce les
captures ordinaires pendant au moins une seconde et couvre exactement le pire
cas production : 64 pending, une active et 64 historiques, soit 129 Tasks. Les
ABI historiques `TaskSnapshot`, `ResourceSnapshot`, `AppState` et layout sont
préservées ; les identités typées, comptes durables, contrats installés,
SwapTotal et vues riches passent par des types/fonctions additifs.
La progression exacte emploie toujours le préfixe métier durable quand il est
connu. Un terminal 2/7 est une erreur d'intégrité et un terminal typé sans
compteur scientifique reste indéterminé ; aucun des deux ne devient un faux
100 %. L'EWMA/ETA exclut le préfixe repris et exige deux intervalles positifs ;
les états calculating, indeterminate, stalled, throttled et complete restent
explicites. Le pipeline expose Acquisition, RAW, Quality, Features, Visual
Index, Candidate, Matcher, GV, Tracks, Sparse SfM et Dense, avec
`NOT_READY/READY/QUEUED/RUNNING/THROTTLED/BLOCKED/COMPLETE/FAILED/NOT_APPLICABLE`.
Dense future reste `NOT_APPLICABLE` et n'est jamais montrée en cours.
Le panneau ressources présente CPU actif/admis/disponible et sa raison,
GPU/mémoire/busy/backend, RAM/MemAvailable/réserve, swap total/utilisé/deltas,
lot/inflight/helpers/I/O/scratch et Governor GREEN/YELLOW/RED. Le contrat de
l'exacte Task active est prioritaire ; backend/inflight/helpers ou raison
restent `UNKNOWN` lorsqu'une association Task+séquence n'est pas prouvée.
L'espace scratch enregistré auprès du Governor est présenté séparément des
détails physiques contrôleur et n'est jamais additionné à la RAM.
Les layouts validés sont full à partir de 100×30, compact à 72×20 et jusqu'au
minimum 60×15 ; en dessous, seul `Terminal trop petit` est rendu. Vert/jaune/
rouge/cyan/bleu/magenta et dim/bold ont toujours des libellés de repli sans
couleur. `F10 SSD` est un segment littéral garanti à 60 colonnes dans les modes
idle, saisie et import. L'aide est contextuelle : saisie = Enter/Échap/F10,
import actif = X/F10 avec q/Échap désactivés, et les écrans idle n'annoncent que
les touches réellement traitées.
L'écran optique effectue seulement des lookup metadata exacts, accepte le
Meike manuel sans EXIF comme cas normal, crée de nouveaux profils/configurations
immuables et affecte explicitement groupe de campagne ou Capture. Il liste et
sélectionne uniquement les calibrations exactement compatibles ; unresolved,
absence, ambiguïté, incompatibilité, BUSY, I/O et corruption restent visibles.
Les pages de 16 rapportent leur compte local et l'existence exacte d'une suite ;
`[` revient à la première page, `]` avance et `R` retente explicitement.
## Contrôleur SSD externe optionnel
Le contrôleur SSD est la frontière physique synchrone et bornée vers UDisks2
via GIO/GDBus. Il découvre exactement les labels `LARDON_SWAP` et
`LARDON_SCRATCH`, exige des UUID stables et une même identité UDisks Drive, et
ne dépend jamais d'un `/dev/sdX`, modèle, numéro de série ou object path comme
identité produit. Un renommage de device node ne change donc pas l'identité.
Les états publics sont `ABSENT`, `DETECTED`, `ENABLING`, `ENABLED`, `IN_USE`,
`DRAINING`, `SAFE_TO_UNPLUG` et `ERROR`. Les snapshots sont des copies bornées ;
les valeurs inconnues restent explicitement inconnues. Le poll ordinaire est
coalescé autour d'une seconde. Le chemin production n'appelle pas `statvfs`
depuis le poll synchrone : un scratch monté peut donc rapporter total/libre
inconnus.
La conversion vers le Governor valide l'état complet et échoue fermée. Toute
paire ou autorité (`pairing_valid`, allocation, enable/disable/cancel, ou état
`ENABLING/ENABLED/IN_USE/DRAINING/SAFE_TO_UNPLUG`) exige la détection actuelle
du Drive et des deux partitions, les trois identités exactes non vides, des
tailles de partition connues strictement positives et des faits mount/activité/
capacité cohérents. `ABSENT` rejette toute télémétrie active, montée, louée ou
capacitaire ; un `DETECTED` partiel reste visible mais non actionnable. Un
hazard `ERROR` déconnecté peut conserver son tuple pour la sécurité, jamais
pour allouer ; `can_disable` n'est acceptable qu'après reconnexion prouvée de
l'exact tuple original.
L'activation appelle directement UDisks et exige le point de montage exact
`/mnt/lardon-scratch`. Elle ne formate, partitionne, répare, fsck, poweroff ni
supprime rien. Avant toute action potentiellement side-effecting, l'ownership
physique est lié conservativement au tuple Drive+deux UUID. Un timeout ou une
vérification indéterminée laisse un hazard sticky ; aucun disque de remplacement
ne reçoit alors Start/Mount/Stop/Unmount. Seul le même tuple reconnecté peut
être vérifié et drainé jusqu'à `SAFE_TO_UNPLUG`.
Le scratch utilise au plus 64 leases explicites. L'adresse de l'objet lease
fait partie de son ownership process-local : copie, reconstruction, objet
étranger, stale ou double release ne décrémentent rien. `DRAINING` refuse de
nouvelles leases. Le swap n'est arrêté que si les leases sont nulles, que les
PSI et deltas swap sont calmes, et que son usage peut être absorbé tout en
conservant la réserve dure de 3 Gio. La propriété UDisks
`Swapspace.Active` doit être un booléen exact connu ; absence ou mauvais type
échoue fermé. Aucun arrêt forcé n'existe.
Le contrôleur ne crée ni thread de polling, ni scheduler, ni capacité Task
scratch implicite. L'adaptateur TUI possède au plus un thread joinable pendant
un poll ou contrôle UDisks borné ; il ne touche jamais ncurses. F10 consomme
exclusivement `can_enable`, `can_disable` ou `can_cancel_drain` du snapshot
validé, jamais une inférence depuis l'état. Les huit états, identité stable,
modèle, lien, swap, scratch, mount, usage, drain et raison sont présentés ; une
valeur inconnue reste `UNKNOWN`, et `SAFE_TO_UNPLUG` est explicite.
Après chaque snapshot ou contrôle validé, l'adaptateur enregistre l'état
physique auprès du Governor. Un snapshot malformé enregistre `ERROR` et bloque
les nouvelles allocations. Les wrappers Governor sont l'unique voie de lease
scratch de production ; le contrôleur conserve seulement son API physique
basse. Les quatorze Task kinds actuels n'ont aucun consommateur scratch, donc zéro
lease est la vérité courante et une capacité disponible n'est pas un usage.
La génération source peut légalement saturer à `UINT64_MAX`. Une update publique
matériellement différente au même watermark reste stale et ne peut pas rendre
de l'autorité ; seule la fin sérialisée du wrapper exact déjà engagé peut
réconcilier à ce watermark sa propre acquisition/libération et le compte de
leases fondé sur les adresses. Même son erreur ne peut recopier un compte stale.
Le shutdown suit Queue/leases → fermeture projet → join+unregister SSD →
contrôleur → Governor. Tous les tests SSD/TUI utilisent un provider factice et
n'exécutent aucune mutation réelle de l'hôte.
## GPU, build et ABI
Le backend Vulkan ORB Matcher est une voie production validée et GPU-first en
mode AUTO quand l'admission GPU/UMA est sûre. CPU reste le fallback portable.
Le batch AUTO utile est 8 ; batch 12 et inflight 2 restent des bornes de sûreté
et de benchmark rejetées pour la politique normale faute de gain supérieur à
5 %. Candidate, Feature et Visual Index restent CPU ; SIFT/RootSIFT Vulkan est
rejeté par l'évidence disponible ; GV reste CPU et conserve son USAC interne
non parallèle. Sur la Radeon 780M réelle RADV PHOENIX, la feasibility explicite
compare 24 160 requêtes SIFT et 24 161 RootSIFT : chaque mode produit une
divergence d'index, respectivement 20 251 et 20 824 divergences de bits de
distance, mais zéro divergence de décision Lowe. C'est une preuve de
feasibility décisionnelle, pas une équivalence binaire ; le hot path
SIFT/RootSIFT Vulkan reste donc rejeté. Les overrides CPU/Vulkan sont des outils
de debug, benchmark ou reproductibilité, pas un choix backend exigé de
l'utilisateur normal.
Le dépôt est mixte C17/C++17. Les headers publics sous `include/lardon3d/`
restent compilables comme C17, n'exposent aucun type C++ et décrivent ownership,
bounds et erreurs. Aucune exception C++ ne traverse une frontière C. Meson et
Ninja sont le build de référence ; Clang est préféré et GCC reste couvert pour
les contrôles C17 pertinents. GIO/GDBus est la dépendance directe du contrôleur
SSD ; aucune commande shell `mount`, `swapon` ou équivalente n'est construite.
## Évidence finale acquise et revue indépendante
### Builds normaux, Vulkan et ABI
Deux répertoires entièrement frais utilisent Clang/Clang++ 22.1.8, C17 et
C++17 :
- portable `-Dvulkan_orb=disabled` : graphe complet **931/931** puis suite
sérielle **64/64** ;
- Vulkan `-Dvulkan_orb=enabled` : graphe complet **939/939** puis suite
sérielle **65/65**, dont `orb-vulkan-backend` sur l'iGPU réel
`AMD Radeon 780M Graphics (RADV PHOENIX)` ; la dépendance Vulkan détectée
vaut 1.4.357 ;
- la feasibility SIFT/RootSIFT, cible non enregistrée dans la suite normale,
a été compilée et exécutée explicitement ; ses divergences exactes sont
consignées ci-dessus et n'autorisent aucun backend production nouveau ;
- les probes autonomes stricts GCC/Clang C17 et C++17 passent **76/76** sur les
**19 headers publics modifiés ou nouveaux**, le fixture ABI historique, le
lien de l'application et `git diff --check` passent, et
`git status --short -- scan3d` reste vide.
La correction de narrowing Sparse SfM, postérieure à la capture des deux logs
complets, a ensuite passé son test focalisé, 20 répétitions, ASan/UBSan ciblé,
les inclusions C17/C++17 et le lien application. Elle retire les conversions
publiques `uint32_t → int` sans changer les appels valides ni la science.
### Déterminisme post-freeze du fixture Feature
La validation post-freeze a exposé une dépendance exclusivement test à
l'activité de l'hôte : `test-feature-task` construisait des profils et
Governors synthétiques, mais leur admission capturait encore le vrai
`/proc/loadavg`, les PSI et les deltas de swap. Une charge minute suffisante
pouvait donc maintenir correctement la Queue en `WAIT` jusqu'à son délai de
30 s, sans divergence scientifique ni défaut de la politique production.
Le fixture possède désormais un override privé par Governor qui copie sous
mutex un `ResourceSnapshot` complet et ne rafraîchit que son horodatage. Il est
compilé exclusivement dans `test-feature-task` ; la capture réelle et les
symboles production restent inchangés. Une régression directe prouve que la
charge synthétique 5 produit toujours `WAIT`, puis que la charge 0 produit
`START` : le seam contrôle la télémétrie, jamais la politique. La revue bornée
a ensuite identifié le second Governor créé par le helper runtime ; celui-ci
reçoit maintenant le même snapshot avant toute création de Queue, avec cleanup
explicite des échecs.
Après cette correction et sa revue, le test Feature complet passe **100/100**,
la matrice ordonnée Candidate→Matcher→Matcher pipeline→Feature **4/4**, la suite
portable finale **64/64** et la suite Vulkan finale **65/65**. Les exécutions
ciblées ASan/UBSan avec `detect_leaks=0` et TSan passent également. La
qualification LSan externe décrite ci-dessous reste inchangée. Une suite
normale mixte lancée après une reconstruction large a aussi observé un unique
timeout `task` ; le ciblé immédiat et la matrice de revue **100/100** passent.
Ce cas reste qualifié non reproductible/environnemental : ni le timeout ni le
comportement Task n'ont été modifiés.
### ASan, UBSan et LeakSanitizer
Le build frais Clang 22.1.8 `address,undefined`, portable/Vulkan désactivé, a
d'abord été exécuté avec détection de fuites : **57 OK, 6 FAIL, 1 TIMEOUT**.
Cette première passe n'est pas masquée :
- `feature-store`, `visual-index`, `candidate-pair-task`,
`precision-features` et `precision-consolidation` terminent chacun sur la
même fuite externe, exactement **3 808 octets en 68 allocations**, avec la
dernière frame `/opt/cuda/lib64/libOpenCL.so+0x2fd4`, Build ID
`b3217362255db6f1188e7596454ffe8bc4606b53`, résolue vers
`/opt/cuda/targets/x86_64-linux/lib/libOpenCL.so.1.0.0`, sans frame projet ;
- `feature-task` atteint une fois son attente adaptative interne de 30 s et
`task` le timeout Meson de 30 s, sans diagnostic ASan/UBSan/LSan.
Après cette attribution, la matrice complète repasse avec
`detect_leaks=0` mais ASan et UBSan toujours actifs : **64/64**, zéro timeout.
Une matrice séparée de vingt cibles dont `ldd` prouve qu'elles ne chargent ni
OpenCV ni OpenCL passe **20/20** avec LeakSanitizer actif. Les anomalies de
temps initiales ne s'étaient pas reproduites dans le suivi initial : 20/20
exécutions `feature-task` à affinité complète et 100/100 exécutions `task` avec
perturbations allocateur. Le cas Feature a été reproduit plus tard par la suite
portable ordonnée, puis déterminisé comme décrit ci-dessus ; le cas Task reste
non reproductible. La conclusion sanitizer exacte demeure : ASan/UBSan projet
**64/64**, chemins projet loader-free LSan **20/20**, limitation LSan du loader
OpenCL externe conservée explicitement — jamais un faux « LSan global 64/64 ».
### Concurrence
Le build frais GCC/G++ 16.2.1 TSan, volontairement Vulkan-disabled, passe les
quatorze cibles concurrentes Task/Project/Queue/Governor/SSD/TUI/Candidate/
Visual/Feature/Matcher/GV **14/14**, puis **220/220** répétitions déterministes,
soit **234/234** exécutions. `tests/tsan-opencv.supp` ne supprime que les races
issues des objets externes non instrumentés `libopencv_features.so`,
`libopencv_core.so` et `libtbb.so`; aucune frame Lardon3D n'est supprimée.
Cette preuve ne couvre pas Vulkan sous TSan : le backend réel est couvert par
le build/suite Vulkan ci-dessus et ses tests propres.
### Audit des avertissements
Le build Vulkan complet et ses cibles non-default ont produit 785 émissions :
117 dans des sources projet/tests/benchmarks et 668 dans les headers OpenCV
installés. L'audit n'a trouvé qu'une famille matérielle pour la frontière
publique courante : le narrowing Sparse `max_iterations`/
`minimum_inliers → int`, corrigé et revalidé comme décrit plus haut. Les
avertissements restants sont classés :
| Classe | Emplacements | Qualification |
| --- | --- | --- |
| Baseline production, non matériel pour cette maintenance | `src/acquisition_campaign_task.cpp:68`, `src/raw_development.cpp:185`, `src/acquisition_pairing.cpp:549`, `src/feature_extractor_opencv.cpp:231` | conversions sur valeurs déjà bornées par leur structure/validation ; aucune identité ni sortie modifiée dans ce ticket |
| Baseline géométrique bornée | `src/sparse_sfm_geometry.cpp:312-313` | index OpenCV signé sur les deux vecteurs construits avec le même nombre de colonnes ; distinct du narrowing public corrigé |
| Tests/benchmarks seulement | `tests/test_dense_mvs.cpp:45`, `tests/test_precision_consolidation.c:846-847`, shadows dans `tests/test_sparse_sfm_incremental.cpp:441-723`, API d'initialisation OpenCV dépréciée et indices signés dans `tests/benchmark_geometric_verifier.cpp:101-190` | aucun chemin production ni format durable |
| GCC TSan, non matériel | `src/feature_task.c:209-215`, `src/sift_task.c:170-175` | `-Wmaybe-uninitialized` interprocédural ; le callback fournit une Task non nulle et le helper initialise le contrôle avant toute autre sortie d'échec |
| Externe | headers OpenCV 5 installés ; découverte CMake Ceres | conversions/extensions et warnings de generator expressions hors sources Lardon3D |
Les catégories baseline ne sont pas promues en dette scientifique cachée :
elles sont consignées pour une maintenance ultérieure, tandis que le seul
narrowing pouvant accepter une valeur publique non représentable a été traité
avant la revue finale.
### Revue finale et clôture de la gate
Les tests focalisés TUI/runtime/SSD, les répétitions déterministes et la
validation F10 par providers factices restent acquis. La preuve GV finale
comprend 8/8 tests d'intégration focalisés, 60/60 répétitions de stress, 3/3
ASan/UBSan et 3/3 TSan ; les détails Project DB v23 sont consignés plus haut.
L'unique revue finale indépendante GPT-5.6 SOL/ULTRA a conclu **PASS**, avec
zéro finding bloquant. Elle a indépendamment exécuté le build portable, la
suite complète **64/64**, la matrice focalisée **15/15**, les probes strictes
GCC/Clang C17+C++17 **76/76** sur 19 headers publics modifiés/nouveaux, le
fixture ABI, les négatifs de symboles/strings des seams production, la
vérification du SHA-256 du manifest GV retenu et `git diff --check`.
Cette preuve indépendante ferme la gate sans effacer les qualifications
sanitizer consignées ci-dessus. L'ordre autorisé est désormais :
```text
science réelle acquise jusqu'à GV
→ GLOBAL_MAINTENANCE_AUDIT (PASS/FROZEN)
→ prochaine tranche séparée : poursuite réelle depuis Tracks
→ Sparse SfM réel / Dense selon calibration et roadmap
```
`scan3d/` et les contrats scientifiques Tracks/SfM/Dense restent protégés et
n'ont pas été modifiés par cette réconciliation.

View file

@ -8,7 +8,8 @@
Ce document décrit le contrat validé pour `features.extract`,
`features.extract.sift`, `visual_index.update`, `candidate_pair.generate` et
`matcher.run`. Le gel v1 porte sur le parallélisme interne borné, les
`matcher.run`, ainsi que la maintenance outer-parallel du
`geometric_verifier.run`. Le gel v1 porte sur le parallélisme interne borné, les
réservations Governor et les sorties canoniques. Il ne préjuge pas du choix
opérationnel CPU/Vulkan ajouté par la tranche v2.
@ -17,14 +18,18 @@ opérationnel CPU/Vulkan ajouté par la tranche v2.
La Task Queue conserve un seul callback actif. Une Task peut exploiter à
l'intérieur de ce callback les `cpu_threads` effectivement admis par le
Resource Governor. Ce parallélisme interne ne crée ni pool global, ni deuxième
scheduler, ni file de travail persistante. Le Governor dérive désormais un
masque privé depuis l'affinité permise et la topologie package/core Linux. Il
réserve des coeurs physiques complets, frères SMT inclus, en ordre décroissant
package/core. Sur l'hôte unrestricted validé, cela donne `0-5,8-13` pour le
calcul lourd et `6,7,14,15` pour le desktop, l'audio et l'interaction. Un masque
caller déjà borné à la capacité de calcul est repris sans seconde réserve ; si
topologie ou affinité manque, seul le budget portable est conservé et aucun ID
n'est inventé.
scheduler, ni file de travail persistante. Le Governor dérive un masque privé
depuis l'affinité permise et la topologie package/core Linux. La cible par
défaut réserve quatre CPU logiques lorsque praticable et conserve au moins un
CPU de calcul sur les petits hôtes. Avec une topologie fiable, un sous-ensemble
déterministe de coeurs physiques complets, frères SMT inclus, satisfait cette
cible avec le dépassement minimal. Les CPU exclus par une affinité externe
comptent déjà dans la réserve. Si topologie ou affinité manque, seul le budget
de compte portable est conservé et aucun masque/ID n'est inventé.
Sur l'hôte unrestricted 16-CPU validé, cette politique donne `0-5,8-13` pour le
calcul lourd et `6,7,14,15` pour le desktop, l'audio et l'interaction. Ces IDs
restent une preuve de cet hôte et ne bornent aucune autre machine.
L'unique worker Queue applique et vérifie son propre masque (`pid=0`) avant les
callbacks ; le creator/main/TUI reste inchangé et les enfants créés depuis le
@ -47,7 +52,8 @@ sortie partielle. `backend_info` et les requêtes vides restent non initialisant
Cette défense couvre les consumers publics ou de feasibility qui ne traversent
pas la Queue ; seuls leurs `main` autonomes peuvent prendre le défaut sûr avant
tout pthread.
Le compute-pool borne `cpu_threads` à l'admission. Un échec d'application est
Le compute-pool borne `cpu_threads` à l'admission ; il n'existe aucun plafond
global 12. Un échec d'application est
diagnostiqué sans altérer Task, Queue, durabilité ou science. Cette couture v2
est **PASS / FROZEN**, sans persistance ni ABI publique. Elle complète le gel
v1 sans le redéfinir.
@ -157,9 +163,10 @@ CPU/Vulkan, le fallback CPU complet et les diagnostics bornés sont
## Extraction OpenCV
Le processus conserve une limite OpenCV globale. Sous l'unique callback Queue,
RAW et Photo Quality appliquent/restaurent CPU1. ORB, SIFT et RootSIFT
Extraction appliquent/restaurent exactement le `cpu_threads` immutable admis
dans `1..min(12, compute_pool)`. Matcher applique OpenCV1 à l'intérieur de sa
RAW, Photo Quality et la campagne appliquent/restaurent leur CPU admis (CPU1
dans les estimations courantes). ORB, SIFT et RootSIFT Extraction déclarent le
maximum `int` positif accepté par OpenCV, puis appliquent/restaurent exactement
le `cpu_threads` immutable borné au compute-pool. Matcher applique OpenCV1 à l'intérieur de sa
propre fenêtre et utilise les participants Task admis autour de cette
primitive. La réservation vit pendant l'exécution bornée et aucun second pool
runtime n'est créé.
@ -167,12 +174,14 @@ runtime n'est créé.
Le nombre admis est une politique de ressources, pas une identité scientifique.
Les tests OpenCV 5.0.0 à 1/2/4/8/12 threads obtiennent les mêmes keypoints,
descripteurs et métriques ORB, SIFT et RootSIFT. Aucun fingerprint, format ou
schéma n'est modifié. Les utilisateurs imbriqués d'OpenCV ne peuvent pas faire
schéma n'est modifié ; 12 est une preuve de l'hôte courant, pas un maximum
portable. Les utilisateurs imbriqués d'OpenCV ne peuvent pas faire
varier cette configuration process-wide pendant une extraction. Le callback
assure donc qu'une admission CPU est réellement consommée, sans confondre cette
dimension avec le lot admis.
Le Governor slow-start les dimensions CPU validées selon `1/2/4/8/12`. Chaque
Le Governor slow-start les dimensions CPU réductibles par doubles depuis 1,
puis le maximum exact de la capacité, toujours borné au compute-pool. Chaque
palier utilise deux observations et exige au moins 5 % de débit durable en plus.
Un seul essai CPU ou lot est actif à la fois ; après plafonnement ou refus CPU,
un kind dont le lot est adaptable peut seulement alors explorer le lot. Les
@ -183,8 +192,8 @@ essai. Visual Index suit la même règle par
segment, tandis que Candidate et Matcher observent chaque séquence. Les formes
fixes restent fixes mais conservent un diagnostic d'admission Governor-owned.
Pour Candidate Generation, l'estimation demande jusqu'à douze threads CPU et
un slot I/O. Le callback de Queue compte comme un de ces threads ; il crée donc
Pour Candidate Generation, l'estimation demande jusqu'à soixante-quatre threads
CPU et un slot I/O. Le callback de Queue compte comme un de ces threads ; il crée donc
au plus `cpu_threads - 1` threads enfants. Tous les enfants sont joints avant
la fin de la séquence, la libération de réservation ou
`lardon3d_task_sequence_break()`.
@ -192,7 +201,7 @@ la fin de la séquence, la libération de réservation ou
## Calcul et publication Visual Index
Une séquence sélectionne le même préfixe durable d'au plus seize Feature Sets
qu'en mode sériel. Jusqu'à douze participants effectivement admis lisent les
qu'en mode sériel. Jusqu'à seize participants effectivement admis lisent les
Feature Files immuables ; chaque reader, son descripteur de fichier et sa
tranche de 256 descripteurs appartiennent à un seul participant. Aucun enfant
n'utilise le handle Project DB partagé.
@ -259,17 +268,22 @@ réelle soutenue doit attendre un état hôte qui admet répétitivement
Les sources sont les memberships durables du Visual Index, pagés en ordre
croissant de `feature_set_id`. Les IDs peuvent être clairsemés. Une fenêtre
contient au plus `2 * cpu_threads` sources et jamais plus de 24. Chaque résultat
contient au plus `2 * cpu_threads` sources et jamais plus de 64 ni du lot admis.
Chaque résultat
conserve au plus le top-K existant de 256 propositions ; l'estimation de Task
reste conservatrice à 256 Kio fixes et 64 Kio par item admis.
reste conservatrice à 256 Kio fixes et 8 Mio par item admis. Ce coût couvre la
pile enfant bornée de 4 Mio, le reader/Visual Index et le handle SQLite privé de
chaque participant.
Chaque participant possède son propre handle `project.db` pour les lectures et
le ferme avant de terminer. Le calcul parallèle charge le Feature Set source,
exécute exactement la requête Visual Index existante et forme les paires
canoniques en mémoire. Il ne publie rien.
La reprise normalise uniquement l'estimation Candidate v1 sérielle historique
de forme exacte vers la demande courante de douze participants. Cette
La reprise normalise uniquement les estimations Candidate historiques
complètes reconnues — CPU1/128 Kio fixes et CPU12/256 Kio fixes, toutes deux à
64 Kio par item — vers la demande courante CPU64/256 Kio fixes/8 Mio par item.
Cette
estimation effective reste privée et n'est pas checkpointée avant admission ; le Governor peut
toujours la réduire à son budget disponible. Le Task ID, le curseur typé, le
lot 164 et les paramètres scientifiques restent inchangés. Les autres kinds
@ -284,6 +298,39 @@ fingerprints et identités scientifiques ne changent pas. Le nombre de threads
ne peut donc modifier ni les identités persistées, ni leur ordre de publication,
ni leur cardinalité.
## Préparation et publication Geometric Verifier
La policy scientifique GV v3, les paramètres, le fingerprint, la seed, le
masque, la matrice et `cv::UsacParams::isParallel=false` restent FROZEN. Le
parallélisme courant est externe à l'estimator : une séquence sélectionne un
préfixe d'au plus seize Match Results indépendants et jusqu'à huit participants
utiles les préparent en parallèle. La fenêtre/participant a été validée sûre
jusqu'à seize, mais la demande production s'arrête à huit selon la preuve de
débit durable ci-dessous.
Chaque préparation possède ses handles de fichiers Match/Feature et son objet
opaque borné ; le handle Project DB partagé reste sérialisé par son mutex
interne. Les enfants ne publient jamais. Après toutes les jointures, le callback
Queue propriétaire publie seul dans l'ordre des parents, avance uniquement le
préfixe contigu et checkpoint avant toute libération de réservation à
`sequence_break`. Un échec de création partielle stoppe les nouvelles prises de
travail, joint tous les enfants créés et détruit exactement une fois chaque
préparation déjà produite.
La preuve représentative réelle porte sur 4 113 parents, dont 4 102
applicables : 578 vérifiés et 3 524 rejetés. Les IDs canoniques, toutes les
colonnes GVR et le digest scientifique
`9401ef6168804b6f1d51f4cdf64cd6b33cbebd2934e5294c8feacc87f9c8ce86`
sont identiques à CPU1/2/4/8/12. Les débits complets, incluant préparation,
publication ordonnée, curseur et checkpoint, valent respectivement
60,7514/83,9556/104,7545/116,6329/119,7606 parents/s. CPU12 n'ajoute que
2,68 % sur CPU8, sous le deadband Governor de 5 % : CPU utile maximal 8,
fenêtre CPU sûre 16 et batch maximal 16.
La preuve S21 complète historique (172 741 parents traversés, 172 275
applicables, 24 065 vérifiés et 148 210 rejetés) reste valide avec son chemin
CPU1/batch8 original. Elle n'est pas présentée comme un rerun du nouveau chemin.
## Échec, reprise et progression
Une erreur de calcul à la source `S` interdit toute publication de `S` et de

View file

@ -15,13 +15,11 @@ Task
Estimate
Governor
Task Queue
Reservation
Resource Governor / Reservation
Scheduler
Worker
callback worker admis
Résultat atomique
@ -44,8 +42,9 @@ des fichiers admissibles et maintenance d'un manifeste cohérent.
**Statut :** IMPLEMENTED
### Import Task
Premier type métier persistant. Il s'exécute par lots bornés dans le scheduler
générique, cible explicitement un ScanSet et peut être reconstruit puis repris.
Premier type métier persistant. Il s'exécute par lots bornés dans le runtime et
la Queue génériques, cible explicitement un ScanSet et peut être reconstruit
puis repris.
**Statut :** IMPLEMENTED
@ -56,12 +55,40 @@ mémoire depuis `manifest.tsv` reste une façade legacy pour la TUI.
**Statut :** IMPLEMENTED
### Profils optiques et calibrations
Project DB v23 ajoute un overlay générique distinct pour profils de boîtiers,
objectifs électroniques ou manuels, configurations optiques, affectations aux
campagnes/Captures et calibrations explicitement compatibles. La migration
n'infère ni ne backfill aucune donnée S21/A6000 ; ces appareils restent des
preuves de validation, pas des identités produit.
**Statut :** IMPLEMENTED — OVERLAY ADDITIF v23
### Image View
Vues triées et filtrées du catalogue pour la TUI. Ne modifie pas le
catalogue, le manifeste ou les images.
**Statut :** IMPLEMENTED
### TUI observatoire / centre de contrôle
Le thread principal possède ncurses, l'entrée et le rendu. Un modèle pur reçoit
des copies bornées de l'unique Queue et du Governor, coalescées autour d'une
seconde, et présente progression durable, ETA honnête, pipeline, ressources,
profils optiques et SSD. Les dimensions validées sont full ≥100×30, compact de
référence 72×20, minimum 60×15, puis le fallback `Terminal trop petit`. Les
couleurs s'accompagnent toujours de libellés textuels et `F10 SSD` reste visible
au minimum supporté.
Ouvrir, fermer ou changer de projet détruit/joint l'unique Queue avant de
fermer Project DB, puis recrée une Queue vide et rebranche l'observation. Les
ABI historiques Task/Resource/AppState/layout restent inchangées ; les vues
riches utilisent des structures et fonctions additives décrites dans
[Runtime](runtime.md).
**Statut :** CURRENT / VALIDATED OPERATIONAL
### Feature Store
Extraction ORB réelle par tâche persistante, Feature Sets logiques et assets
binaires content-addressed lisibles par plages bornées.
@ -104,10 +131,22 @@ adaptatifs, réservations opaques et historique borné de métriques.
**Statut :** IMPLEMENTED
### Contrôleur SSD externe optionnel
Frontière physique UDisks2 pour la paire de labels `LARDON_SWAP` et
`LARDON_SCRATCH`, avec identité Drive+UUID stable, état borné, leases scratch
et drain sûr. Il ne formate, ne répare ni ne force jamais l'hôte et ne remplace
pas le Resource Governor. La TUI exécute ses actions dans un seul thread
joinable et le Governor enregistre l'état physique puis orchestre seul les
leases scratch de production. Les quatorze Task kinds courants n'en consomment
aucun ; capacité visible ne signifie donc pas usage.
**Statut :** CURRENT / VALIDATED OPERATIONAL
### Candidate Pair
Sous-système de génération et persistance de paires d'images candidates pour
le matching géométrique. Répond uniquement à « quelles paires valent la
peine d'être试探ées ? » sans validation géométrique.
le matching géométrique. Répond uniquement à « quelles paires valent la peine
d'être explorées ? » sans validation géométrique.
**Statut :** IMPLEMENTED
@ -165,7 +204,7 @@ frontière de séquence connue.
## Invariants fondamentaux
- Aucun callback de tâche n'est lancé sans réservation active validée.
- Le scheduler ne décide jamais des ressources.
- La Queue ne décide jamais des ressources.
- Le Resource Governor est l'unique propriétaire des budgets.
- Les réservations sont libérées exactement une fois.
- ncurses appartient exclusivement au thread principal.
@ -174,7 +213,7 @@ frontière de séquence connue.
## Limites actuelles
- File à worker unique avec FIFO strict.
- File à worker unique avec FIFO stable et bypass des seuls `WAIT` ressources.
- Absence de DAG de dépendances.
- Absence de priorités.
- Absence de pools de workers multiples (CPU/GPU/IO).

View file

@ -2,7 +2,10 @@
## Vision
Lardon3D doit stocker les métadonnées de reconstruction dans une base de données persistante légère, probablement SQLite, tandis que les données numériques massives restent dans des fichiers/binaires adaptés.
Lardon3D stocke les métadonnées de reconstruction dans Project DB SQLite,
tandis que les données numériques massives restent dans des fichiers/binaires
adaptés. Le schéma courant est v23 ; les sections v7 ci-dessous documentent la
fondation historique sans prétendre être la tête de migration.
## Principes fondamentaux
@ -37,6 +40,10 @@ Les éléments suivants sont des concepts de domaine, PAS des tables SQL imposé
- track
- observation
- camera
- camera_body_profile
- lens_profile
- optical_configuration
- optical_calibration_profile
- pose
- point3d
- reconstruction_layer
@ -108,7 +115,7 @@ par le `time_t` local avant conversion. Le format reste donc lisible entre
plateformes uniquement pour les valeurs communes à leurs domaines `size_t` et
`time_t`.
## Project Database v7
## Project Database v7 — fondation historique
SQLite contient l'état logique interrogable et les références aux fichiers ;
les checkpoints et artefacts volumineux restent externes. L'enregistrement du
@ -200,8 +207,9 @@ Les records sont parcourus par task ID croissant. Un checkpoint
conserve cet avertissement jusqu'au prochain checkpoint durable. Une tâche
terminale n'appartient pas à la requête de reprise.
**NOT_YET_WIRED** — autosave complet, réconciliation des fichiers orphelins et
retry piloté par l'utilisateur pour les sources indisponibles.
**NOT_YET_WIRED** — réconciliation des fichiers orphelins et retry piloté par
l'utilisateur pour les sources indisponibles. Les checkpoints existants sont
kind-owned aux frontières métier ; aucun timer générique ne peut les remplacer.
**NOT_YET_WIRED** — migration de la vue TUI en mémoire vers la pagination
SQLite, scrub des assets et réconciliation globale des orphelins.
@ -212,4 +220,25 @@ transactionnellement à ses métadonnées SQLite après publication.
**IMPLEMENTED** — Visual Index externe segmenté, memberships transactionnels
et tâche `visual_index.update` récupérable.
**PLANNED** — migrations v7+ et reprise avec dépendances.
**IMPLEMENTED** — migrations additives et séquentielles jusqu'à Project DB
v23. Les versions v16 à v22 restent l'histoire scientifique et de persistance
gelée ; v23 ajoute uniquement l'overlay optique générique.
Les neuf relations v23 séparent profils de boîtier et alias, profils d'objectif
et alias, configurations optiques, affectations de configuration aux groupes
de campagne et aux Captures, profils de calibration et sélection explicite par
Capture. Une configuration référence exactement un boîtier et un objectif ; un
objectif manuel sans EXIF est normal. La compatibilité d'une
calibration est exacte sur la configuration optique et ses dimensions/champs
scientifiques. Aucun profil S21, A6000 ou Meike n'est inséré ou déduit par la
migration : les tables nouvelles restent vides tant qu'un caller ne fournit
pas explicitement les données.
La migration v22→v23 est une transaction additive. Elle ne réinterprète ni les
Captures, ni les Images, ni les résultats scientifiques historiques. Une copie
S21 et une copie A6000 ont atteint v23 avec intégrité et clés étrangères
valides, comptes scientifiques inchangés et tables optiques vides. Les détails
normatifs sont dans [Project Database](project_database.md).
**NOT_YET_WIRED** — reprise ordonnée par dépendances/DAG et réconciliation
globale des artefacts orphelins.

View file

@ -56,9 +56,10 @@ validé. Après crash, la registry statique reconstruit le même `task_id`; l'im
est recommencée, sans micro-checkpoint trompeur. Aucun Feature Set partiel ne
devient READY. Le SHA-256 de l'Image Asset géré est vérifié avant décodage.
L'estimation SIFT demande jusqu'à douze threads CPU. Le réglage OpenCV du
démarrage est une baseline/plafond sûre ; pour chaque séquence, l'unique
callback Queue applique temporairement le compte immuable admis dans `1..12`,
L'estimation SIFT publie la capacité sûre de l'API OpenCV, `INT_MAX`; le
Governor la borne toujours au compute-pool réel. Le réglage OpenCV du démarrage
est une baseline sûre ; pour chaque séquence, l'unique callback Queue applique
temporairement le compte immuable admis dans `1..compute-pool`,
le vérifie puis restaure la baseline sur toute sortie. Une mutation process-wide
concurrente par plusieurs workers n'est pas supportée. La tâche réserve aussi
un slot IO, aucun GPU et environ 1,06 Gio structurels :
@ -67,8 +68,8 @@ zéro. Le Resource Governor admet donc le fan-out OpenCV exact ; la Queue
conserve un seul callback actif et ne superpose pas un second pool SIFT.
Le nombre de threads reste opérationnel et absent du fingerprint SIFT/RootSIFT.
La reprise accepte la forme CPU12 courante et normalise uniquement l'ancienne
forme CPU1 complète et exacte dans la copie privée restaurée. Elle ne publie
La reprise accepte les formes historiques CPU12 et CPU1 complètes et exactes,
et les normalise vers la demande portable dans la copie privée restaurée. Elle ne publie
aucun checkpoint d'estimation seule ; une forme voisine est rejetée.
L'audit contrôlé OpenCV 5.0.0 à 1, 2, 4, 8 et 12 threads compare exactement le
count, l'ordre et les valeurs binary32 des champs keypoint persistés, les
@ -88,8 +89,8 @@ après `imread`.
La build OpenCV/TBB système n'est pas instrumentée par TSan et rapporte ses
propres accès de teardown TLS/`cv::Mat`. Les tests TSan suppriment uniquement
les frames des objets partagés `libopencv_core.so` et
`libopencv_features.so`; les frames Lardon3D et toutes les autres bibliothèques
restent contrôlées.
`libopencv_features.so`, ainsi que le runtime externe `libtbb.so`; les frames
Lardon3D et toutes les autres bibliothèques restent contrôlées.
## Consolidation intra-image
@ -153,6 +154,9 @@ maximum 8192, les totaux sont 15,52 GB (14,45 Gio) de descriptors et 16,25 GB
images ne sont jamais chargées ensemble.
Le Visual Index v1 reste exclusivement ORB-LSH. Candidate Pair Generator,
matching production, vérification géométrique, tracks et SfM sont **PLANNED**.
Matcher ORB/SIFT/RootSIFT, Geometric Verifier v3, Track Model/Builder et Sparse
SfM Gates CG sont désormais **IMPLEMENTED / PASS-FROZEN à leurs frontières
respectives**. Le pipeline dense complet, mesh, texturing et viewer restent
futurs ; MVS-M1 ne fournit que sa frontière externe bornée FROZEN.
ALIKED est **PLANNED / BLOCKED ON MODEL PROVENANCE + VALIDATED ONNX EXPORT** :
aucun code ALIKED, ONNX ou Python production n'appartient à v1A.

View file

@ -1,5 +1,76 @@
# Base de données projet Lardon3D
## Contexte optique générique — Project DB v23
**IMPLEMENTED / VALIDATED / REVIEWED.** La tête courante v23 est une migration
transactionnelle additive au-dessus de la fondation scientifique et de
persistance v22 **PASS / FROZEN**. Elle crée neuf tables sans aucune ligne :
`camera_body_profiles`, `camera_body_aliases`, `lens_profiles`,
`lens_profile_aliases`, `optical_configurations`,
`acquisition_campaign_group_optics`, `capture_optical_configurations`,
`optical_calibration_profiles` et `capture_calibration_selections`.
Boîtier, objectif, configuration optique et calibration sont quatre identités
distinctes. Une configuration lie exactement un boîtier, un objectif et une
focale entière optionnelle en micromètres. Un objectif manuel sans EXIF est un
cas normal : son profil explicite ne requiert aucun alias metadata. Le fixture
Meike vérifie ce chemin générique ; il ne crée aucun branchement produit par
marque ou modèle. Les alias make/model, lorsqu'ils existent, sont des couples
exacts `BINARY`, jamais une recherche fuzzy, une correction de casse ou une
preuve d'identité implicite.
Une campagne peut assigner une configuration différente à chaque groupe avant
sa matérialisation. Lorsque S3-E retourne le Capture, la même transaction
retient la correspondance groupe→Capture, copie l'affectation optique avec sa
provenance campagne et avance le curseur. Un rollback tardif annule les trois
mutations. Une affectation explicite post-import est possible seulement pour un
Capture encore non affecté ; l'absence reste `NOT_FOUND` et ne crée pas de
profil « inconnu ».
Un profil de calibration référence une calibration sparse immuable existante et
une seule configuration exacte. Il ne copie, recalcule, interpole ni fabrique
aucun coefficient. Les listes compatibles filtrent uniquement cette identité
exacte. La sélection sur Capture est toujours explicite et exige la même
configuration des deux côtés ; plusieurs profils compatibles restent ambigus
jusqu'à cette sélection. Retry exact et naturel converge, un conflit durable
valide retourne `CONSTRAINT`, tandis qu'une ligne ou dépendance persistée
malformée retourne `CORRUPT` avant comparaison avec une demande alternative.
La migration v22→v23 n'inspecte ni EXIF, chemin, basename, SHA-256, dimensions,
nom d'appareil, ni calibration historique. Elle ne réinterprète donc aucune
Capture, image ou calibration v16v22. Le marqueur `schema_version=23` est le
point de publication durable ; un échec de création ou un update de marqueur
qui ne cible pas exactement la v22 rollbacke toute la migration.
La validation a migré via l'API production des copies des projets réels S21 et
A6000 : version 23, `integrity_check` et clés étrangères propres, comptes et
lignes scientifiques inchangés, neuf tables optiques vides et SHA-256 des
sources inchangés. La suite normale 57/57, dix tests focalisés, cinq cibles
ASan/UBSan, les contrôles de headers C17/C++ et la revue indépendante après
correction sont acquis. Cette preuve porte sur l'overlay et sa compatibilité ;
elle ne crée aucune calibration scientifique pour ces campagnes.
### Workflow TUI optique courant
La TUI v23 appelle exclusivement les API publiques bornées ; elle ne modifie
pas directement SQLite. Elle peut inspecter un Capture, rechercher des alias
metadata par égalité exacte, parcourir par pages de 16 les boîtiers, objectifs,
configurations et calibrations compatibles, créer un nouveau profil ou une
nouvelle configuration immuable, puis affecter explicitement un groupe de
campagne ou un Capture lorsque le contrat ci-dessus l'autorise. Un objectif
Meike manuel sans électronique ni alias reste une donnée normale, et un lookup
sans correspondance demeure unresolved.
La sélection d'une calibration reste explicite et exige l'exacte configuration
du Capture. Une absence, plusieurs candidats, une incompatibilité, `BUSY`, I/O
ou corruption sont présentés comme tels ; la TUI ne fabrique jamais de profil
« inconnu », de compatibilité ou d'identité. `R` permet un retry explicite après
un échec de bind/chargement. `[` revient à la première page et `]` charge la
suivante ; chaque libellé annonce seulement le compte de la page courante et
l'existence exacte d'une suite. Le modèle TUI emprunte la DB jusqu'à unbind et
doit être détaché avant la fermeture projet, conformément au
[runtime](runtime.md#observatoire-tui-actuel).
## Snapshot d'exécution scientifique sélectionnée — Project DB v22
**PASS / FROZEN.** La migration additive v21→v22 ajoute
@ -76,7 +147,9 @@ avancement de `k` vers `k+1` après publication du résultat `k`, et valeur term
Un éventuel `group_index = group_id - 1` reste strictement privé à l'exécuteur. Résultat et
curseur sont atomiques avant le checkpoint Task générique. Les lectures valident les types,
signes, bornes et relations dans les valeurs SQLite 64 bits avant toute conversion vers
les champs C étroits. Recommandation mesurée et override humain restent distincts.
les champs C étroits et exigent le dispatch générique exact
`photo_quality.triage/v1`. Recommandation mesurée et override humain restent
distincts.
## Capture / Asset Provenance v1 — Project DB v19 (historique FROZEN)
@ -189,7 +262,7 @@ mutation de campagne complète n'a été effectuée.
Restent hors S3 : TUI, scrub d'assets et réconciliation des orphelins, ainsi
que vidéo et reconstruction aval lorsqu'ils sont applicables. L'identité
opérationnelle persistante de campagne, son exécution par Task/Scheduler/Governor
opérationnelle persistante de campagne, son exécution par Task/Queue/Governor
et sa reprise sont définies par Project DB v20 ci-dessous ; ils ne modifient pas
le contrat S3.
@ -205,9 +278,12 @@ existante ; un échec de migration laisse la v19 complète et utilisable.
`task_id`, qui est l'identité opérationnelle de la campagne. La création et les
checkpoints enregistrent dans une même transaction le snapshot Task générique,
sa référence de checkpoint et le record typé de campagne. Ce record conserve le
`scanset_id`, le curseur `next_group_id`, le nombre de groupes et une requête
immuable : un upsert ne peut faire progresser le curseur que si ScanSet, nombre
de groupes et octets de requête sont identiques.
`scanset_id`, le curseur historique `next_group_id`, le nombre de groupes et
une requête immuable. Pour cette table de campagne, malgré son nom historique,
le curseur est une position de prochain travail à base zéro dans `0..N` : sa
valeur est le nombre de groupes one-based déjà retenus. Un upsert ne peut le
faire progresser que si ScanSet, nombre de groupes et octets de requête sont
identiques.
La requête v1 est un codec borné, déterministe, à magic/version explicites et
champs entiers de largeur fixe little-endian. Elle sérialise les sources, leurs
@ -219,11 +295,15 @@ et ne confèrent que la basis `CALLER_EXPLICIT`, jamais une inférence `STRONG`.
Les groupes de plan gardent leurs IDs un-based. Pour chaque groupe achevé,
`acquisition_campaign_captures` conserve la relation unique
`(task_id, group_id) → capture_id` et interdit qu'un même Capture corresponde à
deux groupes de la même tâche. Après le retour de S3-E, une transaction unique
retient cette relation et avance `next_group_id`, avant la progression générique
et le checkpoint. La reprise peut alors transmettre le `capture_id` retenu à
S3-E, sans réhoming ni inférence depuis un chemin, SHA, basename, timestamp,
métadonnée ou `image_id`.
deux groupes de la même tâche. L'état durable valide contient exactement le
préfixe de mappings `1..next_group_id`, aucun trou et aucun mapping en avance ;
chaque Capture existe dans le ScanSet de la campagne. Après le retour de S3-E,
une transaction unique retient la relation du groupe courant et avance ce
curseur, avant la progression générique et le checkpoint. La reprise peut alors
transmettre le `capture_id` retenu à S3-E, sans réhoming ni inférence depuis un
chemin, SHA, basename, timestamp, métadonnée ou `image_id`. Une storage class,
borne, dispatch kind/version ou relation durable malformée retourne `CORRUPT`
avant toute mutation et ne fournit jamais un faux `resume_capture_id`.
Chaque séquence matérialise un seul groupe. Pause et annulation restent
coopératives aux frontières de groupe ; après un groupe non terminal,
@ -291,9 +371,9 @@ exactes conservent leur sémantique.
## Gate F — décision historique Project DB v17
**PASS / FROZEN.** La migration historique v15→v16 et le
modèle de reconstruction Sparse SfM v16 restent inchangés. Gate F fait avancer
la tête de schéma à v17 par une migration strictement additive contenant une
seule table métier de tâche : `sparse_sfm_tasks`.
modèle de reconstruction Sparse SfM v16 restent inchangés. Gate F a fait
avancer la tête de schéma alors courante à v17 par une migration strictement
additive contenant une seule table métier de tâche : `sparse_sfm_tasks`.
Cette table suit le modèle des autres Task Kinds durables : une ligne par
`task_id`, clé primaire et clé étrangère vers `tasks(task_id)` avec suppression
@ -326,8 +406,10 @@ persistée exactement ; seule une composante de graphe non reconstruite est
omise. Les comptes et métriques globaux décrivent exclusivement la géométrie
effectivement persistée. Aucun placeholder ni table diagnostique n'est ajouté.
> Version courante : **v20**. La migration transactionnelle additive v19→v20
> ajoute les records de campagne durable décrits ci-dessus sans modifier les
> Version publiée par cette section historique : **v20** ; la tête actuelle
> est v23 comme défini en ouverture du document. La migration transactionnelle
> additive v19→v20 ajoute les records de campagne durable décrits ci-dessus
> sans modifier les
> tables ni identités Capture/Asset de v19. La migration transactionnelle
> v18→v19 ajoute la fondation Capture/Asset Provenance sans modifier les tables
> scientifiques historiques. La migration transactionnelle v17→v18 ajoute le
@ -924,23 +1006,27 @@ fermeture/réouverture et payload corrompu.
## Ouverture et migrations
Une DB vide reçoit la chaîne de schémas jusqu'à v15 dans une transaction
Une DB vide reçoit la chaîne de schémas jusqu'à v23 dans une transaction
`BEGIN IMMEDIATE`. Une DB v1 reçoit transactionnellement les colonnes nullable
`task_kind` et `task_kind_version`, puis les migrations v2→v3. Les anciennes lignes restent
`NULL/NULL`, sans type inventé et sans perte des projets, tâches, checkpoints ou
artefacts. Une interruption ou erreur provoque un rollback complet. Les DB v1
à v14 sont migrées séquentiellement vers v15.
à v22 sont migrées séquentiellement vers v23.
Une v10 publiée est validée comme telle avant que v10→v11 crée
`matcher_tasks` ; son absence n'est donc pas une corruption. Une version future est refusée et une DB contenant
des tables sans métadonnée de version est considérée corrompue. La fonction
interne de migration applique uniquement la chaîne séquentielle connue jusqu'à
v15 ; une valeur hors de 1..15 est refusée. La failure injectée v12 rollbacke
v23 ; une valeur hors de 1..23 est refusée. Les failures injectées de chaque
étape rollbackent les objets et le marqueur de version de cette étape. Par
exemple, la failure v12 rollbacke
la table, l'index et le changement de version, laissant une vraie v11 utilisable.
La failure injectée v13 conserve une vraie v12 sans `geometric_verifier_tasks` ;
un retry applique ensuite v12→v13. La failure injectée v14 conserve une vraie
v13 sans `track_sets` ; un retry applique ensuite v13→v14. La failure injectée
v15 conserve une vraie v14 sans `track_builder_tasks` ; un retry applique
ensuite v14→v15.
ensuite v14→v15. Le même contrat couvre v16→v22 ; en v23, aucune des neuf
tables optiques ni aucun marqueur v23 ne subsiste après la failure injectée, et
un retry exact converge depuis la v22 intacte.
Migration v1→v2 exacte, exécutée entre `BEGIN IMMEDIATE` et `COMMIT` :
@ -1066,7 +1152,7 @@ l'attente. Il choisit le canonique codec/version valide dont les champs du
résumé DB correspondent exactement. Ce choix est prioritaire et ignore une
`.next` périmée ou corrompue. Si le canonique ne correspond pas, une `.next`
codec/version valide dont le même résumé correspond est promue sous le verrou
puis utilisée. L'absence de `.next` reste normale pour un projet v22 legacy
puis utilisée. L'absence de `.next` reste normale pour un projet v22 ou antérieur
contenant seulement `.chk`; toute autre absence, corruption, version future ou
divergence classe seulement la tâche comme non récupérable.
@ -1089,9 +1175,17 @@ ouvert.
## Statut
**PASS / FROZEN** — SQLite système, schéma v20 et migrations séquentielles
v1→v2→v3→v4→v5→v6→v7→v8→v9→v10→v11→v12→v13→v14→v15→v16→v17→v18→v19→v20, identité projet, transactions
tâche+checkpoint, pagination de reprise et artefacts génériques.
**IMPLEMENTED / VALIDATED / REVIEWED** — SQLite système, tête de schéma v23 et
migrations séquentielles v1→…→v23, identité projet, transactions
tâche+checkpoint, pagination de reprise, artefacts génériques et overlay
optique additif. Les contrats scientifiques et de persistance v16v22 conservent
leurs statuts historiques PASS / FROZEN.
**IMPLEMENTED / VALIDATED / REVIEWED — Project DB v23** : profils boîtier et
objectif data-driven, alias exacts optionnels, configurations multiples par
campagne/Capture, profils de calibration exactement compatibles et sélection
explicite. La migration ne backfill aucune identité ; les projets v22 restent
récupérables et scientifiquement inchangés.
**PASS / FROZEN** — Project DB v20 : migration additive
v19→v20, record typé de campagne lié au Task ID, requête immuable bornée,

View file

@ -38,7 +38,9 @@ vagues et relations géométriques entre acquisitions restent planifiées.
| **Asset** | Le contenu physique est identifié par `asset_id`, SHA-256, taille et chemin géré. Plusieurs images logiques peuvent partager cet asset. |
**Statut :** IMPLEMENTED v1 — catalogue SQLite paginé et assets
content-addressed. Les états Feature/Matching/Reconstruction restent planifiés.
content-addressed. Feature Sets, Match Results, GVR, Tracks et Sparse
Reconstruction sont désormais persistés par leurs relations spécialisées ; ils
ne deviennent pas pour autant un champ d'état mutable de l'image catalogue.
---
@ -69,7 +71,8 @@ descriptors.
| **Proximité temporelle comme signal secondaire** | Lorsque les images portent un horodatage EXIF, la proximité temporelle sert de signal complémentaire au contenu visuel, mais ne remplace jamais l'analyse visuelle. |
**Statut :** IMPLEMENTED v1 — LSH binaire déterministe, segments immuables,
updates incrémentales et query top-K bornée. Le générateur de paires reste planifié.
updates incrémentales et query top-K bornée. Le générateur de paires en aval est
également implémenté et conserve sa propre identité/persistance.
---
@ -405,7 +408,7 @@ Ces invariants s'appliquent à toutes les étapes du pipeline :
points 3D existants ne jamais supprimés implicitement. Toute
suppression est une action explicite et traçable.
4. **Le scheduler ne décide jamais des ressources.**
4. **La Queue/runtime ne décide jamais des ressources.**
Seul le Resource Governor arbitre les budgets, les lots et les
réservations.

View file

@ -13,11 +13,12 @@ desktop. Les signaux d'admission combinent `MemAvailable`, charge CPU, PSI CPU,
PSI mémoire, PSI I/O et deltas `pswpin`/`pswpout`. Un seuil dépassé empêche une
nouvelle admission ; il ne rompt pas une réservation saine déjà active.
Le Governor maintient trois zones. GREEN emploie le lot adapté normal. La soft
floor RAM, un PSI au seuil ou un premier intervalle avec swap actif produit
YELLOW et interdit toute croissance. Deux observations de pression
consécutives, ou `MemAvailable` sous la hard floor, produisent RED et suspendent
toute admission. Le premier snapshot swap établit seulement la baseline.
Le Governor maintient trois zones. GREEN emploie le lot adapté normal. La zone
de prudence RAM entre 3 et 4 Gio, un PSI au seuil ou un premier intervalle avec
swap actif produit YELLOW et interdit toute croissance. Deux observations de
pression consécutives, ou `MemAvailable` sous la réserve dure de 3 Gio,
produisent RED et suspendent toute admission. Le premier snapshot swap établit
seulement la baseline.
La récupération possède deux phases distinctes : trois observations saines
font `RED → YELLOW`, puis trois nouvelles observations saines font
@ -43,8 +44,11 @@ observe maintenant le RSS/HWM courant dans un buffer borné, uniquement comme
diagnostic du processus : il ne le confond ni avec la réservation Task ni avec
un coût attribuable. Le modèle cible un hôte Linux natif non contraint ; cgroups,
limites systemd/RLIMIT, multi-GPU, historique/monitoring RSS long terme,
redimensionnement d'admission depuis le RSS, scratch et stockage externe restent
différés.
redimensionnement d'admission depuis le RSS et consommation Task du scratch
restent différés. Le contrôleur SSD optionnel gère le cycle de vie physique ;
son état est enregistré auprès du Governor, qui est l'unique orchestrateur des
leases scratch de production. Aucun des quatorze kinds actuels ne les consomme,
et ni le registre, ni les leases, ni le swap ne créent un budget RAM.
## Feature Extraction
@ -53,9 +57,10 @@ publication Feature Store, métadonnées DB, checkpoint terminal et libération
buffer. Le batch vaut donc une image et la granularité de reprise est une image.
Le worker unique et la file bornée fournissent la backpressure actuelle.
Le démarrage configure une baseline et un plafond OpenCV sûrs avant la création
de Queue. L'unique callback lourd applique ensuite temporairement le compte CPU
immuable admis pour sa séquence, dans `1..12`, et restaure la baseline sur toute
Le démarrage configure une baseline OpenCV issue du compute-pool réellement
disponible avant la création de Queue. L'unique callback lourd applique ensuite
temporairement le compte CPU immuable admis pour sa séquence, dans
`1..compute-pool`, et restaure la baseline sur toute
sortie, y compris après une mutation suivie d'un échec de vérification. La tâche
réserve donc le nombre réellement appliqué au lieu d'annoncer artificiellement
un thread pendant qu'une primitive interne en utilise davantage. Une mutation
@ -83,11 +88,15 @@ recalcule pas les descripteurs.
## Geometric Verification
Project DB v12 stocke un résultat borné à 1024 octets de masque et neuf
binary64. `geometric_verifier.run` v1 traite un Match Result atomique, publie
par une transaction courte puis checkpoint son curseur par lots 1/2/4/8 avant
binary64. `geometric_verifier.run` v1 traite chaque Match Result comme unité
scientifique atomique, publie par une transaction courte puis checkpoint son
curseur par lots 1..16 avant
`task_sequence_break()`. Sa ligne durable appartient à Project DB v13. Le job
réserve un CPU logique et 4 Mio, sans slot GPU. Admission, pression, lots et
slow-start restent exclusivement décidés par Runtime et Governor.
peut employer jusqu'à huit participants utiles et seize participants sûrs, par
lots au plus seize, avec 8 Mio par parent et sans slot GPU. Le propriétaire
publie le préfixe canonique dans l'ordre. Admission, pression, lots et
slow-start restent exclusivement décidés par Runtime et Governor ; l'USAC
scientifique interne conserve `isParallel=false`.
## GPU et files
@ -110,12 +119,13 @@ reste inchangée.
## Profil interactif 8845HS mesuré
- budget CPU Lardon3D : 12 threads logiques, 4 réservés au desktop ;
- réserve `MemAvailable` : un quart de la RAM, environ 3,8 Gio ;
- hard floor `MemAvailable` : un huitième, environ 1,9 Gio ;
- budget CPU Lardon3D observé : 12 threads logiques, 4 réservés au desktop ;
- réserve dure `MemAvailable` : 3 Gio ;
- zone de prudence `MemAvailable` : de 3 à 4 Gio ;
- Feature workers : 1 ; batch : 1 image ;
- Matcher workers : 1 ; lots adaptatifs 1, 2, 4 ou 8 Candidate Pairs ;
- Geometric Verifier workers : 1 ; lots adaptatifs 1, 2, 4 ou 8 Match Results ;
- Geometric Verifier : un callback propriétaire, jusqu'à 8 participants utiles,
lots adaptatifs jusqu'à 16 Match Results ;
- profondeur de la Task Queue : 64 tâches légères, un seul callback actif ;
- PSI CPU avg10 : nouvelle admission suspendue à 20 % ;
- PSI mémoire avg10 : nouvelle admission suspendue à 1 % ;
@ -134,10 +144,13 @@ une distribution de latence estimator-only.
## Limites
Le profil maximal explicite et les pools multi-workers restent hors périmètre.
Les pools multi-workers restent hors périmètre. La capacité CPU portable est
désormais bornée par le compute-pool de l'hôte et la limite intrinsèque du kind,
jamais par un plafond global 12.
SIFT/RootSIFT et Feature Extraction Vulkan restent hors de ce contrat.
Swap, zram et disque externe ne sont jamais ajoutés au budget RAM. Aucun chemin
scratch/spill ni aucune action modifiant l'hôte n'appartient à Gate G core.
scratch/spill Task n'appartient à Gate G core. La commande SSD optionnelle est
une intégration opérationnelle additive, pas une admission scientifique.
La validation B3 du modèle Sparse SfM v16 a utilisé des processus frais, un
fixture synthétique de 100 000 landmarks et 500 000 observations, cinq passes

View file

@ -5,10 +5,14 @@
**ACCEPTED** — architecture decision for the post-Gate C documentation freeze.
Current Sparse SfM gates A through G are **PASS / FROZEN**.
Project Database: current schema **v22**; historical v16 remains frozen.
Project Database: current schema **v23**; the v22 foundation and historical
v16 science remain frozen.
This record is normative for the current architecture. It does not introduce
an implementation, a public API, a persistence format or a roadmap commitment.
This record is normative for the separation of responsibilities. Its original
Gate C/G decision introduced no implementation or syntax. Later explicitly
authorized additions, including the additive Project DB v23 optics overlay and
the bounded UDisks2 SSD controller, must remain on their own side of this
boundary and do not create a generic Resource System.
## Context
@ -24,7 +28,8 @@ resolver, runtime cache, resource handle or runtime dependency graph.
### 1. Persistent identity and metadata
Project Database v16 owns logical identities and relations. SQLite IDs identify
Project Database v23 owns current logical identities and relations; v16 remains
the historical Sparse SfM persistence boundary. SQLite IDs identify
projects, ScanSets, images, assets, Feature Sets, results, tracks and Sparse
SfM records. SHA-256 values identify published file contents. Metadata stores
sizes, states, provenance and project-relative artifact paths.
@ -55,7 +60,10 @@ generic unload operation or generic dependency lifetime.
The Resource Governor owns policy decisions for RAM, GPU, CPU and IO budgets.
It consumes immutable task estimates, samples system pressure, chooses bounded
lots and creates opaque transient reservations. A task must hold an active
reservation before executing its callback and releases it exactly once.
reservation before executing its callback and releases it exactly once. The
later additive SSD integration also makes it the sole production orchestrator
for scratch leases, without adding scratch to RAM admission or to the Task
estimate.
The Governor does not own persistent IDs, resolve paths, validate file formats,
load application objects, cache data or manage semantic dependencies.
@ -88,11 +96,13 @@ System by this decision.
## Resource Governor boundary
The Governor remains resource-type agnostic. It answers whether an operation
fits current budgets; it does not answer what an asset is or where it lives.
Tasks create estimates, request reservations, execute bounded work and release
reservations. The Governor owns only policy, budgets, pressure, lots and
reservations.
The Governor remains independent of scientific/artifact identity. It answers
whether an operation fits current budgets; it does not answer what an asset is
or where it lives. Tasks create estimates, request reservations, execute
bounded work and release reservations. The additive external-storage registry
copies physical capacity/state and arbitrates process-local scratch leases, but
does not resolve paths, own files, persist device identity or add SSD/swap to
RAM.
### Task demand declaration versus admission policy
@ -169,9 +179,12 @@ available_ram = max(
)
```
The implementation retains its existing checked, saturating subtraction order.
The host reserve remains one quarter of physical RAM and the emergency floor
remains one eighth of physical RAM.
The implementation retains its checked, saturating subtraction order. On a
capable host, the default hard/admission reserve is approximately 3 GiB of
`MemAvailable`; the 34 GiB interval is a non-escalating caution band and does
not subtract 4 GiB from capacity. Smaller hosts use a deterministic fractional
reserve so at least bounded work remains possible. A custom policy can still
set explicit reserve/floor values.
Possible overlap between `MemAvailable` and already-materialized memory from an
active reservation is accepted. The deliberate bias is false `WAIT`, not
overcommit. Gate G adds no RSS tracking, materialization state, allocation
@ -215,17 +228,43 @@ topologies are deferred.
#### Scratch, persistence and science
Scratch and external-storage support is excluded from Gate G core. It adds no
scratch estimate fields, demand/availability API, manager, allocator, removable
media monitor, mount logic, capacity policy, spill contract, cleanup contract,
project scratch path or SSD detection. Sparse SfM has no scratch consumer,
spill algorithm or out-of-core path. Swap, zram and external SSD capacity never
Scratch demand remains excluded from Gate G core: no scratch field was added to
`Lardon3DResourceEstimate`, and Sparse SfM still has no scratch consumer, spill
algorithm or out-of-core path. Swap, zram and external SSD capacity never
enlarge scientific RAM admission.
No automatic `swapon`, `swapoff`, mount, unmount, formatting, partitioning or
destructive cleanup is authorized. A future feature requires a real task
consumer, an explicit task-specific contract, user opt-in where appropriate and
a separate architecture review. No dormant generic API is introduced.
An explicitly authorized, bounded SSD controller owns physical discovery and
lifecycle through UDisks2/GDBus: exact labels, stable Drive+filesystem UUID
identity, fixed-capacity low-level scratch capabilities and safe drain. It is
not a scheduler, Governor, Task admission path, generic allocator or project
persistence layer. It never formats, partitions, repairs, powers off, forces
swap removal or performs destructive cleanup. No code outside that reviewed
controller may add ad-hoc `swapon`, `swapoff`, mount/unmount or shell control.
The controller's validated bounded snapshot is registered with the Governor.
The Governor is the only production entry for scratch acquire/release and
fails closed during drain, error, absence, replacement or unknown authority.
This is process-local operational ownership, not a new scientific resource
dimension: no scratch field is added to `Lardon3DResourceEstimate`, and all
fourteen current Task kinds have zero scratch consumers. A future consumer
still requires an explicit Task-specific eligibility, lifetime and cleanup
contract. Registered scratch/swap totals remain telemetry and never enlarge
host-memory capacity.
Validation is state-complete, not a friendly-enum shortcut. Pairing or any
control/allocation authority requires the currently detected Drive and both
UUID-bearing partitions, exact nonempty identities, known positive partition
extents and coherent activity/mount/drain/capability facts. Contradictory
`ABSENT` data is malformed; partial `DETECTED` is non-actionable. A disconnected
sticky hazard remains a non-allocating `ERROR`, and only the exact original
tuple after complete reconnection can regain drain authority.
The source generation legally saturates at `UINT64_MAX`. Public same-generation
material changes remain stale and cannot regrant authority. The only exception
is private provenance for the exact serialized Governor wrapper operation: it
may reconcile its own saturated acquire/release and the address-backed lease
count, then permit checked unregister. This does not create a generic
same-generation update or weaken fail-closed telemetry.
Gate G core requires no Project DB v18, resource-history, reservation,
telemetry, policy or scratch table. Reservations and snapshots remain ephemeral;
@ -266,24 +305,40 @@ allocations and is not classified as a Lardon3D Gate G leak. The real-machine
`orb-vulkan-backend` test, `git diff --check`, and the complete human diff review
pass. No Gate G human decision or implementation blocker remains.
The later global-maintenance sanitizer run preserves that qualification rather
than overwriting it: its first full LSan pass reports the same exact external
signature in five OpenCL-loading tests, plus two nonreproduced 30-second timing
events. ASan/UBSan then pass 64/64 with leak detection disabled, and a
loader-free project-owned subset passes LSan 20/20. Fresh GCC TSan covers the
resource/controller/Queue/TUI concurrency boundary inside its 14/14 plus 220
repeat matrix; Vulkan is deliberately outside TSan and is validated by the
fresh hardware suite 65/65. The independent final review subsequently passed
with zero blocking findings, so the enclosing lifecycle is now
`GLOBAL_MAINTENANCE_AUDIT=PASS/FROZEN`. This freezes the audited resource
boundary without turning scratch, swap or future dense work into RAM capacity.
## Project DB boundary
The historical Project DB v16 migration and Sparse SfM reconstruction model
remain unchanged and frozen. Gate F is authorized to advance the current schema
head to v17 only with the dedicated `sparse_sfm_tasks` typed-payload table; that
task persistence does not add a resource table, handle table, cache table,
dependency table or resource-policy field.
remain unchanged and frozen. Gate F advanced that historical head to v17 only
with `sparse_sfm_tasks`; later authorized migrations through v23 likewise add no
resource table, handle table, cache table, dependency table or Governor policy
field. The v23 optical tables describe camera/lens/calibration context, not
runtime resource residency.
No further schema expansion, bundle redesign, Track Model change, Track Builder
change or persistence API redesign is part of this resource-boundary scope.
This resource-boundary decision itself authorizes no future schema expansion,
bundle redesign, Track Model change, Track Builder change or persistence API
redesign.
## Options considered
### A. Governor evolution
Rejected for the current ticket. The Governor is already the correct owner of
budgets, estimates, pressure, lots and reservations. Extending it toward
identity or artifact ownership would violate the current boundary.
Rejected by the original no-new-subsystem decision when it meant scientific
identity or artifact ownership. The later explicitly authorized, additive
external-storage registry stays within the same boundary: it copies physical
state and arbitrates leases, but owns no file, path, persistent identity or
generic loaded resource.
### B. Internal artifact resolver
@ -315,8 +370,9 @@ resolver or Governor asset integration as part of the current architecture.
- Existing persistent identities are sufficient.
- Existing stores already validate their own artifacts.
- Existing task and backend ownership is explicit and bounded.
- Existing Governor policy is sufficient for resource admission.
- No new public syntax or ABI is required.
- Existing Governor policy remains sufficient for RAM/CPU/GPU/I/O admission.
- No new persistent syntax or replacement ABI is required; later observation
and scratch APIs are additive C17 surfaces.
- Gate B v16 remains untouched.
- The decision is reversible if a concrete future requirement appears.
@ -337,7 +393,9 @@ resolver or Governor asset integration as part of the current architecture.
- Generic dependency graph or cycle management.
- Streaming or asynchronous resource loading.
- CPU/GPU dual-residency manager.
- Project DB schema beyond the additive Gate F v17 typed-task migration.
- Project DB schema changes justified only as a generic Resource System. The
separately authorized historical v18v22 and current v23 migrations remain
governed by their own persistence contracts.
- Bundle redesign.
- Governor integration with persistent asset identity.
- BA, new orchestration, Task Runtime redesign or renderer redesign.
@ -351,20 +409,29 @@ contracts remain authoritative in their own domains.
## Persistence impact
The historical Project DB v16 migration remains unchanged. Gate F adds only the
v17 typed-task payload already described above. Existing IDs, hashes, metadata
and relative paths are not renamed, generalized or duplicated.
The historical Project DB v16 migration remains unchanged. Gate F added only
the v17 typed-task payload described above; the current v23 overlay is additive
and creates no backfill. Existing scientific IDs, hashes, metadata and relative
paths are not renamed, generalized or duplicated by resource management.
## Memory / resource impact
None beyond existing task estimates and Governor reservations. No new cache or
resident object ownership is introduced. Existing bounded readers and backend
buffers remain responsible for their own memory.
The original no-new-subsystem decision added none beyond existing task
estimates and Governor reservations. The later SSD controller owns only its
fixed-capacity snapshot/lease/action state; the Governor retains one bounded
copy and fixed wrapper-operation/lease accounting. Neither adds a cache, Task
residency or RAM budget. Existing bounded readers and backend buffers remain
responsible for their own memory.
## Concurrency impact
None. No new shared mutable state, worker, callback, lock or lifetime protocol
is introduced. Existing Governor mutex/condition ownership remains unchanged.
The original decision introduced no shared mutable state, worker, callback,
lock or lifetime protocol. The SSD controller owns its own mutex and fixed
lease table. The TUI binding owns at most one joinable bounded-operation thread
and the Governor owns its registry mutex state. Governor never holds its mutex
while entering the controller, and the controller never calls Governor. At
shutdown Queue/Task leases end before unregister, then controller ends before
Governor; no background polling loop or second scheduler exists.
## Security impact

View file

@ -13,18 +13,27 @@ Cette couture privée est gelée ; seule une séquence ultérieure peut recevoir
un autre contrat. Les dimensions retenues, le corpus S21 complet, les suites
portable/Vulkan, les sanitizers et l'audit XHIGH ferment le statut v2.
Sur le profil validé Ryzen 7 8845HS, le Governor lit le masque d'affinité permis
et la topologie package/core Linux. Il réserve des groupes de coeurs physiques
complets, frères SMT inclus, en préférant déterministiquement les plus grands
IDs package/core. Avec le masque unrestricted 015 et une réserve logique de
quatre, il dérive le pool lourd `0-5,8-13` et réserve `6,7,14,15` au desktop
Arch/Sway, à l'audio et à l'interaction ordinaire. Si le caller est déjà limité
à au plus `logical_total - reserve`, son masque permis devient directement le
pool de calcul sans seconde soustraction. Sans affinité/topologie exploitable,
le budget portable `logical_total - reserve` subsiste sans exclusion arbitraire
de frères SMT et l'affinité est diagnostiquée inactive. Cette politique privée
est **PASS / FROZEN** sur le profil validé ; aucun ID CPU n'est persisté ni ne
devient une identité scientifique.
**PORTABLE_HOST_POLICY_MAINTENANCE — IMPLEMENTED / VALIDATED / REVIEWED.** La
maintenance ultérieure retire les plafonds matériels globaux issus de l'hôte
de preuve, sans changer la science, les formats, les fingerprints ni la
durabilité v2. Son lifecycle global reste suivi par
[`GLOBAL_MAINTENANCE_AUDIT`](global_maintenance_audit.md).
Le Governor lit le masque d'affinité permis et la topologie package/core Linux.
La politique par défaut demande quatre CPU logiques de réserve lorsque c'est
praticable et conserve au moins un CPU de calcul sur un petit hôte. Avec une
topologie complète, elle réserve des groupes de coeurs physiques complets,
frères SMT inclus, en choisissant le sous-ensemble déterministe dont le
dépassement de la cible logique est minimal. Les CPU déjà exclus par un masque
externe comptent dans la réserve hôte, donc la même capacité n'est jamais
soustraite deux fois. Sans affinité/topologie exploitable, le budget de compte
portable subsiste sans fabriquer de masque ni d'ID CPU.
Sur le profil de preuve Ryzen 7 8845HS unrestricted 015, la cible logique de
quatre donne le pool lourd `0-5,8-13` et la réserve `6,7,14,15` pour Arch/Sway,
l'audio et l'interaction ordinaire. Ces IDs sont une observation historique de
cet hôte, pas une politique portable. Aucun ID CPU n'est persisté ni ne devient
une identité scientifique.
Seul le worker lourd de l'unique Queue applique et relit son propre masque
(`pid=0`) avant les callbacks. Le caller/main/TUI reste unrestricted et aucun
@ -52,15 +61,20 @@ Le nombre de CPUs du pool borne directement l'admission.
CPU Matcher reste validé dans `1..12`, indépendamment du lot :
`cpu_threads=12, batch_size=1` demeure un contrat valide.
La cible Compute Governor v2 conserve 3 GiB de `MemAvailable` et ne franchit
pas intentionnellement le plancher dur de 2 GiB sur cette classe d'hôte 16 GiB.
La politique par défaut conserve environ 3 GiB de `MemAvailable` comme réserve
dure d'admission sur un hôte capable. Entre 3 et 4 GiB, elle entre en prudence
YELLOW et bloque la croissance, mais ne soustrait jamais 4 GiB à la capacité et
ne transforme pas cette bande stable en RED. Un hôte de moins de 3 GiB dégrade
la réserve en fraction déterministe afin de garder une capacité bornée.
Les entrées de pression actives sont `MemAvailable`, les PSI CPU/mémoire/I/O et
les deltas swap-in/swap-out entre observations. L'occupation totale du swap est
un état historique, pas à elle seule une activité récente. Ces objectifs v2 ne
réécrivent pas rétroactivement les constantes Gate G core gelées ci-dessous.
Les observations saines autorisent une croissance lente. Pour une dimension
CPU réellement réductible, la rampe est exactement `1 → 2 → 4 → 8 → 12`,
bornée par l'enveloppe et le compute-pool. Deux séquences établissent d'abord
CPU réellement réductible, la rampe double depuis 1 et inclut toujours le
maximum exact de la capacité lorsqu'il n'est pas une puissance de deux. Elle
est bornée par l'enveloppe et le compute-pool, sans plafond global 12. Deux
séquences établissent d'abord
une référence de débit durable puis ouvrent un essai borné au palier supérieur.
Les dimensions CPU/génériques conservent deux observations d'essai et un gain
d'au moins 5 % avant une nouvelle croissance. Le lot ORB Vulkan normal exige
@ -136,7 +150,8 @@ lorsque cette petite capacité reste incertaine. Sur l'hôte validé, les preuve
exactes sont 512 Mio de VRAM visible et 7 986 020 352 octets de GTT pour environ
16 Gio de RAM. Une classification UMA conservatrice d'un GPU à faible VRAM peut
refuser inutilement une admission ; classer à tort cet iGPU comme mémoire libre
séparée pourrait contourner les cibles 3 Gio/2 Gio et est interdit. La capacité
séparée pourrait contourner la réserve dure de 3 Gio et la prudence 34 Gio,
ce qui est interdit. La capacité
rapportée ne forme donc aucun second budget VRAM indépendant sur UMA.
La politique canonique v2 est GPU-first lorsqu'un backend est validé,
@ -265,39 +280,42 @@ privée de sûreté/benchmark peut retenir deux slots, soit 1,25 Mio, seulement
sous un contrat forcé depth 2. Device, pipeline, layouts et cache restent partagés; leurs
allocations driver opaques ne reçoivent pas un coût inventé.
| Kind v1 | Estimation courante | Dimensions réellement consommées / constat Phase 1 |
| Kind v1 | Estimation courante | Dimension consommée et raison |
| --- | --- | --- |
| `raw.develop` | MIXED; CPU 1; lot 1; hôte `2 Gio + contexte`, 0/item; I/O 1; GPU 0 | Fixe et atomique. Un garde Queue applique CPU1 au pool OpenCV process-wide puis restaure la valeur précédente. |
| `photo_quality.triage` | IMPORT; CPU 1; lot 1; hôte `contexte retenu + 20 Mio`, 0/item; I/O 1; GPU 0 | Un groupe par séquence. Un garde Queue applique/restaure CPU1 ; lot et contexte sont honnêtes. |
| `acquisition_campaign.run` | IMPORT; CPU 1; lot 1; hôte `contexte retenu + 256 Kio` + 64 Kio/item; I/O 1; GPU 0 | Sources, confirmations, requête et plan sont chargés avant admission. La reprise remplace seulement l'enveloppe opérationnelle historique sous-estimée par ce coût exact ; le snapshot durable reste inchangé. |
| `import.images` | IMPORT; CPU 1; lot 1..32; hôte 128 Kio + `NAME_MAX+64`/item; I/O 1; GPU 0 | Le callback consomme exactement le lot admis et réadmet entre lots. |
| `features.extract` | CPU; CPU 1..12; lot 1; hôte 64 Mio + 512 Mio/item; I/O 1; GPU 0 | Une image. Le callback applique/restaure exactement le CPU admis dans OpenCV, y compris si la vérification échoue après mutation. Sortie égale à 1/2/4/8/12. |
| `features.extract.sift` | CPU; CPU 1..12; lot 1; hôte 64 Mio + 1 Gio/item; I/O 1; GPU 0 | Même enforcement/rollback adaptatif. La forme durable courante reste CPU12 et la forme historique CPU1 exacte est normalisée en mémoire. |
| `features.extract.rootsift` | CPU; CPU 1..12; lot 1; hôte 64 Mio + 1 Gio/item; I/O 1; GPU 0 | Même exécution adaptative que SIFT ; aucune couture GPU validée. |
| `visual_index.update` | CPU; CPU 1..12; lot 1..16; hôte 8 Mio + 2 Mio/item; I/O 1; GPU 0 | CPU et lot sont lus du contrat; au plus `cpu_threads-1` enfants joints avant publication. |
| `candidate_pair.generate` | CPU; CPU 1..12; lot 1..64; hôte 256 Kio + 64 Kio/item; I/O 1; GPU 0 | CPU et lot sont consommés; fenêtre `min(2*CPU, 24)`. La forme sérielle historique exacte est normalisée en mémoire. |
| `matcher.run` | CPU: CPU 1..12, lot 1..12, hôte 0 + 10 Mio/item, I/O 1, GPU 0. ORB Vulkan normal: CPU 1, lot 1..8, même hôte/item, I/O 1, GPU 1 + 640 Kio, inflight 1. | ORB AUTO adapte seulement le lot sur huit observations pures par palier; CPU complet en fallback. Le benchmark privé conserve batch 12 et depth 2 (1,25 Mio), pas la politique normale. Vulkan explicite et contrôle synchrone restent depth 1; SIFT/RootSIFT et CPU explicite restent fixes. Helpers=0. |
| `geometric_verifier.run` | CPU; CPU 1; lot 1..8; hôte 4 Mio, 0/item; I/O 1; GPU 0 | Le lot est consommé séquentiellement; USAC conserve `isParallel=false`. |
| `track_builder.run` | CPU; CPU 1; lot 1; fixe `(4 Mio + arêtes * (48 + 2*160)) * facteur`, facteur 2 jusqu'à 400k arêtes puis 8; 0/item; I/O 1; GPU 0 | Rebuild atomique. La reprise valide le scope mais ne recalcule pas l'estimation avant admission. |
| `sparse_sfm.run` | CPU; CPU 1; lot 1; fixe `ceil_Mio(128 Mio + 64 Kio/image + 2 Kio/track + 512 octets/observation)`; 0/item; I/O 1; GPU 0 | Exécution atomique; BA à un thread. La forme est redérivée à la reprise sans réconciliation de l'estimation persistée. |
| `incremental_reconstruction.run` | CPU; CPU 1; lot 1; fixe `ceil_Mio(256 Mio + 128 Kio*(caméras base + images extension) + 4 Kio*(landmarks base + tracks extension) + 1 Kio*(observations base + extension))`; 0/item; I/O 1; GPU 0 | Exécution atomique; la forme est redérivée à la reprise sans réconciliation de l'estimation persistée. |
| `raw.develop` | MIXED; CPU 1; lot 1; hôte `2 Gio + contexte`, 0/item; I/O 1; GPU 0 | Un Capture atomique. Le garde applique/restaure CPU1 au pool OpenCV global. Les 2 Gio sont une allowance de travail opérationnelle, pas une limite de dataset. |
| `photo_quality.triage` | IMPORT; CPU 1; lot 1; hôte `contexte retenu + 20 Mio`, 0/item; I/O 1; GPU 0 | Un groupe par séquence ; garde OpenCV CPU1. Aucun scaling utile déterministe n'est acquis. |
| `acquisition_campaign.run` | JPEG: IMPORT ; RAW: MIXED. CPU 1; lot 1; hôte `contexte retenu + requête transitoire exacte + 256 Kio` + 64 Kio/item ; DEVELOP_RAW ajoute 2 Gio; I/O 1; GPU 0 | Un groupe S3-E par séquence, sans Task imbriqué. La création et la reprise dérivent la même estimation ; la forme historique exacte est normalisée seulement en mémoire. |
| `import.images` | IMPORT; CPU 1; lot 1..32; hôte 128 Kio + `NAME_MAX+64`/item; I/O 1; GPU 0 | Copie/hash I/O-bound. Le callback consomme le lot admis et réadmet entre lots. |
| `features.extract` | CPU; CPU 1..compute-pool; lot 1; hôte 64 Mio + 512 Mio/item; I/O 1; GPU 0 | La demande durable emploie le maximum `int` positif de l'API OpenCV ; le Governor la borne à l'hôte. Le garde applique/restaure exactement le CPU admis. |
| `features.extract.sift` | CPU; CPU 1..compute-pool; lot 1; hôte 64 Mio + 1 Gio/item; I/O 1; GPU 0 | Même contrat OpenCV. Les formes CPU12 et CPU1 historiques complètes sont acceptées et normalisées en mémoire. |
| `features.extract.rootsift` | CPU; CPU 1..compute-pool; lot 1; hôte 64 Mio + 1 Gio/item; I/O 1; GPU 0 | Même contrat que SIFT ; aucune couture GPU scientifiquement compatible n'est validée. |
| `visual_index.update` | CPU; CPU 1..16; lot 1..16; hôte 8 Mio + 2 Mio/item; I/O 1; GPU 0 | Le segment contient au plus 16 Feature Sets indépendants. Au plus `cpu_threads-1` enfants sont joints avant publication owner-only. |
| `candidate_pair.generate` | CPU; CPU 1..64; lot 1..64; hôte 256 Kio + 8 Mio/item; I/O 1; GPU 0 | Le batch de 64 est la borne algorithmique et de ressources. Fenêtre `min(2*CPU, 64, reste_du_lot)`, un handle DB privé par participant, piles enfants de 4 Mio facturées. |
| `matcher.run` | CPU: CPU 1..12, lot sûr 1..12, hôte 0 + 10 Mio/item, I/O 1, GPU 0. ORB Vulkan AUTO: CPU 1, lot utile 1..8, même hôte/item, GPU 1 + 640 Kio, inflight 1. | Le batch/participant 12 est une borne intrinsèque mesurée du Matcher. Batch 12 et depth 2 (1,25 Mio) restent sûrs pour preuves privées, mais insuffisamment utiles en AUTO. CPU complet est le fallback. |
| `geometric_verifier.run` | CPU; CPU utile 1..8, fenêtre sûre 16; lot 1..16; hôte 0 + 8 Mio/item; I/O 1; GPU 0 | Des parents indépendants sont préparés en parallèle, puis publiés/checkpointés en ordre par le propriétaire. L'USAC scientifique conserve `isParallel=false`. |
| `track_builder.run` | CPU; CPU 1; lot 1; fixe `(4 Mio + arêtes * (48 + 2*160)) * facteur`, facteur 2 jusqu'à 400k arêtes puis 8; 0/item; I/O 1; GPU 0 | Rebuild DSU atomique et publication owner-only ; aucune partition scientifiquement validée n'est acquise. |
| `sparse_sfm.run` | CPU; CPU 1; lot 1; fixe `ceil_Mio(128 Mio + 64 Kio/image + 2 Kio/track + 512 octets/observation)`; 0/item; I/O 1; GPU 0 | Exécution atomique FROZEN ; Gate D/E et BA restent à un thread. |
| `incremental_reconstruction.run` | CPU; CPU 1; lot 1; fixe `ceil_Mio(256 Mio + 128 Kio*(caméras base + images extension) + 4 Kio*(landmarks base + tracks extension) + 1 Kio*(observations base + extension))`; 0/item; I/O 1; GPU 0 | Recalcul atomique FROZEN depuis les entrées immuables, sans état solveur persistant. |
Ces écarts sont des constats d'implémentation Compute Governor v2. Ils ne
créent ni nouvelle limite scientifique, ni nouvelle identité, ni modification
du schéma Project DB. Une enveloppe privée de capacités peut corriger la
sélection/réconciliation sans modifier l'ABI C public du Task Kind Registry.
Ces bornes sont opérationnelles, externes-library, algorithmiques, mesurées ou
scientifiques selon la dernière colonne. Elles ne créent aucune limite de
cardinalité scientifique, identité ou migration Project DB. Les formes
historiques CPU1/CPU8/CPU12 citées sont des signatures exactes de reprise, pas
des plafonds portables de production.
## SIFT v1A
Une extraction SIFT demande jusqu'à douze threads CPU, un slot IO, aucun GPU,
pour une image. L'estimation structurelle conservatrice est environ 1,06 Gio
Une extraction SIFT demande jusqu'au compute-pool hôte, un slot IO, aucun GPU,
pour une image. Sa demande durable utilise `INT_MAX`, borne valide de l'API
OpenCV, mais ce nombre n'est jamais une admission matérielle. L'estimation
structurelle conservatrice est environ 1,06 Gio
(décodage, pyramides, candidats et F32×128), lot 1, pic de record batch zéro.
Le callback applique exactement le contrat admis dans `1..12` au pool OpenCV
Le callback applique exactement le contrat positif admis au pool OpenCV
process-wide sous l'unique propriétaire Queue, puis restaure la valeur
précédente sur toutes les sorties. Les tests déterministes ORB, SIFT et
RootSIFT couvrent 1/2/4/8/12 et obtiennent les mêmes keypoints, descripteurs et
métriques. Cette dimension opérationnelle ne change ni fingerprint ni Feature
Set.
métriques ; 12 reste une preuve de l'hôte courant, pas un maximum portable.
Cette dimension opérationnelle ne change ni fingerprint ni Feature Set.
## Responsabilité
@ -305,19 +323,20 @@ Le Resource Governor est l'unique propriétaire des budgets (RAM, GPU, CPU,
IO). Il arbitre les ressources disponibles et calcule les lots adaptatifs pour
chaque tâche.
Le profil interactif par défaut conserve un quart de la RAM détectée et un
quart des threads logiques pour le système hôte. Sur 16 Gio/16 threads, cela
donne environ 3,8 Gio et 4 threads de headroom. Une nouvelle admission attend
également lorsque PSI CPU `some avg10` atteint 20 %, ou PSI mémoire 1 %. Ces
signaux n'interrompent jamais le petit job déjà réservé. Ces valeurs décrivent
le profil Gate G core gelé; la cible opérationnelle v2 active est le couple
3 GiB/2 GiB et les signaux différentiels définis en tête de document.
Le profil interactif par défaut conserve quatre threads logiques lorsque
praticable et au moins un thread de calcul sur un petit hôte. Sur 16 threads,
cela donne 4 threads de headroom ; avec une topologie fiable, la sélection porte
sur des coeurs physiques complets et peut dépasser minimalement la cible
logique. Une nouvelle admission attend également lorsque PSI CPU `some avg10`
atteint 20 %, ou PSI mémoire 1 %. Ces signaux n'interrompent jamais le petit job
déjà réservé.
La soft floor vaut un quart et la hard floor un huitième de la RAM détectée. La
soft floor place le Governor au minimum en YELLOW ; la hard floor le place
immédiatement en RED. Le premier delta swap entre deux snapshots produit
YELLOW. Un second delta consécutif produit RED. Le premier snapshot ne constitue
qu'une baseline et n'est jamais interprété comme une activité récente.
La réserve dure/admission RAM vaut environ 3 GiB sur un hôte capable. La bande
34 GiB place le Governor au minimum en YELLOW et remet la croissance à 1 sans
soustraire 4 GiB de la capacité. La hard floor place immédiatement en RED. Le
premier snapshot swap constitue seulement une baseline ; un delta actif produit
YELLOW, puis RED s'il persiste. L'occupation totale du swap n'est pas un signal
d'activité récente.
La récupération interdit `RED → GREEN` : trois observations saines produisent
RED vers YELLOW, puis trois autres YELLOW vers GREEN. Le plafond reste 1 pendant
@ -326,8 +345,7 @@ le plafond : 1, 2, 4, 8, puis les paliers supérieurs utiles aux autres kinds.
## Contrat Gate G gelé
**PASS / FROZEN.** Les constantes existantes ci-dessus
restent inchangées. La RAM disponible conserve le modèle conservateur
**PASS / FROZEN.** Le contrat d'admission conserve le modèle conservateur
`min(MemAvailable, RAM physique) - réserve hôte - réservations actives`, borné à
zéro. Le double comptage conservateur possible d'une allocation déjà visible
dans `MemAvailable` est accepté : un faux `WAIT` est préféré à un overcommit.
@ -351,6 +369,65 @@ scratch, historique RSS long terme, redimensionnement de réservation depuis le
RSS ou monitoring live n'appartient à Gate G core. L'observation courante
bornée RSS/HWM de Compute Governor v2 reste strictement diagnostique.
## Registre de stockage externe et leases scratch
**EXTERNAL_STORAGE_OPERATIONAL_INTEGRATION — CURRENT / VALIDATED
OPERATIONAL.** Cette couture additive postérieure ne modifie pas Gate G core,
`Lardon3DResourceEstimate`, les budgets RAM/GPU/CPU/I/O ni une identité
scientifique. Elle permet au contrôleur physique SSD exact d'enregistrer sous
le mutex Governor une `Lardon3DResourceExternalStorage` bornée : génération
source, état
`ABSENT/DETECTED/AVAILABLE/IN_USE/DRAINING/SAFE/ERROR`, permission de nouvelle
allocation, total/libre scratch connus ou inconnus, swap connu ou inconnu,
nombre de leases, identité stable et raison.
L'enregistrement est exclusif à un objet contrôleur emprunté. Les updates stale
ne peuvent pas restaurer une disponibilité ; une observation malformée ou
indéterminée devient `ERROR` conservateur et bloque les nouvelles allocations.
Un changement matériel incrémente la génération Governor et réveille les
waiters. L'état n'est ni persisté, ni relié à Project DB, ni interprété comme
capacité RAM. Les détails physiques et les capacités F10 restent la propriété
du contrôleur ; le panneau ressources lit l'usage enregistré auprès du
Governor.
La conversion contrôleur→Governor est fail-closed par état. Toute paire ou
autorité exige détection courante du Drive et des deux partitions, identité
Drive+deux UUID exacte/non vide, tailles de partition connues et positives,
ainsi que mount, activité, leases, drain et capacités cohérents. `ABSENT`
interdit tout fait détecté/actif/monté/loué/capacitaire ; `DETECTED` incomplet
reste seulement observable. Un `ERROR` sticky peut conserver le tuple original
malgré la disparition, mais n'alloue rien ; il n'offre un drain qu'après preuve
du même tuple complètement reconnecté. Aucun état amical ni bit
`pairing_valid` ne remplace cette preuve physique.
Les wrappers `lardon3d_resource_governor_acquire_scratch()` et
`lardon3d_resource_governor_release_scratch()` sont l'unique entrée de
production pour les leases. Ils exigent le contrôleur exactement enregistré,
son identité courante et l'autorité explicite d'allocation. `DRAINING`,
`ERROR`, absence, remplacement, état stale ou non-enregistré refusent une
acquisition ; une release exacte reste possible pendant le drain ou l'erreur.
Le Governor ne tient jamais son mutex pendant un appel contrôleur, et le
contrôleur ne rappelle jamais le Governor. La génération et l'état sont
revalidés avant publication afin que la transition ne crée ni inversion de
verrou ni lease non comptée.
`UINT64_MAX` est une saturation source valide, pas un sentinel. L'API publique
continue de refuser une update matériellement différente au même watermark et
ne peut donc pas réaccorder une autorité stale. Seule la complétion du wrapper
exact, déjà sérialisé pour l'objet contrôleur et l'adresse de lease enregistrés,
peut réconcilier à `UINT64_MAX` sa propre acquisition/libération. Le compte
interne fondé sur les adresses reste l'autorité ; une erreur de complétion peut
retirer l'autorité mais ne peut jamais recopier un ancien compte physique.
Le teardown production suit strictement : Queue détruite/jointe et chaque
lease Task rendu, worker SSD joint puis unregister vérifié, contrôleur détruit,
Governor détruit. Un unregister est refusé tant qu'une opération wrapper ou un
lease exact subsiste. Les quatorze Task kinds courants n'ont aucun consommateur
scratch : le compte normal est donc réellement zéro et la capacité disponible
n'autorise aucun usage implicite. Une future Task consommatrice devra définir
son propre contrat d'éligibilité et son ownership sans transformer scratch ou
swap en RAM.
## API principale
### Création et destruction
@ -376,9 +453,19 @@ bornée RSS/HWM de Compute Governor v2 reste strictement diagnostique.
- `lardon3d_resource_governor_wait_for_change()` - Attendre un changement
- `lardon3d_resource_governor_pressure()` - Lire GREEN, YELLOW ou RED
### Stockage externe additif
- `lardon3d_resource_governor_register_external_storage()` /
`lardon3d_resource_governor_update_external_storage()` /
`lardon3d_resource_governor_unregister_external_storage()` /
`lardon3d_resource_governor_get_external_storage()` - Copier et observer
l'état physique borné d'un contrôleur exact
- `lardon3d_resource_governor_acquire_scratch()` /
`lardon3d_resource_governor_release_scratch()` - Posséder un lease scratch
de production sous l'orchestration du Governor
## Invariants
1. Le scheduler ne décide jamais des ressources
1. La Queue/runtime ne décide jamais des ressources
2. Le Resource Governor est l'unique propriétaire des budgets
3. Les réservations sont obligatoires avant toute exécution
4. Les réservations sont libérées exactement une fois
@ -386,6 +473,9 @@ bornée RSS/HWM de Compute Governor v2 reste strictement diagnostique.
6. L'historique des métriques est strictement borné (8 entrées par classe)
7. Un contrat de séquence est immutable jusqu'à sa libération; seule la
séquence suivante peut être adaptée
8. Swap, scratch et espace SSD n'augmentent jamais la capacité RAM
9. Un lease scratch de production passe par le Governor et reste attaché à
l'objet lease exact jusqu'à sa libération
## Cycle de vie
@ -424,35 +514,44 @@ bornée RSS/HWM de Compute Governor v2 reste strictement diagnostique.
l'échantillon peut conserver taille/durée mais n'alimente jamais l'adaptation
mémoire.
- Pas de communication inter-classes de tâches
- `features.extract` réserve un lot de 1, demande jusqu'à douze threads CPU et
- `features.extract` réserve un lot de 1, demande la borne positive `int`
d'OpenCV et
un slot I/O, avec
64 Mio fixes et 512 Mio par image. Cette estimation conservatrice couvre le
chemin actuel sans prétendre mesurer les allocations internes d'OpenCV.
l'admission choisit 1..`min(12, compute_pool)` et le callback applique ce
L'admission choisit 1..`compute_pool` et le callback applique ce
nombre immutable au pool OpenCV. `record_batch` couvre la validation source,
le décodage, ORB, la publication
et la finalisation DB ; `peak_memory_bytes == 0` signifie « mesure inconnue ».
- `visual_index.update` demande jusqu'à douze threads CPU, un slot I/O, 8 Mio
- `visual_index.update` demande jusqu'à seize threads CPU, un slot I/O, 8 Mio
fixes et 2 Mio par Feature Set, par lots de 1 à 16. Le GPU vaut zéro. Le
callback compte comme participant et crée au plus `cpu_threads - 1` enfants,
tous joints avant publication et rupture de séquence. Chaque participant
possède au plus un reader/FD Feature File ; les tranches de postings privées
partitionnent le buffer borné du segment. `record_batch` compte uniquement
les memberships commités et conserve la mémoire inconnue à zéro.
- `candidate_pair.generate` demande jusqu'à douze threads CPU, un slot I/O,
256 Kio fixes et 64 Kio par Feature Set, par lots de 1 à 64. Le GPU vaut
zéro. La reconstruction reconnaît uniquement l'ancienne estimation exacte
(128 Kio fixes, 64 Kio par item, un thread CPU, un slot I/O, aucun GPU, lots
1 à 64, classe CPU) et la normalise éphémèrement vers l'estimation courante
complète ; aucun checkpoint d'estimation seule n'est publié et une forme
voisine n'est jamais réinterprétée comme legacy.
- `candidate_pair.generate` demande jusqu'à soixante-quatre threads CPU, un
slot I/O, 256 Kio fixes et 8 Mio par Feature Set, par lots de 1 à 64. Le GPU vaut
zéro. La reconstruction reconnaît la forme immédiatement antérieure exacte
(256 Kio fixes, 64 Kio par item, CPU12) et la plus ancienne forme sérielle
exacte (128 Kio fixes, 64 Kio par item, CPU1), toutes deux à lot 1..64,
I/O1, GPU0, classe CPU. Elle les normalise éphémèrement vers la forme courante
256 Kio + 8 Mio/item, CPU64 ; aucun checkpoint d'estimation seule n'est
publié et une forme voisine n'est jamais réinterprétée comme legacy.
`record_batch` compte le nombre de paires générées par séquence et la durée
réelle du lot ; `peak_memory_bytes == 0` signifie « mesure inconnue ».
Chaque séquence interroge le Visual Index pour jusqu'à 64 memberships. Le
calcul emploie des fenêtres internes d'au plus deux sources par thread admis
et 24 sources au total ; le propriétaire de Task persiste ensuite seul et en
et 64 sources au total ; le propriétaire de Task persiste ensuite seul et en
ordre canonique. Cette estimation opérationnelle ne limite pas la taille
scientifique du dataset.
- `geometric_verifier.run` réserve 8 Mio par Match Result admis, un slot I/O,
CPU 1..8 utile et lot 1..16. La fenêtre/participant est indépendamment sûre
jusqu'à 16, mais CPU12 n'a ajouté que 2,68 % sur CPU8 dans la preuve réelle
de 4113 parents, sous le deadband 5 %. Les enfants préparent des parents
indépendants et sont tous joints avant la publication owner-only, le curseur
contigu, le checkpoint et `sequence_break`. L'USAC interne garde
`isParallel=false`; CPU/lot ne modifient ni fingerprint ni GVR.
- `matcher.run` demande jusqu'à douze threads CPU, un slot IO et 10 Mio par
Candidate Pair admise, correspondant au working set contrôlé inférieur à
environ 10 Mio par paire au
@ -554,3 +653,10 @@ sont gelées ; il ne reste aucune décision humaine Gate G.
## Statut
**GATE G — PASS / FROZEN.**
L'intégration opérationnelle SSD/Governor ci-dessus est
**CURRENT / VALIDATED OPERATIONAL**. Le jalon global qui la contient est
`GLOBAL_MAINTENANCE_AUDIT=PASS/FROZEN`. Les validations fraîches
portable/Vulkan, ASan/UBSan, LSan qualifié, TSan et ABI sont acquises ; l'unique
revue finale indépendante a conclu PASS sans finding bloquant. Le gel porte sur
la frontière auditée et ne crée aucun consommateur scratch ni budget RAM.

View file

@ -3,8 +3,12 @@
## Modèle d'exécution
### Threads
- Thread principal : TUI ncursesw (exclusif)
- Thread worker : exécution des tâches métier
- Thread principal : entrée, modèle de vue et rendu TUI ncursesw (exclusif)
- Thread worker Queue : exécution sérielle des tâches métier
- Participants internes : uniquement ceux du contrat Task admis, joints par le
callback propriétaire avant publication
- Opération SSD : au plus un thread joinable, uniquement pendant une opération
UDisks bornée ; il ne rend rien et ne devient ni Queue ni scheduler
### Synchronisation
- Mutex pour les accès partagés
@ -16,7 +20,7 @@
```text
1. Création (PENDING)
2. Soumission à la file
3. Sélection par le scheduler
3. Sélection FIFO/adaptative par la Queue
4. Réservation obligatoire
5. Exécution (RUNNING)
- Pause/reprise coopérative
@ -35,7 +39,7 @@
### Variables de condition
- Coordination entre threads
- Notification de changement d'état
- Attente passive (pas de polling)
- Attente passive ; timeout borné seulement pour réévaluer un `WAIT` ressources
### Atomicité
- Opérations indivisibles
@ -44,9 +48,10 @@
## Gestion des erreurs
### Rollback
- Retour à l'état précédent en cas d'échec
- Nettoyage complet des ressources
- Aucun état partiellement appliqué
- Rollback des transactions locales avant publication
- Nettoyage complet des ressources possédées par l'opération
- Une publication fichier réussie suivie d'un échec DB peut laisser un orphelin
valide ; aucune transaction distribuée fichier+SQLite n'est revendiquée
### Récupération
- Reprise à la dernière frontière connue
@ -55,8 +60,9 @@
## Limites actuelles
- Worker unique (pas de pools multiples)
- Pas de parallélisme inter-tâches
- Worker Queue unique (pas de pools inter-Tasks multiples)
- Pas de parallélisme inter-Tasks ; certains kinds possèdent des participants
internes bornés, comptés par leur contrat Governor
- Reprise automatique limitée aux tâches indépendantes reconstructibles
## Reprise durable
@ -84,15 +90,20 @@ transaction catalogue restent hors mutex Task et hors mutex DB pendant l'I/O.
resoumet automatiquement les tâches production valides. Il retourne après
l'enqueue et n'attend jamais leur terminaison.
L'ordre d'initialisation production est : profil matériel, governor, queue et
worker, TUI, puis ouverture DB/projet et reprise synchrone. Une fermeture ne
peut commencer qu'après le retour de `project_open()`. Le worker peut consommer
pendant le scan ; chaque tâche exécutée est néanmoins réadmise normalement.
L'ordre d'initialisation production est : politique driver, profil matériel,
Governor, backend, Queue/worker, contrôleur SSD optionnel et binding
Governor, puis TUI. L'ouverture DB/projet et la reprise synchrone sont pilotées
ensuite depuis le thread principal. Une fermeture ne peut commencer qu'après le
retour de `project_open()`. Le worker peut consommer pendant le scan ; chaque
tâche exécutée est néanmoins réadmise normalement.
**NOT_YET_WIRED** — autosave générique des autres kinds et reprise ordonnée par
dépendances.
**NOT_YET_WIRED** — reprise ordonnée par dépendances/DAG. Les kinds de
production reconstructibles checkpointent déjà à leurs frontières métier ;
aucun timer autosave générique ne doit avancer devant leur publication durable.
**PLANNED** — reprise globale du scheduler via la Project Database.
**IMPLEMENTED** — reprise sélective des kinds reconstructibles via Project DB,
Task Kind Registry et Queue. Les dépendances/DAG restent différées ; il
n'existe pas de scheduler global distinct à restaurer.
## Accès Project Database
@ -100,8 +111,14 @@ dépendances.
les opérations multi-tables sont transactionnelles et bornées.
**IMPLEMENTED** — le cycle de vie projet ouvre/crée `project.db`, vérifie
l'identité et ferme la connexion. L'application arrête la task queue avant la
fermeture finale du projet.
l'identité et ferme la connexion. Ouvrir, fermer ou changer de projet est une
frontière exacte : l'observateur et la vue optique libèrent leurs borrows, puis
l'unique Queue est annulée, jointe et détruite, callbacks terminaux inclus,
avant la fermeture de Project DB. Une seule Queue vide est ensuite recréée et
les observateurs sont rebondés. Il n'existe jamais deux schedulers simultanés.
L'historique terminal et l'espace d'IDs Queue sont ainsi propres à la session ;
les mêmes Task IDs durables de deux projets restent indépendants et aucun
historique fourni n'est affiché lorsqu'aucun projet n'est chargé.
**IMPLEMENTED** — la registry reconstruit explicitement callback/userdata hors
mutex DB pour un kind connu ; elle ne soumet aucune tâche.
@ -119,11 +136,179 @@ bloquent pas l'ouverture.
commitée. Un segment temporaire n'est jamais visible et un rejeu exclut les
Feature Sets déjà membres.
## Durée de vie terminale et fermeture Queue
Une Task terminale reste vivante jusqu'au retour complet de son callback
terminé. Queue la retire alors de la liste active et la détruit hors de son
mutex ; seule une histoire de 64 snapshots reste observable. Les appels déjà
enregistrés avant `task_queue_destroy()` sont attendus. Le propriétaire doit
empêcher tout nouvel appel dès le début de la destruction, règle nécessaire à
toute API C adressée par pointeur brut.
Un callback terminé peut consulter les vues Queue tant que le propriétaire la
maintient vivante. Il ne peut pas détruire cette Queue, retirer son propre
record ni attendre une opération dépendante de son retour.
## Observatoire TUI actuel
**CURRENT / VALIDATED OPERATIONAL.** Ce statut décrit l'implémentation et ses
tests courants. L'audit global qui contient cette frontière est désormais
`PASS/FROZEN` après revue indépendante ; le statut TUI reste volontairement
opérationnel et n'interdit pas ses évolutions futures sous un ticket distinct.
### Séparation modèle, observation et rendu
Le modèle `tui_model` est pur et testable sans terminal. Le renderer reçoit
seulement des copies bornées et n'interroge ni Queue, ni Governor, ni Project
DB, ni contrôleur SSD. Toutes les fonctions ncurses, l'entrée clavier et le
rendu demeurent sur le thread principal.
L'observateur runtime emprunte Queue et Governor et conserve une seule copie
cohérente. Les captures ordinaires sont coalescées pendant au moins une seconde
monotone ; un échec conserve la dernière vue bornée en la marquant stale.
Il observe au plus 129 Tasks : les 64 pending possibles, l'unique active et les
64 snapshots terminaux récents. L'ordre Queue place le travail vivant du plus
récent au plus ancien, puis l'histoire par terminaison décroissante ; une Task
active ne peut donc pas être masquée par un vieux préfixe historique. Il
n'existe ni scan DB par frame, ni lecture `/proc` volumineuse, ni historique
non borné.
Les ABI historiques restent exactes : `Lardon3DTaskSnapshot`,
`Lardon3DResourceSnapshot`, `Lardon3DAppState` et
`lardon3d_layout_draw()` ne sont pas étendus en place. Les surfaces additives
`Lardon3DTaskObservation`, `lardon3d_task_queue_observe()`,
`Lardon3DResourceObservation`, `Lardon3DRuntimeSnapshot` et
`lardon3d_layout_draw_runtime()` portent les nouveaux champs. De même,
`lardon3d_tui_run()` reste le symbole historique ; l'application utilise
`lardon3d_tui_run_with_ssd_operation()` avec un owner SSD conservé hors de
`Lardon3DAppState`.
### Progression et ETA
Une Task typée publie `completed/total` seulement après son propre commit
métier durable. Quand ces compteurs sont connus, la TUI les affiche toujours et
en dérive le pourcentage sans utiliser le message ou le nom. Une Task marquée
`COMPLETED` avec un préfixe durable incomplet est une erreur d'intégrité
visible, jamais 100 %. Quand les comptes typés sont inconnus, le lifecycle peut
être terminal mais la progression scientifique reste indéterminée. Le
pourcentage générique non typé, lorsqu'il est utile, porte explicitement le
libellé runtime.
Le débit est un EWMA borné. La première observation établit seulement le
préfixe de reprise et ne contribue pas au taux ; une reprise de RUNNING remet
également la fenêtre temporelle à zéro. Deux intervalles strictement positifs
sont nécessaires avant un débit et une ETA connus. Une absence de progrès,
une pression Governor, une régression ou une preuve insuffisante produit
respectivement `STALLED`, `THROTTLED`, reset ou `INDETERMINATE/CALCULATING`.
Seule une complétion cohérente vaut exactement 100 % et ETA zéro ; aucune fausse
précision n'est affichée.
### Pipeline et ressources
La synthèse utilise les étapes Acquisition, RAW, Quality, Features, Visual
Index, Candidate, Matcher, GV, Tracks, Sparse SfM et future Dense. Les états
sont `NOT_READY`, `READY`, `QUEUED`, `RUNNING`, `THROTTLED`, `BLOCKED`,
`COMPLETE`, `FAILED` et `NOT_APPLICABLE`. Dense reste explicitement
`NOT_APPLICABLE` tant qu'aucun Task kind de production n'existe ; une étape
future n'est jamais devinée active depuis un nom ou un message.
Le panneau ressources expose CPU actif/admis/disponible et sa raison, GPU
présent/mémoire/busy/backend lorsqu'ils sont connus, RAM/MemAvailable/réserve,
swap total/utilisé et deltas actifs, lot/inflight/helpers/I/O, scratch et
pression Governor GREEN/YELLOW/RED. Le contrat installé de l'exacte Task active
est l'autorité pour CPU et lot. Un dernier diagnostic privé seulement indexé
par kind peut appartenir à une autre Task ou séquence : backend, inflight,
helpers, utilisation ou raison restent donc `UNKNOWN` sans association exacte
Task+séquence. La mémoire UMA est comptée une seule fois et ni swap ni scratch
ne sont ajoutés à la capacité RAM.
### Dimensions, couleurs et clavier
Le layout complet demande au moins 100×30. Le layout compact est validé à la
frontière 72×20 et reste supporté jusqu'au minimum 60×15. En dessous, le rendu
se réduit au message borné `Terminal trop petit`; un resize recalcule la classe
sans faire travailler un worker. Les rôles sémantiques sont healthy vert,
warning jaune, error rouge, GPU cyan, CPU bleu, SSD magenta, plus dim/bold.
Les libellés textuels demeurent l'autorité lorsqu'il n'y a pas de couleur ou
pas assez de paires terminal.
Les écrans courants sont accueil, projets, import, viewer futur, tâches,
ressources, optique, SSD et aide. `F1..F7` naviguent respectivement vers aide,
projets, import, viewer, tâches, ressources et optique. Le segment littéral
`F10 SSD` est réservé au début du footer et reste visible à 60 colonnes dans
tous les modes pertinents. Les footers dérivent du même mode que le handler :
- saisie active : Enter valide, Échap annule, F10 reste disponible ;
- import actif : `X` demande l'annulation et F10 reste disponible ; `q` et
Échap sont affichés comme désactivés ;
- mode idle : `q`, Échap/navigation et les commandes propres à l'écran sont
annoncés seulement lorsqu'ils sont réellement traités ;
- Tasks : flèches/`j`/`k`, `P` pause, `R` reprise, `C` annulation ;
- Optique : Tab change de panneau, flèches/`j`/`k` sélectionnent, `[` revient à
la première page et `]` charge la suivante ; `B/L/C/V/A/G/K/E` déclenchent
les opérations indiquées et `R` retente explicitement un bind/chargement.
### Workflow optique
La TUI consomme les API v23 décrites dans
[Project Database](project_database.md), sans SQL direct ni édition d'une ligne
immuable. Elle inspecte une affectation Capture, effectue seulement des lookup
metadata exacts, liste les profils de boîtier/objectif/configuration et accepte
un objectif manuel sans électronique ni alias — le Meike de test est un cas
normal, pas une branche produit. « Modifier » signifie créer un nouveau profil
ou une nouvelle configuration immuable, puis l'assigner explicitement à un
groupe de campagne encore éligible ou à un Capture non affecté. Les
calibrations listées doivent être compatibles avec l'exacte configuration et
la sélection reste explicite ; absence, ambiguïté, incompatibilité, BUSY, I/O
et corruption sont rendues sans profil fabriqué. Les pages ont 16 lignes,
rapportent un compte page-local et un indicateur « suite » exact.
### SSD F10 et lifetime application
La TUI affiche les huit états physiques `ABSENT`, `DETECTED`, `ENABLING`,
`ENABLED`, `IN_USE`, `DRAINING`, `SAFE_TO_UNPLUG` et `ERROR`, avec identité
stable, modèle/télémétrie lorsqu'ils sont connus, swap, scratch, mount, usage,
leases, drain et raison. `UNKNOWN` n'est jamais remplacé par zéro ou par une
supposition ; `SAFE_TO_UNPLUG` est mis en évidence comme endpoint sûr. F10
choisit exclusivement l'une des capacités exactes
`can_enable`, `can_disable` ou `can_cancel_drain` publiée par le contrôleur ; un
état incomplet, une paire de remplacement ou un résultat malformé n'accorde
aucune autorité. L'opération synchrone UDisks s'exécute dans au plus un thread
joinable, tandis que le main continue de rendre et de poller sans blocage.
La validation qui alimente ces capacités est fail-closed par état : toute
autorité exige Drive et deux partitions détectés, identités Drive+UUID exactes,
extents positifs connus et faits mount/activité/drain cohérents. `ABSENT` ne
peut transporter aucun fait actif, `DETECTED` partiel n'a aucune action et un
hazard `ERROR` déconnecté ne peut que retenir l'identité originale sans
allocation. Seule la reconnexion complète de ce tuple peut autoriser son drain.
Après chaque observation ou résultat validé, l'adaptateur enregistre une copie
bornée de l'état physique auprès du Governor. Une copie malformée devient
`ERROR` et interdit les nouvelles allocations ; l'observation ressources lit
cet état Governor-owned, tandis que les détails/permissions F10 restent dans
le snapshot physique. La génération source peut saturer à `UINT64_MAX` : une
update publique égale ne réaccorde jamais une autorité stale ; seule la
complétion du wrapper exact déjà engagé réconcilie son lease adressé. À l'arrêt,
l'ordre est : destruction/join de la Queue et
libération de chaque lease Task, fermeture du projet/DB, join puis unregister
vérifié de l'adaptateur SSD, destruction du contrôleur, puis destruction du
Governor. Les tests utilisent un provider factice et n'exécutent aucune vraie
mutation SSD.
## Invariants
- ncurses appartient exclusivement au thread principal
- Aucune tâche ne démarre sans réservation active
- Les réservations sont libérées exactement une fois
- Les buffers sont strictement bornés
- Le Resource Governor reste l'unique propriétaire de l'admission ; ni Queue,
ni contrôleur SSD ne constituent un second orchestrateur de ressources
## Statut : DOCUMENTATION DE L'IMPLÉMENTATION ACTUELLE
## Statut : CURRENT / VALIDATED OPERATIONAL
La TUI/runtime et son raccordement SSD sont implémentés, testés et relus dans
leur tranche. Le statut global est
`GLOBAL_MAINTENANCE_AUDIT=PASS/FROZEN`. Les builds portables/Vulkan,
sanitizers, contrôles de concurrence et ABI frais sont acquis ; l'unique revue
finale indépendante a conclu PASS sans finding bloquant.

View file

@ -1,13 +1,16 @@
# Intégration Scheduler ↔ Resource Governor
# Intégration Task Queue ↔ Resource Governor
## Responsabilité
Documenter l'intégration architecturale entre le scheduler de tâches et le Resource Governor, incluant les frontières de responsabilités, le cycle de vie des réservations et le comportement en cas de pause.
Documenter l'intégration architecturale entre le runtime/Task Queue et le
Resource Governor, incluant les frontières de responsabilités, le cycle de vie
des réservations et le comportement en cas de pause. Le nom historique de ce
fichier ne désigne pas un scheduler distinct.
## Frontières de responsabilités
### Scheduler
- Ordonnancement FIFO
### Task Queue / runtime
- Ordonnancement FIFO stable avec bypass des seuls `WAIT`
- Exécution des callbacks
- Gestion des états de tâche
- Sélection de la première tâche admissible
@ -17,14 +20,16 @@ Documenter l'intégration architecturale entre le scheduler de tâches et le Res
- Calcul des lots adaptatifs
- Réservations opaques
- Historique de métriques
- Registre borné de l'état SSD physique et orchestration des leases scratch de
production, sans transformer cet espace en RAM
## Cycle d'exécution
```text
1. Task → Estimate (estimation des ressources)
2. Scheduler → Governor → decide() (décision d'admission)
2. Queue → Governor → decide() (décision d'admission)
3. Governor → Reservation (réservation opaque)
4. Scheduler → Worker (exécution)
4. Queue → Worker (exécution)
5. Worker → Governor → record_batch() (métriques)
6. Governor → release() (libération)
```
@ -38,7 +43,7 @@ Documenter l'intégration architecturale entre le scheduler de tâches et le Res
- `REJECT` : rejeter la tâche
### Comportement en cas de WAIT
- Le scheduler saute la tâche en tête de file
- La Queue saute la tâche en tête de file seulement lorsqu'elle est en `WAIT`
- Il évalue la tâche suivante
- Pas de blocage de la file
- Si aucune tâche n'est admissible mais qu'un `WAIT` PENDING subsiste, le worker
@ -77,7 +82,7 @@ Documenter l'intégration architecturale entre le scheduler de tâches et le Res
## Invariants
1. Aucune tâche ne passe de PENDING à RUNNING sans réservation active
2. Le scheduler ne prend jamais de décision sur les ressources
2. La Queue ne prend jamais de décision sur les ressources
3. La libération des réservations s'effectue exactement une fois par cycle/séquence
4. Les atomicités sont garanties sous mutex
@ -86,6 +91,20 @@ Documenter l'intégration architecturale entre le scheduler de tâches et le Res
- Worker unique (pas de pools multiples)
- Pas de DAG de dépendances
- Pas de priorités
- Pas de notification automatique de libération externe
- Aucun Task kind courant ne consomme le scratch externe ; toute future
éligibilité reste kind-owned et explicitement bornée
Les changements matériels du registre SSD incrémentent la génération Governor
et réveillent ses waiters. Ils ne modifient pas un contrat Task déjà installé,
n'ajoutent pas le swap/scratch à la RAM disponible et ne créent aucune Task.
Une acquisition scratch de production passe par le wrapper Governor, puis sa
release exacte demeure autorisée pendant un drain. Le contrôleur physique ne
rappelle jamais le Governor et aucun mutex Governor n'est tenu pendant l'appel
au contrôleur.
La fermeture Queue bloque l'ingress, attend le worker et tous les appels déjà
enregistrés, puis détruit exactement une fois. Une Task terminale n'est retirée
qu'après retour de son callback terminé ; Queue détruit alors Task/userdata hors
mutex et ne conserve qu'une histoire bornée de 64 snapshots.
## Statut : GATE G — PASS / FROZEN

View file

@ -725,7 +725,11 @@ translations are exhaustive:
Every row is fingerprinted. Integer fields require no normalization beyond
their fixed-width little-endian translation; every floating field uses the
canonical-zero rule above.
canonical-zero rule above. The four relative-pose/PnP iteration/inlier fields
retain this `u32` syntax for F0 compatibility, while an executable call through
the public Gate C OpenCV boundary additionally requires the `INT_MAX` bound
defined below. That operational validation neither rewrites nor normalizes
fingerprint bytes.
The policy IDs above freeze the full named Gate D and Gate E v1 semantics,
including canonical order and tie breaks,
@ -840,8 +844,8 @@ path. Swap, zram and external storage do not enlarge its RAM capacity.
### Gate F durable task payload
**FROZEN.** Gate F advances the current Project Database schema head from v16
to v17 with one strictly additive `sparse_sfm_tasks` table. The historical v16
**FROZEN.** Gate F advanced the then-current Project Database schema head from
v16 to v17 with one strictly additive `sparse_sfm_tasks` table. The historical v16
migration and its immutable reconstruction model remain unchanged. The new
table follows the existing one-to-one typed-task pattern: its primary key is a
foreign key to `tasks(task_id)` with cascade cleanup, and creation records the
@ -1134,6 +1138,26 @@ cheirality and inlier diagnostics. The tested v1 parameter set is frozen by the
Gate C ground-truth and degeneracy evidence; future orchestration may choose
other explicitly fingerprinted configurations.
#### Public OpenCV signed-boundary contract
The C17 parameter fields for relative pose and calibrated PnP remain
`uint32_t`, but OpenCV accepts signed `int` iteration and inlier arguments.
Consequently, both `max_iterations` and `minimum_inliers` must be at most
`INT_MAX`; `max_iterations == 0` retains its existing invalid semantics, while
`minimum_inliers == 0` retains the existing PnP effective minimum of four.
Values above `INT_MAX` return `INVALID_ARGUMENT` before allocation, OpenCV,
RNG work or mutation of the caller-owned result/mask. Conversion to `int`
occurs only after that check.
This is an operational language/library boundary, not a scientific-policy
change. The public field widths, F0 `u32` encoding, FROZEN defaults, thresholds,
seeds, fingerprints and every representable run remain byte-identical. The
maintenance regression rejects `INT_MAX+1` and `UINT32_MAX` for both primitives
without output mutation, while a degenerate no-solver fixture proves
`INT_MAX` itself remains representable without attempting that many
iterations. Focused validation passed 1/1 plus 20 repeats, targeted
ASan/UBSan 1/1, GCC/Clang C17/C++17 inclusion and the application link.
### Gate C tested threshold set
The pure API has no hidden defaults; callers provide all acceptance settings.

View file

@ -53,15 +53,15 @@ même fichier. Le détail chiffré des capacités est centralisé dans l'[audit
| --- | --- | --- |
| `raw.develop` | `raw_development_task.cpp`; `lardon3d_raw_development_task_reconstruct`; `run` | Aucune |
| `photo_quality.triage` | `photo_quality_task.cpp`; `lardon3d_photo_quality_task_reconstruct`; `run` | Aucune |
| `acquisition_campaign.run` | `acquisition_campaign_task.cpp`; `lardon3d_acquisition_campaign_task_reconstruct`; `run` | Aucune |
| `acquisition_campaign.run` | `acquisition_campaign_task.cpp`; `lardon3d_acquisition_campaign_task_reconstruct`; `run` | Forme courante ou forme v22 exacte vérifiée contre la requête immuable → capacité courante en mémoire |
| `import.images` | `import_task.c`; `lardon3d_image_import_reconstruct`; `run_image_import` | Aucune |
| `features.extract` | `feature_task.c`; `lardon3d_feature_extract_reconstruct`; `run` | Durable CPU12; admission/runtime OpenCV 1..12 |
| `features.extract.sift` | `sift_task.c`; `lardon3d_sift_extract_reconstruct`; `run` | CPU1 historique exact → durable CPU12; runtime 1..12 |
| `features.extract.rootsift` | `sift_task.c`; `lardon3d_sift_extract_reconstruct`; `run` | CPU1 historique exact → durable CPU12; runtime 1..12 |
| `visual_index.update` | `visual_index_task.c`; `lardon3d_visual_index_update_reconstruct`; `run` | CPU runtime 1..12 ; feedback par segment durable |
| `candidate_pair.generate` | `candidate_pair_task.c`; `lardon3d_candidate_pair_generate_reconstruct`; `run` | Forme sérielle historique exacte → CPU12 ; runtime CPU 1..12 et lot 1..64 |
| `features.extract` | `feature_task.c`; `lardon3d_feature_extract_reconstruct`; `run` | Formes CPU12/CPU1 historiques exactes → demande OpenCV portable ; runtime borné au compute-pool |
| `features.extract.sift` | `sift_task.c`; `lardon3d_sift_extract_reconstruct`; `run` | CPU12/CPU1 historiques exacts → demande OpenCV portable ; runtime borné au compute-pool |
| `features.extract.rootsift` | `sift_task.c`; `lardon3d_sift_extract_reconstruct`; `run` | Même réconciliation SIFT ; aucune voie GPU validée |
| `visual_index.update` | `visual_index_task.c`; `lardon3d_visual_index_update_reconstruct`; `run` | Formes CPU12/CPU1 historiques exactes → CPU/lot 1..16 |
| `candidate_pair.generate` | `candidate_pair_task.c`; `lardon3d_candidate_pair_generate_reconstruct`; `run` | Formes CPU12 et CPU1 historiques exactes → CPU/lot 1..64 |
| `matcher.run` | `matcher_task.c`; `lardon3d_matcher_task_reconstruct`; `run` | Signatures CPU/Vulkan historiques exactes → formes courantes en mémoire |
| `geometric_verifier.run` | `geometric_verifier_task.c`; `lardon3d_geometric_verifier_task_reconstruct`; `run` | Aucune |
| `geometric_verifier.run` | `geometric_verifier_task.c`; `lardon3d_geometric_verifier_task_reconstruct`; `run` | Forme sérielle CPU1/batch8 exacte → CPU utile 1..8, lot 1..16 |
| `track_builder.run` | `track_builder_task.cpp`; `lardon3d_track_builder_task_reconstruct`; `run` | Aucune |
| `sparse_sfm.run` | `sparse_sfm_task.cpp`; `lardon3d_sparse_sfm_task_reconstruct`; `run` | Aucune |
| `incremental_reconstruction.run` | `incremental_reconstruction_task.cpp`; `lardon3d_incremental_reconstruction_task_reconstruct`; `run` | Aucune |
@ -87,10 +87,11 @@ diagnostic ; ni l'enveloppe ni ce choix ne sont persistés.
La politique CPU hôte reste privée au Governor : masque permis, groupes
package/core/SMT, compute-pool et résultat d'application du worker Queue. Le
compute-pool borne l'admission de chaque kind, y compris une capacité durable
CPU12. Feature/SIFT/RootSIFT consomment le compte immutable 1..12 dans OpenCV ;
les kinds CPU1 restent fixes. Aucun ID CPU ou choix d'affinité n'entre dans le
descriptor, le checkpoint ou le Project DB.
compute-pool borne l'admission de chaque kind. Feature/SIFT/RootSIFT utilisent
le maximum `int` positif comme borne de l'API OpenCV, puis consomment le compte
immutable réellement admis ; les CPU12 durables ne sont plus que des signatures
historiques exactes. Les kinds CPU1 justifiés restent fixes. Aucun ID CPU ou
choix d'affinité n'entre dans le descriptor, le checkpoint ou le Project DB.
Le feedback ne requalifie pas un succès de reprise en travail durable : les
kinds Feature/SIFT/RootSIFT comptent un item seulement après extraction et
@ -99,8 +100,10 @@ incertaine compte zéro ; Visual Index compte pareillement zéro pour un segment
`PUBLISHED_NOT_DURABLE`.
L'état privé par kind/backend coordonne désormais une seule dimension d'essai.
Les CPU validés slow-startent `1/2/4/8/12`; après deux observations de baseline,
deux observations à au moins +5 % sont nécessaires pour accepter le palier.
Les CPU réductibles slow-startent par doubles successifs depuis 1, puis le
maximum exact de leur capacité, toujours bornés par le compute-pool. Après deux
observations de baseline, deux observations à au moins +5 % sont nécessaires
pour accepter le palier.
Une fois CPU stabilisé, seuls les kinds dont le callback consomme réellement
son lot peuvent ouvrir un essai de lot. `features.extract`, SIFT et RootSIFT
enregistrent une observation atomique réussie partagée entre Tasks ; Visual
@ -127,7 +130,9 @@ userdata sans `AppState *` ancien.
**IMPLEMENTED** — `project_open()` utilise la registry production immutable
pour restaurer hors mutex DB et transférer chaque tâche acceptée à la queue.
**NOT_YET_WIRED** — autosave complet et réconciliation orpheline.
**NOT_YET_WIRED** — réconciliation orpheline et dépendances/DAG. Les kinds
reconstructibles checkpointent déjà à leurs frontières métier ; la Registry ne
possède pas un timer autosave et ne doit pas devancer leurs curseurs durables.
**IMPLEMENTED** — `features.extract` version 1 reconstruit une extraction ORB
depuis `image_id` et ses paramètres bornés.
@ -138,11 +143,11 @@ depuis `image_id` et ses paramètres bornés.
**IMPLEMENTED** — `candidate_pair.generate`, version 1, recharge
`visual_index_id + after_feature_set_id + top_k + minimum_evidence_count
+ scanset_filter + exclude_same_asset` depuis `candidate_pair_generate_tasks`
et reconstruit un contexte boundé. La restauration reconnaît uniquement le
snapshot opérationnel sériel historique v1 exact et remplace éphémèrement sa
demande d'un thread CPU par douze avant admission. Le checkpoint historique et
le curseur typé restent inchangés. Les snapshots Candidate courants et tous les
autres kinds restent inchangés.
et reconstruit un contexte boundé. La restauration reconnaît le snapshot
opérationnel sériel historique v1 exact et la forme CPU12 immédiatement
antérieure, puis les remplace éphémèrement par la demande courante CPU64 avec
8 Mio par item avant admission. Le checkpoint historique et le curseur typé
restent inchangés ; une forme voisine est rejetée.
**PASS / FROZEN — Compute Governor v2.** `matcher.run`, version 1,
recharge la configuration Matcher, l'identité Feature Set et le curseur
@ -227,18 +232,22 @@ une Task Matcher doit être reprise. La Registry continue donc à reconstruire
uniquement la politique AUTO/fixe déduite de la signature durable, jamais un
pipeline de benchmark.
**IMPLEMENTED** — `features.extract.sift` et `features.extract.rootsift`
acceptent leur forme CPU12 courante et normalisent uniquement leur forme CPU1
historique exacte. ORB/SIFT/RootSIFT appliquent ensuite le compte CPU admis dans
`1..12`; les sorties testées à 1/2/4/8/12 restent égales. Cette compatibilité
opérationnelle n'altère ni fingerprint, Feature Set, checkpoint durable, ni
politique scientifique.
**IMPLEMENTED** — ORB, SIFT et RootSIFT acceptent leurs formes CPU12/CPU1
historiques complètes et les normalisent vers la demande OpenCV portable
`INT_MAX`. Le Governor borne l'exécution au compute-pool ; les sorties testées
à 1/2/4/8/12 restent égales. Cette compatibilité opérationnelle n'altère ni
fingerprint, Feature Set, checkpoint durable, ni politique scientifique.
**IMPLEMENTED** — `geometric_verifier.run`, version 1, recharge la configuration
Fundamental immuable, en revalide le fingerprint et reprend `after_match_result_id`.
Project DB v13 ajoute uniquement `geometric_verifier_tasks`, car le checkpoint
générique v1 ne possède aucun payload propre au kind.
La forme historique série exacte (4 Mio fixes, CPU1, batch 1..8) est normalisée
en mémoire vers 8 Mio par item, CPU utile 8 et batch maximal 16. Cette évolution
ne touche ni fingerprint, GVR, ordre, curseur ni checkpoint historique ; une
forme voisine est refusée.
**IMPLEMENTED** — `track_builder.run`, version 1, reconstruit un scope explicite
depuis son payload Project DB v15 et son asset little-endian validé. Le callback
réutilise l'orchestration Gate C et le reconstructeur refuse toute corruption,

View file

@ -7,9 +7,10 @@ traitement. Il orchestre l'exécution séquentielle des tâches via un worker
unique, gère la sélection de la prochaine tâche admissible et transmet les
callbacks de résultat.
La file est le point central entre le scheduler (soumission) et les workers
(exécution). Elle ne décide jamais des ressources — elle applique uniquement
l'ordre FIFO et consulte le gouverneur via le scheduler.
La file est le point central entre les producteurs et le worker d'exécution.
Les responsabilités historiques de scheduler sont portées par cette Queue et
le runtime existants : aucun second scheduler n'existe. La Queue ne décide
jamais des ressources ; elle demande l'admission au Governor.
## Fichiers
@ -32,17 +33,19 @@ Le type public réel est l'opaque `Lardon3DTaskQueue`.
| `lardon3d_task_queue_try_add_ex()` | Distingue succès, saturation, arrêt, collision d'ID et erreur |
| `lardon3d_task_queue_pause()` / `resume()` | Contrôle une tâche par son ID stable |
| `lardon3d_task_queue_cancel()` | Demande l'annulation par ID |
| `lardon3d_task_queue_remove()` | Retire une tâche terminale |
| `lardon3d_task_queue_remove()` | Retire un snapshot terminal retenu |
| `lardon3d_task_queue_snapshot()` | Copie une vue bornée de la file |
| `lardon3d_task_queue_observe()` | Copie la vue additive typée/durable/admission |
## Comportement FIFO
## Comportement FIFO avec bypass stable des WAIT
1. `lardon3d_task_queue_add()` ajoute la tâche en fin de file.
2. Le sélecteur interne parcourt la file du début vers la fin.
3. La première tâche `PENDING` admissible (non terminale) est
retournée.
4. Si aucune tâche n'est admissible, le worker attend un changement.
5. L'ordre de soumission est toujours respecté entre tâches de même priorité.
5. L'ordre de soumission est respecté entre tâches admissibles ; une tâche en
`WAIT` ressources peut être dépassée sans être réordonnée ou supprimée.
Lorsqu'au moins une tâche PENDING reste en `WAIT` de ressources, cette attente
est temporisée à 500 ms maximum. À l'expiration, le worker reprend le scan stable
@ -61,30 +64,47 @@ le blocage par la tête de file lorsqu'une tâche ne peut pas démarrer.
1. **Worker unique** : une seule tâche s'exécute à la fois. Pas de parallélisme
interne à la file.
2. **FIFO strict** : l'ordre de soumission détermine l'ordre d'exécution.
2. **FIFO stable** : le scan commence à la tête et ne contourne que les
`WAIT` d'admission. Il n'existe ni priorité cachée ni réordonnancement.
3. **Réservation obligatoire** : aucune tâche n'est exécutée sans réservation
validée par le gouverneur.
4. **Callback unique** : chaque tâche reçoit exactement un callback (succès,
échec ou annulation).
4. **Notification terminale unique** : le callback terminé de la Task retourne
avant la destruction de son userdata. Les callbacks métier peuvent avoir
plusieurs séquences admises.
5. **Annulation sûre** : annuler une tâche en cours la met en état
`CANCELLED` sans interrompre brutalement le worker.
6. **Nettoyage complet** : `task_queue_destroy()` libère toutes les tâches
restantes, y compris celles en cours d'exécution.
6. **Retraite prompte et bornée** : après retour du callback terminé, la vraie
Task est détruite hors mutex et seule une copie de snapshot demeure. Les 64
terminaisons les plus récentes sont retenues ; les plus anciennes sont
évincées sans conserver userdata ni Task.
7. **Fermeture coordonnée** : `destroy()` ferme d'abord l'ingress, attend le
worker et tout appel enregistré avant la fermeture, puis libère mutex et
mémoire exactement une fois. Le propriétaire doit interdire tout nouvel
appel API dès que la destruction commence.
8. **IDs non réutilisés** : la génération est monotone. Après consommation ou
génération de `UINT64_MAX`, l'épuisement est permanent pour la vie de la
Queue ; un ID restauré plus petit ou une éviction d'historique ne la réarme
jamais.
9. **Observation complète bornée** : la Queue de production accepte au plus 64
pending, possède au plus une active et retient 64 terminaux. Une capacité
additive de 129 observe donc tout l'état courant. Les Tasks vivantes sont
copiées d'abord en ordre de soumission décroissant, puis l'histoire en ordre
de terminaison décroissant ; l'actif ne peut pas être caché par l'histoire.
## Interactions
- **task** : chaque entrée de la file est un `Lardon3DTask` avec son état et sa
progression.
- **scheduler** : la file applique la soumission FIFO et orchestre
l'exécution via le worker.
- **resource_governor** : la file consulte le gouverneur (via le scheduler)
avant d'exécuter chaque tâche.
- **runtime / producteurs** : soumettent les Tasks ; la Queue applique FIFO et
orchestre l'exécution via le worker.
- **resource_governor** : la file consulte directement le gouverneur avant
d'exécuter chaque tâche.
- **hardware_profile / resource_snapshot** : informations matériel utilisées
par le gouverneur pour les réservations.
## Statut
**GATE G — PASS / FROZEN** — file FIFO avec worker unique,
**GATE G — PASS / FROZEN** — file FIFO stable avec worker unique,
sélection adaptative, réévaluation autonome des `WAIT`, pause, annulation
coopérative et accueil des tâches restaurées avec ID préassigné.
@ -93,11 +113,23 @@ La reprise projet utilise `try_add_ex()` et ne bloque jamais `project_open()`.
`PENDING` en DB et seront réévaluées lors d'une ouverture ultérieure. Ce n'est
pas un second scheduler.
Ouvrir, fermer ou changer de projet détruit/joint cette Queue avant fermeture
de Project DB, puis recrée une unique Queue vide. L'histoire et son namespace
d'IDs ne traversent donc jamais une frontière projet ; ce reset ne modifie pas
les Task IDs durables conservés dans chaque DB.
Les callbacks terminés sont appelés sans le mutex Queue. Tant que le
propriétaire maintient la Queue vivante, ils peuvent employer les lectures
`get`, `get_at`, `count` et `snapshot`. Ils ne doivent pas appeler
synchroniquement `remove()` sur leur propre record, `destroy()` sur la même
Queue, ni une opération dont l'achèvement dépend de leur propre retour.
## Limites
- Worker unique : pas de parallélisme interne.
- Worker unique : pas de parallélisme inter-Tasks. Un kind peut posséder un
parallélisme interne borné et chargé dans sa réservation.
- Pas de DAG ni de dépendances inter-tâches.
- Pas de priorités (FIFO strict).
- Pas de priorités (FIFO stable avec bypass des seuls `WAIT`).
- La reprise ne possède pas encore de DAG ni de déclenchement différé lorsque
une place se libère pendant la session courante.
- Pas de pool de workers CPU/IO/GPU.

View file

@ -41,6 +41,21 @@ une opération de réadmission, pas un état supplémentaire.
| `lardon3d_task_sequence_break()` | Libère puis renouvelle la réservation |
| `lardon3d_task_snapshot()` | Copie l'état d'observation runtime |
### Observation additive et ABI
`Lardon3DTaskSnapshot` conserve exactement son layout historique. La structure
additive `Lardon3DTaskObservation` répète ce préfixe dans un autre type et ajoute
kind/version, compteurs durables, nombre de séquences et copie du contrat
d'exécution installé. `lardon3d_task_observation()` initialise une sortie
caller-owned et cohérente sous le mutex Task ; aucun pointeur interne ne sort.
`lardon3d_task_set_durable_progress()` ne peut être appelé par un owner typé
qu'après publication de son préfixe métier. Ces compteurs ne sont ni déduits du
pourcentage générique, ni d'un message, ni du nom de Task. Un
`set_progress()` ultérieur les efface afin qu'une observation ne conserve pas
une valeur exacte devenue stale. Cette couture sert la TUI mais ne change ni le
codec checkpoint, ni l'identité scientifique, ni l'estimation immutable.
### API durable
| Fonction | Description |
@ -80,8 +95,9 @@ compteur de séquences. Les timestamps et l'estimation complète ne sont pas
dupliqués par la DB et ne participent donc pas à ce test. Un canonique valide
dont ce résumé correspond est prioritaire ; une `.next` absente, périmée ou
corrompue est alors ignorée. Sinon, une `.next` valide qui correspond au même
résumé est promue sous le même verrou puis reprise. Un ancien projet v22 qui ne
possède que `.chk` reste ainsi récupérable. Toute autre absence, version non
résumé est promue sous le même verrou puis reprise. Un ancien projet v22 ou
antérieur qui ne possède que `.chk` reste ainsi récupérable. Toute autre
absence, version non
supportée, corruption ou divergence interdit la reprise de cette tâche sans
affecter les autres entrées de l'inventaire.
@ -98,8 +114,8 @@ affecter les autres entrées de l'inventaire.
`lardon3d_task_checkpoint()`. Aucun autre thread ne force son arrêt.
4. **Progression bornée** : la progression ne peut jamais dépasser la valeur
maximale définie par l'estimation.
5. **Reprise réadmise** : une tâche restaurée non terminale repasse par la file,
le scheduler et le Resource Governor avec une nouvelle réservation.
5. **Reprise réadmise** : une tâche restaurée non terminale repasse par la
Queue/runtime et le Resource Governor avec une nouvelle réservation.
6. **Snapshot court** : seuls les champs durables sont copiés sous le mutex ;
la sérialisation et les I/O ont lieu après déverrouillage.
@ -178,8 +194,10 @@ n'avancent pas la rampe.
L'A/B forcé ABBA a mesuré seulement +2,077617 % à depth 2
(54,661652238 contre 55,797311953 paires/s), sous le deadband 5 %, avec digest identique,
quatre séquences de fallback local par exécution et zéro panne/discard. Depth 2 est donc
**REJECTED_WITH_MEASURED_REASON** pour AUTO normal. Compute Governor v2 reste
en cours jusqu'aux réconciliations restantes.
**REJECTED_WITH_MEASURED_REASON** pour AUTO normal. Cette décision Matcher est
gelée ; l'intégration TUI, la validation exécutable et la revue finale
indépendante sont acquises. L'audit global qui contient cette frontière est
désormais `GLOBAL_MAINTENANCE_AUDIT=PASS/FROZEN`.
Pour Matcher Vulkan, le lease privé de capacité matérialise le contrat de la
séquence seulement après son admission. Il alloue un ou deux payloads de
@ -190,8 +208,9 @@ complète; elle ne laisse ni réservation suivante sous-facturée, ni résultat
Vulkan partiel.
Cette boucle est maintenant `observe → choose → execute → measure → adapt next`.
Les CPU réductibles progressent uniquement par `1/2/4/8/12`, bornés par le
compute-pool ; CPU et lot ne sont jamais essayés ensemble. Les observations
Les CPU réductibles progressent par paliers de puissances de deux, puis vers la
capacité exacte du kind ou du compute-pool lorsque ce dernier palier diffère ;
CPU et lot ne sont jamais essayés ensemble. Les observations
hôte privées comprennent utilisation du pool, mémoire/PSI/swap actif, GPU busy
et RSS observé, sans confondre RSS et réservation. Le dernier diagnostic peut
être tiré par numéro de série ou formaté dans un buffer borné ; le runtime ne
@ -266,7 +285,9 @@ lors de sa soumission explicite à la file.
**IMPLEMENTED** — resoumission automatique sélective des tâches production à
l'ouverture du projet.
**NOT_YET_WIRED** — autosave générique et dépendances entre tâches.
**NOT_YET_WIRED** — dépendances/DAG entre Tasks. Il n'existe volontairement pas
de timer autosave générique : chaque kind persiste d'abord son curseur métier à
sa frontière atomique, puis publie son checkpoint générique.
**IMPLEMENTED** — `visual_index.update` traite jusqu'à seize Feature Sets par
séquence, checkpoint après commit de segment et repasse par le Governor.
@ -280,7 +301,7 @@ Governor via `sequence_break`. La reprise est idempotente avec le curseur
**PASS / FROZEN — Compute Governor v2.** `matcher.run` traite une
Candidate Pair atomique à la fois, par lots opérationnels bornés jusqu'à 12.
Le code courant consomme honnêtement CPU, lot et GPU du contrat choisi. Feature,
SIFT et RootSIFT appliquent l'admission OpenCV `1..12`; RAW et Photo Quality
SIFT et RootSIFT appliquent l'admission OpenCV `1..compute-pool`; RAW et Photo Quality
restent CPU1. Pour ORB normal, le Governor choisit GPU-first ou CPU complet pour la prochaine
séquence, sans mutation pendant son exécution. Ces dimensions ne changent ni
identité scientifique ni publication. Le callback persiste le curseur
@ -297,11 +318,13 @@ handles encore privés. Les buffers du second slot ne sont mappés que pendant
une séquence depth 2 admise et sont libérés avant la rupture suivante. Helpers
reste 0 et le contrôle synchrone force depth 1.
**IMPLEMENTED** — `geometric_verifier.run` v1 traite un Match Result atomique à
la fois, par lots adaptatifs de 1, 2, 4 ou 8. Project DB v13 conserve sa
configuration scientifique et `after_match_result_id`. Chaque résultat est
publié avant le curseur ; pause, annulation, checkpoint et rupture de séquence
restent coopératifs aux frontières des parents et des lots.
**IMPLEMENTED** — `geometric_verifier.run` v1 traite des Match Results
indépendants sous un callback propriétaire, avec jusqu'à 16 participants sûrs,
8 utiles et 16 parents par lot. Project DB v13 conserve sa configuration
scientifique et `after_match_result_id`. L'USAC interne reste
`isParallel=false`; le propriétaire publie le préfixe canonique ordonné avant
le curseur. Pause, annulation, checkpoint et rupture de séquence restent
coopératifs aux frontières des parents et des lots.
**IMPLEMENTED** — `track_builder.run` v1 est une tâche durable de rebuild
complet. Le scope GVR est persistant et immuable ; la reprise rejoue depuis le
@ -340,12 +363,18 @@ avant l'appel hors mutex. `join()` attend la fin du callback ; le userdata reste
donc valide pendant celui-ci et son destructeur n'est appelé qu'ensuite par la
destruction de la tâche.
L'observation TUI ne change pas cette ownership : elle ne retient ni Task ni
userdata et ne pilote aucune transition. Le contrat installé de la Task active
est l'unique source exacte pour CPU/lot ; un diagnostic Governor par kind ne
peut pas être attribué à cette Task sans association Task+séquence.
Une tâche reconstruite mais refusée avant transfert à la queue est abandonnée
localement : son userdata est détruit, sans callback terminal ni écriture
durable d'une fausse annulation. Une annulation explicitement demandée conserve
le contrat de notification terminale.
La Project Database v7 peut enregistrer transactionnellement un résumé
Depuis Project Database v7, la base peut enregistrer transactionnellement un
résumé
`Lardon3DTaskDurableSnapshot` et la référence de son checkpoint. Elle ne stocke
ni estimation sérialisée complète, ni callback, ni réservation, et ne remplace
pas la validation du fichier checkpoint avant `task_restore()`.
@ -357,9 +386,17 @@ snapshots dont le codec/version et le résumé DB ont été validés, mais ne pe
appeler `task_restore()` que via un descriptor connu ; aucun pointeur n'est
persistant.
## Project DB courant
Le schéma Project DB courant est v23. Cette évolution additive ne modifie ni le
codec checkpoint v1, ni les règles de reprise ci-dessus ; elle ajoute le modèle
optique générique décrit dans [Project Database](project_database.md).
## Limites
- Aucune priorité interne : l'ordre est uniquement FIFO.
- Aucune priorité interne : l'ordre est FIFO stable avec bypass des seuls
`WAIT` ressources.
- Pas encore de DAG pour ordonner des reprises interdépendantes.
- Aucune dépendance inter-tâches (pas de DAG).
- Pas encore de références d'artefacts métier validés.
- Pas encore de références génériques d'artefacts métier validés dans le codec
checkpoint ; les payloads typés conservent leurs propres références durables.

View file

@ -161,7 +161,7 @@ valide. La reprise recommence au dernier curseur commité et l'unicité des
memberships rend le rejeu idempotent.
**IMPLEMENTED — parallélisme interne borné.** La Queue exécute toujours un seul
callback. L'estimation demande jusqu'à douze threads CPU, un slot I/O, GPU zéro,
callback. L'estimation demande jusqu'à seize threads CPU, un slot I/O, GPU zéro,
8 Mio fixes et 2 Mio par Feature Set, lot 1..16. Le callback compte comme un
participant et crée au plus `cpu_threads - 1` enfants. Chaque enfant lit
exclusivement des Feature Files immuables et écrit une tranche privée ; il ne

View file

@ -141,9 +141,12 @@ sortie, soit 8,125 Mio de payload lazy en plus des 640 Kio ORB.
Sur 24 160 requêtes SIFT et 24 161 requêtes RootSIFT contrôlées, y compris
des descriptors produits par OpenCV SIFT et les frontières `nextafter` autour
de Lowe 0,7, aucune divergence Lowe n'a été observée. Ce résultat de corpus
n'est pas une garantie universelle. FP32 ne reproduit pas les distances bit à
bit et un corpus d'égalités adversariales démontre
de Lowe 0,7, aucune divergence Lowe n'a été observée. La comparaison exacte
compte néanmoins une divergence d'index top-2 et 20 251 distances aux bits
différents pour SIFT, puis une divergence d'index et 20 824 distances aux bits
différents pour RootSIFT. Ce résultat de corpus au niveau de la décision Lowe
n'est donc ni une identité bit à bit ni une garantie universelle. FP32 ne
reproduit pas les distances bit à bit et un corpus d'égalités adversariales démontre
une divergence d'indices reproductible : OpenCV choisit `(0, 1)` et FP32
Vulkan `(7, 14)`. FP64 choisit `(0, 128)` et ne restaure donc pas le contrat
OpenCV. Le recalcul CPU des distances des deux candidats ne peut corriger une

View file

@ -8,7 +8,14 @@ Ces contraintes exploitent la géométrie projective des caméras, la structure
## Statut
**PLANNED** — Concepts mathématiques fondamentaux, pas encore implémentés comme module distinct. Utilisés implicitement dans les futurs algorithmes de matching et reconstruction.
**MIXED / EXPLICIT BOUNDARIES.** Il n'existe pas de module générique unique
« Geometric Constraints ». La matrice fondamentale et son support sont
implémentés/PASS-FROZEN dans Geometric Verifier v3. Essential, pose relative,
cheirality, parallaxe, triangulation et reprojection calibrées sont
implémentées/PASS-FROZEN dans Sparse SfM Gates CG. La compétition explicite
Fundamental/Essential/Homography, les contraintes d'occlusion complètes et les
relations entre ScanSets restent futures. Ces distinctions remplacent
l'ancienne formule où tout le matching et la reconstruction étaient futurs.
## Place dans le pipeline
@ -128,7 +135,7 @@ typedef struct {
### 6. Triangle Quality Constraints
La qualité des triangles de triangulation影响 la précision de la reconstruction :
La qualité des triangles de triangulation influence la précision de la reconstruction :
| Métrique | Seuil recommandé | Description |
|----------|------------------|-------------|
@ -149,9 +156,12 @@ La qualité des triangles de triangulation影响 la précision de la reconstruct
## Contraintes de conception
- Les seuils géométriques (parallaxe minimale, erreur de reprojection) sont calibrables mais immuables pendant un traitement.
- Les seuils géométriques courants sont explicites, fingerprintés lorsqu'ils
appartiennent à l'identité, et immuables pendant un traitement ; les valeurs
FROZEN ne sont pas « calibrables » implicitement.
- L'estimation de F ou E utilise RANSAC avec un nombre d'itérations borné.
- Les homographies sont détectées automatiquement mais ne remplacent pas E pour les scènes non planes.
- La compétition/détection Homography explicite reste future et ne doit pas
être attribuée au Geometric Verifier v3 actuel.
- Les contraintes de visibilité sont recalculées à chaque ajout de caméra.
- Les résultats de filtrage géométrique sont auditables (log des rejets avec raison).
- En cas d'incertitude, les contraintes sont conservatistes (rejeter plutôt qu'accepter).

View file

@ -6,13 +6,15 @@
> [tracks.md](../architecture/tracks.md). Les différences notables :
> le Track Model v1 ne contient aucune coordonnée 3D, aucun statut
> (ACTIVE/OPTIMIZED/REJECTED), aucune matrice de co-visibilité et aucun
> plafond de longueur arbitraire. La triangulation, le Sparse SfM et le
> Bundle Adjustment sont des étapes ultérieures séparées.
> plafond de longueur arbitraire. Le Matcher, le Geometric Verifier et le
> Track Builder réels sont désormais implémentés et documentés par leurs
> contrats d'architecture ; triangulation, Sparse SfM et Bundle Adjustment
> demeurent des étapes séparées de ce concept historique.
> Frontière v1A : les groupes de support ORB/SIFT sont uniquement des preuves
> locales intra-image. Ils ne comparent pas les espaces Hamming et L2, ne sont
> pas des matches multivues et ne créent aucun track. Le futur matcher choisira
> explicitement SIFT ou RootSIFT après génération des paires.
> pas des matches multivues et ne créent aucun track. Le Matcher production
> choisit explicitement le Feature kind retenu après génération des paires.
## Définition
@ -22,8 +24,10 @@ Le matching transforme les features individuelles en relations inter-images. Les
## Statut
**PLANNED** — Étape critique du pipeline, pas encore implémentée. Visual Index
v1 fournit désormais les candidats, leur score et leurs preuves distinctes.
**ARCHIVE HISTORIQUE — SUPERSEDED.** Candidate Pair, Matching v1, Geometric
Verification v3 et Track Model/Builder v1 sont implémentés et gelés dans leurs
documents d'architecture. Les structures et politiques conceptuelles ci-dessous
ne redéfinissent pas ces contrats courants.
## Place dans le pipeline
@ -44,10 +48,10 @@ Reconstruction Layers (triangulation)
Le matching est le pont entre les caractéristiques 2D des images et la structure 3D de la scène.
Le score Visual Index est un signal de retrieval. Il ne constitue ni un match
descriptor-descriptor final, ni une preuve épipolaire. La chaîne reste :
candidate Visual Index → futur matching → vérification géométrique → tracks.
descriptor-descriptor final, ni une preuve épipolaire. La chaîne courante reste :
candidate Visual Index → matching → vérification géométrique → tracks.
La génération future de paires candidates combinera `image_id`, appartenance
La génération production de paires candidates combine `image_id`, appartenance
au ScanSet, résultats du Visual Index et provenance. Une proximité temporelle
pourra servir de signal secondaire ; la proximité dans un dossier et le nom de
fichier ne constituent jamais l'identité principale.

View file

@ -8,7 +8,11 @@ L'idée centrale est de décomposer la reconstruction en couches de complexité
## Statut
**PLANNED** — Concept architectural pour la décomposition du pipeline de reconstruction. Pas encore implémenté.
**ARCHIVE / PLANNED POUR CETTE ABSTRACTION GÉNÉRIQUE.** Le type de snapshot et
les cinq layers proposés ci-dessous ne sont pas une API implémentée. Les jalons
réels Track, Sparse SfM, BA, Phase H et MVS-M1 ont depuis été acquis par leurs
contrats canoniques distincts ; ils ne valident ni ce modèle générique de
layers, ni Dense complet, mesh, texturing ou viewer.
## Place dans le pipeline
@ -124,7 +128,7 @@ Les snapshots sont persistés sur disque et consultables par le viewer.
| **Matching & Tracks** | Fournissent l'entrée de la couche 0. |
| **Geometric Constraints** | Utilisées dans toutes les couches pour filtrer et valider. |
| **Resource Governor** | Le gouverneur alloue les budgets pour chaque couche et contrôle la taille des lots. |
| **Task** | Chaque couche est une tâche candidate. Les couches sont séquencées par le scheduler. |
| **Task** | Chaque couche est une tâche candidate. Les couches sont séquencées par le runtime/Queue. |
| **Hardware Profile** | Les couches 2-4 peuvent exploiter le GPU si disponible. |
| **Image Catalog** | Les couches 2-4 lisent les images originales pour le dense matching et texturing. |
| **Viewer** | Le viewer affiche les snapshots des couches validées. |

View file

@ -34,8 +34,9 @@ Project
courante proposée au pipeline scientifique ultérieur. La sélection ne modifie
ni l'image, ni son asset, ni les résultats existants.
- Une dérivation asset bornée parent→enfant, versionnée par fingerprint, rend
possible une future image de travail RAW ou frame vidéo sans prétendre que
RAW ou vidéo sont déjà importés.
possible l'image de travail DERIVED de `raw.develop` sans confondre SOURCE et
dérivé. La frame vidéo reste future et ne doit pas être présentée comme
importée.
Un contenu identique n'implique pas une identité logique unique. Dans un même
ScanSet, le couple `(scanset_id, asset_id)` est unique : réimporter le même
@ -135,6 +136,9 @@ Track Model / Track Builder v1 peuvent consommer le catalogue et ses assets.
Les primitives géométriques Sparse SfM Gate C et le noyau incrémental
synchrone en mémoire Gate D sont **IMPLEMENTED / PASS**.
**PLANNED** — BA, orchestration Sparse SfM, MVS et relations géométriques
entre ScanSets. La vérification/scrub des assets et la réconciliation globale
restent également NOT_YET_WIRED.
**IMPLEMENTED / PASS / FROZEN aux frontières acquises** — BA finale Gate E,
orchestration Sparse SfM Gate F, admission Gate G et Phase H v1. MVS-M1 fournit
la frontière externe bornée OpenMVS, mais pas une publication dense durable ni
le pipeline dense complet. Les relations géométriques entre ScanSets,
vérification/scrub des assets et réconciliation globale restent
`NOT_YET_WIRED` ; ce futur n'annule pas les gates déjà acquises.

View file

@ -85,7 +85,7 @@ Image → [Feature 1, Feature 2, ..., Feature N]
| **Image Catalog** | Le catalogue fournit les métadonnées nécessaires à l'indexation (dimensions, modèle caméra). |
| **Image View** | Les vues peuvent filtrer les images avant indexation (par zone, par qualité). |
| **Resource Governor** | Le gouverneur alloue la mémoire pour la structure d'index et contrôle la taille des lots d'indexation. |
| **Task** | L'indexation visuelle est une tâche candidate pour le scheduler. Elle est CPU-intensive mais parallélisable. |
| **Task** | L'indexation visuelle est une tâche candidate pour le runtime/Queue. Elle est CPU-intensive mais parallélisable. |
| **Hardware Profile** | Le profil matériel détermine le type d'index optimal (KD-Tree pour peu de features, LSH pour beaucoup). |
## Contraintes de conception

View file

@ -5,10 +5,16 @@
- **OS** : Linux (testé sur distributions récentes)
- **Compilateur** : Clang (recommandé) ou GCC
- **Système de build** : Meson + Ninja
- **Dépendances** : ncursesw (ncurses avec support Unicode)
- **Langage** : C17
- **Dépendances principales** : ncursesw, SQLite, OpenSSL, GIO/GLib, OpenCV,
LibRaw, libexif, libpng, libdeflate, Ceres ; Vulkan reste optionnel
- **Langages** : API publiques C17 et implémentation mixte C17/C++17
## Installation des dépendances
## Bootstrap des outils
Les commandes ci-dessous installent seulement le compilateur, Meson/Ninja,
`pkg-config` et ncurses. Les bibliothèques listées plus haut doivent aussi être
disponibles dans les versions acceptées par `meson.build`; Meson reste la source
de vérité et refuse explicitement une dépendance absente ou incompatible.
```sh
# Debian / Ubuntu
@ -24,7 +30,11 @@ sudo pacman -S clang meson ninja ncurses pkgconf
## Build standard
```sh
CC=clang meson setup build --wipe
# Première configuration
CC=clang meson setup build
# Arbre existant
meson setup --reconfigure build
meson compile -C build -j8
```
@ -49,8 +59,69 @@ meson test -C build --print-errorlogs
# Vérification du style (whitespace)
git diff --check
# Vérification autonome d'un header C public modifié
cc -x c -std=c17 -fsyntax-only -Iinclude \
-include lardon3d/<header>.h /dev/null
```
### Preuve fraîche de maintenance globale — 1er septembre 2026
Le [registre canonique](../architecture/global_maintenance_audit.md) conserve
le détail et les qualifications. Les résultats reproductibles acquis sont :
| Configuration fraîche | Compilateurs/options | Build | Suite |
| --- | --- | ---: | ---: |
| portable | Clang/Clang++ 22.1.8, C17/C++17, `-Dvulkan_orb=disabled` | 931/931 | 64/64 sériel |
| Vulkan | Clang/Clang++ 22.1.8, C17/C++17, `-Dvulkan_orb=enabled` | 939/939 | 65/65 sériel |
| ASan/UBSan portable | Clang/Clang++ 22.1.8, `address,undefined` | graphe complet | 64/64 avec LSan désactivé après attribution externe |
| TSan portable | GCC/G++ 16.2.1, Vulkan désactivé | cibles concurrentes | 14/14 + 220 répétitions |
La suite Vulkan comprend `orb-vulkan-backend` sur la Radeon 780M RADV PHOENIX
réelle. La cible de feasibility SIFT/RootSIFT, non enregistrée dans la suite,
a été compilée/exécutée séparément : zéro divergence de décision Lowe mais des
divergences d'index et de bits de distance, donc aucune promotion en backend
production. Les probes stricts GCC/Clang C17+C++17 passent 76/76 sur les
19 headers publics modifiés ou nouveaux, ainsi que le fixture ABI, le lien
application et `git diff --check`; `scan3d/` reste intact.
L'unique revue finale indépendante GPT-5.6 SOL/ULTRA a conclu PASS sans finding
bloquant. Elle a indépendamment rejoué le build portable, la suite complète
64/64, une matrice focalisée 15/15, les 76/76 probes de headers, l'ABI, les
négatifs de seams production, le SHA-256 du manifest GV retenu et le diff-check.
Le statut canonique est donc `GLOBAL_MAINTENANCE_AUDIT=PASS/FROZEN` ; les
qualifications sanitizer ci-dessous restent néanmoins partie de la preuve.
Une validation post-freeze a ensuite attribué le délai intermittent de
`test-feature-task` à la capture de la télémétrie hôte réelle par ses Governors
synthétiques. Le fixture utilise maintenant un `ResourceSnapshot` complet,
privé, par Governor et compilé pour cette seule cible ; production continue de
lire la télémétrie réelle et n'exporte aucun seam. Après correction du second
Governor relevé en revue, Feature passe 100/100, la matrice ordonnée 4/4 et les
suites finales portable/Vulkan 64/64 et 65/65 ; ASan/UBSan ciblé avec
`detect_leaks=0` et TSan passent. Le registre canonique conserve la régression
charge 5 `WAIT`/charge 0 `START` et la qualification exacte. Un timeout `task`
isolé dans une suite normale mixte après reconstruction large reste
non reproductible : le ciblé immédiat et sa matrice de revue 100/100 passent,
sans modification de Task ni de son timeout.
La première suite LSan complète est volontairement conservée comme non-PASS :
57 OK, 6 FAIL et 1 timeout. Cinq échecs partagent exactement la fuite externe
OpenCL de 3 808 octets/68 allocations ; les deux anomalies de 30 s n'ont aucun
diagnostic sanitizer. Le délai Feature, absent du suivi initial, a ensuite été
reproduit et corrigé comme décrit ci-dessus ; le délai Task reste non
reproductible. La suite entière passe 64/64 avec ASan/UBSan actifs et
`detect_leaks=0`, tandis qu'un sous-ensemble prouvé sans loader OpenCV/OpenCL
passe 20/20 avec LSan actif. Il est donc incorrect de résumer cette preuve par
« LSan 64/64 ».
Le log Clang complet a aussi été audité. La conversion publique Sparse SfM
`uint32_t → int` était matérielle et a été corrigée avec validation ciblée ; les
autres émissions sont soit des conversions baseline déjà bornées, soit des
tests/benchmarks, soit des headers OpenCV/Ceres externes. Les emplacements et
justifications exacts restent centralisés dans le registre afin de ne pas
dupliquer une seconde liste normative ici.
## Build ASan/UBSan (debug mémoire)
À exécuter pour tout ticket touchant la mémoire, les durées de vie ou les

View file

@ -19,6 +19,9 @@ Worker thread
├── Exécution des tâches
├── Calculs métier
└── Écritures de résultats
SSD operation thread (0 ou 1, joinable)
└── Un poll ou contrôle UDisks synchrone borné, sans ncurses ni Task
```
## Règles fondamentales
@ -48,7 +51,7 @@ pthread_mutex_unlock(&queue->mutex);
### 3. Variables de condition pour la synchronisation
```c
// Producteur (scheduler)
// Producteur (caller de la Task Queue)
pthread_mutex_lock(&queue->mutex);
queue->ready = true;
pthread_cond_signal(&queue->cond);
@ -89,7 +92,7 @@ void worker_callback(task_t *task, void *userdata) {
| `pthread_cond_t` | Synchronisation producteur/consommateur |
| `pthread_create()` | Création des workers |
| `pthread_join()` | Attente de fin des workers |
| `pthread_cancel()` | Annulation d'un worker (dernier recours) |
| `pthread_cancel()` | Non utilisé pour interrompre une Task ; annulation coopérative |
## Invariants de concurrence
@ -101,7 +104,7 @@ void worker_callback(task_t *task, void *userdata) {
le même ordre pour éviter les deadlocks.
3. **Annulation coopérative** : les workers vérifient périodiquement un
drapeau d'annulation. Pas d'interruption brutale sauf dernier recours.
drapeau d'annulation. Une Task n'est pas interrompue brutalement.
4. **Réservation atomique** : la réservation du gouverneur est atomique.
Deux threads ne peuvent pas obtenir la même réservation.
@ -110,6 +113,46 @@ void worker_callback(task_t *task, void *userdata) {
invoqué sans réservation active. Cet invariant est maintenu même en
présence d'erreurs.
6. **Retraite après callback** : la notification terminale finit avant la
destruction du userdata. Queue détruit la Task hors de son mutex et ne
conserve ensuite qu'un snapshot borné.
7. **Fermeture d'ingress** : le propriétaire empêche les nouveaux appels Queue
avant `destroy()`. La fermeture interne attend le worker et chaque appel
enregistré avant le close ; elle ne peut rendre sûr un appel démarré après
la libération d'un pointeur C brut.
8. **Parallélisme scientifique propriétaire** : lorsqu'un kind emploie des
participants internes, le callback Queue demeure l'unique propriétaire. Le
nombre de participants et leur mémoire sont admis par le Governor ; seul le
propriétaire publie le préfixe durable ordonné et joint tous les enfants.
9. **Lease SSD par objet** : un lease scratch appartient à l'adresse exacte de
l'objet fourni par le caller. Tous ses champs sont lus/écrits sous le mutex
du contrôleur. Le caller lui garantit un accès exclusif et ne le copie, ne le
déplace ni ne le présente simultanément à deux contrôleurs. En production,
acquire/release passent par les wrappers Governor ; le Governor relâche son
mutex avant l'appel contrôleur, et le contrôleur ne rappelle jamais le
Governor. À la saturation légale `generation == UINT64_MAX`, seule la fin
du wrapper exact déjà sérialisé peut réconcilier sa propre opération et le
compte fondé sur les adresses ; une update publique au même watermark ne
peut pas rendre une autorité stale.
10. **Owner SSD unique** : la TUI/main demande et poll l'opération ; au plus un
thread joinable exécute une opération bornée et ne touche jamais ncurses.
Le destroy le joint avant unregister. Une observation malformée enregistre
`ERROR` et ne confère aucune autorité de contrôle ou de lease.
11. **Frontière projet** : les vues libèrent leurs borrows, puis la Queue est
annulée/jointe/détruite avant Project DB. Une Queue vide est créée ensuite.
Aucun callback terminal ne peut donc déréférencer une DB déjà fermée et
l'histoire d'un projet ne fuit pas dans le suivant.
12. **Ordre d'arrêt global** : Queue et leases Task, puis fermeture projet,
join/unregister du binding SSD, contrôleur SSD, et enfin Governor. Un
unregister encore bloqué par un lease est un échec observable, jamais un
pointeur abandonné.
## Anti-patterns
### Deadlock
@ -149,7 +192,7 @@ pthread_mutex_unlock(&task->mutex);
task_destroy(task);
task_callback(task); // task est libéré
// ✅ Correct
// ✅ Correct : le callback est entièrement revenu avant la destruction
task_callback(task);
task_destroy(task);
```
@ -171,10 +214,33 @@ meson test -C build-tsan --print-errorlogs
```
TSan détecte automatiquement :
- les race conditions
- les deadlocks potentiels
- les signaux perdus
- les verrous non libérés
- les accès concurrents conflictuels instrumentés ;
- certaines utilisations incohérentes des primitives de synchronisation.
Il ne prouve pas l'absence de deadlock, de signal perdu ou de bug dans une
bibliothèque non instrumentée. Les invariants de lifetime et d'ordre de locks
restent donc soumis aux tests déterministes et à la revue.
### Preuve TSan globale courante
La matrice fraîche emploie GCC/G++ 16.2.1 et désactive explicitement Vulkan.
Elle passe 14/14 cibles couvrant Task, Project, Queue, Governor, registre/leases
SSD, contrôleur SSD, observateur/TUI async, Candidate, Visual Index, Feature,
Matcher et GV, puis 220/220 répétitions déterministes : **234/234** au total.
La seule liste de suppressions est `tests/tsan-opencv.supp`, limitée aux objets
partagés externes non instrumentés `libopencv_features.so`,
`libopencv_core.so` et `libtbb.so`. Elle ne masque aucune frame Lardon3D. Les
warnings GCC `-Wmaybe-uninitialized` des contrôles OpenCV Feature/SIFT sont
classés non matériels : le callback fournit une Task non nulle et le helper
initialise la structure avant toute autre sortie d'échec. Les warnings OpenCV
du build GV appartiennent aux headers externes.
Cette preuve TSan ne vaut pas validation de concurrence Vulkan. Le backend
ORB Vulkan réel est couvert séparément par le build Clang Vulkan-on 939/939,
la suite 65/65 et ses tests de backend/handle/publication ; cette séparation
doit rester explicite dans tout rapport.
## Checklist de concurrence
@ -184,6 +250,6 @@ Avant de livrer un ticket touchant la concurrence :
- [ ] Les mutex sont toujours libérés (même en cas d'erreur)
- [ ] Les variables de condition sont vérifiées dans une boucle `while`
- [ ] Aucun appel ncurses depuis un worker
- [ ] L'annulation est coopérative (pas de `pthread_cancel` sauf dernier recours)
- [ ] L'annulation des Tasks est coopérative (pas de `pthread_cancel`)
- [ ] TSan ne signale aucune erreur
- [ ] Le build ASan ne signale aucune fuite mémoire liée aux threads

View file

@ -1,9 +1,13 @@
# Profil de performance de la machine cible
# Profil de performance de l'hôte de validation
Ce document décrit une politique de performance mesurée. Il ne modifie aucun
contrat de correction du Matcher, du Match Store ou du Match Result.
## Cible principale actuelle
## Hôte principal de validation
Ce profil est une preuve de performance et de stabilité pour la politique
portable. Il ne constitue ni une identité produit, ni une exigence matérielle,
ni un plafond CPU/GPU applicable aux autres hôtes.
- AMD Ryzen 7 8845HS, Zen 4, 8 cœurs et 16 threads SMT ;
- Radeon 780M à mémoire système partagée ;
@ -22,18 +26,22 @@ Le sysfs amdgpu de cet hôte expose 512 Mio dans `mem_info_vram_total` et
aperture/GTT à l'échelle de la RAM suffit à classer ce GPU shared/UMA sans
hardcoder son device ID. Le Governor débite alors les ressources GPU exactement
une fois de `MemAvailable` et n'utilise jamais cet aperture comme mémoire libre
séparée pour contourner les objectifs 3 Gio/2 Gio.
séparée pour contourner la réserve dure de 3 Gio et la zone de prudence jusqu'à
4 Gio.
Le runtime actuel de Lardon3D possède un worker lourd. Le profil interactif
établit au démarrage une baseline/plafond OpenCV de 12 threads et réserve quatre
threads logiques au desktop. Pour chaque séquence, l'unique callback Queue
applique temporairement le compte CPU immuable admis dans `1..12`, le vérifie et
établit au démarrage une baseline OpenCV depuis le compute-pool réellement
disponible. Sur cet hôte précis, le Governor réserve quatre threads logiques au
desktop et en admet douze ; douze est une observation matérielle, pas un plafond
produit. Pour chaque séquence, l'unique callback Queue applique temporairement
le compte CPU immuable admis dans `1..compute-pool`, le vérifie et
restaure la baseline sur toute sortie. `cv::setNumThreads()` reste une
configuration process-wide : sa mutation concurrente par plusieurs workers
n'est pas supportée. Un futur pool multi-worker devrait donc changer ce modèle
explicitement, pas multiplier silencieusement ces mutations globales.
Le worker Queue applique à lui-même le compute-pool `0-5,8-13`, tandis que le
Sur cette machine mesurée, le worker Queue applique à lui-même le compute-pool
`0-5,8-13`, tandis que le
creator/main reste unrestricted `0-15`. Certains helpers de cache disque Mesa
observés avaient réélargi leur affinité après l'initialisation lazy. Comme un
pidfd ne stabilise pas le TID numérique pour `sched_setaffinity(tid)`, Lardon3D
@ -96,9 +104,10 @@ environ 0,10, 0,96, 14,7 et 59,6 ms pour BFMatcher CPU lors de la campagne
production. L'initialisation lazy mesurée vaut environ 129136 ms. Le seuil
`feature_count_a × feature_count_b >= 768²` évite le GPU pour les petits travaux.
La mémoire directement contrôlée vaut 640 Kio par slot; rolling AUTO peut
réserver un ou deux slots, soit au plus 1,25 Mio, débités une fois de la RAM sur
la 780M UMA. Le backend mappe exactement cette capacité pendant la séquence et
La mémoire directement contrôlée vaut 640 Kio par slot. Rolling AUTO normal
réserve exactement un slot ; seule la couture privée de sûreté/benchmark peut
réserver deux slots, soit au plus 1,25 Mio, débités une fois de la RAM sur la
780M UMA. Le backend mappe exactement cette capacité pendant la séquence et
rend le second slot avant une admission depth 1 suivante. Les tests de parité
couvrent exactement le top-2 jusqu'à 8192, le Match File complet et le fallback
CPU. Ces nombres décrivent la machine mesurée et ne sont pas un contrat portable
@ -131,14 +140,23 @@ des proxies objectifs, pas une mesure subjective de fluidité.
Le coût court et le faible working set rendent Vulkan `NOT_JUSTIFIED` pour ce verifier sur la
Radeon 780M. Le GPU reste utilisé uniquement par le Matcher ORB existant.
Le profil production réserve un CPU logique, 4 Mio et un worker, avec lots 1/2/4/8. Un run de la
vraie Task sur 1000 parents, suivi de ses chemins de reprise/configuration, a traversé environ
La validation scientifique initiale réservait un CPU logique, 4 Mio et un
worker, avec lots 1/2/4/8. Un run de la vraie Task sur 1000 parents, suivi de
ses chemins de reprise/configuration, a traversé environ
2001 parents réutilisés en 5,870 s (environ 341/s). Ce chiffre inclut DB, checkpoints et fixture ;
il ne remplace pas la latence estimator-only Gate A. Le processus de test a culminé à 25 964 Kio
RSS. `MemAvailable` a varié de 10 702 988 à 10 692 916 Kio, sans swap ; PSI avg10 final était
0,34 % CPU et 0 % mémoire/I/O. Les latences médiane/p95 par parent et le RSS début/fin n'étaient
pas mesurables avec ce harness et ne sont donc pas extrapolés.
La maintenance ultérieure a conservé les octets GVR et l'USAC interne sériel,
mais parallélisé les parents indépendants sous un callback propriétaire. Sur le
fixture réel de 4113 parents, CPU1/2/4/8/12 donnent respectivement
60,7514/83,9556/104,7545/116,6329/119,7606 parents/s, avec digest littéral
identique. La capacité sûre est 16 participants et 16 parents par lot ; la
politique utile s'arrête à CPU8, car CPU12 n'ajoute que 2,68 %. Ces chiffres
restent une preuve locale, pas une identité ni un optimum portable.
## Feasibility Vulkan SIFT / RootSIFT
Sur le même RADV PHOENIX, `shaderFloat64`, les timestamps compute, un subgroup
@ -151,8 +169,10 @@ et 1,11×. Les quatre paires asymétriques testées perdent face au CPU. FP64
atteint environ 271 ms à 8192².
La campagne numérique trouve une divergence top-2 sur des sommes égales
adversariales pour FP32 comme FP64, des distances et Match Files différents,
mais aucune divergence Lowe sur le corpus testé. Le backend devrait donc porter
adversariales pour FP32 comme FP64. Sur 24 160 requêtes SIFT et 24 161 requêtes
RootSIFT, elle compte respectivement une divergence d'index, 20 251 divergences
de bits de distance et zéro divergence Lowe, puis une divergence d'index,
20 824 divergences de bits et zéro divergence Lowe. Le backend devrait donc porter
une identité scientifique propre et ne pourrait pas employer le fallback CPU
transparent d'ORB. Cette complexité n'est pas justifiée par le profil de
performance : SIFT et RootSIFT Vulkan sont rejetés pour Lardon3D v1 sur cette

View file

@ -23,10 +23,10 @@ en RAM ou termine dans une seule vie de processus.
[Project DB](../architecture/project_database.md).
- MVS-M1, frontière OpenMVS v2.4.0, identité dense et export COLMAP/PLY borné :
[pipeline de reconstruction](../architecture/reconstruction_pipeline.md).
- Task Runtime, checkpoints atomiques, Queue, Scheduler et Resource Governor :
- Task Runtime, checkpoints atomiques, Queue et Resource Governor :
[Task](../architecture/task_system.md), [Queue](../architecture/task_queue.md)
et [Governor](../architecture/resource_governor.md).
- Project DB v19 et S1S3 Capture / Acquisition Ingestion : provenance
- Project DB v22 gelé, overlay optique additif v23 courant, et S1S3 Capture / Acquisition Ingestion : provenance
Capture/Asset, import capture-safe, publication dérivée, développement RAW,
siblings multi-source, évidence S3-D, orchestration S3-E et campagne bornée :
[Project DB et ingestion](../architecture/project_database.md).
@ -177,15 +177,17 @@ réduction à 1024 pixels et une réservation incluant le contexte retenu plus 2
de buffers d'analyse par groupe ; ces bornes ne sont pas des limites scientifiques
de campagne ou de dataset.
Elle doit conserver une voie honnête pour les RAW sans proxy plutôt que de
présumer que chaque RAW a un JPEG sibling. La revue TUI détaillée, la guidance
de capture et les keyframes vidéo restent des intégrations ultérieures qui
réutiliseront ce même chemin de qualité, sans second pipeline.
présumer que chaque RAW a un JPEG sibling. La revue interactive détaillée des
recommandations Photo Quality, la guidance de capture et les keyframes vidéo
restent des intégrations ultérieures qui réutiliseront ce même chemin de
qualité, sans second pipeline. Cette limite ne concerne pas l'observatoire
runtime TUI courant.
## NEXT REAL-DATA MILESTONE — SELECTED SCIENTIFIC EXECUTION ON ENGINE BAY INPUTS — IMPLEMENTATION / VALIDATION IN PROGRESS
## SELECTED SCIENTIFIC EXECUTION ON ENGINE BAY INPUTS — PASS / FROZEN
L'intégration réelle opt-in porte uniquement sur les acquisitions sélectionnées
des campagnes Engine Bay et s'arrête à la frontière pré-SfM validable. Pour
chaque campagne, un exécutable éphémère crée un projet temporaire Project DB v22
chaque campagne, l'exécutable d'évidence a créé un projet temporaire Project DB v22
distinct : Visual Index v1 est borné à 4096 images, tandis que l'ensemble
A6000+S21 en compte 4497. Cette séparation est opérationnelle ; elle ne redéfinit
ni Capture, ni Asset, ni `image_id`, ni l'identité scientifique des acquisitions.
@ -202,20 +204,23 @@ qualité
→ candidats du même ScanSet
→ matching ORB
→ GV gelée
→ tracks
arrêt durable avant Tracks
```
Il rouvre ensuite le Project DB afin de rapporter l'évidence durable produite.
Il n'introduit ni coordinateur réutilisable, ni DAG, ni sidecar, ni scheduler,
ni version Project DB v23 : Task, Queue, Scheduler et Resource Governor existants
conservent leurs responsabilités.
Il n'introduit ni coordinateur réutilisable, ni DAG, ni sidecar, ni scheduler
distinct : Task, Queue et Resource Governor existants conservent leurs
responsabilités. L'overlay optique Project DB v23, ajouté ultérieurement, ne
réinterprète pas cette preuve v22 ; les copies des deux projets migrent avec les
comptes scientifiques inchangés et les nouvelles tables optiques vides.
Le Sparse SfM réel final reste `BLOCKED_BY_KNOWN_CALIBRATION_DATA` pour ces
campagnes ; aucune pseudo-calibration, interpolation de métadonnées ou inférence
d'identité ne le contourne. Dense, mesh et publication aval ne font pas partie de
ce jalon pré-SfM. Ce milestone est une intégration réelle, pas une nouvelle série
de micro-gates S3, et son statut ne revendique encore ni implémentation achevée,
ni validation réussie, ni PASS/FROZEN.
ce jalon pré-SfM. Ce milestone est une intégration réelle, pas une nouvelle
série de micro-gates S3. Son statut acquis ne rend pas disponibles les données
de calibration physique manquantes et n'autorise pas à devancer la maintenance
globale.
## INTERNAL PARALLELISM + COMPUTE RESOURCES v1 — PASS / FROZEN
@ -249,7 +254,8 @@ choix CPU/Vulkan explicites restent des overrides de debug, benchmark et
reproductibilité. Les dimensions retenues et toutes les validations v2 sont
closes.
CPU12 est validé. Le Governor dérive désormais le pool lourd depuis le masque
Le pool CPU12 est validé comme preuve de cet hôte, pas comme plafond produit.
Le Governor dérive désormais le pool lourd depuis le masque
permis et les groupes package/core/SMT. Sur l'hôte unrestricted courant, il
obtient `0-5,8-13` et réserve `6,7,14,15`; un caller déjà précontraint ne subit
pas une seconde réserve. Le worker Queue seul applique/vérifie son propre
@ -261,12 +267,15 @@ valeur explicite est préservée mais refusée. Cette politique non scientifique
supprime les helpers de cache Mesa observés qui élargissaient leur masque. Les
threads runtime restants héritent le compute-pool ; il n'existe plus de sweep,
latch ou retry auxiliaire et le diagnostic expose la politique réelle.
Creator/main/TUI reste unrestricted. Le fallback au budget portable ne crée
aucune exclusion
Creator/main/TUI reste unrestricted. Les CPU déjà exclus de l'affinité du
processus comptent dans la réserve hôte ; le fallback count-only ne fabrique
aucun masque. Le fallback au budget portable ne crée aucune exclusion
inventée. Cette couture est **PASS / FROZEN** sur le profil validé; ces IDs ne
sont pas une politique portable. La cible RAM conserve
3 GiB de `MemAvailable` et ne franchit pas intentionnellement le plancher dur de
2 GiB. Les PSI CPU/mémoire/I/O et les deltas swap-in/swap-out sont des signaux
sont pas une politique portable. La cible RAM conserve une réserve dure de
3 GiB de `MemAvailable`; la zone 34 GiB est une prudence qui ne soustrait pas
4 GiB à toute capacité. Les petits hôtes dégradent le budget en conservant au
moins une unité de calcul. Les PSI CPU/mémoire/I/O et les deltas
swap-in/swap-out sont des signaux
actifs ; l'occupation totale du swap reste historique. Admission CPU et lot
sont indépendantes. Sur la 780M, Hardware Profile classe conservativement comme
UMA le petit aperture VRAM amdgpu de 512 Mio accompagné d'environ 7,99 Go de
@ -277,7 +286,7 @@ L'audit Phase 1 couvre les 14 kinds de production et sépare leurs dimensions
fixes des dimensions réellement adaptables. Il confirme que tous passent par
l'unique Governor, même les formes fixes, et que le contrat reste immutable
pendant une séquence. Cette tranche ferme l'enveloppe privée, la sélection AUTO,
les diagnostics bornés, l'enforcement OpenCV adaptatif 1..12, la politique
les diagnostics bornés, l'enforcement OpenCV borné au compute-pool, la politique
d'affinité privée et la réconciliation du contexte retenu des campagnes
nouvelles ou restaurées. La création/reprise AUTO ne touche plus Vulkan sur le
main ; le premier begin appartient au worker contraint. Inflight ORB normal est
@ -314,8 +323,9 @@ et GPU busy DRM, avec `unknown` sur absence ou parse non strict. Le backend
Vulkan fournit des compteurs cumulatifs bornés de submit/complétion/fence/
readback/GPU/starvation/panne/discard ; Matcher agrège en plus CPU et publication
par séquence. Le diagnostic est tirable par numéro de série, sans log ncurses ni
histoire persistée. Les CPU réductibles progressent `1/2/4/8/12` après deux
observations de baseline et deux gains d'au moins 5 % ; CPU et lot ne changent
histoire persistée. Les CPU réductibles progressent par puissances de deux vers
la capacité exacte du kind/compute-pool après deux observations de baseline et
deux gains d'au moins 5 % ; CPU et lot ne changent
jamais dans le même essai. Sous pression, une admission adaptative encore
permise réserve immédiatement CPU1 et lot minimum. Feature/SIFT/RootSIFT ne
comptent un item qu'après extraction et publication durable propre ; READY,
@ -425,8 +435,9 @@ complet. Le digest Matcher reste
La seconde reprise crée zéro ligne ; une Task interrompue sur une copie dédiée
reprend le même ID et converge vers les mêmes lignes exactes. Feature,
Candidate et Matcher ne sont pas rejoués. Track Builder et Sparse SfM ne sont
pas exécutés. `REAL_S21_GV_V3=PASS/FROZEN` ; la prochaine tranche S21 commence
donc après GV, sans prétendre que Tracks est acquis.
pas exécutés. `REAL_S21_GV_V3=PASS/FROZEN` ; la prochaine tranche scientifique
S21 commencera donc à Tracks maintenant que la gate de maintenance globale
ci-dessous est fermée. Tracks n'est pas acquis.
Les gates de fermeture sont acquis :
@ -477,34 +488,99 @@ fondation scientifique pré-SfM courante
correction progression/Resource Governor, audit GPU)
→ COMPUTE GOVERNOR v2 / ORB VULKAN ASYNC EXECUTION (PASS / FROZEN)
→ REAL S21 GV v3 (PASS / FROZEN; arrêt avant Tracks)
→ GLOBAL MAINTENANCE AUDIT
(PASS / FROZEN; validation et revue finale indépendante acquises)
→ poursuite pré-SfM réelle de S21 à partir de Tracks
→ acquisition dédiée de calibration
→ Sparse SfM réel
→ Dense / MVS
```
## GLOBAL MAINTENANCE AUDIT — PASS / FROZEN
Le jalon scientifique acquis reste arrêté après GV. Avant Tracks, Sparse SfM
ou Dense/MVS, la gate de maintenance globale a réconcilié l'architecture, les
ressources, la persistance et l'observation TUI. Son état et ses preuves
consolidées sont consignés dans le
[registre canonique de maintenance](../architecture/global_maintenance_audit.md).
Les résultats déjà réglés sont :
- Project DB v23 ajoute neuf relations optiques sans backfill ni inférence ;
les boîtiers, objectifs manuels/électroniques, configurations et calibrations
restent distincts, et la sélection de calibration exige une compatibilité
exacte et explicite ;
- le Governor réserve d'abord l'hôte, sans plafond CPU global 12 : réserve de
quatre CPU logiques sur hôte capable, groupes cœur/SMT complets lorsque la
topologie est fiable, au moins une unité de calcul sur petit hôte, réserve
RAM dure 3 GiB et prudence 34 GiB, PSI/swap actif et UMA comptée une fois ;
- Candidate porte une capacité sûre 64, Visual Index 16, ORB/SIFT/RootSIFT sont
bornés par le compute-pool, Matcher conserve sa borne intrinsèque sûre 12 et
utile 8, et GV conserve son USAC interne sériel mais admet 16 participants
sûrs/8 utiles et 16 parents par lot ;
- le contrôleur SSD UDisks2 optionnel est une frontière physique revue, avec
identité Drive+labels+UUID, leases, drain sûr et latch de danger pour action
indéterminée. Son état est enregistré auprès du Governor, seul orchestrateur
des leases de production ; il n'est ni scheduler ni second Governor ;
- la TUI est un observatoire/centre de contrôle validé opérationnellement :
ncurses main-thread, modèle pur, observation coalescée et bornée, progression
durable/ETA, pipeline et ressources honnêtes, profils optiques, layouts
100×30/72×20/60×15, repli texte/couleur et F10 SSD asynchrone toujours
visible ;
- la frontière de session détruit/joint la Queue avant Project DB, puis recrée
une seule Queue ; l'arrêt global libère les leases Task avant unregister SSD,
contrôleur et Governor.
Les validations finales exécutables sont acquises : build Clang portable
931/931 + suite 64/64, build Clang Vulkan 939/939 + suite 65/65 sur Radeon
réelle, ASan/UBSan 64/64 avec la limitation LSan OpenCL externe explicitement
qualifiée, LSan loader-free 20/20, TSan 14/14 + 220 répétitions, headers publics
76/76 sur 19 headers modifiés/nouveaux et contrôles ABI/diff/`scan3d`.
L'unique revue finale indépendante GPT-5.6 SOL/ULTRA a conclu PASS sans finding
bloquant après avoir indépendamment rejoué le build portable, la suite 64/64,
15/15 tests focalisés, les 76/76 probes de headers, l'ABI, les négatifs de seams
production, le SHA du manifest GV retenu et le diff-check. La gate de
maintenance est donc fermée ; la poursuite réelle depuis Tracks devient la
prochaine tranche séparée, sans avoir été exécutée par cette synchronisation.
## NEAR TERM
### Scratch SSD externe et swap optionnel
Matériel envisagé : SSD externe d'environ 500 Go dans un boîtier USB-C 10 Gb/s.
La capacité planifiée est : détecter un disque externe adapté, le présenter dans
la configuration TUI/projet et demander si ce projet doit l'utiliser.
Le contrôleur physique actuel découvre par UDisks2 une paire exacte de labels
`LARDON_SWAP`/`LARDON_SCRATCH`, exige des UUID stables et la même identité
Drive, et ignore les renommages de nœud `/dev`. Modèle, série et vitesse USB
restent de la télémétrie optionnelle, jamais une identité produit. Les états
bornés sont `ABSENT`, `DETECTED`, `ENABLING`, `ENABLED`, `IN_USE`, `DRAINING`,
`SAFE_TO_UNPLUG` et `ERROR`.
Usages possibles : workspace/scratch, intermédiaires dense/mesh/texturing et,
sur activation explicite, swap de sécurité. Le contrat devra être possédé par
l'exécution Task, le Resource Governor et la politique temporaire du projet ;
il ne doit pas devenir un gestionnaire de stockage ad hoc.
Les usages futurs possibles sont workspace/scratch et intermédiaires
dense/mesh/texturing. Leur consommation devra être explicitement possédée par
les Tasks et passer par les wrappers de lease du Resource Governor ; le
contrôleur physique et le registre actuel n'inventent aucune éligibilité Task.
Les quatorze kinds courants ne consomment aucun scratch. Le swap de sécurité,
lui, reste une fonction du drain physique explicite et jamais un budget de
travail.
- le scratch déplace les gros intermédiaires hors de la RAM et du disque système ;
- il peut permettre des jobs bornés plus grands ;
- les leases scratch ont une ownership explicite ; `DRAINING` refuse les
nouveaux leases et attend leur libération exacte ;
- le swap n'est arrêté que si son usage est absorbable tout en conservant la
réserve hôte de 3 GiB, sans PSI élevé ni swap-in/out actif ;
- SSD/swap ne sont jamais de la RAM ni une extension du budget scientifique ;
- latence et débit USB restent distincts de la mémoire ;
- l'utilisation reste optionnelle et Governor-controlled ;
- retrait, déconnexion, ownership, nettoyage et publication atomique exigent
des sémantiques explicites ;
- aucun montage, formatage, `swapon` ou nettoyage destructif automatique n'est
actuellement implémenté ou autorisé.
- l'utilisation reste optionnelle ; le Governor demeure l'unique orchestrateur
de ressources et seul owner des leases scratch de production ;
- une action UDisks potentiellement appliquée mais non vérifiable verrouille le
tuple physique original ; un remplacement n'obtient aucune autorité ;
- aucun formatage, partitionnement, fsck, réparation, arrêt forcé ou suppression
n'est permis. La production n'appelle pas `statvfs`; espace total/libre reste
`UNKNOWN` quand UDisks ne le fournit pas.
Le contrôleur, son registre Governor, le worker joinable unique et sa
présentation/actions F10 sont **CURRENT / VALIDATED OPERATIONAL**. Les tests
emploient un provider factice et ne montent, n'arrêtent ni ne modifient un vrai
disque. Ce statut ne crée aucun consommateur scratch ; c'est la validation
consolidée et la revue indépendante ci-dessus qui ont fermé la gate globale.
### Publication durable dense / mesh
@ -517,12 +593,16 @@ dense → mesh → refinement → texturing → consolidation → export
Le scratch devient particulièrement pertinent à cette frontière.
### Workflow TUI-first
### Workflow TUI-first courant
La TUI exposera état projet, découverte de campagne, confirmation des candidats,
progression, pause/reprise/annulation disponible, Governor, choix du scratch et
étapes aval. ncurses reste au thread principal. Aucun projet GUI général n'est
introduit.
La TUI expose actuellement état projet, Tasks et contrôles disponibles,
progression durable/ETA, Governor/ressources, profils optiques et SSD F10.
ncurses reste au thread principal ; les opérations SSD bornées utilisent un
seul thread joinable sans devenir un scheduler. La découverte/édition optique
ne devine aucune identité : objectif manuel sans EXIF, profils/configurations
immuables, affectations campagne/Capture et sélection de calibration exacte
restent explicites. Le viewer général, la capture guidance, la consommation
dense du scratch et les workflows scientifiques aval restent futurs.
### Sources mixtes et multi-ScanSet
@ -795,6 +875,8 @@ CURRENT NEXT
→ COMPUTE GOVERNOR v2 / ORB VULKAN ASYNC EXECUTION
(PASS / FROZEN; AUTO GPU-first ORB et preuve S21 Matcher complète)
→ REAL S21 GV v3 (PASS / FROZEN; Tracks/Sparse SfM non exécutés)
→ GLOBAL MAINTENANCE AUDIT
(PASS / FROZEN; gate fermée avant la tranche Tracks séparée)
→ poursuite pré-SfM réelle de S21 à partir de Tracks
→ acquisition physique dédiée de calibration
→ calibration connue validée → Sparse SfM réel multi-campagne
@ -830,7 +912,7 @@ un second runtime, Governor ou système de persistance.
1. stabilité et intégrité scientifique avant débit ;
2. résultats atomiques et durables avant parallélisme ;
3. lots bornés et reprise avant taille de campagne ;
4. un Task Runtime, un Scheduler et un Resource Governor ;
4. un Task Runtime/Queue et un Resource Governor, sans second scheduler ;
5. scratch optionnel sans élargissement implicite des budgets RAM ;
6. TUI de contrôle avant visualisation riche ;
7. documentation canonique alignée sur le code validé.

View file

@ -33,6 +33,7 @@ enum {
128u)
typedef struct {
/* Arrays are caller-owned and borrowed only for encode/create/enqueue. */
const Lardon3DAcquisitionCampaignSource *sources;
size_t source_count;
const Lardon3DAcquisitionCampaignConfirmation *confirmations;
@ -40,23 +41,43 @@ typedef struct {
Lardon3DAcquisitionIngestOptions ingest_options;
} Lardon3DAcquisitionCampaignTaskRequest;
/* Create one durable, initially unqueued campaign Task for an existing
* ScanSet. request arrays are copied into the bounded deterministic codec and
* are not retained. task_id is required and receives zero on failure. The
* caller owns the returned Task and must destroy it or transfer it to a Queue.
* Exact durable checkpoint retry is idempotent; changed request bytes conflict. */
Lardon3DTask *lardon3d_project_create_acquisition_campaign_task(
Lardon3DAppState *state, uint64_t scanset_id,
const Lardon3DAcquisitionCampaignTaskRequest *request, uint64_t *task_id);
/* Create and transfer a durable campaign Task to state's Queue. Inputs follow
* the create contract; success transfers Task ownership to the Queue. task_id
* is required and is initialized to zero before validation. If transfer
* fails after durable creation, false is returned while task_id keeps only that
* recoverable durable identity; the transient Task object is destroyed. */
bool lardon3d_project_enqueue_acquisition_campaign(
Lardon3DAppState *state, uint64_t scanset_id,
const Lardon3DAcquisitionCampaignTaskRequest *request, uint64_t *task_id);
/* Request payload is treated as immutable durable input: task execution and
* checkpoint/recovery flow from the encoded blob, not from caller memory after
* enqueue. */
/* Rebuild bounded callback context from the immutable durable request.
* snapshot, the Lardon3DTaskReconstructionContext passed through context, and
* binding are required and caller-owned. On success binding owns newly
* allocated userdata through userdata_destroy. Group count/cursor and the
* exact generic kind/version are validated; no Capture identity is guessed. */
bool lardon3d_acquisition_campaign_task_reconstruct(
const Lardon3DTaskDurableSnapshot *snapshot, void *context,
Lardon3DTaskKindBinding *binding);
/* Exposed for deterministic codec validation without executing ingestion. */
/* Encode a deterministic v1 payload without retaining input storage. output
* may be NULL only with capacity==0 for a size probe. size is required: after a
* valid request it receives the exact required byte count even when capacity is
* insufficient and false is returned; invalid input or an encoding exception
* sets it to zero. On success that same value is the written byte count. */
bool lardon3d_acquisition_campaign_request_encode(
const Lardon3DAcquisitionCampaignTaskRequest *request,
unsigned char *output, size_t capacity, size_t *size);
/* Strictly decode one bounded immutable payload into caller-owned arrays.
* Their capacities must cover the encoded counts. On success request borrows
* those arrays for the caller-controlled lifetime; malformed/truncated input,
* trailing bytes, invalid enums/counts, or insufficient capacity fail. */
bool lardon3d_acquisition_campaign_request_decode(
const unsigned char *input, size_t size,
Lardon3DAcquisitionCampaignSource *sources, size_t source_capacity,

View file

@ -21,7 +21,9 @@ typedef enum {
LARDON3D_SCREEN_VIEWER,
LARDON3D_SCREEN_HELP,
LARDON3D_SCREEN_TASKS,
LARDON3D_SCREEN_RESOURCES
LARDON3D_SCREEN_RESOURCES,
LARDON3D_SCREEN_OPTICS,
LARDON3D_SCREEN_SSD
} Lardon3DScreen;
typedef struct Lardon3DAppState {

View file

@ -27,6 +27,12 @@ Lardon3DVisualIndexResult lardon3d_candidate_pair_generate_batch(
Lardon3DCandidatePairGenStats *total_stats,
uint64_t *last_feature_set_id);
/* Produce the frozen Candidate-generation v1 SHA-256 fingerprint in caller-owned
* 32-byte storage. Integer inputs are encoded at fixed width, little-endian;
* the filter is one u32 and exclude_same_asset is exactly one 0/1 byte. This
* preserves acquired little-endian fingerprints while making identity portable.
* query_options may be NULL for the historical ID-only form; fingerprint must
* be non-NULL. */
void lardon3d_candidate_pair_generation_fingerprint(
uint64_t visual_index_id, uint64_t source_feature_set_id,
const Lardon3DVisualIndexQueryOptions *query_options,

View file

@ -13,23 +13,64 @@
enum {
LARDON3D_GEOMETRIC_VERIFIER_TASK_KIND_VERSION = 1,
LARDON3D_GEOMETRIC_VERIFIER_TASK_MINIMUM_BATCH = 1,
LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_BATCH = 8,
/* Sixteen is the independently safe preparation/participant window, not a
* scientific dataset limit. Eight is the largest width with a material gain
* at the durable Task boundary; Governor admits only within that lower
* operational range. */
LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_BATCH = 16,
LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_SAFE_CPU_THREADS = 16,
LARDON3D_GEOMETRIC_VERIFIER_TASK_VALIDATED_USEFUL_CPU_THREADS = 8,
};
typedef struct {
Lardon3DGeometricVerifierParameters verifier;
} Lardon3DGeometricVerifierTaskConfiguration;
/* Creates and durably pre-creates one recoverable GV Task. `state` retains
* ownership of Project DB/Governor; the returned Task owns its private
* execution context and must be destroyed or transferred to Queue. A non-NULL
* `task_id` is initialized to zero and retains the durable ID only after Task
* creation succeeds. Each GVR identity remains the exact parent, verifier kind
* and version, and parameter fingerprint; operational CPU/batch choices do not
* alter its scientific payload. */
Lardon3DTask *lardon3d_project_create_geometric_verifier_task(
Lardon3DAppState *state,
const Lardon3DGeometricVerifierTaskConfiguration *configuration,
uint64_t *task_id);
/* Transfers the created Task to Queue on success. If durable creation succeeds
* but transfer fails, the returned false result retains its nonzero `task_id`
* so recovery can find the pending durable work; no GVR identity is guessed. */
bool lardon3d_project_enqueue_geometric_verifier_task(
Lardon3DAppState *state,
const Lardon3DGeometricVerifierTaskConfiguration *configuration,
uint64_t *task_id);
/* Registry reconstruction borrows snapshot/runtime inputs and returns one
* binding whose userdata is owned by the restored Task on success. Exact
* historical resource envelopes may be normalized in memory by Registry;
* durable scientific fingerprints and cursors remain unchanged. */
bool lardon3d_geometric_verifier_task_reconstruct(
const Lardon3DTaskDurableSnapshot *snapshot, void *context,
Lardon3DTaskKindBinding *binding);
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TASK_TESTING
/* Test-only observation of real callback contracts; one bit per admitted CPU
* width. It changes neither Governor selection nor production execution. */
void lardon3d_geometric_verifier_task_test_reset_cpu_contracts(void);
unsigned int lardon3d_geometric_verifier_task_test_cpu_contracts(void);
/* Deterministic test acknowledgement immediately before sequence_break. Arm
* before enqueue, wait for the callback, change Governor policy, then release;
* no production reservation or scheduling rule is bypassed. */
void lardon3d_geometric_verifier_task_test_arm_sequence_barrier(void);
bool lardon3d_geometric_verifier_task_test_wait_sequence_barrier(void);
void lardon3d_geometric_verifier_task_test_release_sequence_barrier(void);
/* Test-only acknowledgement after every preparation participant is joined and
* the complete batch has passed status/ownership preflight, but before the
* callback owner publishes its first row. An external control request made
* while this barrier is held must be observed only after this already-engaged
* batch is published and checkpointed. */
void lardon3d_geometric_verifier_task_test_arm_prepublication_barrier(void);
bool lardon3d_geometric_verifier_task_test_wait_prepublication_barrier(void);
void lardon3d_geometric_verifier_task_test_release_prepublication_barrier(void);
#endif
#endif

View file

@ -3,9 +3,19 @@
#include <lardon3d/app_state.h>
#include <lardon3d/import_task.h>
#include <lardon3d/task_queue.h>
#include <lardon3d/resource_governor.h>
#include <lardon3d/runtime_observer.h>
#include <lardon3d/tui_model.h>
#include <lardon3d/tui_optics.h>
#include <lardon3d/tui_ssd_async.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Frozen legacy renderer ABI. It accepts only the historical bounded Task and
* resource views; the implementation adapts them without reading beyond any
* caller object compiled against the original declarations. Main thread only.
*/
void lardon3d_layout_draw(
const Lardon3DAppState *state,
const char *input_text,
@ -19,4 +29,29 @@ void lardon3d_layout_draw(
int cols
);
/* Main-thread-only extended renderer. `state` and `runtime` are required;
* import/SSD/optics/palette views are nullable. Every supplied view is a
* borrowed, bounded caller-owned copy for this call; rendering performs no
* Queue, Governor, Project DB, controller, or scientific mutation.
* interaction_mode is the actual input owner and is the sole source of footer
* capabilities. */
void lardon3d_layout_draw_runtime(
const Lardon3DAppState *state,
const char *input_text,
const char *input_label,
const Lardon3DImportTaskSnapshot *import_snapshot,
const Lardon3DRuntimeSnapshot *runtime,
const Lardon3DTuiSsdAsyncSnapshot *ssd_operation,
const Lardon3DTuiOpticsSnapshot *optics,
size_t selected_task,
const Lardon3DTuiPalette *palette,
Lardon3DTuiInteractionMode interaction_mode,
int rows,
int cols
);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -0,0 +1,254 @@
#ifndef LARDON3D_OPTICAL_PROFILES_H
#define LARDON3D_OPTICAL_PROFILES_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/project_db.h>
#ifdef __cplusplus
extern "C" {
#endif
enum {
LARDON3D_OPTICAL_TEXT_CAPACITY = 128,
LARDON3D_OPTICAL_PROVENANCE_CAPACITY = 256,
LARDON3D_OPTICAL_PAGE_MAX = 128,
};
typedef enum {
/* No electronic identity is required. A manual lens legitimately has no
* metadata alias and is selected only through an explicit profile/config. */
LARDON3D_OPTICAL_LENS_MANUAL = 1,
LARDON3D_OPTICAL_LENS_ELECTRONIC = 2,
LARDON3D_OPTICAL_LENS_INTEGRATED = 3,
} Lardon3DOpticalLensInterface;
typedef enum {
LARDON3D_OPTICAL_FOCAL_RANGE_UNKNOWN = 1,
LARDON3D_OPTICAL_FOCAL_RANGE_PRIME = 2,
LARDON3D_OPTICAL_FOCAL_RANGE_ZOOM = 3,
} Lardon3DOpticalFocalRangeKind;
typedef enum {
LARDON3D_OPTICAL_ASSIGNMENT_CAMPAIGN = 1,
LARDON3D_OPTICAL_ASSIGNMENT_CALLER_EXPLICIT = 2,
} Lardon3DOpticalAssignmentProvenance;
typedef enum {
/* The profile applies only to its exact optical_configuration_id. It never
* authorizes interpolation or borrowing across a body, lens, or focal setup. */
LARDON3D_OPTICAL_CALIBRATION_EXACT_CONFIGURATION = 1,
} Lardon3DOpticalCalibrationApplicability;
typedef struct {
uint64_t camera_body_profile_id;
char manufacturer[LARDON3D_OPTICAL_TEXT_CAPACITY];
char model[LARDON3D_OPTICAL_TEXT_CAPACITY];
char name[LARDON3D_OPTICAL_TEXT_CAPACITY];
} Lardon3DOpticalCameraBodyProfile;
typedef struct {
uint64_t alias_id;
uint64_t camera_body_profile_id;
char metadata_make[LARDON3D_OPTICAL_TEXT_CAPACITY];
char metadata_model[LARDON3D_OPTICAL_TEXT_CAPACITY];
} Lardon3DOpticalCameraBodyAlias;
typedef struct {
uint64_t lens_profile_id;
char manufacturer[LARDON3D_OPTICAL_TEXT_CAPACITY];
char model[LARDON3D_OPTICAL_TEXT_CAPACITY];
char name[LARDON3D_OPTICAL_TEXT_CAPACITY];
Lardon3DOpticalLensInterface interface_kind;
Lardon3DOpticalFocalRangeKind focal_range_kind;
/* Micrometres make profile identity locale-independent and exact. Unknown
* range uses 0/0; a prime uses equal positive values; a zoom uses min<max. */
uint32_t minimum_focal_um;
uint32_t maximum_focal_um;
} Lardon3DOpticalLensProfile;
typedef struct {
uint64_t alias_id;
uint64_t lens_profile_id;
char metadata_make[LARDON3D_OPTICAL_TEXT_CAPACITY];
char metadata_model[LARDON3D_OPTICAL_TEXT_CAPACITY];
} Lardon3DOpticalLensAlias;
typedef struct {
uint64_t optical_configuration_id;
uint64_t camera_body_profile_id;
uint64_t lens_profile_id;
bool has_focal_length;
uint32_t focal_length_um;
} Lardon3DOpticalConfiguration;
typedef struct {
uint64_t campaign_task_id;
uint32_t group_id;
uint64_t optical_configuration_id;
} Lardon3DOpticalCampaignGroupAssignment;
typedef struct {
uint64_t capture_id;
uint64_t optical_configuration_id;
Lardon3DOpticalAssignmentProvenance provenance;
bool has_campaign_origin;
uint64_t campaign_task_id;
uint32_t campaign_group_id;
} Lardon3DOpticalCaptureAssignment;
typedef struct {
uint64_t calibration_profile_id;
uint64_t optical_configuration_id;
/* Existing immutable sparse calibration; numerical coefficients, model,
* dimensions and scientific provenance remain owned by that v22 object. */
uint64_t sparse_calibration_id;
char name[LARDON3D_OPTICAL_TEXT_CAPACITY];
uint32_t profile_version;
char provenance[LARDON3D_OPTICAL_PROVENANCE_CAPACITY];
Lardon3DOpticalCalibrationApplicability applicability;
int64_t created_at;
} Lardon3DOpticalCalibrationProfile;
typedef struct {
uint64_t capture_id;
uint64_t calibration_profile_id;
uint64_t optical_configuration_id;
uint64_t sparse_calibration_id;
} Lardon3DOpticalCaptureCalibrationSelection;
/* All profile/config creation calls borrow input only for the call and return a
* caller-owned, NUL-terminated copy; input and output storage must not overlap.
* The generated row-ID field in a create input must be zero; referenced IDs
* must fit positive SQLite INTEGER values.
* For otherwise valid arguments, every non-OK scalar-load/create result zeroes
* its output. List capacity must be 1..OPTICAL_PAGE_MAX; failures clear the
* caller-owned item array/count and restore next_after_id to the supplied
* cursor. Successful pages are ascending. Exact natural-identity retries return
* the existing row; the same user-visible identity with conflicting properties
* is CONSTRAINT. A stored row whose own SQLite types or property bounds are
* malformed is CORRUPT rather than a valid property conflict. */
Lardon3DProjectDbResult lardon3d_optical_camera_body_create(
Lardon3DProjectDb *database, const Lardon3DOpticalCameraBodyProfile *input,
Lardon3DOpticalCameraBodyProfile *output);
Lardon3DProjectDbResult lardon3d_optical_camera_body_load(
Lardon3DProjectDb *database, uint64_t camera_body_profile_id,
Lardon3DOpticalCameraBodyProfile *output);
Lardon3DProjectDbResult lardon3d_optical_camera_body_list(
Lardon3DProjectDb *database, uint64_t after_profile_id,
Lardon3DOpticalCameraBodyProfile *items, size_t capacity, size_t *count,
uint64_t *next_after_profile_id);
/* Aliases are exact BINARY metadata make/model pairs: no case folding, fuzzy
* lookup, EXIF inference, path lookup, or default device branch is performed.
* Text inputs are borrowed for the call and must not overlap output storage. A
* pair maps to at most one profile. Alias lists are ordered by alias_id. */
Lardon3DProjectDbResult lardon3d_optical_camera_body_alias_add(
Lardon3DProjectDb *database, uint64_t camera_body_profile_id,
const char *metadata_make, const char *metadata_model,
Lardon3DOpticalCameraBodyAlias *output);
Lardon3DProjectDbResult lardon3d_optical_camera_body_find_exact_alias(
Lardon3DProjectDb *database, const char *metadata_make,
const char *metadata_model, Lardon3DOpticalCameraBodyProfile *output);
Lardon3DProjectDbResult lardon3d_optical_camera_body_alias_list(
Lardon3DProjectDb *database, uint64_t camera_body_profile_id,
uint64_t after_alias_id, Lardon3DOpticalCameraBodyAlias *items,
size_t capacity, size_t *count, uint64_t *next_after_alias_id);
Lardon3DProjectDbResult lardon3d_optical_lens_create(
Lardon3DProjectDb *database, const Lardon3DOpticalLensProfile *input,
Lardon3DOpticalLensProfile *output);
Lardon3DProjectDbResult lardon3d_optical_lens_load(
Lardon3DProjectDb *database, uint64_t lens_profile_id,
Lardon3DOpticalLensProfile *output);
Lardon3DProjectDbResult lardon3d_optical_lens_list(
Lardon3DProjectDb *database, uint64_t after_profile_id,
Lardon3DOpticalLensProfile *items, size_t capacity, size_t *count,
uint64_t *next_after_profile_id);
Lardon3DProjectDbResult lardon3d_optical_lens_alias_add(
Lardon3DProjectDb *database, uint64_t lens_profile_id,
const char *metadata_make, const char *metadata_model,
Lardon3DOpticalLensAlias *output);
Lardon3DProjectDbResult lardon3d_optical_lens_find_exact_alias(
Lardon3DProjectDb *database, const char *metadata_make,
const char *metadata_model, Lardon3DOpticalLensProfile *output);
Lardon3DProjectDbResult lardon3d_optical_lens_alias_list(
Lardon3DProjectDb *database, uint64_t lens_profile_id,
uint64_t after_alias_id, Lardon3DOpticalLensAlias *items, size_t capacity,
size_t *count, uint64_t *next_after_alias_id);
/* A configuration is the exact body+lens context plus an optional focal value
* encoded as integer micrometres. Known prime/zoom bounds are enforced. A
* missing focal value is not an inferred value and distinct 16/24/50 mm
* configurations remain distinct rows. */
Lardon3DProjectDbResult lardon3d_optical_configuration_create(
Lardon3DProjectDb *database, const Lardon3DOpticalConfiguration *input,
Lardon3DOpticalConfiguration *output);
Lardon3DProjectDbResult lardon3d_optical_configuration_load(
Lardon3DProjectDb *database, uint64_t optical_configuration_id,
Lardon3DOpticalConfiguration *output);
Lardon3DProjectDbResult lardon3d_optical_configuration_list(
Lardon3DProjectDb *database, uint64_t after_configuration_id,
Lardon3DOpticalConfiguration *items, size_t capacity, size_t *count,
uint64_t *next_after_configuration_id);
/* A one-based campaign group can be assigned only while its durable cursor is
* zero and no Capture mapping exists. Exact retries remain idempotent; another
* configuration is CONSTRAINT. Unassigned groups are valid and unresolved.
* Task/group IDs remain operational provenance and never become Capture
* identity. */
Lardon3DProjectDbResult lardon3d_optical_campaign_group_assign(
Lardon3DProjectDb *database, uint64_t campaign_task_id, uint32_t group_id,
uint64_t optical_configuration_id);
Lardon3DProjectDbResult lardon3d_optical_campaign_group_load(
Lardon3DProjectDb *database, uint64_t campaign_task_id, uint32_t group_id,
Lardon3DOpticalCampaignGroupAssignment *output);
/* Explicit post-import assignment is allowed only when the Capture has no
* assignment. Its exact retry is idempotent and any other configuration or
* campaign provenance conflicts. Absence (NOT_FOUND on load) is the sole
* unresolved representation; no fabricated "unknown" profile is created. */
Lardon3DProjectDbResult lardon3d_optical_capture_assign_explicit(
Lardon3DProjectDb *database, uint64_t capture_id,
uint64_t optical_configuration_id);
Lardon3DProjectDbResult lardon3d_optical_capture_assignment_load(
Lardon3DProjectDb *database, uint64_t capture_id,
Lardon3DOpticalCaptureAssignment *output);
/* Calibration profiles bind metadata to an existing immutable sparse
* calibration and exactly one optical configuration. Creation never computes,
* copies, interpolates, or fabricates calibration coefficients. Exact creation
* retry is idempotent and a valid immutable-property conflict is CONSTRAINT;
* malformed durable profile state or its sparse-calibration dependency is
* CORRUPT and takes precedence over caller-property comparison. Compatible lists
* are bounded and ordered by calibration_profile_id; zero rows is valid. */
Lardon3DProjectDbResult lardon3d_optical_calibration_profile_create(
Lardon3DProjectDb *database,
const Lardon3DOpticalCalibrationProfile *input,
Lardon3DOpticalCalibrationProfile *output);
Lardon3DProjectDbResult lardon3d_optical_calibration_profile_load(
Lardon3DProjectDb *database, uint64_t calibration_profile_id,
Lardon3DOpticalCalibrationProfile *output);
Lardon3DProjectDbResult lardon3d_optical_calibration_profile_list_compatible(
Lardon3DProjectDb *database, uint64_t optical_configuration_id,
uint64_t after_profile_id, Lardon3DOpticalCalibrationProfile *items,
size_t capacity, size_t *count, uint64_t *next_after_profile_id);
/* Selection is always explicit. The Capture assignment and profile must name
* exactly the same configuration; incompatibility is CONSTRAINT. Exact retry
* is idempotent, conflict is rejected, and multiple compatible profiles remain
* ambiguous/NOT_FOUND until one is selected. */
Lardon3DProjectDbResult lardon3d_optical_capture_calibration_select(
Lardon3DProjectDb *database, uint64_t capture_id,
uint64_t calibration_profile_id);
Lardon3DProjectDbResult lardon3d_optical_capture_calibration_selection_load(
Lardon3DProjectDb *database, uint64_t capture_id,
Lardon3DOpticalCaptureCalibrationSelection *output);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -11,7 +11,10 @@
#include <lardon3d/sparse_sfm_incremental.h>
enum {
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 22,
/* v23 is an additive optical-context layer. Historical v22 rows and sparse
* calibration identities remain untouched; migration never guesses profiles
* or assignments for existing Captures. */
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 23,
LARDON3D_PROJECT_DB_ID_CAPACITY = 65,
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,
@ -104,6 +107,8 @@ typedef struct {
typedef struct {
uint64_t task_id;
uint64_t scanset_id;
/* Zero-based next-work position in 0..group_count. Its numeric value is the
* number of one-based campaign groups whose Capture mapping is durable. */
uint32_t next_group_id;
uint32_t group_count;
const unsigned char *request;
@ -691,24 +696,57 @@ Lardon3DProjectDbResult lardon3d_project_db_list_candidate_pairs(
Lardon3DProjectDbResult
lardon3d_project_db_load_image_import(Lardon3DProjectDb *database, uint64_t task_id,
Lardon3DProjectDbImageImport *parameters);
/* Atomically record generic Task/checkpoint state and one immutable typed
* campaign request for an existing ScanSet. All pointers are required except
* checkpoint; request bytes are borrowed only for the call. task_kind/version
* must exactly identify acquisition_campaign.run/v1. Exact replay is
* idempotent, while a changed request or generic kind conflicts. The cursor is
* the zero-based next-work position and never a Capture or scientific image
* identity. */
Lardon3DProjectDbResult lardon3d_project_db_record_acquisition_campaign_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
const char *task_kind, uint32_t task_kind_version,
const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbAcquisitionCampaignTask *parameters, int64_t updated_at);
/* Load and validate the typed campaign row and its exact generic kind/version.
* With request==NULL and request_capacity==0, this is a size probe: metadata
* and request_size are returned while parameters->request remains NULL.
* Otherwise request is required caller-owned storage borrowed into parameters
* on success. Insufficient capacity returns CONSTRAINT without truncation;
* malformed SQLite types, signs, bounds, cursor relation, dispatch identity, or
* a retained mapping set other than the exact one-based prefix 1..next_group_id
* return CORRUPT. Every retained Capture must still exist in this campaign's
* ScanSet; validation is bounded by the frozen 4096-group maximum. */
Lardon3DProjectDbResult lardon3d_project_db_load_acquisition_campaign_task(
Lardon3DProjectDb *database, uint64_t task_id, unsigned char *request,
size_t request_capacity, Lardon3DProjectDbAcquisitionCampaignTask *parameters);
/* Retain the explicit one-based group_id -> capture_id mapping and advance the
* zero-based next-work position atomically. next_group_id must equal group_id;
* exact retry is idempotent, conflicting identity/order is rejected. Before
* mutation, the generic kind/version, SQLite storage classes and complete
* retained prefix are validated signed-wide; malformed durable state returns
* CORRUPT, while a valid caller conflict returns CONSTRAINT. The Capture must
* already exist in the campaign ScanSet. In v23, any explicit group optical
* assignment is copied to Capture provenance in this same transaction before
* cursor publication; an unassigned group remains honestly unresolved. Generic
* Task progress may advance only after this call succeeds. */
Lardon3DProjectDbResult lardon3d_project_db_retain_acquisition_campaign_capture(
Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id,
uint64_t capture_id, uint32_t next_group_id);
/* Load one explicit retained mapping into caller-owned output. task_id and
* group_id must be positive; persisted SQLite types, bounds, Capture identity,
* plan range, zero-based cursor, exact durable prefix, Capture ScanSet, and
* generic campaign kind/version are validated before narrowing. A mapping ahead
* of the cursor or across ScanSets is CORRUPT and never yields a resume ID. No
* identity is inferred from request bytes or metadata. */
Lardon3DProjectDbResult lardon3d_project_db_load_acquisition_campaign_capture(
Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id,
Lardon3DProjectDbAcquisitionCampaignCapture *capture);
/* Atomically record generic Task state and the immutable typed request for an
* existing ScanSet. All pointers are required except checkpoint; request bytes
* are borrowed only for the call. The request identifies campaign groups
* operationally and never creates or infers Capture/Asset/image identities. */
* are borrowed only for the call. task_kind/version must exactly identify
* photo_quality.triage/v1. The request identifies campaign groups operationally
* and never creates or infers Capture/Asset/image identities. */
Lardon3DProjectDbResult lardon3d_project_db_record_photo_quality_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
const char *task_kind, uint32_t task_kind_version,
@ -716,7 +754,10 @@ Lardon3DProjectDbResult lardon3d_project_db_record_photo_quality_task(
const Lardon3DProjectDbPhotoQualityTask *parameters, int64_t updated_at);
/* On success, parameters receives validated one-based cursor/count fields and
* borrows caller-owned request storage containing the immutable request bytes.
* Insufficient capacity is reported as corrupt durable state, never truncated. */
* The generic kind/version and existing ScanSet identity must match exactly.
* For otherwise valid arguments, any non-OK result zeroes parameters and leaves
* request storage untouched. A present typed row with a missing parent is
* CORRUPT, not NOT_FOUND; insufficient capacity is also CORRUPT, never truncated. */
Lardon3DProjectDbResult lardon3d_project_db_load_photo_quality_task(
Lardon3DProjectDb *database, uint64_t task_id, unsigned char *request,
size_t request_capacity, Lardon3DProjectDbPhotoQualityTask *parameters);
@ -784,6 +825,9 @@ Lardon3DProjectDbResult lardon3d_project_db_create_selected_execution(
Lardon3DProjectDbResult lardon3d_project_db_load_selected_execution(
Lardon3DProjectDb *database, uint64_t execution_id,
Lardon3DProjectDbSelectedExecution *execution);
/* Load one immutable bridge item into caller-owned output. Required and
* nullable identifiers must use SQLite INTEGER/NULL storage exactly; numeric
* TEXT/REAL values are corruption even if SQLite could coerce them. */
Lardon3DProjectDbResult lardon3d_project_db_load_selected_execution_item(
Lardon3DProjectDb *database, uint64_t execution_id, uint32_t item_index,
Lardon3DProjectDbSelectedExecutionItem *item);

View file

@ -8,18 +8,29 @@
#include <lardon3d/hardware_profile.h>
#include <lardon3d/resource_snapshot.h>
#include <lardon3d/ssd_controller.h>
enum {
LARDON3D_RESOURCE_REASON_CAPACITY = 256,
LARDON3D_RESOURCE_EXTERNAL_IDENTITY_CAPACITY = 256,
};
typedef struct Lardon3DResourceGovernor Lardon3DResourceGovernor;
typedef struct Lardon3DResourceReservation Lardon3DResourceReservation;
typedef struct {
/* Host RAM below this MemAvailable floor is never assigned to new work.
* The default keeps approximately 3 GiB on capable hosts; smaller hosts
* use a deterministic fractional reserve. This is an operational desktop
* safety budget, never a scientific dataset-size limit. */
uint64_t system_memory_reserve_bytes;
/* Custom policies may set a lower emergency threshold for immediate RED
* pressure. The default equals the normal reserve; its separate 4 GiB
* caution band is private Governor policy and does not reduce capacity. */
uint64_t emergency_memory_floor_bytes;
uint64_t gpu_memory_reserve_bytes;
/* Requested host reserve in logical CPUs. Complete topology may reserve a
* minimally larger whole-core group, while at least one compute CPU remains. */
unsigned int system_cpu_reserve;
double maximum_cpu_load_ratio;
double maximum_cpu_pressure_avg10;
@ -120,6 +131,40 @@ typedef struct {
size_t active_reservations;
} Lardon3DResourceAvailability;
/* External storage is physical operational capacity, never Task/scientific
* identity and never an extension of host RAM. `generation` is the monotonic
* source/controller generation; the Governor has a separate aggregate change
* generation. Unknown byte metrics have their *_known flag clear and value
* zero. Strings are always NUL-terminated bounded copies; AVAILABLE, IN_USE,
* DRAINING and SAFE require a nonempty exact stable identity. Controller
* generation UINT64_MAX is legal saturation, not an invalid sentinel. */
typedef enum {
LARDON3D_RESOURCE_EXTERNAL_STORAGE_ABSENT = 0,
LARDON3D_RESOURCE_EXTERNAL_STORAGE_DETECTED,
LARDON3D_RESOURCE_EXTERNAL_STORAGE_AVAILABLE,
LARDON3D_RESOURCE_EXTERNAL_STORAGE_IN_USE,
LARDON3D_RESOURCE_EXTERNAL_STORAGE_DRAINING,
LARDON3D_RESOURCE_EXTERNAL_STORAGE_SAFE,
LARDON3D_RESOURCE_EXTERNAL_STORAGE_ERROR,
} Lardon3DResourceExternalStorageStatus;
typedef struct {
uint64_t generation;
Lardon3DResourceExternalStorageStatus status;
bool new_scratch_allocations_allowed;
bool scratch_total_known;
bool scratch_free_known;
uint64_t scratch_total_bytes;
uint64_t scratch_free_bytes;
bool swap_total_known;
bool swap_used_known;
uint64_t swap_total_bytes;
uint64_t swap_used_bytes;
size_t active_scratch_leases;
char stable_identity[LARDON3D_RESOURCE_EXTERNAL_IDENTITY_CAPACITY];
char reason[LARDON3D_RESOURCE_REASON_CAPACITY];
} Lardon3DResourceExternalStorage;
bool lardon3d_resource_policy_default(
const Lardon3DHardwareProfile *profile,
Lardon3DResourcePolicy *policy
@ -128,6 +173,9 @@ Lardon3DResourceGovernor *lardon3d_resource_governor_create(
const Lardon3DHardwareProfile *profile,
const Lardon3DResourcePolicy *policy
);
/* No call may race with destruction. A registered external controller must be
* unregistered and every Governor scratch lease released first; the bound TUI
* adapter performs this ordering during normal application shutdown. */
void lardon3d_resource_governor_destroy(
Lardon3DResourceGovernor *governor
);
@ -135,6 +183,13 @@ bool lardon3d_resource_governor_set_policy(
Lardon3DResourceGovernor *governor,
const Lardon3DResourcePolicy *policy
);
/* Copies the currently active operational policy under the Governor mutex.
* The caller owns the output. Observation cannot mutate admission, reserve
* resources, or turn swap/scratch into RAM capacity. */
bool lardon3d_resource_governor_get_policy(
Lardon3DResourceGovernor *governor,
Lardon3DResourcePolicy *policy
);
bool lardon3d_resource_governor_decide(
Lardon3DResourceGovernor *governor,
const Lardon3DResourceSnapshot *snapshot,
@ -185,6 +240,77 @@ bool lardon3d_resource_governor_availability(
const Lardon3DResourceSnapshot *snapshot,
Lardon3DResourceAvailability *availability
);
/* Validates and converts one controller-owned bounded snapshot into the
* Governor's physical-storage vocabulary. `storage` is initialized to zero on
* every failure. Pairing, allocation or control authority requires current
* detection of both partitions, exact Drive/UUID identity and positive known
* partition extents. Contradictory ABSENT facts are rejected; a disconnected
* sticky hazard remains representable only as non-authoritative ERROR. Missing
* optional telemetry remains unknown. This pure conversion performs no
* controller call and acquires no lock. */
bool lardon3d_resource_external_storage_from_ssd_snapshot(
const Lardon3DSsdSnapshot *snapshot,
Lardon3DResourceExternalStorage *storage
);
/* Registers one exact borrowed controller object and copies its initial state
* under the Governor mutex. The controller must outlive the registration.
* Registration is exclusive and fails if a controller is already registered;
* update exact-retries are idempotent. A valid newer source generation may
* change state, while stale or materially different equal-generation public
* data cannot restore availability. A conservative ERROR may replace
* same/older evidence to fail closed. The exact serialized scratch wrapper may
* reconcile its own completion at a saturated UINT64_MAX watermark; this
* exception is private provenance and is unavailable to update(). Material
* changes wake Governor generation waiters; source-generation-only refreshes
* do not.
* Unregister is rejected during a wrapper operation or while the last exact
* snapshot reports any active lease. Outputs from get are caller-owned and
* zeroed on failure/unregistered state. */
bool lardon3d_resource_governor_register_external_storage(
Lardon3DResourceGovernor *governor,
Lardon3DSsdController *controller,
const Lardon3DResourceExternalStorage *storage
);
bool lardon3d_resource_governor_update_external_storage(
Lardon3DResourceGovernor *governor,
Lardon3DSsdController *controller,
const Lardon3DResourceExternalStorage *storage
);
bool lardon3d_resource_governor_unregister_external_storage(
Lardon3DResourceGovernor *governor,
Lardon3DSsdController *controller
);
bool lardon3d_resource_governor_get_external_storage(
Lardon3DResourceGovernor *governor,
Lardon3DResourceExternalStorage *storage
);
/* Production scratch ownership crosses the physical-controller boundary only
* through these wrappers. A lease object remains caller-owned, exclusive, and
* unmoved exactly as required by the low-level controller. Acquire requires
* the exact registered controller and current allocation authority; draining,
* ERROR, absent, stale, or unregistered state fails closed. Release remains
* available for an exact wrapper-acquired lease during drain/ERROR. The
* Governor never holds its mutex while entering the controller, and the
* controller never calls back into the Governor. Registration/controller/
* Governor must outlive every successful lease; unregister is rejected while
* one remains. */
bool lardon3d_resource_governor_acquire_scratch(
Lardon3DResourceGovernor *governor,
Lardon3DSsdController *controller,
Lardon3DSsdScratchLease *lease
);
bool lardon3d_resource_governor_release_scratch(
Lardon3DResourceGovernor *governor,
Lardon3DSsdController *controller,
Lardon3DSsdScratchLease *lease
);
const char *lardon3d_resource_external_storage_status_name(
Lardon3DResourceExternalStorageStatus status
);
uint64_t lardon3d_resource_governor_generation(
Lardon3DResourceGovernor *governor
);

View file

@ -33,6 +33,16 @@ typedef struct {
uint64_t swap_pages_out;
} Lardon3DResourceSnapshot;
/* Additive host observation. The legacy snapshot remains ABI-exact and is
* embedded by value; SwapTotal is telemetry only and never enlarges the
* Governor RAM budget. A successful capture makes swap_total_known true,
* including on a host whose exact total is zero. */
typedef struct {
Lardon3DResourceSnapshot snapshot;
bool swap_total_known;
uint64_t swap_total_bytes;
} Lardon3DResourceObservation;
bool lardon3d_resource_snapshot_capture(
const Lardon3DHardwareProfile *profile,
Lardon3DResourceSnapshot *snapshot,
@ -40,4 +50,18 @@ bool lardon3d_resource_snapshot_capture(
size_t error_message_size
);
/* Captures the same bounded /proc and GPU observation as the legacy entry
* point plus exact SwapTotal telemetry. `profile` and `observation` are
* required; a non-NULL observation is zeroed before validation and remains
* caller-owned. `error_message` may be NULL; when supplied with positive size
* it is always NUL-terminated. No retained pointer or reservation is
* created, and UMA availability remains the legacy snapshot's one host-RAM
* quantity rather than an additional budget. */
bool lardon3d_resource_observation_capture(
const Lardon3DHardwareProfile *profile,
Lardon3DResourceObservation *observation,
char *error_message,
size_t error_message_size
);
#endif

View file

@ -0,0 +1,72 @@
#ifndef LARDON3D_RUNTIME_OBSERVER_H
#define LARDON3D_RUNTIME_OBSERVER_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/hardware_profile.h>
#include <lardon3d/resource_governor.h>
#include <lardon3d/ssd_controller.h>
#include <lardon3d/task_queue.h>
#include <lardon3d/tui_model.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct Lardon3DRuntimeObserver Lardon3DRuntimeObserver;
typedef struct {
uint64_t generation;
uint64_t captured_monotonic_ns;
bool stale;
char status[LARDON3D_TUI_TEXT_CAPACITY];
Lardon3DTaskObservation tasks[LARDON3D_TUI_TASK_CAPACITY];
size_t task_count;
Lardon3DTaskQueueSummary task_summary;
bool active_task_known;
size_t active_task_index;
Lardon3DTuiProgressView active_progress;
Lardon3DTuiStageView stages[LARDON3D_TUI_STAGE_COUNT];
Lardon3DTuiResourceView resources;
bool ssd_controller_available;
Lardon3DSsdSnapshot ssd;
} Lardon3DRuntimeSnapshot;
/* Creates a passive observer over borrowed runtime owners. Queue and Governor
* must outlive it; Hardware Profile is required only for this call and is
* copied by value. Observation owns no Task/reservation and never changes
* admission. Governor-registered SSD usage is copied here under the Governor
* mutex; physical D-Bus identity/action observation still belongs to the
* TUI's sole bounded SSD worker and is merged only into the separate physical
* view. */
Lardon3DRuntimeObserver *lardon3d_runtime_observer_create(
const Lardon3DHardwareProfile *profile,
Lardon3DTaskQueue *queue,
Lardon3DResourceGovernor *governor
);
void lardon3d_runtime_observer_destroy(Lardon3DRuntimeObserver *observer);
/* Copies a caller-owned coherent view. Ordinary calls are coalesced for at
* least one monotonic second, bounding Queue/Governor and /proc work. The SSD
* worker independently applies the same minimum telemetry cadence. `force` is
* reserved for explicit user refresh/testing, never per-frame use.
* A failed refresh preserves the last bounded view, marks it stale, and
* returns false; no internal pointer escapes. Thread-safe only through the
* borrowed ownerscall this object from its single TUI/main owner. */
bool lardon3d_runtime_observer_refresh(
Lardon3DRuntimeObserver *observer,
bool project_loaded,
bool force,
Lardon3DRuntimeSnapshot *snapshot
);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -0,0 +1,30 @@
#ifndef LARDON3D_RUNTIME_SESSION_H
#define LARDON3D_RUNTIME_SESSION_H
#include <stdbool.h>
#include <stddef.h>
#include <lardon3d/app_state.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Establishes the interactive Project/Queue lifetime boundary. The existing
* sole Queue is cancelled, joined, and destroyed (including finished
* callbacks and retained history) before the Project DB is closed; only then
* is one empty Queue created with a fresh ID namespace. `state`, its Governor,
* and a positive capacity are required. On allocation failure the Project is
* still safely closed and state->task_queue is NULL. Main-thread owner only;
* callers must release Queue/DB observers before entry and rebind them after.
*/
bool lardon3d_runtime_project_boundary(
Lardon3DAppState *state,
size_t queue_capacity
);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -53,6 +53,14 @@ typedef struct {
typedef struct {
double robust_threshold_px;
double confidence;
/*
* OpenCV's robust-estimator ABI accepts signed iteration/inlier counts.
* max_iterations must be in [1, INT_MAX] and minimum_inliers in
* [0, INT_MAX]. A value outside those bounds returns INVALID_ARGUMENT
* before allocation, OpenCV execution, or mutation of the caller-owned
* result and inlier mask. The signed bound is operational only; it does not
* change the FROZEN defaults or scientific identity of representable runs.
*/
uint32_t max_iterations;
uint32_t minimum_inliers;
double minimum_inlier_ratio;
@ -73,6 +81,14 @@ typedef struct {
typedef struct {
double reprojection_threshold_px;
double confidence;
/*
* OpenCV's robust-estimator ABI accepts signed iteration/inlier counts.
* max_iterations must be in [1, INT_MAX] and minimum_inliers in
* [0, INT_MAX]. A value outside those bounds returns INVALID_ARGUMENT
* before allocation, OpenCV execution, or mutation of the caller-owned
* result and inlier mask. minimum_inliers == 0 retains the established PnP
* effective minimum of four correspondences.
*/
uint32_t max_iterations;
uint32_t minimum_inliers;
double minimum_inlier_ratio;

View file

@ -0,0 +1,245 @@
#ifndef LARDON3D_SSD_CONTROLLER_H
#define LARDON3D_SSD_CONTROLLER_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
enum {
LARDON3D_SSD_TEXT_CAPACITY = 128,
LARDON3D_SSD_IDENTITY_CAPACITY = 256,
LARDON3D_SSD_PATH_CAPACITY = 256,
LARDON3D_SSD_REASON_CAPACITY = 256,
LARDON3D_SSD_MAX_SCRATCH_LEASES = 64,
};
#define LARDON3D_SSD_SWAP_LABEL "LARDON_SWAP"
#define LARDON3D_SSD_SCRATCH_LABEL "LARDON_SCRATCH"
#define LARDON3D_SSD_SCRATCH_MOUNT_PATH "/mnt/lardon-scratch"
typedef struct Lardon3DSsdController Lardon3DSsdController;
typedef enum {
LARDON3D_SSD_ABSENT = 0,
LARDON3D_SSD_DETECTED,
LARDON3D_SSD_ENABLING,
LARDON3D_SSD_ENABLED,
LARDON3D_SSD_IN_USE,
LARDON3D_SSD_DRAINING,
LARDON3D_SSD_SAFE_TO_UNPLUG,
LARDON3D_SSD_ERROR,
} Lardon3DSsdState;
typedef enum {
/* The requested transition was observed and verified. */
LARDON3D_SSD_CONTROL_OK = 0,
/* No unsafe action was attempted; retry after the snapshot blocker clears. */
LARDON3D_SSD_CONTROL_PENDING,
/* Provider/identity/operation failure; the snapshot retains truthful state. */
LARDON3D_SSD_CONTROL_ERROR,
} Lardon3DSsdControlResult;
/* A scratch lease is a caller-owned, process-local capability. Its opaque
* words are never persistent identity and must not be inspected or copied to
* represent additional use. The exact object address is part of ownership:
* after acquire, that object must remain alive and unmoved until its successful
* release. The caller must provide exclusive access to that object for this
* whole lifetime and must never present the same storage concurrently to
* different controllers, whose distinct mutexes cannot serialize caller
* memory. Calls using separate lease objects are thread-safe. Acquire writes a
* fresh token; release zeros the same object. A copied, constructed, stale,
* foreign, or already released object is rejected without changing the
* controller's exact bounded lease count. */
typedef struct {
uintptr_t opaque_controller;
uint64_t opaque_lease_id;
} Lardon3DSsdScratchLease;
/* The snapshot is a bounded caller-owned copy. Every string is NUL-terminated.
* `generation` is monotonic and legally saturates at UINT64_MAX; serialized
* controller operations may therefore change material state at that terminal
* watermark. Arbitrary equal-generation copies are not control authority.
* Unknown optional telemetry has its *_known flag clear and its text, when
* present, set to "UNKNOWN"; zero is therefore never guessed as knowledge.
* Device nodes are current observations only. Stable product identity is the
* paired UDisks Drive identity plus the two filesystem UUIDs. If an owned
* device disappears or is replaced, that tuple remains reported while paths
* become UNKNOWN; polling alone cannot clear the physical hazard. */
typedef struct {
Lardon3DSsdState state;
uint64_t generation;
bool device_detected;
bool pairing_valid;
bool model_known;
bool serial_known;
bool connection_speed_known;
char model[LARDON3D_SSD_TEXT_CAPACITY];
char serial[LARDON3D_SSD_TEXT_CAPACITY];
char drive_identity[LARDON3D_SSD_IDENTITY_CAPACITY];
uint64_t connection_speed_mbps;
bool swap_detected;
bool scratch_detected;
char swap_uuid[LARDON3D_SSD_TEXT_CAPACITY];
char scratch_uuid[LARDON3D_SSD_TEXT_CAPACITY];
char swap_device[LARDON3D_SSD_PATH_CAPACITY];
char scratch_device[LARDON3D_SSD_PATH_CAPACITY];
/* UDisks Block.Size is exact partition extent. The usable swap/filesystem
* totals below are separate and remain unknown until a bounded telemetry
* source can measure them; a mounted scratch filesystem may legitimately
* report total/free UNKNOWN. The controller never substitutes one meaning
* for another or performs an unbounded path lookup during a UI poll. */
bool swap_partition_size_known;
bool scratch_partition_size_known;
uint64_t swap_partition_size_bytes;
uint64_t scratch_partition_size_bytes;
bool swap_active;
bool swap_total_known;
bool swap_used_known;
uint64_t swap_total_bytes;
uint64_t swap_used_bytes;
bool scratch_mounted;
bool scratch_total_known;
bool scratch_free_known;
uint64_t scratch_total_bytes;
uint64_t scratch_free_bytes;
char scratch_mount_path[LARDON3D_SSD_PATH_CAPACITY];
size_t scratch_lease_count;
size_t scratch_lease_capacity;
bool drain_requested;
/* Exact physical lease authority. This is true only for the validated
* current Drive/UUID pair at the exact mount path, outside drain/error and
* below the fixed lease/token capacity. Presentation and the Governor must
* consume this flag rather than reconstructing authority from fields. */
bool scratch_allocations_allowed;
/* Sole control authority for UI/automation. These flags are derived under
* the controller mutex from the validated current pair plus sticky
* ownership state. Callers must not reconstruct authority from `state`,
* paths, labels, activity bits, or an apparently clean ERROR. At most one
* flag is true in a valid snapshot. */
bool can_enable;
bool can_disable;
bool can_cancel_drain;
char reason[LARDON3D_SSD_REASON_CAPACITY];
} Lardon3DSsdSnapshot;
/* Creates one synchronous physical-lifecycle controller. The controller owns
* its GDBus connection/provider and serializes calls internally; it creates no
* scheduler or background polling thread. Its bounded system-bus connection
* helper is joined before this call returns. Discovery is cached for at least
* one second between ordinary observations. NULL reports allocation or
* provider-initialization failure. */
Lardon3DSsdController *lardon3d_ssd_controller_create(void);
/* Releases all controller/provider resources without mounting, unmounting, or
* changing swap state. NULL is an idempotent success.
* Destruction returns false and leaves the controller valid while any scratch
* lease is outstanding; callers must release those capabilities and retry.
* No other call may race with a successful destroy. */
bool lardon3d_ssd_controller_destroy(Lardon3DSsdController *controller);
/* Refreshes discovery and telemetry. force=false honors the monotonic
* one-second cache interval; force=true performs one bounded provider poll and
* is intended for explicit user actions and tests. A failed poll preserves
* truthful known active state where possible and publishes ERROR with a
* bounded reason. Thread-safe. */
bool lardon3d_ssd_controller_refresh(
Lardon3DSsdController *controller,
bool force
);
/* Copies the latest state after an ordinary cached refresh. The caller owns
* `snapshot`; no internal pointer escapes. A non-NULL output is initialized to
* a bounded all-UNKNOWN/ABSENT snapshot even when controller is NULL. Returns
* false for a NULL output, invalid controller, or refresh failure (the copied
* ERROR snapshot is still provided for a valid controller). Thread-safe. */
bool lardon3d_ssd_controller_get_snapshot(
Lardon3DSsdController *controller,
Lardon3DSsdSnapshot *snapshot
);
/* Copies the already-cached bounded snapshot without polling UDisks or other
* provider telemetry. This is the post-operation reconciliation seam used
* after a serialized physical lease call. Output is initialized on every
* failure and owned by the caller. Thread-safe. */
bool lardon3d_ssd_controller_copy_snapshot(
Lardon3DSsdController *controller,
Lardon3DSsdSnapshot *snapshot
);
/* Enables only the exact healthy paired LARDON_SWAP/LARDON_SCRATCH device.
* UDisks Swapspace.Start precedes Filesystem.Mount, whose observed path must be
* exactly LARDON3D_SSD_SCRATCH_MOUNT_PATH. Calls are synchronous but have a
* bounded D-Bus timeout. Partial success remains active and visible in ERROR;
* immediately before each potentially side-effecting call, control ownership
* is conservatively bound to the exact Drive/UUID tuple. A timeout or failed
* verification can then be recovered only by observing and draining that same
* pair; a replacement receives no control authority. The controller never
* formats, repairs, powers off, or deletes data. A verified already-enabled
* pair returns OK without repeating either action. */
Lardon3DSsdControlResult lardon3d_ssd_controller_enable(
Lardon3DSsdController *controller
);
/* Requests an idempotent drain. New leases are rejected immediately. Active
* leases produce PENDING and must be released before stop/unmount can proceed.
* Swap is stopped only when current PSI/swap-delta evidence is quiet and its
* used bytes can be absorbed while retaining the 3 GiB MemAvailable reserve.
* A last lease release automatically retries the pending bounded drain;
* pressure/telemetry PENDING results are retryable by calling disable again.
* After an unsafe disappearance, only the reconnected original Drive/UUID
* tuple is authorized for this drain; its verified inactive endpoint clears
* the sticky hazard and reaches SAFE_TO_UNPLUG. */
Lardon3DSsdControlResult lardon3d_ssd_controller_disable(
Lardon3DSsdController *controller
);
/* Cancels a pending drain without changing mount or swap state. A fully active
* pair resumes ENABLED/IN_USE according to the exact current lease count;
* partial state remains truthfully DETECTED/ERROR. Returns false for invalid
* arguments or when no drain is pending. */
bool lardon3d_ssd_controller_cancel_drain(
Lardon3DSsdController *controller
);
/* Low-level physical seam used by the Governor wrapper and controller tests.
* Production Task ownership must call
* lardon3d_resource_governor_acquire_scratch(), not this function directly.
* Acquires one of the fixed-capacity scratch-use capabilities. `lease` must be
* non-NULL and zero-initialized (or previously released), and its exclusively
* owned storage must not move or be copied until release succeeds. Acquisition
* requires a valid mounted pair in ENABLED/IN_USE and is rejected during
* DRAINING or ERROR. The lease is operational resource ownership, never
* Task/scientific identity. */
bool lardon3d_ssd_controller_acquire_scratch(
Lardon3DSsdController *controller,
Lardon3DSsdScratchLease *lease
);
/* Low-level physical seam used by the Governor wrapper and controller tests;
* production Task ownership releases through the matching Governor wrapper.
* Releases exactly the originally acquired object and zeros it on success.
* Invalid, copied, constructed, stale, foreign, or double-release objects
* return false without decrementing use. When this is the final lease of a
* pending drain, the same call advances the bounded stop/unmount sequence. */
bool lardon3d_ssd_controller_release_scratch(
Lardon3DSsdController *controller,
Lardon3DSsdScratchLease *lease
);
const char *lardon3d_ssd_state_name(Lardon3DSsdState state);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -2,6 +2,7 @@
#define LARDON3D_TASK_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <time.h>
@ -30,6 +31,17 @@ typedef void (*Lardon3DTaskFinishedCallback)(
void *userdata
);
typedef struct {
size_t batch_size;
uint64_t memory_bytes;
uint64_t gpu_memory_bytes;
unsigned int cpu_threads;
unsigned int gpu_slots;
unsigned int io_slots;
} Lardon3DTaskExecutionContract;
/* Historical ABI snapshot. Its field order and size are frozen: additive
* runtime observability belongs to Lardon3DTaskObservation below. */
typedef struct {
uint64_t id;
char name[LARDON3D_TASK_NAME_CAPACITY];
@ -40,14 +52,32 @@ typedef struct {
struct timespec finished_at;
} Lardon3DTaskSnapshot;
/* Additive, caller-owned, mutex-consistent runtime observation. The legacy
* prefix is deliberately repeated rather than extending TaskSnapshot: old
* binaries must never receive a write larger than their compiled object.
* Typed identity, durable progress, sequence count and the installed
* execution contract are operational observations only. Durable counts exist
* only after the typed owner publishes its committed prefix; they are never
* inferred from generic percentage, name, or message. When
* has_execution_contract is false every contract field is zero. */
typedef struct {
size_t batch_size;
uint64_t memory_bytes;
uint64_t gpu_memory_bytes;
unsigned int cpu_threads;
unsigned int gpu_slots;
unsigned int io_slots;
} Lardon3DTaskExecutionContract;
uint64_t id;
char name[LARDON3D_TASK_NAME_CAPACITY];
unsigned int progress;
Lardon3DTaskState state;
char message[LARDON3D_TASK_MESSAGE_CAPACITY];
struct timespec started_at;
struct timespec finished_at;
bool has_task_kind;
char task_kind[LARDON3D_TASK_KIND_CAPACITY];
uint32_t task_kind_version;
bool durable_progress_known;
uint64_t durable_completed;
uint64_t durable_total;
unsigned int sequence_count;
bool has_execution_contract;
Lardon3DTaskExecutionContract execution_contract;
} Lardon3DTaskObservation;
typedef struct {
uint64_t id;
@ -96,11 +126,32 @@ bool lardon3d_task_set_progress(
unsigned int progress,
const char *message
);
/* Publishes an already-durable typed-business prefix as operational
* observation and derives the generic percentage without overflow. The
* caller must invoke this only after its own transaction/cursor commit;
* Task/Queue do not persist or reinterpret these counts. A later ordinary
* set_progress clears them rather than retaining a stale exact-looking value.
* Untyped Tasks are rejected. Generic Task completion sets its percentage to
* 100 but never fabricates a missing typed-business commit. total must be
* positive and completed <= total. */
bool lardon3d_task_set_durable_progress(
Lardon3DTask *task,
uint64_t completed,
uint64_t total,
const char *message
);
bool lardon3d_task_fail(Lardon3DTask *task, const char *message);
bool lardon3d_task_snapshot(
const Lardon3DTask *task,
Lardon3DTaskSnapshot *snapshot
);
/* Copies one caller-owned coherent value under the Task mutex. A non-NULL
* output is zeroed before validation, so invalid Task arguments never leave a
* stale typed identity, durable count, or execution contract visible. */
bool lardon3d_task_observation(
const Lardon3DTask *task,
Lardon3DTaskObservation *observation
);
bool lardon3d_task_durable_snapshot(
const Lardon3DTask *task,
Lardon3DTaskDurableSnapshot *snapshot

View file

@ -9,6 +9,17 @@
typedef struct Lardon3DTaskQueue Lardon3DTaskQueue;
enum {
/* Application Queue ingress is intentionally bounded independently from
* terminal history. The runtime observer can therefore size one finite
* copy for every possible pending/active/history record. */
LARDON3D_TASK_QUEUE_PRODUCTION_CAPACITY = 64,
/* The TUI consumes at most 64 rows. Retaining the same bounded number of
* terminal snapshots keeps recent work observable without retaining Task
* userdata for the Queue lifetime. */
LARDON3D_TASK_QUEUE_HISTORY_CAPACITY = 64,
};
typedef enum {
LARDON3D_TASK_QUEUE_ADD_OK = 0,
LARDON3D_TASK_QUEUE_ADD_FULL,
@ -20,21 +31,55 @@ typedef enum {
typedef struct {
size_t running;
size_t pending;
/* Saturating count of every Task that became terminal since Queue
* creation, including terminal records later removed or aged out. */
size_t completed;
/* Saturating completed + currently non-terminal Tasks. */
size_t total;
} Lardon3DTaskQueueSummary;
/* Creates a bounded FIFO Queue with adaptive admissible-work selection and one
* serialized worker. capacity bounds pending Tasks, not terminal history; the
* Governor retains resource-admission ownership and must outlive the Queue.
*
* File d'exécution bornée, à ordre d'attente FIFO avec sélection adaptative du
* premier travail admissible, et un seul worker: ownership d'ordonnancement et
* de backpressure seulement. L'admission des demandes reste au Governneur.
*
* CONTRACT: after a successful add, the Queue owns Task and its userdata. Once
* the Task is terminal and its finished callback has returned, the Queue keeps
* only a snapshot and promptly destroys the real Task outside the Queue lock.
* The oldest terminal snapshot is evicted above
* LARDON3D_TASK_QUEUE_HISTORY_CAPACITY.
*
* Finished callbacks execute without the Queue mutex. While the Queue owner
* keeps it alive, callbacks may use read-only get/get_at/count/snapshot APIs.
* They must not synchronously remove their own still-active record, destroy the
* same Queue, or invoke another operation whose completion depends on that
* callback returning; defer such work until after the callback completes.
*/
Lardon3DTaskQueue *lardon3d_task_queue_create(
Lardon3DResourceGovernor *governor,
size_t capacity
);
/* File d'exécution bornée, à ordre d'attente FIFO avec sélection adaptative du
* premier travail admissible, et un seul worker: ownership d'ordonnancement et
* de backpressure seulement. L'admission des demandes reste au Governneur.
*/
/* Atomically closes call ingress, cancels live work, waits for the worker and
* every call whose registration preceded closing (including blocked
* producers), and destroys all remaining Queue-owned objects exactly once.
*
* The owner must prevent new API invocations before calling destroy and must
* invoke destroy only once. This is the standard external lifetime rule for an
* object addressed through a raw C pointer: the internal gate resolves calls
* already registered at the close race, but cannot make an invocation that
* starts after destruction safe. NULL is accepted. Never call destroy
* synchronously from a Task finished callback running on this Queue. */
void lardon3d_task_queue_destroy(Lardon3DTaskQueue *queue);
/* La file devient propriétaire de task uniquement en cas de succès.
Bloquante : attend une place libre si la file est pleine. */
* Bloquante : attend une place libre si la file est pleine. A zero Task ID is
* assigned from a nonzero monotonic sequence and is never generated twice
* during this Queue lifetime, including after terminal-history eviction or
* removal. Once UINT64_MAX has been generated (or consumed by a restored
* Task), automatic generation remains exhausted for this Queue lifetime;
* adding a lower preassigned ID cannot re-arm it. */
bool lardon3d_task_queue_add(
Lardon3DTaskQueue *queue,
Lardon3DTask *task,
@ -52,22 +97,49 @@ Lardon3DTaskQueueAddResult lardon3d_task_queue_try_add_ex(
Lardon3DTask *task,
uint64_t *task_id
);
/* Removes a retained terminal snapshot. Active/pending IDs are not removable;
* an evicted/unknown ID returns false. A finished callback must not remove its
* own record synchronously because retirement awaits that callback. */
bool lardon3d_task_queue_remove(Lardon3DTaskQueue *queue, uint64_t task_id);
/* Requests cancellation of a live Task. A retained terminal ID returns true
* as the request is already satisfied; an evicted/unknown ID returns false. */
bool lardon3d_task_queue_cancel(Lardon3DTaskQueue *queue, uint64_t task_id);
/* Pause/resume act only on retained live, non-terminal Tasks. Terminal-history
* and evicted/unknown IDs return false. */
bool lardon3d_task_queue_pause(Lardon3DTaskQueue *queue, uint64_t task_id);
bool lardon3d_task_queue_resume(Lardon3DTaskQueue *queue, uint64_t task_id);
void lardon3d_task_queue_resources_changed(Lardon3DTaskQueue *queue);
/* Number of Tasks still awaiting dispatch; active and history are excluded. */
size_t lardon3d_task_queue_count(Lardon3DTaskQueue *queue);
/* Returns a copy for live or retained terminal work. IDs that aged out of the
* bounded terminal history return false. */
bool lardon3d_task_queue_get(
Lardon3DTaskQueue *queue,
uint64_t task_id,
Lardon3DTaskSnapshot *snapshot
);
/* Extended counterpart of get(); retained terminal observations preserve
* exact typed/durable/admission state after the real Task is destroyed. A
* non-NULL output is zeroed before lookup, including for unknown/aged-out IDs
* and closing/error returns. */
bool lardon3d_task_queue_get_observation(
Lardon3DTaskQueue *queue,
uint64_t task_id,
Lardon3DTaskObservation *observation
);
/* Uses the same ordering as lardon3d_task_queue_snapshot(). */
bool lardon3d_task_queue_get_at(
Lardon3DTaskQueue *queue,
size_t index,
Lardon3DTaskSnapshot *snapshot
);
/* Copies the most operationally relevant records first: live Tasks in reverse
* submission order, followed by terminal snapshots in reverse completion
* order. Thus a bounded caller sees current and newest work instead of an old
* prefix. The returned count never exceeds capacity; summary, when non-NULL,
* describes all current live Tasks plus the saturating lifetime terminal
* count, independently of snapshot eviction or explicit removal. snapshots may
* be NULL only when capacity is zero. */
size_t lardon3d_task_queue_snapshot(
Lardon3DTaskQueue *queue,
Lardon3DTaskSnapshot *snapshots,
@ -75,4 +147,17 @@ size_t lardon3d_task_queue_snapshot(
Lardon3DTaskQueueSummary *summary
);
/* Additive extended observation. Ordering and lifetime-summary semantics are
* identical to snapshot(), but each record carries exact typed/durable/
* admitted Task data. With the production capacity of 64, a capacity of 129
* covers the maximum 64 pending + one active + 64 recent terminal records, so
* live work cannot be hidden by history. observations may be NULL only when
* capacity is zero. */
size_t lardon3d_task_queue_observe(
Lardon3DTaskQueue *queue,
Lardon3DTaskObservation *observations,
size_t capacity,
Lardon3DTaskQueueSummary *summary
);
#endif

View file

@ -5,8 +5,40 @@
#include <lardon3d/app_state.h>
typedef struct Lardon3DSsdController Lardon3DSsdController;
typedef struct Lardon3DTuiSsdAsync Lardon3DTuiSsdAsync;
#ifdef __cplusplus
extern "C" {
#endif
bool lardon3d_tui_init(void);
bool lardon3d_tui_run(Lardon3DAppState *state);
/* Additive compatibility entry point. The controller is borrowed for the call
* and remains outside the frozen AppState ABI; NULL preserves the legacy run.
* It creates an unbound physical observer, so production code that needs
* Governor scratch orchestration uses run_with_ssd_operation() instead. The
* App owner must keep the controller alive until this function returns. */
bool lardon3d_tui_run_with_ssd(
Lardon3DAppState *state,
Lardon3DSsdController *ssd_controller
);
/* Production borrowed-operation entry point. The application owns `operation`
* across this call and the TUI never destroys it. After the UI returns, the
* owner must first destroy/join the sole Task Queue (so every Task-owned
* scratch lease is released), then destroy the operation with
* lardon3d_tui_ssd_async_destroy_checked(), then its controller and Governor.
* `state` must be non-NULL; a bound operation must use that state's Governor.
* NULL preserves a TUI without external-storage observation/control. Returns
* false for invalid input or a runtime/observation/control-owner failure. */
bool lardon3d_tui_run_with_ssd_operation(
Lardon3DAppState *state,
Lardon3DTuiSsdAsync *operation
);
void lardon3d_tui_shutdown(void);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -0,0 +1,326 @@
#ifndef LARDON3D_TUI_MODEL_H
#define LARDON3D_TUI_MODEL_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/resource_governor.h>
#include <lardon3d/ssd_controller.h>
#include <lardon3d/task.h>
#ifdef __cplusplus
extern "C" {
#endif
enum {
LARDON3D_TUI_STAGE_COUNT = 11,
LARDON3D_TUI_TEXT_CAPACITY = 256,
/* Production Queue capacity is 64 pending. This bound observes every
* possible live Task plus the complete bounded recent history. */
LARDON3D_TUI_TASK_CAPACITY = 129,
};
/* The viewport decision is pure and terminal-independent so resize behavior is
* testable without entering ncurses. Compact is a supported layout, not an
* error path; only dimensions below 60x15 display the bounded fallback text. */
typedef enum {
LARDON3D_TUI_VIEWPORT_TOO_SMALL = 0,
LARDON3D_TUI_VIEWPORT_COMPACT,
LARDON3D_TUI_VIEWPORT_FULL,
} Lardon3DTuiViewport;
typedef enum {
LARDON3D_TUI_SEMANTIC_NORMAL = 0,
LARDON3D_TUI_SEMANTIC_HEALTHY,
LARDON3D_TUI_SEMANTIC_WARNING,
LARDON3D_TUI_SEMANTIC_ERROR,
LARDON3D_TUI_SEMANTIC_GPU,
LARDON3D_TUI_SEMANTIC_CPU,
LARDON3D_TUI_SEMANTIC_SSD,
LARDON3D_TUI_SEMANTIC_DIM,
LARDON3D_TUI_SEMANTIC_COUNT,
} Lardon3DTuiSemantic;
enum {
LARDON3D_TUI_STYLE_BOLD = 1U << 0,
LARDON3D_TUI_STYLE_DIM = 1U << 1,
};
typedef struct {
bool color_enabled;
short color_pair[LARDON3D_TUI_SEMANTIC_COUNT];
unsigned int attributes[LARDON3D_TUI_SEMANTIC_COUNT];
} Lardon3DTuiPalette;
Lardon3DTuiViewport lardon3d_tui_viewport_classify(int rows, int columns);
/* `color_pairs` is the terminal's total pair capacity, including pair zero.
* Missing/limited color never removes semantic text labels: styles fall back
* deterministically to bold/dim attributes and pair zero. */
void lardon3d_tui_palette_plan(
bool colors_supported,
int color_pairs,
Lardon3DTuiPalette *palette
);
typedef enum {
LARDON3D_TUI_STAGE_ACQUISITION = 0,
LARDON3D_TUI_STAGE_RAW,
LARDON3D_TUI_STAGE_QUALITY,
LARDON3D_TUI_STAGE_FEATURES,
LARDON3D_TUI_STAGE_VISUAL_INDEX,
LARDON3D_TUI_STAGE_CANDIDATE,
LARDON3D_TUI_STAGE_MATCHER,
LARDON3D_TUI_STAGE_GV,
LARDON3D_TUI_STAGE_TRACKS,
LARDON3D_TUI_STAGE_SPARSE_SFM,
LARDON3D_TUI_STAGE_DENSE,
} Lardon3DTuiStage;
typedef enum {
LARDON3D_TUI_STAGE_NOT_READY = 0,
LARDON3D_TUI_STAGE_READY,
LARDON3D_TUI_STAGE_QUEUED,
LARDON3D_TUI_STAGE_RUNNING,
LARDON3D_TUI_STAGE_THROTTLED,
LARDON3D_TUI_STAGE_BLOCKED,
LARDON3D_TUI_STAGE_COMPLETE,
LARDON3D_TUI_STAGE_FAILED,
LARDON3D_TUI_STAGE_NOT_APPLICABLE,
} Lardon3DTuiStageState;
typedef struct {
Lardon3DTuiStage stage;
Lardon3DTuiStageState state;
char reason[LARDON3D_TUI_TEXT_CAPACITY];
} Lardon3DTuiStageView;
const char *lardon3d_tui_stage_name(Lardon3DTuiStage stage);
const char *lardon3d_tui_stage_state_name(Lardon3DTuiStageState state);
Lardon3DTuiSemantic lardon3d_tui_stage_semantic(
Lardon3DTuiStageState state
);
/* Builds a runtime observation only from typed Task snapshots. Absence of a
* Task is never interpreted as scientific completion. Dense is deliberately
* NOT_APPLICABLE until a later production Task kind exists, so future science
* can never be rendered as active by a name/message coincidence. */
void lardon3d_tui_stage_views_build(
bool project_loaded,
const Lardon3DTaskObservation *tasks,
size_t task_count,
Lardon3DResourcePressure pressure,
Lardon3DTuiStageView stages[LARDON3D_TUI_STAGE_COUNT]
);
typedef enum {
LARDON3D_TUI_ETA_INDETERMINATE = 0,
LARDON3D_TUI_ETA_CALCULATING,
LARDON3D_TUI_ETA_KNOWN,
LARDON3D_TUI_ETA_STALLED,
LARDON3D_TUI_ETA_THROTTLED,
LARDON3D_TUI_ETA_COMPLETE,
} Lardon3DTuiEtaState;
typedef struct {
uint64_t task_id;
uint64_t monotonic_ns;
Lardon3DTaskState task_state;
bool typed_task;
unsigned int progress_percent;
bool durable_counts_known;
uint64_t durable_completed;
uint64_t durable_total;
bool pressure_limited;
} Lardon3DTuiProgressSample;
typedef struct {
uint64_t task_id;
uint64_t prefix_completed;
uint64_t previous_completed;
uint64_t previous_sample_ns;
uint64_t last_positive_ns;
double ewma_units_per_second;
unsigned int positive_sample_count;
Lardon3DTaskState previous_state;
bool initialized;
bool using_durable_counts;
} Lardon3DTuiProgressTracker;
typedef struct {
bool present;
uint64_t task_id;
bool durable_counts_known;
bool runtime_percentage;
bool integrity_error;
uint64_t completed;
uint64_t total;
bool percentage_known;
unsigned int percentage;
bool elapsed_known;
uint64_t elapsed_seconds;
bool throughput_known;
double units_per_second;
Lardon3DTuiEtaState eta_state;
uint64_t eta_seconds;
bool resumed_prefix_excluded;
} Lardon3DTuiProgressView;
typedef enum {
LARDON3D_TUI_INTERACTION_IDLE = 0,
LARDON3D_TUI_INTERACTION_TEXT_INPUT,
LARDON3D_TUI_INTERACTION_IMPORT_RUNNING,
} Lardon3DTuiInteractionMode;
typedef struct {
bool enter;
bool escape;
bool f10;
bool cancel_import;
bool quit;
bool navigate;
} Lardon3DTuiKeyContract;
/* Pure key/footer contract shared by the actual handler mode and renderer.
* A displayed action is therefore never broader than the active input owner. */
Lardon3DTuiKeyContract lardon3d_tui_key_contract(
Lardon3DTuiInteractionMode mode
);
/* Updates one fixed-size tracker. The first observation of a Task is a resume
* baseline and contributes no throughput. A transition into RUNNING also
* resets rate timing, excluding pending/pause time. ETA becomes known only
* after two positive intervals, and rate remains UNKNOWN until that same
* evidence exists. Five seconds without progress is a stall, regressions reset
* the baseline, and pressure is explicit. Only coherent durable completion or
* an untyped runtime completion is shown as exactly 100% with ETA zero; a typed
* terminal lifecycle with incomplete/unknown durable science is never filled
* in. */
bool lardon3d_tui_progress_update(
Lardon3DTuiProgressTracker *tracker,
const Lardon3DTuiProgressSample *sample,
Lardon3DTuiProgressView *view
);
const char *lardon3d_tui_eta_state_name(Lardon3DTuiEtaState state);
typedef enum {
LARDON3D_TUI_GPU_BACKEND_UNKNOWN = 0,
LARDON3D_TUI_GPU_BACKEND_UNINITIALIZED,
LARDON3D_TUI_GPU_BACKEND_AVAILABLE,
LARDON3D_TUI_GPU_BACKEND_UNAVAILABLE,
LARDON3D_TUI_GPU_BACKEND_CPU,
LARDON3D_TUI_GPU_BACKEND_ORB_VULKAN,
LARDON3D_TUI_GPU_BACKEND_MIXED,
} Lardon3DTuiGpuBackendStatus;
typedef struct {
bool valid;
uint64_t captured_monotonic_ns;
Lardon3DResourcePressure governor_pressure;
char governor_reason[LARDON3D_TUI_TEXT_CAPACITY];
unsigned int cpu_logical_total;
bool cpu_admitted_known;
unsigned int cpu_admitted;
unsigned int cpu_available;
unsigned int cpu_active;
bool cpu_utilization_known;
uint32_t cpu_utilization_basis_points;
char cpu_reason[LARDON3D_TUI_TEXT_CAPACITY];
bool gpu_present;
bool gpu_uses_shared_memory;
bool gpu_memory_known;
uint64_t gpu_memory_reserved_bytes;
uint64_t gpu_memory_available_bytes;
bool gpu_busy_known;
uint32_t gpu_busy_basis_points;
unsigned int gpu_slots_active;
unsigned int gpu_slots_available;
Lardon3DTuiGpuBackendStatus gpu_backend;
char gpu_backend_reason[LARDON3D_TUI_TEXT_CAPACITY];
uint64_t ram_total_bytes;
uint64_t ram_available_bytes;
uint64_t ram_reserve_bytes;
uint64_t ram_reserved_bytes;
bool swap_total_known;
uint64_t swap_total_bytes;
bool swap_used_known;
uint64_t swap_used_bytes;
bool swap_delta_known;
uint64_t swap_pages_in_delta;
uint64_t swap_pages_out_delta;
bool batch_known;
size_t batch_size;
bool inflight_known;
size_t inflight_limit;
bool helpers_known;
unsigned int helper_limit;
unsigned int io_active;
unsigned int io_available;
bool scratch_known;
bool scratch_mounted;
bool scratch_total_known;
bool scratch_free_known;
uint64_t scratch_total_bytes;
uint64_t scratch_free_bytes;
size_t scratch_leases;
/* Governor-owned external usage. Physical device paths and F10 authority
* remain in the separate controller snapshot; this view cannot grant a
* lease or infer availability from a mount bit. */
bool external_storage_registered;
Lardon3DResourceExternalStorageStatus external_storage_status;
bool scratch_new_allocations_allowed;
bool external_swap_total_known;
bool external_swap_used_known;
uint64_t external_swap_total_bytes;
uint64_t external_swap_used_bytes;
char external_storage_identity[LARDON3D_TUI_TEXT_CAPACITY];
char external_storage_reason[LARDON3D_TUI_TEXT_CAPACITY];
} Lardon3DTuiResourceView;
const char *lardon3d_tui_gpu_backend_name(
Lardon3DTuiGpuBackendStatus status
);
const char *lardon3d_tui_pressure_name(Lardon3DResourcePressure pressure);
typedef enum {
LARDON3D_TUI_OPTICS_NO_PROJECT = 0,
LARDON3D_TUI_OPTICS_UNRESOLVED,
LARDON3D_TUI_OPTICS_CONFIGURATION_ONLY,
LARDON3D_TUI_OPTICS_SELECTION_REQUIRED,
LARDON3D_TUI_OPTICS_SELECTED,
LARDON3D_TUI_OPTICS_INCOMPATIBLE,
LARDON3D_TUI_OPTICS_CORRUPT,
} Lardon3DTuiOpticsStatus;
/* This presentation classifier never creates an "unknown" profile or guesses
* identity. A manual/no-electronics lens is ordinary configuration data and
* does not change compatibility or selection rules. */
Lardon3DTuiOpticsStatus lardon3d_tui_optics_classify(
bool project_loaded,
bool assignment_found,
bool assignment_valid,
bool manual_lens,
size_t compatible_calibration_count,
bool selection_found,
bool selection_compatible
);
const char *lardon3d_tui_optics_status_name(Lardon3DTuiOpticsStatus status);
const char *lardon3d_tui_optics_status_explanation(
Lardon3DTuiOpticsStatus status,
bool manual_lens
);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -0,0 +1,161 @@
#ifndef LARDON3D_TUI_OPTICS_H
#define LARDON3D_TUI_OPTICS_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/optical_profiles.h>
#include <lardon3d/project_db.h>
#include <lardon3d/tui_model.h>
#ifdef __cplusplus
extern "C" {
#endif
enum {
LARDON3D_TUI_OPTICS_PAGE_CAPACITY = 16,
};
typedef struct Lardon3DTuiOptics Lardon3DTuiOptics;
typedef enum {
LARDON3D_TUI_OPTICS_PANE_BODY = 0,
LARDON3D_TUI_OPTICS_PANE_LENS,
LARDON3D_TUI_OPTICS_PANE_CONFIGURATION,
LARDON3D_TUI_OPTICS_PANE_CALIBRATION,
} Lardon3DTuiOpticsPane;
typedef struct {
bool project_bound;
Lardon3DTuiOpticsPane active_pane;
Lardon3DOpticalCameraBodyProfile
bodies[LARDON3D_TUI_OPTICS_PAGE_CAPACITY];
size_t body_count;
size_t selected_body;
bool bodies_have_next;
Lardon3DOpticalLensProfile
lenses[LARDON3D_TUI_OPTICS_PAGE_CAPACITY];
size_t lens_count;
size_t selected_lens;
bool lenses_have_next;
Lardon3DOpticalConfiguration
configurations[LARDON3D_TUI_OPTICS_PAGE_CAPACITY];
size_t configuration_count;
size_t selected_configuration;
bool configurations_have_next;
Lardon3DOpticalCalibrationProfile
calibrations[LARDON3D_TUI_OPTICS_PAGE_CAPACITY];
size_t calibration_count;
size_t selected_calibration;
bool calibrations_have_next;
bool capture_inspected;
uint64_t capture_id;
Lardon3DTuiOpticsStatus capture_status;
bool capture_assignment_found;
Lardon3DOpticalCaptureAssignment capture_assignment;
bool capture_configuration_found;
Lardon3DOpticalConfiguration capture_configuration;
bool capture_body_found;
Lardon3DOpticalCameraBodyProfile capture_body;
bool capture_lens_found;
Lardon3DOpticalLensProfile capture_lens;
bool capture_selection_found;
Lardon3DOpticalCaptureCalibrationSelection capture_selection;
bool metadata_lookup_performed;
bool metadata_body_found;
Lardon3DOpticalCameraBodyProfile metadata_body;
bool metadata_lens_found;
Lardon3DOpticalLensProfile metadata_lens;
char message[LARDON3D_TUI_TEXT_CAPACITY];
} Lardon3DTuiOpticsSnapshot;
/* One main-thread-only view model caches bounded pages and caller-owned copies;
* no function is thread-safe. NULL is rejected except destroy/unbind, which
* accept it. Binding borrows the Project DB until unbind/destroy and performs
* bounded list calls once; redraw/snapshot never queries SQLite. On bind
* failure no DB borrow is retained and snapshot.message preserves the exact
* BUSY/IO/CORRUPT-style reason for explicit retry. Unbind before DB close. */
Lardon3DTuiOptics *lardon3d_tui_optics_create(void);
void lardon3d_tui_optics_destroy(Lardon3DTuiOptics *optics);
bool lardon3d_tui_optics_bind(
Lardon3DTuiOptics *optics,
Lardon3DProjectDb *database
);
void lardon3d_tui_optics_unbind(Lardon3DTuiOptics *optics);
/* Initializes a non-NULL output before validation; on success it is a
* caller-owned value with no internal pointers and remains valid after the
* next model call, unbind, or destroy. */
bool lardon3d_tui_optics_snapshot(
const Lardon3DTuiOptics *optics,
Lardon3DTuiOpticsSnapshot *snapshot
);
bool lardon3d_tui_optics_select_pane(
Lardon3DTuiOptics *optics,
Lardon3DTuiOpticsPane pane
);
bool lardon3d_tui_optics_move_selection(
Lardon3DTuiOptics *optics,
int direction
);
/* `next=true` loads the next ascending page; false returns to the first page.
* Calibration pages use the currently inspected Capture's exact optical
* configuration. There is no unbounded accumulated history. */
bool lardon3d_tui_optics_page(
Lardon3DTuiOptics *optics,
Lardon3DTuiOpticsPane pane,
bool next
);
/* Creation is immutable: editing means creating/selecting a new versioned
* profile/configuration. Inputs are borrowed for the call and exact retry or
* conflict semantics remain those of optical_profiles.h. */
bool lardon3d_tui_optics_create_body(
Lardon3DTuiOptics *optics,
const Lardon3DOpticalCameraBodyProfile *input
);
bool lardon3d_tui_optics_create_lens(
Lardon3DTuiOptics *optics,
const Lardon3DOpticalLensProfile *input
);
bool lardon3d_tui_optics_create_configuration(
Lardon3DTuiOptics *optics,
bool has_focal_length,
uint32_t focal_length_um
);
bool lardon3d_tui_optics_assign_campaign_group(
Lardon3DTuiOptics *optics,
uint64_t campaign_task_id,
uint32_t group_id
);
bool lardon3d_tui_optics_assign_capture(
Lardon3DTuiOptics *optics,
uint64_t capture_id
);
bool lardon3d_tui_optics_inspect_capture(
Lardon3DTuiOptics *optics,
uint64_t capture_id
);
bool lardon3d_tui_optics_select_capture_calibration(
Lardon3DTuiOptics *optics
);
/* Exact metadata lookup only. Empty lens make is valid for data-driven manual
* aliases; no alias match leaves that side unresolved and creates nothing. */
bool lardon3d_tui_optics_lookup_exact_metadata(
Lardon3DTuiOptics *optics,
const char *body_make,
const char *body_model,
const char *lens_make,
const char *lens_model
);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -0,0 +1,142 @@
#ifndef LARDON3D_TUI_SSD_ASYNC_H
#define LARDON3D_TUI_SSD_ASYNC_H
#include <stdbool.h>
#include <stdint.h>
#include <lardon3d/resource_governor.h>
#include <lardon3d/ssd_controller.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct Lardon3DTuiSsdAsync Lardon3DTuiSsdAsync;
typedef enum {
LARDON3D_TUI_SSD_ACTION_NONE = 0,
/* Internal coalesced telemetry operation; F10 never selects it. */
LARDON3D_TUI_SSD_ACTION_OBSERVE,
LARDON3D_TUI_SSD_ACTION_ENABLE,
LARDON3D_TUI_SSD_ACTION_DRAIN,
LARDON3D_TUI_SSD_ACTION_CANCEL_DRAIN,
} Lardon3DTuiSsdAction;
typedef struct {
bool running;
Lardon3DTuiSsdAction action;
bool result_known;
Lardon3DSsdControlResult result;
uint64_t generation;
bool controller_snapshot_known;
bool controller_snapshot_actionable;
Lardon3DSsdSnapshot controller_snapshot;
char reason[LARDON3D_SSD_REASON_CAPACITY];
} Lardon3DTuiSsdAsyncSnapshot;
typedef struct {
bool (*monotonic_now_ns)(void *context, uint64_t *now_ns);
bool (*snapshot)(void *context, Lardon3DSsdSnapshot *snapshot);
Lardon3DSsdControlResult (*enable)(void *context);
Lardon3DSsdControlResult (*disable)(void *context);
bool (*cancel_drain)(void *context);
void (*destroy)(void *context);
} Lardon3DTuiSsdAsyncProviderOps;
typedef struct {
const Lardon3DTuiSsdAsyncProviderOps *ops;
void *context;
} Lardon3DTuiSsdAsyncProvider;
/* The caller owns one object; TUI request/poll calls remain on the
* ncurses/main thread. `controller` is borrowed and must outlive this object.
* Exactly one joinable operation thread may exist. It runs only one bounded
* synchronous controller transition; it never renders, schedules a Task,
* polls in a loop, or becomes a second Queue. */
Lardon3DTuiSsdAsync *lardon3d_tui_ssd_async_create(
Lardon3DSsdController *controller
);
/* Production binding. Both borrowed owners must outlive the async object.
* Creation registers a conservative ERROR until the first validated worker
* observation; each later worker outcome updates the Governor registry. The
* registry is unregistered after the worker is joined and before destruction
* returns. Existing create() remains an unbound compatibility entry point. */
Lardon3DTuiSsdAsync *lardon3d_tui_ssd_async_create_with_governor(
Lardon3DSsdController *controller,
Lardon3DResourceGovernor *governor
);
/* Deterministic provider seam. All five callbacks are required; destroy is
* optional. Ownership of provider.context transfers only on success. Tests use
* condition-controlled providers and never invoke real mount/swap operations.
* Production callers normally use create() above. */
Lardon3DTuiSsdAsync *lardon3d_tui_ssd_async_create_with_provider(
Lardon3DTuiSsdAsyncProvider provider
);
/* Checked production teardown. `operation` and `*operation` must be non-NULL.
* It joins the bounded worker, then unregisters the Governor binding before
* destroying provider/synchronization state. On success it frees the object,
* stores NULL, and returns true. If an exact scratch lease still prevents
* unregister, it returns false and retains the stopped object plus binding so
* the owner can release the lease and retry; no borrowed pointer is orphaned. */
bool lardon3d_tui_ssd_async_destroy_checked(
Lardon3DTuiSsdAsync **operation
);
/* Compatibility teardown for objects made by the unbound create() or provider
* constructor. Bound production owners must use destroy_checked() so a failed
* unregister remains observable and retryable. A
* D-Bus call is not asynchronously cancelled: production controller timeouts
* bound the join and preserve its exact side-effect verification contract. */
void lardon3d_tui_ssd_async_destroy(Lardon3DTuiSsdAsync *operation);
/* Starts one exact controller-authorized transition from the last validated
* snapshot. Requests are rejected while busy, after shutdown, for invalid
* actions, or when the corresponding capability flag is absent. */
bool lardon3d_tui_ssd_async_request(
Lardon3DTuiSsdAsync *operation,
Lardon3DTuiSsdAction action
);
/* Starts one background telemetry refresh only when the one-second cache is
* due and the sole worker is idle. Returning true means the request was
* accepted, already cached, or coalesced behind an active control operation;
* it never performs controller/D-Bus work on the caller/ncurses thread. */
bool lardon3d_tui_ssd_async_refresh(Lardon3DTuiSsdAsync *operation);
/* Poll is non-blocking except for joining a worker already known complete.
* Output is always initialized; caller owns the bounded copy. Controller
* telemetry remains unknown until one background refresh completes. A later
* failed controller refresh publishes its truthful ERROR snapshot. Malformed
* provider output is replaced by a bounded ERROR copy with
* controller_snapshot_actionable=false, so it cannot authorize F10. */
bool lardon3d_tui_ssd_async_poll(
Lardon3DTuiSsdAsync *operation,
Lardon3DTuiSsdAsyncSnapshot *snapshot
);
/* Waits on the operation condition using CLOCK_MONOTONIC, then joins a
* completed worker. A timeout returns false without cancelling or losing
* ownership. This is also the deterministic validation seam. */
bool lardon3d_tui_ssd_async_wait_idle(
Lardon3DTuiSsdAsync *operation,
uint64_t timeout_ns
);
/* Selects the exact F10 transition solely from controller-owned capability
* flags. State/activity/path fields are presentation only and never grant
* authority. Malformed or ambiguous capability combinations return false. */
bool lardon3d_tui_ssd_action_for_snapshot(
const Lardon3DSsdSnapshot *snapshot,
Lardon3DTuiSsdAction *action
);
const char *lardon3d_tui_ssd_action_name(Lardon3DTuiSsdAction action);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -24,6 +24,7 @@ ncursesw = dependency('ncursesw', required: true)
threads = dependency('threads')
sqlite3 = dependency('sqlite3', required: true)
openssl = dependency('openssl', required: true)
gio = dependency('gio-2.0', required: true, include_type: 'system')
ceres = dependency(
'Ceres',
method: 'cmake',
@ -145,6 +146,11 @@ lardon3d_app = executable(
'src/app_state.c',
'src/tui.c',
'src/layout.c',
'src/tui_model.c',
'src/runtime_observer.c',
'src/runtime_session.c',
'src/tui_ssd_async.c',
'src/tui_optics.c',
'src/import.c',
'src/import_task.c',
'src/image_catalog.c',
@ -159,7 +165,6 @@ lardon3d_app = executable(
'src/photo_quality_task.cpp',
'src/feature_extractor_opencv.cpp',
'src/feature_store.c',
'src/feature_extractor_opencv.cpp',
'src/feature_task.c', 'src/sift_task.c', 'src/precision_features.c',
'src/visual_index.c',
'src/visual_index_task.c',
@ -170,6 +175,7 @@ lardon3d_app = executable(
'src/image_view.c',
'src/project.c',
'src/project_db.c', 'src/project_db_sparse_sfm.c',
'src/optical_profiles.c',
'src/calibration_bootstrap.c',
'src/sparse_sfm_geometry.cpp',
'src/sparse_sfm_incremental.cpp',
@ -187,8 +193,10 @@ lardon3d_app = executable(
'src/task_kinds.c',
'src/task_queue.c',
'src/resource_governor.c',
'src/resource_governor_external_storage.c',
'src/resource_snapshot.c',
'src/hardware_profile.c',
'src/ssd_controller.c',
'src/match_file.c',
'src/matcher.cpp',
'src/geometric_verifier.cpp',
@ -210,6 +218,7 @@ lardon3d_app = executable(
threads,
sqlite3,
openssl,
gio,
libraw,
libexif,
libpng,
@ -317,7 +326,10 @@ persistent_image_catalog_test = executable(
'src/resource_governor.c',
'src/resource_snapshot.c',
],
c_args: ['-DLARDON3D_PROJECT_DB_TESTING'],
c_args: [
'-DLARDON3D_PROJECT_DB_TESTING',
'-DLARDON3D_IMAGE_CATALOG_PERSISTENT_TESTING',
],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl],
)
@ -731,6 +743,7 @@ feature_task_test = executable(
'-DLARDON3D_FEATURE_STORE_TESTING',
'-DLARDON3D_VISUAL_INDEX_TASK_TESTING',
'-DLARDON3D_VISUAL_INDEX_TESTING',
'-DLARDON3D_RESOURCE_GOVERNOR_CAPTURE_TESTING',
],
cpp_args: ['-DLARDON3D_FEATURE_TASK_TESTING'],
include_directories: include_directories('include'),
@ -864,6 +877,21 @@ calibration_bootstrap_test = executable(
test('calibration-bootstrap', calibration_bootstrap_test, timeout: 30)
optical_profiles_test = executable(
'test-optical-profiles',
sources: [
'tests/test_optical_profiles.c',
'src/optical_profiles.c',
'src/project_db.c', 'src/project_db_sparse_sfm.c',
'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c',
],
c_args: ['-DLARDON3D_PROJECT_DB_TESTING'],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl],
)
test('optical-profiles', optical_profiles_test, timeout: 30)
sparse_sfm_model_test = executable(
'test-sparse-sfm-model',
sources: [
@ -975,6 +1003,7 @@ project_test = executable(
'tests/test_project.c',
'src/app_state.c',
'src/project.c',
'src/runtime_session.c',
'src/project_db.c', 'src/project_db_sparse_sfm.c',
'src/task.c',
'src/task_checkpoint.c',
@ -1045,6 +1074,7 @@ task_queue_test = executable(
'src/resource_governor.c',
'src/resource_snapshot.c',
],
c_args: ['-DLARDON3D_TASK_QUEUE_TESTING'],
include_directories: include_directories('include'),
dependencies: [threads],
)
@ -1062,6 +1092,112 @@ hardware_profile_test = executable(
test('hardware-profile', hardware_profile_test, timeout: 30)
ssd_controller_test = executable(
'test-ssd-controller',
sources: ['tests/test_ssd_controller.c', 'src/ssd_controller.c'],
c_args: ['-DLARDON3D_SSD_CONTROLLER_TESTING'],
include_directories: include_directories('include'),
dependencies: [threads, gio],
)
test('ssd-controller', ssd_controller_test, timeout: 30)
resource_external_storage_test = executable(
'test-resource-external-storage',
sources: [
'tests/test_resource_external_storage.c',
'src/resource_governor.c',
'src/resource_governor_external_storage.c',
'src/resource_snapshot.c',
'src/ssd_controller.c',
'src/task.c',
'src/task_queue.c',
'src/tui_ssd_async.c',
],
c_args: [
'-DLARDON3D_SSD_CONTROLLER_TESTING',
'-DLARDON3D_RESOURCE_EXTERNAL_STORAGE_TESTING',
'-DLARDON3D_TASK_QUEUE_TESTING',
],
include_directories: include_directories('include'),
dependencies: [threads, gio],
)
test('resource-external-storage', resource_external_storage_test, timeout: 30)
tui_model_test = executable(
'test-tui-model',
sources: ['tests/test_tui_model.c', 'src/tui_model.c'],
include_directories: include_directories('include'),
)
test('tui-model', tui_model_test, timeout: 30)
tui_layout_test = executable(
'test-tui-layout',
sources: [
'tests/test_tui_layout.c',
'src/layout.c',
'src/tui_model.c',
],
include_directories: include_directories('include'),
dependencies: [ncursesw],
)
test('tui-layout', tui_layout_test, timeout: 30)
public_runtime_abi_test = executable(
'test-public-runtime-abi',
sources: ['tests/test_public_runtime_abi.c'],
include_directories: include_directories('include'),
)
test('public-runtime-abi', public_runtime_abi_test, timeout: 30)
runtime_observer_test = executable(
'test-runtime-observer',
sources: [
'tests/test_runtime_observer.c',
'src/runtime_observer.c',
'src/tui_model.c',
],
include_directories: include_directories('include'),
)
test('runtime-observer', runtime_observer_test, timeout: 30)
tui_ssd_async_test = executable(
'test-tui-ssd-async',
sources: [
'tests/test_tui_ssd_async.c',
'src/tui_ssd_async.c',
'src/ssd_controller.c',
'src/resource_governor.c',
'src/resource_governor_external_storage.c',
'src/resource_snapshot.c',
],
include_directories: include_directories('include'),
dependencies: [threads, gio],
)
test('tui-ssd-async', tui_ssd_async_test, timeout: 30)
tui_optics_test = executable(
'test-tui-optics',
sources: [
'tests/test_tui_optics.c',
'src/tui_optics.c',
'src/tui_model.c',
'src/optical_profiles.c',
'src/project_db.c', 'src/project_db_sparse_sfm.c',
'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c',
],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl],
)
test('tui-optics', tui_optics_test, timeout: 30)
resource_snapshot_test = executable(
'test-resource-snapshot',
sources: [

View file

@ -6,6 +6,9 @@ extern "C" {
#include "task_internal.h"
}
#include "opencv_task_thread_guard.h"
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@ -18,6 +21,10 @@ namespace {
constexpr unsigned char magic[8] = {'L', '3', 'D', 'A', 'C', 'T', '1', '\0'};
constexpr size_t max_request_size =
LARDON3D_ACQUISITION_CAMPAIGN_TASK_REQUEST_MAX_BYTES;
constexpr uint64_t kRawWorkingBytes =
UINT64_C(2) * 1024u * 1024u * 1024u;
constexpr uint64_t kCampaignWorkingBytes = UINT64_C(256) * 1024u;
constexpr uint64_t kGroupWorkingBytes = UINT64_C(64) * 1024u;
/* Codec is transport-safe and deterministic: no native struct serialization is
* used, integers are fixed-width with explicit endianness, and strings/counts are
@ -183,6 +190,20 @@ struct Context {
std::vector<unsigned char> encoded;
Lardon3DAcquisitionCampaignPlan plan{};
Lardon3DAcquisitionIngestOptions options{};
#ifdef LARDON3D_ACQUISITION_CAMPAIGN_TASK_TESTING
enum { kTestGroupCapacity = 8 };
/* Test builds can substitute only S3-E's returned Capture IDs. The complete
* Task/DB/checkpoint/sequence-break path remains production code, while no
* filesystem decoder or scientific grouping behavior is replaced. Fixed
* storage keeps the production resource estimate deterministic in tests. */
uint64_t test_capture_ids[kTestGroupCapacity]{};
unsigned int test_observed_threads[kTestGroupCapacity]{};
size_t test_capture_count{};
size_t test_observed_count{};
uint32_t test_failure_group{};
unsigned int test_mutate_threads_before_break{};
bool test_materialization_fixture{};
#endif
};
bool context_owned_bytes(const Context &context, uint64_t &bytes) {
@ -209,6 +230,64 @@ bool context_owned_bytes(const Context &context, uint64_t &bytes) {
return true;
}
bool estimate_equals(const Lardon3DResourceEstimate &left,
const Lardon3DResourceEstimate &right) {
return left.memory_fixed_bytes == right.memory_fixed_bytes &&
left.gpu_memory_fixed_bytes == right.gpu_memory_fixed_bytes &&
left.memory_bytes_per_item == right.memory_bytes_per_item &&
left.gpu_memory_bytes_per_item == right.gpu_memory_bytes_per_item &&
left.minimum_batch_size == right.minimum_batch_size &&
left.maximum_batch_size == right.maximum_batch_size &&
left.desired_cpu_threads == right.desired_cpu_threads &&
left.desired_gpu_slots == right.desired_gpu_slots &&
left.desired_io_slots == right.desired_io_slots &&
left.task_class == right.task_class;
}
bool campaign_estimate(const Context &context, bool historical,
Lardon3DResourceEstimate &estimate) {
if (context.options.representation !=
LARDON3D_ACQUISITION_SELECT_JPEG_SOURCE &&
context.options.representation !=
LARDON3D_ACQUISITION_SELECT_DEVELOP_RAW)
return false;
uint64_t fixed = 0;
if (!context_owned_bytes(context, fixed) ||
kCampaignWorkingBytes > UINT64_MAX - fixed)
return false;
fixed += kCampaignWorkingBytes;
Lardon3DResourceTaskClass task_class = LARDON3D_RESOURCE_TASK_IMPORT;
if (!historical) {
const uint64_t transient_request =
static_cast<uint64_t>(context.encoded.size());
if (transient_request > UINT64_MAX - fixed)
return false;
fixed += transient_request;
if (context.options.representation ==
LARDON3D_ACQUISITION_SELECT_DEVELOP_RAW) {
if (kRawWorkingBytes > UINT64_MAX - fixed)
return false;
fixed += kRawWorkingBytes;
task_class = LARDON3D_RESOURCE_TASK_MIXED;
}
}
estimate = Lardon3DResourceEstimate{
fixed, 0, kGroupWorkingBytes, 0, 1, 1, 1, 0, 1, task_class};
return true;
}
bool apply_admitted_opencv_threads(
const Lardon3DTaskExecutionContract &contract) noexcept {
if (contract.cpu_threads == 0 || contract.cpu_threads > INT_MAX)
return false;
try {
cv::setNumThreads(static_cast<int>(contract.cpu_threads));
return cv::getNumThreads() == static_cast<int>(contract.cpu_threads);
} catch (...) {
return false;
}
}
void destroy(void *p) { delete static_cast<Context *>(p); }
void runtime(Context *c, Lardon3DAppState &s) {
lardon3d_app_state_init(&s);
@ -234,18 +313,38 @@ bool checkpoint(Context *c, Lardon3DTask *t, uint32_t cursor) {
bool run_impl(Lardon3DTask *t, void *p) {
auto *c = static_cast<Context *>(p);
Lardon3DProjectDbAcquisitionCampaignTask persisted{};
// Request is reconstructed from persisted blob and treated as immutable durable
// input during recovery and replay.
std::vector<unsigned char> blob(max_request_size);
/* The retained exact encoding is the immutable replay input and also bounds
* this transient verification buffer. A maximum-capacity allocation would
* charge almost 20 MiB even for a small campaign without adding safety. */
std::unique_ptr<unsigned char[]> blob(
new unsigned char[c->encoded.size()]);
if (lardon3d_project_db_load_acquisition_campaign_task(
c->db, lardon3d_task_id(t), blob.data(), blob.size(), &persisted) !=
c->db, lardon3d_task_id(t), blob.get(), c->encoded.size(),
&persisted) !=
LARDON3D_PROJECT_DB_OK)
return lardon3d_task_fail(t, "Campagne durable introuvable.");
if (persisted.scanset_id != c->scanset ||
persisted.group_count != c->plan.group_count ||
persisted.request_size != c->encoded.size() ||
std::memcmp(blob.get(), c->encoded.data(), c->encoded.size()) != 0)
return lardon3d_task_fail(t, "Requête durable de campagne incohérente.");
if (!lardon3d_task_set_durable_progress(
t, persisted.next_group_id, persisted.group_count,
"Préfixe durable de campagne observé."))
return false;
for (uint32_t cursor = persisted.next_group_id; cursor < c->plan.group_count;
++cursor) {
const uint32_t group_id = cursor + 1u;
if (!lardon3d_task_checkpoint(t))
return false;
#ifdef LARDON3D_ACQUISITION_CAMPAIGN_TASK_TESTING
if (c->test_materialization_fixture) {
if (c->test_observed_count >= Context::kTestGroupCapacity)
return lardon3d_task_fail(t, "Trop d'observations CPU de test.");
c->test_observed_threads[c->test_observed_count++] =
static_cast<unsigned int>(cv::getNumThreads());
}
#endif
Lardon3DProjectDbAcquisitionCampaignCapture retained{};
uint64_t resume = 0;
/* Durable replay uses the stable mapping (task_id, group_id) -> capture_id.
@ -258,10 +357,24 @@ bool run_impl(Lardon3DTask *t, void *p) {
resume = retained.capture_id;
else if (lr != LARDON3D_PROJECT_DB_NOT_FOUND)
return lardon3d_task_fail(t, "Lecture de rétention impossible.");
#ifdef LARDON3D_ACQUISITION_CAMPAIGN_TASK_TESTING
if (c->test_materialization_fixture &&
c->test_failure_group == group_id)
return lardon3d_task_fail(t, "Échec de callback campagne injecté.");
#endif
auto options = c->options;
options.grouping = LARDON3D_ACQUISITION_GROUP_CALLER_EXPLICIT;
options.resume_capture_id = resume;
options.producer_task_id = lardon3d_task_id(t);
uint64_t materialized_capture_id = 0;
#ifdef LARDON3D_ACQUISITION_CAMPAIGN_TASK_TESTING
if (c->test_materialization_fixture) {
if (group_id == 0 || group_id > c->test_capture_count)
return lardon3d_task_fail(t, "Fixture de Capture de campagne invalide.");
materialized_capture_id = c->test_capture_ids[group_id - 1u];
} else
#endif
{
Lardon3DAcquisitionIngestOutput out{};
Lardon3DAcquisitionIngestResult ir{};
Lardon3DAppState s;
@ -272,6 +385,8 @@ bool run_impl(Lardon3DTask *t, void *p) {
if (cr != LARDON3D_ACQUISITION_CAMPAIGN_OK ||
ir != LARDON3D_ACQUISITION_INGEST_OK || out.group_count != 1)
return lardon3d_task_fail(t, "Échec de matérialisation de campagne.");
materialized_capture_id = out.groups[0].capture_id;
}
#ifdef LARDON3D_ACQUISITION_CAMPAIGN_TASK_TESTING
const char *before_retention =
std::getenv("LARDON3D_TEST_CAMPAIGN_FAIL_BEFORE_RETENTION");
@ -279,7 +394,7 @@ bool run_impl(Lardon3DTask *t, void *p) {
return lardon3d_task_fail(t, "Échec injecté avant rétention.");
#endif
if (lardon3d_project_db_retain_acquisition_campaign_capture(
c->db, lardon3d_task_id(t), group_id, out.groups[0].capture_id,
c->db, lardon3d_task_id(t), group_id, materialized_capture_id,
group_id) != LARDON3D_PROJECT_DB_OK)
return lardon3d_task_fail(t, "Rétention de Capture impossible.");
/* Limite de reprise acceptée: entre le retour de S3-E et cette rétention
@ -292,27 +407,51 @@ bool run_impl(Lardon3DTask *t, void *p) {
if (after_retention && std::strcmp(after_retention, "1") == 0)
return lardon3d_task_fail(t, "Échec injecté après rétention.");
#endif
unsigned progress = static_cast<unsigned>(
(uint64_t(group_id) * 100u / c->plan.group_count));
if (!lardon3d_task_set_progress(t, progress,
/* The retained mapping/cursor transaction above is the authority for this
* exact TUI count. Observation may lag it, but must never lead it. */
if (!lardon3d_task_set_durable_progress(
t, group_id, c->plan.group_count,
"Groupe de campagne matérialisé.") ||
!checkpoint(c, t, group_id))
return lardon3d_task_fail(t, "Checkpoint de campagne impossible.");
if (group_id < c->plan.group_count) {
// sequence_break: libère la réservation courante et renégocie l'admission
// via le Governor avant le groupe suivant.
/* Campaign cardinality never extends a reservation lifetime: this
* boundary releases the current group admission and Task/Queue asks the
* Governor for a fresh bounded contract before the next group. */
Lardon3DTaskExecutionContract contract{};
Lardon3DResourceReservation *reservation = nullptr;
#ifdef LARDON3D_ACQUISITION_CAMPAIGN_TASK_TESTING
if (c->test_materialization_fixture &&
c->test_mutate_threads_before_break > 0) {
cv::setNumThreads(
static_cast<int>(c->test_mutate_threads_before_break));
if (cv::getNumThreads() !=
static_cast<int>(c->test_mutate_threads_before_break))
return lardon3d_task_fail(t, "Mutation OpenCV de test impossible.");
}
#endif
if (!lardon3d_task_sequence_break(t, c->governor, &reservation,
&contract))
return false;
if (!apply_admitted_opencv_threads(contract))
return lardon3d_task_fail(
t, "Contrat CPU OpenCV renouvelé de campagne invalide.");
}
}
return true;
}
bool run(Lardon3DTask *t, void *p) noexcept {
try {
return run_impl(t, p);
Lardon3DOpenCvTaskThreadGuard threads(t);
if (!threads.valid())
return lardon3d_task_fail(t, "Contrat CPU OpenCV de campagne invalide.");
/* Queue owns the sole campaign callback. Apply its admitted CPU count to
* OpenCV for direct in-task RAW/JPEG work; campaign execution does not
* create a nested Task or a second reservation owner. */
bool result = run_impl(t, p);
if (!threads.restore())
return lardon3d_task_fail(t, "Restauration OpenCV de campagne impossible.");
return result;
} catch (const std::bad_alloc &) {
return lardon3d_task_fail(t, "Mémoire insuffisante pour la campagne.");
} catch (...) {
@ -333,7 +472,9 @@ Context *make_context(const char *path, Lardon3DProjectDb *db,
Lardon3DResourceGovernor *g, uint64_t scanset,
const Lardon3DAcquisitionCampaignTaskRequest &r,
const unsigned char *encoded, size_t n) {
if (!path || !path[0])
if (!path || !path[0] || !db || !g || scanset == 0 || !r.sources ||
r.source_count == 0 || (r.confirmation_count > 0 && !r.confirmations) ||
!encoded || n == 0)
return nullptr;
std::unique_ptr<Context> c(new (std::nothrow) Context);
if (!c)
@ -346,6 +487,7 @@ Context *make_context(const char *path, Lardon3DProjectDb *db,
c->governor = g;
c->scanset = scanset;
c->sources.assign(r.sources, r.sources + r.source_count);
if (r.confirmation_count > 0)
c->confirmations.assign(r.confirmations,
r.confirmations + r.confirmation_count);
c->options = r.ingest_options;
@ -360,26 +502,75 @@ Context *make_context(const char *path, Lardon3DProjectDb *db,
}
} // namespace
#ifdef LARDON3D_ACQUISITION_CAMPAIGN_TASK_TESTING
extern "C" bool lardon3d_acquisition_campaign_task_test_configure_execution(
void *userdata, const uint64_t *capture_ids, size_t capture_count,
uint32_t failure_group, unsigned int mutate_threads_before_break) {
try {
auto *context = static_cast<Context *>(userdata);
if (!context || !capture_ids || capture_count == 0 ||
capture_count > Context::kTestGroupCapacity ||
capture_count != context->plan.group_count ||
failure_group > capture_count || mutate_threads_before_break > INT_MAX)
return false;
for (size_t index = 0; index < capture_count; ++index) {
if (capture_ids[index] == 0 || capture_ids[index] > INT64_MAX)
return false;
context->test_capture_ids[index] = capture_ids[index];
}
context->test_capture_count = capture_count;
context->test_observed_count = 0;
context->test_failure_group = failure_group;
context->test_mutate_threads_before_break = mutate_threads_before_break;
context->test_materialization_fixture = true;
return true;
} catch (...) {
return false;
}
}
extern "C" bool lardon3d_acquisition_campaign_task_test_observed_threads(
void *userdata, unsigned int *threads, size_t capacity, size_t *count) {
if (count)
*count = 0;
auto *context = static_cast<Context *>(userdata);
if (!context || !count ||
(context->test_observed_count > 0 &&
(!threads || capacity < context->test_observed_count)))
return false;
if (context->test_observed_count > 0)
std::memcpy(threads, context->test_observed_threads,
context->test_observed_count * sizeof(*threads));
*count = context->test_observed_count;
return true;
}
#endif
extern "C" bool
lardon3d_acquisition_campaign_task_internal_configure_restored(
Lardon3DTask *task, void *userdata) {
try {
auto *context = static_cast<Context *>(userdata);
uint64_t retained = 0;
if (!task || !context || !context_owned_bytes(*context, retained) ||
retained > UINT64_MAX - 256 * 1024)
Lardon3DTaskDurableSnapshot snapshot{};
Lardon3DResourceEstimate current{};
Lardon3DResourceEstimate historical{};
if (!task || !context || !lardon3d_task_durable_snapshot(task, &snapshot) ||
!campaign_estimate(*context, false, current) ||
!campaign_estimate(*context, true, historical) ||
(!estimate_equals(snapshot.estimate, current) &&
!estimate_equals(snapshot.estimate, historical)))
return false;
Lardon3DTaskCapability capability{};
capability.estimate = Lardon3DResourceEstimate{
retained + 256 * 1024, 0, 64 * 1024, 0, 1, 1, 1, 0, 1,
LARDON3D_RESOURCE_TASK_IMPORT};
capability.estimate = current;
capability.backend = LARDON3D_RESOURCE_BACKEND_FIXED;
capability.inflight_limit = 1;
Lardon3DTaskCapabilityEnvelope envelope{};
envelope.count = 1;
envelope.capabilities[0] = capability;
/* Recovery may carry the historical under-estimate, but admission uses the
* exact newly reconstructed retained context. Durable bytes stay untouched. */
/* Exact v22 historical signatures remain recoverable, but admission uses
* the estimate derived from the immutable request. Operational policy is
* normalized only in memory; durable Task/scientific identities stay
* untouched and arbitrary estimate corruption is rejected. */
return lardon3d_task_internal_set_capability_envelope(task, &envelope);
} catch (...) {
return false;
@ -420,7 +611,8 @@ bool request_decode_impl(
uint64_t imported, maxbytes;
/* Version 1 is the only accepted codec shape; any mismatch rejects replay. */
if (!q.bytes(m, 8) || std::memcmp(m, magic, 8) || !q.u32(version) ||
version != 1 || !q.u32(n) || n == 0 || n > sc ||
version != LARDON3D_ACQUISITION_CAMPAIGN_REQUEST_VERSION ||
!q.u32(n) || n == 0 || n > sc ||
n > LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES || !q.u32(c) || c > cc ||
c > n || !sources || (c > 0 && !confirmations) || !q.u32(rep) ||
rep < 1 || rep > 2 || !q.u32(select) || select > 1 ||
@ -488,10 +680,16 @@ bool task_reconstruct_impl(
auto *rt = static_cast<Lardon3DTaskReconstructionContext *>(userdata);
if (!s || !rt || !b)
return false;
std::vector<unsigned char> blob(max_request_size);
Lardon3DProjectDbAcquisitionCampaignTask measured{};
if (lardon3d_project_db_load_acquisition_campaign_task(
rt->project_db, s->id, nullptr, 0, &measured) !=
LARDON3D_PROJECT_DB_OK)
return false;
std::unique_ptr<unsigned char[]> blob(
new unsigned char[measured.request_size]);
Lardon3DProjectDbAcquisitionCampaignTask p{};
if (lardon3d_project_db_load_acquisition_campaign_task(
rt->project_db, s->id, blob.data(), blob.size(), &p) !=
rt->project_db, s->id, blob.get(), measured.request_size, &p) !=
LARDON3D_PROJECT_DB_OK)
return false;
std::vector<Lardon3DAcquisitionCampaignSource> sources(
@ -500,17 +698,20 @@ bool task_reconstruct_impl(
LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES);
Lardon3DAcquisitionCampaignTaskRequest r{};
if (!lardon3d_acquisition_campaign_request_decode(
blob.data(), p.request_size, sources.data(), sources.size(),
blob.get(), p.request_size, sources.data(), sources.size(),
confirmations.data(), confirmations.size(), &r))
return false;
auto *c =
make_context(rt->project_path, rt->project_db, rt->resource_governor,
p.scanset_id, r, blob.data(), p.request_size);
if (!c)
p.scanset_id, r, blob.get(), p.request_size);
if (!c || p.group_count != c->plan.group_count ||
p.next_group_id > c->plan.group_count) {
delete c;
return false;
// This Task kind has no legacy operational-resource reconciliation. Keep
// every optional Registry hook explicit so a future binding extension
// cannot accidentally inherit non-null recovery behavior.
}
/* Binding owns the reconstructed context. Registry's private post-restore
* hook validates the exact current or historical operational estimate before
* Queue admission; the typed request itself is never rewritten. */
*b = {};
b->callback = run;
b->userdata = c;
@ -549,20 +750,19 @@ Lardon3DTask *create_task_impl(
delete c;
return nullptr;
}
uint64_t retained = 0;
if (!context_owned_bytes(*c, retained) || retained > UINT64_MAX - 256 * 1024) {
Lardon3DResourceEstimate e{};
if (!campaign_estimate(*c, false, e)) {
delete c;
return nullptr;
}
/* The request vectors and inline campaign plan remain live for the Task
* lifetime. Charge them before admission; the per-group working estimate is
* separate and this operational bound does not limit scientific cardinality. */
Lardon3DResourceEstimate e{
retained + 256 * 1024, 0, 64 * 1024, 0, 1,
1, 1, 0, 1, LARDON3D_RESOURCE_TASK_IMPORT};
/* Admission charges retained context, one exact transient request reload,
* and group work. DEVELOP_RAW additionally owns raw.develop's conservative
* 2 GiB callback allowance. These are per-execution operational bounds, not
* scientific limits on campaign cardinality. */
auto *t = lardon3d_task_create_typed("Campagne d'acquisition", &e,
LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND,
1, run, c, destroy);
LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND_VERSION,
run, c, destroy);
if (!t || !lardon3d_task_assign_id(t, task_id) ||
!lardon3d_task_set_finished_callback(t, finished, c) ||
!checkpoint(c, t, 0)) {
@ -633,17 +833,19 @@ extern "C" Lardon3DTask *lardon3d_project_create_acquisition_campaign_task(
extern "C" bool lardon3d_project_enqueue_acquisition_campaign(
Lardon3DAppState *s, uint64_t scanset,
const Lardon3DAcquisitionCampaignTaskRequest *r, uint64_t *id) {
if (id)
*id = 0;
try {
if (!s || !s->task_queue)
if (!s || !s->task_queue || !r || !id)
return false;
auto *t =
lardon3d_project_create_acquisition_campaign_task(s, scanset, r, id);
std::unique_ptr<Lardon3DTask, void (*)(Lardon3DTask *)> t(
lardon3d_project_create_acquisition_campaign_task(s, scanset, r, id),
lardon3d_task_destroy);
if (!t)
return false;
if (!lardon3d_task_queue_add(s->task_queue, t, nullptr)) {
lardon3d_task_destroy(t);
if (!lardon3d_task_queue_add(s->task_queue, t.get(), nullptr))
return false;
}
(void)t.release();
return true;
} catch (const std::bad_alloc &) {
return false;

View file

@ -10,8 +10,10 @@
#include <lardon3d/orb_vulkan_backend.h>
#include <lardon3d/project.h>
#include <lardon3d/resource_governor.h>
#include <lardon3d/ssd_controller.h>
#include <lardon3d/task_queue.h>
#include <lardon3d/tui.h>
#include <lardon3d/tui_ssd_async.h>
#include "resource_governor_internal.h"
@ -48,24 +50,27 @@ lardon3d_app_run(void)
)) {
return EXIT_FAILURE;
}
unsigned int feature_threads = state.hardware_profile.logical_cpu_count
- resource_policy.system_cpu_reserve;
if (feature_threads > 12) {
feature_threads = 12;
}
/* OpenCV owns one process-wide CPU setting. Startup establishes the safe
* audited baseline/ceiling before Queue exists; the sole Queue callback may
* temporarily apply its immutable admitted count and must restore this
* baseline on every path. Concurrent multi-worker mutation is unsupported.
* This operational count is never FeatureSet identity or fingerprint. */
if (!lardon3d_feature_opencv_configure_threads(feature_threads)) {
return EXIT_FAILURE;
}
state.resource_governor = lardon3d_resource_governor_create(
&state.hardware_profile,
&resource_policy
);
if (!state.resource_governor) {
Lardon3DResourceCpuPolicyDiagnostic cpu_policy;
if (!state.resource_governor
|| !lardon3d_resource_governor_internal_cpu_policy(
state.resource_governor, &cpu_policy)
|| cpu_policy.compute_cpu_count == 0) {
lardon3d_resource_governor_destroy(state.resource_governor);
return EXIT_FAILURE;
}
/* OpenCV owns one process-wide CPU setting. Startup establishes the safe
* derived compute-pool baseline before Queue exists; the sole Queue callback
* may temporarily apply its immutable admitted count and must restore this
* baseline on every path. Topology/core-group overshoot and external affinity
* constraints are therefore honored once, rather than independently capped.
* This operational count is never FeatureSet identity or fingerprint. */
if (!lardon3d_feature_opencv_configure_threads(
cpu_policy.compute_cpu_count)) {
lardon3d_resource_governor_destroy(state.resource_governor);
return EXIT_FAILURE;
}
state.orb_vulkan_backend = lardon3d_orb_vulkan_backend_create();
@ -73,29 +78,70 @@ lardon3d_app_run(void)
lardon3d_resource_governor_destroy(state.resource_governor);
return EXIT_FAILURE;
}
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 64);
state.task_queue = lardon3d_task_queue_create(
state.resource_governor, LARDON3D_TASK_QUEUE_PRODUCTION_CAPACITY);
if (!state.task_queue) {
lardon3d_orb_vulkan_backend_destroy(state.orb_vulkan_backend);
lardon3d_resource_governor_destroy(state.resource_governor);
return EXIT_FAILURE;
}
/* SSD support is optional physical-resource control. Failure to connect to
* UDisks leaves the TUI truthful but does not make scientific execution or
* Project DB availability depend on removable hardware. */
Lardon3DSsdController *ssd_controller =
lardon3d_ssd_controller_create();
Lardon3DTuiSsdAsync *ssd_operation = ssd_controller
? lardon3d_tui_ssd_async_create_with_governor(
ssd_controller, state.resource_governor)
: NULL;
if (ssd_controller && !ssd_operation) {
lardon3d_task_queue_destroy(state.task_queue);
state.task_queue = NULL;
(void)lardon3d_ssd_controller_destroy(ssd_controller);
lardon3d_orb_vulkan_backend_destroy(state.orb_vulkan_backend);
lardon3d_resource_governor_destroy(state.resource_governor);
return EXIT_FAILURE;
}
if (!lardon3d_tui_init()) {
lardon3d_task_queue_destroy(state.task_queue);
state.task_queue = NULL;
bool storage_released = !ssd_operation
|| lardon3d_tui_ssd_async_destroy_checked(&ssd_operation);
if (storage_released) {
(void)lardon3d_ssd_controller_destroy(ssd_controller);
}
lardon3d_orb_vulkan_backend_destroy(state.orb_vulkan_backend);
if (storage_released) {
lardon3d_resource_governor_destroy(state.resource_governor);
}
return EXIT_FAILURE;
}
bool success = lardon3d_tui_run(&state);
bool success = lardon3d_tui_run_with_ssd_operation(
&state, ssd_operation);
lardon3d_tui_shutdown();
/* INVARIANT: Queue destruction cancels and joins the sole Task worker, so
* every production Task-owned scratch lease is released before the
* Governor registration loses its exact physical-controller identity. */
lardon3d_task_queue_destroy(state.task_queue);
state.task_queue = NULL;
if (state.project_loaded) {
lardon3d_project_close(&state);
}
bool storage_released = !ssd_operation
|| lardon3d_tui_ssd_async_destroy_checked(&ssd_operation);
if (!storage_released) {
success = false;
} else if (!lardon3d_ssd_controller_destroy(ssd_controller)) {
success = false;
}
lardon3d_orb_vulkan_backend_destroy(state.orb_vulkan_backend);
if (storage_released) {
lardon3d_resource_governor_destroy(state.resource_governor);
}
return success ? EXIT_SUCCESS : EXIT_FAILURE;
}

View file

@ -8,7 +8,7 @@
#include <lardon3d/project_db.h>
#include <lardon3d/visual_index.h>
#include <openssl/sha.h>
#include <openssl/evp.h>
#include "candidate_pair_gen_internal.h"
@ -192,17 +192,36 @@ void lardon3d_candidate_pair_generation_fingerprint(
uint64_t visual_index_id, uint64_t source_feature_set_id,
const Lardon3DVisualIndexQueryOptions *query_options,
unsigned char fingerprint[32]) {
SHA256_CTX sha;
SHA256_Init(&sha);
SHA256_Update(&sha, &visual_index_id, sizeof(visual_index_id));
SHA256_Update(&sha, &source_feature_set_id, sizeof(source_feature_set_id));
if (query_options) {
SHA256_Update(&sha, &query_options->top_k, sizeof(query_options->top_k));
SHA256_Update(&sha, &query_options->minimum_evidence_count,
sizeof(query_options->minimum_evidence_count));
SHA256_Update(&sha, &query_options->scanset_filter, sizeof(query_options->scanset_filter));
bool exclude = query_options->exclude_same_asset;
SHA256_Update(&sha, &exclude, sizeof(exclude));
if (!fingerprint) return;
/* FROZEN SCIENTIFIC CONTRACT: v1 hashes the same 29 bytes historically
* produced on validated little-endian hosts, but spells out widths and byte
* order so a new architecture cannot silently change Candidate identity.
* The optional-query form remains the historical 16-byte ID prefix. */
unsigned char canonical[29];
for (unsigned int index = 0; index < 8; ++index) {
canonical[index] = (unsigned char)(visual_index_id >> (8U * index));
canonical[8 + index] =
(unsigned char)(source_feature_set_id >> (8U * index));
}
size_t size = 16;
if (query_options) {
const uint32_t values[] = {
query_options->top_k,
query_options->minimum_evidence_count,
(uint32_t)query_options->scanset_filter,
};
for (size_t value = 0; value < 3; ++value) {
for (unsigned int index = 0; index < 4; ++index) {
canonical[16 + value * 4 + index] =
(unsigned char)(values[value] >> (8U * index));
}
}
canonical[28] = query_options->exclude_same_asset ? 1U : 0U;
size = sizeof(canonical);
}
unsigned int digest_size = 0;
if (EVP_Digest(canonical, size, fingerprint, &digest_size, EVP_sha256(), NULL) != 1 ||
digest_size != 32) {
memset(fingerprint, 0, 32);
}
SHA256_Final(fingerprint, &sha);
}

View file

@ -38,6 +38,20 @@ Lardon3DVisualIndexResult lardon3d_candidate_pair_publish(
void lardon3d_candidate_pair_task_test_reset_parallel_counters(void);
size_t lardon3d_candidate_pair_task_test_started_participants(void);
size_t lardon3d_candidate_pair_task_test_computed_work_items(void);
/* Deterministic failure/ownership seam for the production partial-create
* cleanup path. SIZE_MAX disables failure; the active-handle count must return
* to zero before the callback exits on every result. */
void lardon3d_candidate_pair_task_test_fail_thread_create_after(
size_t successful_children);
size_t lardon3d_candidate_pair_task_test_active_private_databases(void);
bool lardon3d_candidate_pair_task_test_compute_window(
const char *project_path, Lardon3DProjectDb *database,
uint64_t visual_index_id,
const Lardon3DVisualIndexQueryOptions *query_options,
const uint64_t *source_ids, size_t source_count,
unsigned int admitted_threads,
Lardon3DCandidatePairComputation *computations,
Lardon3DVisualIndexResult *results);
#endif
#endif

View file

@ -19,12 +19,20 @@
enum {
CANDIDATE_PAIR_MINIMUM_BATCH = 1,
CANDIDATE_PAIR_MAXIMUM_BATCH = 64,
CANDIDATE_PAIR_CPU_THREADS = 12,
/* ALGORITHMIC / RESOURCE: a participant needs one independent source, so
* the existing 64-item batch bound is also the portable CPU safety bound.
* Governor admission may select fewer CPUs without changing Candidate
* identity, ordering, or the number of items admitted for this sequence. */
CANDIDATE_PAIR_CPU_MAX_SAFE = CANDIDATE_PAIR_MAXIMUM_BATCH,
CANDIDATE_PAIR_WINDOW_PER_THREAD = 2,
CANDIDATE_PAIR_WINDOW_MAX =
CANDIDATE_PAIR_CPU_THREADS * CANDIDATE_PAIR_WINDOW_PER_THREAD,
CANDIDATE_PAIR_WINDOW_MAX = CANDIDATE_PAIR_MAXIMUM_BATCH,
CANDIDATE_PAIR_CHILD_STACK_BYTES = 4 * 1024 * 1024,
CANDIDATE_PAIR_FIXED_MEMORY = 256 * 1024,
CANDIDATE_PAIR_MEMORY_PER_ITEM = 256 * 256,
/* RESOURCE: one admitted item can own one participant. Eight MiB covers
* its bounded 4 MiB pthread stack plus the Visual Index query buffers and
* private SQLite reader/cache allowance. This is sequence memory, not a
* dataset-size limit; Governor may reduce the batch on smaller hosts. */
CANDIDATE_PAIR_MEMORY_PER_ITEM = 8 * 1024 * 1024,
};
typedef struct {
@ -87,6 +95,8 @@ typedef struct {
#ifdef LARDON3D_CANDIDATE_PAIR_TASK_TESTING
static atomic_size_t test_started_participants;
static atomic_size_t test_computed_work_items;
static atomic_size_t test_active_private_databases;
static atomic_size_t test_fail_thread_create_after = SIZE_MAX;
void lardon3d_candidate_pair_task_test_reset_parallel_counters(void) {
atomic_store(&test_started_participants, 0);
@ -100,8 +110,26 @@ size_t lardon3d_candidate_pair_task_test_started_participants(void) {
size_t lardon3d_candidate_pair_task_test_computed_work_items(void) {
return atomic_load(&test_computed_work_items);
}
size_t lardon3d_candidate_pair_task_test_active_private_databases(void) {
return atomic_load(&test_active_private_databases);
}
void lardon3d_candidate_pair_task_test_fail_thread_create_after(
size_t successful_children) {
atomic_store(&test_fail_thread_create_after, successful_children);
}
#endif
static void close_worker_database(CandidateComputeWorker *worker) {
if (!worker || !worker->database) return;
lardon3d_project_db_close(worker->database);
worker->database = NULL;
#ifdef LARDON3D_CANDIDATE_PAIR_TASK_TESTING
atomic_fetch_sub(&test_active_private_databases, 1);
#endif
}
static void *compute_worker(void *userdata) {
CandidateComputeWorker *worker = userdata;
#ifdef LARDON3D_CANDIDATE_PAIR_TASK_TESTING
@ -121,11 +149,27 @@ static void *compute_worker(void *userdata) {
}
#endif
}
lardon3d_project_db_close(worker->database);
worker->database = NULL;
close_worker_database(worker);
return NULL;
}
static int create_compute_thread(pthread_t *thread,
const pthread_attr_t *attributes,
CandidateComputeWorker *worker,
size_t successful_children) {
#ifdef LARDON3D_CANDIDATE_PAIR_TASK_TESTING
/* This private seam fails before pthread_create, after an exact number of
* successful children. It deterministically exercises the production
* join/handle cleanup path without creating an unowned thread. */
if (atomic_load(&test_fail_thread_create_after) == successful_children) {
return -1;
}
#else
(void)successful_children;
#endif
return pthread_create(thread, attributes, compute_worker, worker);
}
static unsigned int membership_progress(uint64_t completed, uint64_t total) {
if (completed >= total && total != 0) return 99U;
if (total == 0) return 0U;
@ -161,12 +205,29 @@ static bool compute_window(const Lardon3DCandidatePairTaskContext *context,
unsigned int admitted_threads,
Lardon3DCandidatePairComputation *computations,
Lardon3DVisualIndexResult *results) {
if (!context || !query_options || !source_ids || !computations || !results ||
source_count == 0 || source_count > CANDIDATE_PAIR_WINDOW_MAX ||
admitted_threads == 0 ||
admitted_threads > CANDIDATE_PAIR_CPU_MAX_SAFE) {
return false;
}
unsigned int worker_count = admitted_threads;
/* source_count is validated against the 64-item bound before narrowing. */
if (worker_count > source_count) worker_count = (unsigned int)source_count;
CandidateComputeWorker workers[CANDIDATE_PAIR_CPU_THREADS];
pthread_t threads[CANDIDATE_PAIR_CPU_THREADS - 1];
CandidateComputeWorker workers[CANDIDATE_PAIR_CPU_MAX_SAFE];
pthread_t threads[CANDIDATE_PAIR_CPU_MAX_SAFE - 1];
size_t created = 0;
bool started = true;
pthread_attr_t attributes;
if (pthread_attr_init(&attributes) != 0) return false;
/* INVARIANT: child stack reservation is explicitly bounded and included
* in memory_bytes_per_item; relying on the host pthread default would make
* a 64-participant admission unaccountable and non-portable. */
if (pthread_attr_setstacksize(&attributes,
CANDIDATE_PAIR_CHILD_STACK_BYTES) != 0) {
(void)pthread_attr_destroy(&attributes);
return false;
}
for (size_t i = 0; i < source_count; ++i) {
results[i] = LARDON3D_VISUAL_INDEX_IO_ERROR;
}
@ -187,32 +248,59 @@ static bool compute_window(const Lardon3DCandidatePairTaskContext *context,
&workers[i].database, error) !=
LARDON3D_PROJECT_DB_OK) {
for (unsigned int opened = 0; opened < i; ++opened) {
lardon3d_project_db_close(workers[opened].database);
workers[opened].database = NULL;
close_worker_database(&workers[opened]);
}
(void)pthread_attr_destroy(&attributes);
return false;
}
#ifdef LARDON3D_CANDIDATE_PAIR_TASK_TESTING
atomic_fetch_add(&test_active_private_databases, 1);
#endif
}
for (unsigned int i = 1; i < worker_count; ++i) {
if (pthread_create(&threads[created], NULL, compute_worker, &workers[i]) != 0) {
if (create_compute_thread(&threads[created], &attributes, &workers[i],
created) != 0) {
started = false;
break;
}
++created;
}
if (pthread_attr_destroy(&attributes) != 0) started = false;
if (worker_count > 0) (void)compute_worker(&workers[0]);
for (size_t i = 0; i < created; ++i) {
if (pthread_join(threads[i], NULL) != 0) started = false;
}
for (unsigned int i = (unsigned int)created + 1; i < worker_count; ++i) {
if (workers[i].database) {
lardon3d_project_db_close(workers[i].database);
workers[i].database = NULL;
}
close_worker_database(&workers[i]);
}
return started;
}
#ifdef LARDON3D_CANDIDATE_PAIR_TASK_TESTING
bool lardon3d_candidate_pair_task_test_compute_window(
const char *project_path, Lardon3DProjectDb *database,
uint64_t visual_index_id,
const Lardon3DVisualIndexQueryOptions *query_options,
const uint64_t *source_ids, size_t source_count,
unsigned int admitted_threads,
Lardon3DCandidatePairComputation *computations,
Lardon3DVisualIndexResult *results) {
if (!project_path || !database) return false;
Lardon3DCandidatePairTaskContext context = {0};
int written = snprintf(context.project_path, sizeof(context.project_path),
"%s", project_path);
if (written <= 0 || (size_t)written >= sizeof(context.project_path) ||
!lardon3d_project_db_copy_path(database, context.database_path)) {
return false;
}
/* The seam delegates to the production fan-out and owns no publication.
* Tests can therefore exercise all 64 participants without inventing Task
* progress, Candidate identities, or a second scheduling implementation. */
return compute_window(&context, visual_index_id, query_options, source_ids,
source_count, admitted_threads, computations, results);
}
#endif
static bool run(Lardon3DTask *task, void *userdata) {
Lardon3DCandidatePairTaskContext *context = userdata;
@ -245,7 +333,7 @@ static bool run(Lardon3DTask *task, void *userdata) {
contract.batch_size < CANDIDATE_PAIR_MINIMUM_BATCH ||
contract.batch_size > CANDIDATE_PAIR_MAXIMUM_BATCH ||
contract.cpu_threads == 0 ||
contract.cpu_threads > CANDIDATE_PAIR_CPU_THREADS) {
contract.cpu_threads > CANDIDATE_PAIR_CPU_MAX_SAFE) {
return lardon3d_task_fail(task, "Contrat de lot Candidate Pair invalide.");
}
@ -260,11 +348,12 @@ static bool run(Lardon3DTask *task, void *userdata) {
while (processed_in_sequence < contract.batch_size) {
if (!lardon3d_task_checkpoint(task)) return false;
size_t remaining = contract.batch_size - processed_in_sequence;
size_t window_capacity = (size_t)contract.cpu_threads *
size_t window_capacity = contract.cpu_threads >
CANDIDATE_PAIR_WINDOW_MAX /
CANDIDATE_PAIR_WINDOW_PER_THREAD
? CANDIDATE_PAIR_WINDOW_MAX
: (size_t)contract.cpu_threads *
CANDIDATE_PAIR_WINDOW_PER_THREAD;
if (window_capacity > CANDIDATE_PAIR_WINDOW_MAX) {
window_capacity = CANDIDATE_PAIR_WINDOW_MAX;
}
if (window_capacity > remaining) window_capacity = remaining;
uint64_t source_ids[CANDIDATE_PAIR_WINDOW_MAX];
size_t source_count = 0;
@ -298,10 +387,12 @@ static bool run(Lardon3DTask *task, void *userdata) {
Lardon3DCandidatePairComputation computations[CANDIDATE_PAIR_WINDOW_MAX];
Lardon3DVisualIndexResult results[CANDIDATE_PAIR_WINDOW_MAX];
/* The Queue callback is one admitted CPU participant. At most
* cpu_threads-1 child threads are created, every reader owns a
* private SQLite handle, and all are joined before reservation
* release or sequence_break. */
/* CONTRACT: the Queue callback is one admitted CPU participant.
* At most cpu_threads-1 child threads are created, every useful
* participant owns one private SQLite handle, and all are joined
* and closed before reservation release or sequence_break. The
* per-item charge covers this bounded participant state and stack;
* batch=1 therefore cannot allocate idle CPU workers. */
if (!compute_window(context, context->parameters.visual_index_id,
&query_options, source_ids, source_count,
contract.cpu_threads, computations, results)) {
@ -472,12 +563,12 @@ Lardon3DTask *lardon3d_project_create_candidate_pair_generate_task(
Lardon3DCandidatePairTaskContext *context =
make_context(&runtime, &parameters);
if (!context) return NULL;
unsigned int desired_cpu_threads = CANDIDATE_PAIR_CPU_THREADS;
unsigned int desired_cpu_threads = CANDIDATE_PAIR_CPU_MAX_SAFE;
#ifdef LARDON3D_CANDIDATE_PAIR_TASK_TESTING
const char *test_threads = getenv("LARDON3D_TEST_CANDIDATE_PAIR_THREADS");
if (test_threads) {
unsigned long parsed = strtoul(test_threads, NULL, 10);
if (parsed >= 1 && parsed <= CANDIDATE_PAIR_CPU_THREADS) {
if (parsed >= 1 && parsed <= CANDIDATE_PAIR_CPU_MAX_SAFE) {
desired_cpu_threads = (unsigned int)parsed;
}
}

View file

@ -1,5 +1,6 @@
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <openssl/evp.h>
#include <stdbool.h>
#include <stdint.h>
@ -206,13 +207,21 @@ static bool run(Lardon3DTask *task, void *userdata) {
struct timespec end = {0};
bool timing_known = clock_gettime(CLOCK_MONOTONIC, &begin) == 0;
Lardon3DOpenCvTaskThreadControl threads;
if (!lardon3d_opencv_task_threads_begin(task, 12, &threads)) {
if (!lardon3d_opencv_task_threads_begin(task, INT_MAX, &threads)) {
/* begin may have changed OpenCV before verification failed. If its first
* rollback was transiently unsuccessful, this bounded second attempt must
* discharge the process-global ownership before the callback returns. */
if (threads.restore_required &&
!lardon3d_opencv_task_threads_end(&threads)) {
return lardon3d_task_fail(task, "Restauration OpenCV Features impossible.");
}
return lardon3d_task_fail(task, "Contrat CPU OpenCV Features invalide.");
}
/* Queue has one callback owner, so this process-wide OpenCV setting cannot
* race another Task. The immutable admission selects 1..12, and this guard
* applies exactly that count before extraction and restores it on every
* return without changing Feature identity. */
/* EXTERNAL LIBRARY: OpenCV accepts a positive signed-int thread count.
* Queue has one callback owner, so this process-wide setting cannot race
* another Task. Governor clamps the portable INT_MAX capability to the host
* compute pool; the guard applies that immutable admission and restores it
* on every return without changing Feature identity. */
size_t durable_items = 0;
bool result = run_body(task, userdata, &durable_items);
if (!lardon3d_opencv_task_threads_end(&threads)) {
@ -331,9 +340,9 @@ lardon3d_project_create_feature_extract_task(Lardon3DAppState *state, uint64_t i
.memory_bytes_per_item = 512ULL * 1024 * 1024,
.minimum_batch_size = 1,
.maximum_batch_size = 1,
/* Canonical durable maximum. Governor admission may select
* any validated OpenCV count in 1..12 for this execution. */
.desired_cpu_threads = 12,
/* EXTERNAL LIBRARY: OpenCV's positive-int API is the safe
* ceiling; Governor supplies the portable host maximum. */
.desired_cpu_threads = INT_MAX,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU};
Lardon3DTask *task = lardon3d_task_create_typed("Extraction de features", &estimate,

View file

@ -1,4 +1,5 @@
#include <algorithm>
#include <atomic>
#include <climits>
#include <cmath>
#include <cstdio>
@ -11,6 +12,7 @@
#include <openssl/evp.h>
#include <string>
#include <unistd.h>
#include <utility>
#include <vector>
extern "C" {
@ -19,10 +21,23 @@ extern "C" {
#include <lardon3d/match_file.h>
}
#include "geometric_verifier_internal.h"
struct Lardon3DGeometricVerifierPrepared {
uint64_t match_result_id{};
uint32_t verifier_version{};
unsigned char fingerprint[32]{};
Lardon3DGeometricVerificationStatus status{LARDON3D_GEOMETRIC_REJECTED};
uint32_t inlier_count{};
std::vector<unsigned char> inlier_mask;
bool has_model{};
double model[9]{};
};
namespace {
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TESTING
uint32_t estimator_calls;
std::atomic<uint32_t> estimator_calls{0};
#endif
void put_u32(unsigned char *bytes, uint32_t value) {
@ -65,15 +80,23 @@ bool read_points(const char *project_path,
if (lardon3d_feature_reader_open(project_path, &set, &reader, &metadata) !=
LARDON3D_FEATURE_STORE_OK)
return false;
try {
points.resize(set.feature_count);
bool ok = metadata.feature_count == set.feature_count;
for (uint32_t start = 0; ok && start < set.feature_count; start += 256) {
size_t count = std::min<size_t>(256, set.feature_count - start);
ok = lardon3d_feature_reader_keypoints(reader, start, points.data() + start,
count) == LARDON3D_FEATURE_STORE_OK;
ok = lardon3d_feature_reader_keypoints(
reader, start, points.data() + start, count) ==
LARDON3D_FEATURE_STORE_OK;
}
lardon3d_feature_reader_close(reader);
return ok;
} catch (...) {
// The preparation call owns this reader. Allocation failure must not leak
// one descriptor per concurrent participant before the C ABI translates it.
lardon3d_feature_reader_close(reader);
throw;
}
}
bool estimate_fundamental(const std::vector<cv::Point2d> &points_a,
@ -81,7 +104,7 @@ bool estimate_fundamental(const std::vector<cv::Point2d> &points_a,
const Lardon3DGeometricVerifierParameters *parameters,
uint32_t seed, cv::Mat &model, cv::Mat &mask) {
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TESTING
++estimator_calls;
(void)estimator_calls.fetch_add(1, std::memory_order_relaxed);
const char *behavior = std::getenv("LARDON3D_TEST_GEOMETRIC_ESTIMATOR");
if (behavior && std::strcmp(behavior, "error") == 0)
return false;
@ -171,11 +194,11 @@ bool has_minimal_support(const Lardon3DMatchFileEntry *entries, size_t count,
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TESTING
extern "C" void lardon3d_geometric_verifier_test_reset_estimator_calls(void) {
estimator_calls = 0;
estimator_calls.store(0, std::memory_order_relaxed);
}
extern "C" uint32_t lardon3d_geometric_verifier_test_estimator_calls(void) {
return estimator_calls;
return estimator_calls.load(std::memory_order_relaxed);
}
extern "C" bool lardon3d_geometric_verifier_test_has_minimal_support(
@ -332,13 +355,36 @@ lardon3d_geometric_verifier_verify_and_publish_version(
const char *project_path, Lardon3DProjectDb *db, uint64_t match_id,
const Lardon3DGeometricVerifierParameters *p, uint32_t verifier_version,
Lardon3DProjectDbGeometricVerificationResult *result, bool *reused) {
if (!project_path || !db || match_id == 0 || !p || !result || !reused ||
!lardon3d_geometric_verifier_parameters_valid(p) ||
Lardon3DGeometricVerifierPrepared *prepared = nullptr;
Lardon3DGeometricVerifierResult status =
lardon3d_geometric_verifier_internal_prepare_version(
project_path, db, match_id, p, verifier_version, &prepared, result,
reused);
if (status == LARDON3D_GEOMETRIC_VERIFIER_OK && prepared) {
status = lardon3d_geometric_verifier_internal_publish_prepared(
db, prepared, result, reused);
}
lardon3d_geometric_verifier_internal_prepared_destroy(prepared);
return status;
}
extern "C" Lardon3DGeometricVerifierResult
lardon3d_geometric_verifier_internal_prepare_version(
const char *project_path, Lardon3DProjectDb *db, uint64_t match_id,
const Lardon3DGeometricVerifierParameters *p, uint32_t verifier_version,
Lardon3DGeometricVerifierPrepared **prepared,
Lardon3DProjectDbGeometricVerificationResult *result, bool *reused) {
if (prepared)
*prepared = nullptr;
if (reused)
*reused = false;
if (!project_path || !db || match_id == 0 || !p || !prepared || !result ||
!reused || !lardon3d_geometric_verifier_parameters_valid(p) ||
(verifier_version != LARDON3D_GEOMETRIC_VERIFIER_VERSION_V1 &&
verifier_version != LARDON3D_GEOMETRIC_VERIFIER_VERSION_V2 &&
verifier_version != LARDON3D_GEOMETRIC_VERIFIER_VERSION_V3))
return LARDON3D_GEOMETRIC_VERIFIER_INVALID_ARGUMENT;
*reused = false;
std::memset(result, 0, sizeof(*result));
unsigned char fingerprint[32];
if (!lardon3d_geometric_verifier_fingerprint_for_version(
p, verifier_version, fingerprint))
@ -419,8 +465,7 @@ lardon3d_geometric_verifier_verify_and_publish_version(
const bool acceptance_feasible =
verifier_version != LARDON3D_GEOMETRIC_VERIFIER_VERSION_V3 ||
count >= p->min_inlier_count;
const bool sufficient_support = minimal_support && acceptance_feasible;
if (sufficient_support) {
if (minimal_support && acceptance_feasible) {
uint32_t seed = lardon3d_geometric_verifier_seed(
parent.match_asset_sha256, fingerprint);
if (!estimate_fundamental(points_a, points_b, p, seed, model, mask))
@ -454,8 +499,44 @@ lardon3d_geometric_verifier_verify_and_publish_version(
if (mask.ptr<unsigned char>()[index] != 0)
return LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR;
}
bool accepted = !model.empty() && inliers >= p->min_inlier_count &&
const bool accepted =
!model.empty() && inliers >= p->min_inlier_count &&
static_cast<double>(inliers) / count >= p->min_inlier_ratio;
auto *stage = new Lardon3DGeometricVerifierPrepared;
stage->match_result_id = match_id;
stage->verifier_version = verifier_version;
std::memcpy(stage->fingerprint, fingerprint, sizeof(stage->fingerprint));
stage->status = accepted ? LARDON3D_GEOMETRIC_VERIFIED
: LARDON3D_GEOMETRIC_REJECTED;
stage->inlier_count = inliers;
stage->inlier_mask = std::move(bitset);
stage->has_model = accepted;
if (accepted)
std::memcpy(stage->model, canonical, sizeof(stage->model));
*prepared = stage;
return LARDON3D_GEOMETRIC_VERIFIER_OK;
// This catch set governs the complete preparation C entry point: unexpected
// OpenCV behavior is execution failure and can never escape the ABI or be
// converted into a persisted scientific rejection.
} catch (const std::bad_alloc &) {
return LARDON3D_GEOMETRIC_VERIFIER_OUT_OF_MEMORY;
} catch (const cv::Exception &) {
return LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR;
} catch (...) {
return LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR;
}
}
extern "C" Lardon3DGeometricVerifierResult
lardon3d_geometric_verifier_internal_publish_prepared(
Lardon3DProjectDb *db,
const Lardon3DGeometricVerifierPrepared *prepared,
Lardon3DProjectDbGeometricVerificationResult *result, bool *reused) {
if (reused)
*reused = false;
if (!db || !prepared || !result || !reused)
return LARDON3D_GEOMETRIC_VERIFIER_INVALID_ARGUMENT;
std::memset(result, 0, sizeof(*result));
int64_t now = static_cast<int64_t>(std::time(nullptr));
if (now < 0)
return LARDON3D_GEOMETRIC_VERIFIER_DATABASE_ERROR;
@ -467,17 +548,18 @@ lardon3d_geometric_verifier_verify_and_publish_version(
#endif
Lardon3DProjectDbResult created =
lardon3d_project_db_create_geometric_verification_result(
db, match_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL,
verifier_version, fingerprint,
accepted ? LARDON3D_GEOMETRIC_VERIFIED
: LARDON3D_GEOMETRIC_REJECTED,
inliers, bitset.data(), bitset.size(),
accepted ? canonical : nullptr, now, result);
db, prepared->match_result_id,
LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL,
prepared->verifier_version, prepared->fingerprint, prepared->status,
prepared->inlier_count, prepared->inlier_mask.data(),
prepared->inlier_mask.size(),
prepared->has_model ? prepared->model : nullptr, now, result);
if (created == LARDON3D_PROJECT_DB_CONSTRAINT) {
Lardon3DProjectDbResult concurrent =
lardon3d_project_db_find_geometric_verification_result(
db, match_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL,
verifier_version, fingerprint, result);
db, prepared->match_result_id,
LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL,
prepared->verifier_version, prepared->fingerprint, result);
if (concurrent == LARDON3D_PROJECT_DB_OK) {
*reused = true;
return LARDON3D_GEOMETRIC_VERIFIER_OK;
@ -487,14 +569,9 @@ lardon3d_geometric_verifier_verify_and_publish_version(
return created == LARDON3D_PROJECT_DB_OK
? LARDON3D_GEOMETRIC_VERIFIER_OK
: LARDON3D_GEOMETRIC_VERIFIER_DATABASE_ERROR;
// This catch set governs the complete public C entry point: unexpected
// OpenCV/malformed estimator behavior is execution failure and can never
// escape the ABI or be converted into a persisted scientific rejection.
} catch (const std::bad_alloc &) {
return LARDON3D_GEOMETRIC_VERIFIER_OUT_OF_MEMORY;
} catch (const cv::Exception &) {
return LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR;
} catch (...) {
return LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR;
}
}
extern "C" void lardon3d_geometric_verifier_internal_prepared_destroy(
Lardon3DGeometricVerifierPrepared *prepared) {
delete prepared;
}

View file

@ -0,0 +1,52 @@
#ifndef LARDON3D_GEOMETRIC_VERIFIER_INTERNAL_H
#define LARDON3D_GEOMETRIC_VERIFIER_INTERNAL_H
#include <stdbool.h>
#include <stdint.h>
#include <lardon3d/geometric_verifier.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct Lardon3DGeometricVerifierPrepared
Lardon3DGeometricVerifierPrepared;
/* Worker-owned preparation validates every immutable input and computes the
* exact frozen v1/v2/v3 scientific payload, but never mutates Project DB. If
* the exact identity already exists, `prepared` remains NULL and the
* caller-owned `result` is filled with that durable row. Otherwise the caller
* owns one bounded opaque preparation until destroy, including on publication
* failure. Independent preparations may run concurrently against the shared,
* internally serialized Project DB handle. Every Match/Feature file handle is
* owned by one preparation call and closed before that call returns. */
Lardon3DGeometricVerifierResult
lardon3d_geometric_verifier_internal_prepare_version(
const char *project_path, Lardon3DProjectDb *database,
uint64_t match_result_id,
const Lardon3DGeometricVerifierParameters *parameters,
uint32_t verifier_version, Lardon3DGeometricVerifierPrepared **prepared,
Lardon3DProjectDbGeometricVerificationResult *result, bool *reused);
/* The sequence owner alone publishes prepared payloads in canonical parent
* order. Retry is exact: a concurrent/existing identical row is returned as
* reuse, while no different identity is inferred. The function does not
* consume `prepared`; destroy remains mandatory after every return. */
Lardon3DGeometricVerifierResult
lardon3d_geometric_verifier_internal_publish_prepared(
Lardon3DProjectDb *database,
const Lardon3DGeometricVerifierPrepared *prepared,
Lardon3DProjectDbGeometricVerificationResult *result, bool *reused);
/* Releases one preparation. NULL is accepted so a batch cleanup sweep remains
* safe after clearing owned slots; every non-NULL object must be released
* exactly once on partial thread creation, cancellation, success, or failure. */
void lardon3d_geometric_verifier_internal_prepared_destroy(
Lardon3DGeometricVerifierPrepared *prepared);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -1,4 +1,6 @@
#include <pthread.h>
#include <stdbool.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@ -9,11 +11,18 @@
#include <lardon3d/project.h>
#include <lardon3d/task_queue.h>
#include "geometric_verifier_internal.h"
#include "task_internal.h"
enum {
GEOMETRIC_VERIFIER_PAGE_CAPACITY =
LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_BATCH + 1,
GEOMETRIC_VERIFIER_MEMORY_BYTES = 4 * 1024 * 1024,
GEOMETRIC_VERIFIER_CPU_THREADS = 1,
/* Each participant owns at most the established conservative 4 MiB core
* allowance plus one explicitly bounded 4 MiB child stack. Charging 8 MiB
* per batch item safely covers the owner too and remains an operational
* admission bound, never a Match-Result dataset limit. */
GEOMETRIC_VERIFIER_MEMORY_BYTES_PER_ITEM = 8 * 1024 * 1024,
GEOMETRIC_VERIFIER_CHILD_STACK_BYTES = 4 * 1024 * 1024,
};
typedef struct {
@ -24,6 +33,119 @@ typedef struct {
uint32_t verifier_version;
} Lardon3DGeometricVerifierTaskContext;
typedef struct {
Lardon3DGeometricVerifierResult status;
Lardon3DGeometricVerifierPrepared *prepared;
Lardon3DProjectDbGeometricVerificationResult result;
bool reused;
bool applicable;
} Lardon3DGeometricVerifierJob;
typedef struct {
Lardon3DGeometricVerifierTaskContext *context;
const Lardon3DProjectDbMatchResult *parents;
Lardon3DGeometricVerifierJob *jobs;
Lardon3DGeometricVerifierParameters parameters;
size_t count;
atomic_size_t next;
atomic_bool stop;
} Lardon3DGeometricVerifierBatch;
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TASK_TESTING
static atomic_uint observed_cpu_contracts;
typedef struct {
pthread_mutex_t mutex;
pthread_cond_t condition;
bool armed;
bool reached;
bool released;
} Lardon3DGeometricVerifierTestBarrier;
#define GEOMETRIC_VERIFIER_TEST_BARRIER_INITIALIZER \
{ PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, false, false, false }
static Lardon3DGeometricVerifierTestBarrier sequence_barrier =
GEOMETRIC_VERIFIER_TEST_BARRIER_INITIALIZER;
static Lardon3DGeometricVerifierTestBarrier prepublication_barrier =
GEOMETRIC_VERIFIER_TEST_BARRIER_INITIALIZER;
void lardon3d_geometric_verifier_task_test_reset_cpu_contracts(void) {
atomic_store_explicit(&observed_cpu_contracts, 0, memory_order_relaxed);
}
unsigned int lardon3d_geometric_verifier_task_test_cpu_contracts(void) {
return atomic_load_explicit(&observed_cpu_contracts, memory_order_relaxed);
}
static void test_barrier_arm(Lardon3DGeometricVerifierTestBarrier *barrier) {
(void)pthread_mutex_lock(&barrier->mutex);
barrier->armed = true;
barrier->reached = false;
barrier->released = false;
(void)pthread_mutex_unlock(&barrier->mutex);
}
static bool test_barrier_wait(Lardon3DGeometricVerifierTestBarrier *barrier) {
struct timespec deadline;
if (clock_gettime(CLOCK_REALTIME, &deadline) != 0) {
return false;
}
deadline.tv_sec += 5;
(void)pthread_mutex_lock(&barrier->mutex);
bool ok = true;
while (!barrier->reached && ok) {
ok = pthread_cond_timedwait(&barrier->condition, &barrier->mutex,
&deadline) == 0;
}
(void)pthread_mutex_unlock(&barrier->mutex);
return ok;
}
static void test_barrier_release(Lardon3DGeometricVerifierTestBarrier *barrier) {
(void)pthread_mutex_lock(&barrier->mutex);
barrier->released = true;
(void)pthread_cond_broadcast(&barrier->condition);
(void)pthread_mutex_unlock(&barrier->mutex);
}
static void test_barrier_reach(Lardon3DGeometricVerifierTestBarrier *barrier) {
(void)pthread_mutex_lock(&barrier->mutex);
if (barrier->armed) {
barrier->reached = true;
(void)pthread_cond_broadcast(&barrier->condition);
while (!barrier->released) {
(void)pthread_cond_wait(&barrier->condition, &barrier->mutex);
}
barrier->armed = false;
}
(void)pthread_mutex_unlock(&barrier->mutex);
}
void lardon3d_geometric_verifier_task_test_arm_sequence_barrier(void) {
test_barrier_arm(&sequence_barrier);
}
bool lardon3d_geometric_verifier_task_test_wait_sequence_barrier(void) {
return test_barrier_wait(&sequence_barrier);
}
void lardon3d_geometric_verifier_task_test_release_sequence_barrier(void) {
test_barrier_release(&sequence_barrier);
}
void lardon3d_geometric_verifier_task_test_arm_prepublication_barrier(void) {
test_barrier_arm(&prepublication_barrier);
}
bool lardon3d_geometric_verifier_task_test_wait_prepublication_barrier(void) {
return test_barrier_wait(&prepublication_barrier);
}
void lardon3d_geometric_verifier_task_test_release_prepublication_barrier(void) {
test_barrier_release(&prepublication_barrier);
}
#endif
static void destroy_context(void *userdata) { free(userdata); }
static Lardon3DGeometricVerifierParameters
@ -74,36 +196,202 @@ static uint64_t elapsed_ns(struct timespec begin, struct timespec end) {
return seconds * 1000000000ULL + (uint64_t)nanoseconds;
}
static bool process_parent(Lardon3DTask *task,
Lardon3DGeometricVerifierTaskContext *context,
const Lardon3DProjectDbMatchResult *parent) {
if (!lardon3d_task_checkpoint(task)) {
return false;
static void *prepare_worker(void *userdata) {
Lardon3DGeometricVerifierBatch *batch = userdata;
for (;;) {
if (atomic_load_explicit(&batch->stop, memory_order_relaxed)) {
return NULL;
}
size_t index =
atomic_fetch_add_explicit(&batch->next, 1, memory_order_relaxed);
if (index >= batch->count) {
return NULL;
}
const Lardon3DProjectDbMatchResult *parent = &batch->parents[index];
Lardon3DGeometricVerifierJob *job = &batch->jobs[index];
job->status = LARDON3D_GEOMETRIC_VERIFIER_OK;
if (parent->result_status != LARDON3D_MATCH_RESULT_STATUS_MATCHED ||
parent->match_count == 0) {
return true;
continue;
}
Lardon3DGeometricVerifierParameters parameters =
parameters_from_durable(&context->durable);
Lardon3DProjectDbGeometricVerificationResult result;
bool reused = false;
if (lardon3d_geometric_verifier_verify_and_publish_version(
context->project_path, context->database, parent->match_result_id,
&parameters, context->verifier_version, &result,
&reused) != LARDON3D_GEOMETRIC_VERIFIER_OK) {
// Failure transition success does not imply callback success: once an
// estimate fails for this parent, processing must stop before checkpointing.
job->applicable = true;
job->status = lardon3d_geometric_verifier_internal_prepare_version(
batch->context->project_path, batch->context->database,
parent->match_result_id, &batch->parameters,
batch->context->verifier_version, &job->prepared, &job->result,
&job->reused);
}
}
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TASK_TESTING
static bool inject_thread_create_failure(size_t child_index) {
const char *value = getenv("LARDON3D_TEST_GEOMETRIC_THREAD_FAIL_AFTER");
if (!value || !value[0]) {
return false;
}
char *end = NULL;
unsigned long parsed = strtoul(value, &end, 10);
return end && *end == '\0' && parsed == child_index;
}
#endif
static void destroy_jobs(Lardon3DGeometricVerifierJob *jobs, size_t count) {
for (size_t index = 0; index < count; ++index) {
lardon3d_geometric_verifier_internal_prepared_destroy(
jobs[index].prepared);
jobs[index].prepared = NULL;
}
}
static bool prepare_batch(Lardon3DGeometricVerifierTaskContext *context,
const Lardon3DProjectDbMatchResult *parents,
size_t count, unsigned int cpu_threads,
Lardon3DGeometricVerifierJob *jobs) {
memset(jobs, 0, sizeof(*jobs) * count);
Lardon3DGeometricVerifierBatch batch = {
.context = context,
.parents = parents,
.jobs = jobs,
.parameters = parameters_from_durable(&context->durable),
.count = count,
};
atomic_init(&batch.next, 0);
atomic_init(&batch.stop, false);
size_t participants = cpu_threads < count ? cpu_threads : count;
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TASK_TESTING
const char *forced = getenv("LARDON3D_TEST_GEOMETRIC_FORCE_PARTICIPANTS");
if (forced && forced[0]) {
char *end = NULL;
unsigned long parsed = strtoul(forced, &end, 10);
/* This seam exists only to reach partial pthread_create cleanup while the
* production Governor is deliberately in CPU slow-start. It is never an
* equivalence or performance input and cannot exceed the production-safe
* participant/window bounds. */
if (end && *end == '\0' && parsed >= 1 &&
parsed <=
LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_SAFE_CPU_THREADS) {
participants = (size_t)parsed;
}
}
#endif
size_t child_count = participants > 0 ? participants - 1 : 0;
pthread_t children
[LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_SAFE_CPU_THREADS - 1];
pthread_attr_t attributes;
bool attributes_initialized = false;
bool attributes_ready = child_count == 0;
if (child_count > 0 && pthread_attr_init(&attributes) == 0) {
attributes_initialized = true;
attributes_ready = pthread_attr_setstacksize(
&attributes,
GEOMETRIC_VERIFIER_CHILD_STACK_BYTES) == 0;
}
size_t created = 0;
bool creation_ok = attributes_ready;
while (creation_ok && created < child_count) {
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TASK_TESTING
if (inject_thread_create_failure(created)) {
/* Deterministically let already-created participants and the owner claim
* the fresh batch before injecting failure. This exercises destruction of
* real opaque stages after a partial pthread_create sequence; it never
* changes production scheduling or publication. */
(void)prepare_worker(&batch);
creation_ok = false;
break;
}
#endif
if (pthread_create(&children[created], &attributes, prepare_worker,
&batch) != 0) {
creation_ok = false;
break;
}
++created;
}
if (creation_ok) {
(void)prepare_worker(&batch);
} else {
/* A partial-create failure cannot hand stack storage back while a child
* still uses it. Stop new claims, join every created child, then let the
* owner destroy any completed opaque stages. */
atomic_store_explicit(&batch.stop, true, memory_order_relaxed);
}
bool joined = true;
for (size_t index = 0; index < created; ++index) {
joined = pthread_join(children[index], NULL) == 0 && joined;
}
if (attributes_initialized) {
(void)pthread_attr_destroy(&attributes);
}
return creation_ok && joined;
}
static bool preflight_batch(Lardon3DTask *task,
Lardon3DGeometricVerifierJob *jobs, size_t count) {
for (size_t index = 0; index < count; ++index) {
const Lardon3DGeometricVerifierJob *job = &jobs[index];
bool ownership_valid =
!job->applicable
? !job->prepared && !job->reused
: (job->reused ? !job->prepared : job->prepared != NULL);
if (job->status != LARDON3D_GEOMETRIC_VERIFIER_OK || !ownership_valid) {
/* CONTRACT: preparation is the all-or-nothing half of a GV batch. The
* callback owner must discover every participant failure before it may
* publish index zero or advance the contiguous business cursor. */
destroy_jobs(jobs, count);
(void)lardon3d_task_fail(task, "Vérification géométrique impossible.");
return false;
}
}
return true;
}
static bool publish_batch(Lardon3DTask *task,
Lardon3DGeometricVerifierTaskContext *context,
const Lardon3DProjectDbMatchResult *parents,
Lardon3DGeometricVerifierJob *jobs, size_t count,
uint64_t *processed, size_t *durable_items) {
/* INVARIANT: preflight has proved every joined slot. From here through the
* typed cursor + generic checkpoint, this dispatched batch is one engaged
* unit. Pause/cancel is deliberately not observed between ordered owner-only
* publications; the next control boundary follows the durable checkpoint. */
for (size_t index = 0; index < count; ++index) {
Lardon3DGeometricVerifierJob *job = &jobs[index];
if (job->applicable && job->prepared) {
job->status = lardon3d_geometric_verifier_internal_publish_prepared(
context->database, job->prepared, &job->result, &job->reused);
lardon3d_geometric_verifier_internal_prepared_destroy(job->prepared);
job->prepared = NULL;
if (job->status != LARDON3D_GEOMETRIC_VERIFIER_OK) {
destroy_jobs(jobs, count);
(void)lardon3d_task_fail(task,
"Publication géométrique impossible.");
return false;
}
if (!job->reused) {
++*durable_items;
}
}
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TASK_TESTING
const char *pause = getenv("LARDON3D_TEST_GEOMETRIC_PAUSE_AFTER_PUBLICATION");
if (pause && strcmp(pause, "1") == 0) {
const char *pause =
getenv("LARDON3D_TEST_GEOMETRIC_PAUSE_AFTER_PUBLICATION");
if (job->applicable && pause && strcmp(pause, "1") == 0) {
/* This testing-only crash seam intentionally interrupts the normal
* engaged-unit rule to preserve proof of the frozen residual restart
* boundary: publication may be durable while the business cursor still
* names the prior parent. Production never observes controls here. */
(void)lardon3d_task_pause(task);
return lardon3d_task_checkpoint(task);
destroy_jobs(jobs, count);
(void)lardon3d_task_checkpoint(task);
return false;
}
#endif
// Publication/reuse must be durable before the business cursor advances.
// Restart may repeat an already published parent, but exact identity
// reuse makes that retry converge without duplicating or guessing GVRs.
context->durable.after_match_result_id = parents[index].match_result_id;
++*processed;
}
destroy_jobs(jobs, count);
return true;
}
@ -132,11 +420,19 @@ static bool run(Lardon3DTask *task, void *userdata) {
}
Lardon3DTaskExecutionContract contract;
if (!lardon3d_task_execution_contract(task, &contract) ||
contract.cpu_threads == 0 ||
contract.cpu_threads >
LARDON3D_GEOMETRIC_VERIFIER_TASK_VALIDATED_USEFUL_CPU_THREADS ||
contract.batch_size < LARDON3D_GEOMETRIC_VERIFIER_TASK_MINIMUM_BATCH ||
contract.batch_size > LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_BATCH) {
return lardon3d_task_fail(task,
"Contrat de lot Geometric Verifier invalide.");
return lardon3d_task_fail(
task, "Contrat CPU/lot Geometric Verifier invalide.");
}
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TASK_TESTING
(void)atomic_fetch_or_explicit(&observed_cpu_contracts,
1U << contract.cpu_threads,
memory_order_relaxed);
#endif
Lardon3DProjectDbMatchResult page[GEOMETRIC_VERIFIER_PAGE_CAPACITY];
size_t count = 0;
size_t capacity = contract.batch_size + 1;
@ -154,29 +450,63 @@ static bool run(Lardon3DTask *task, void *userdata) {
struct timespec begin;
struct timespec end;
(void)clock_gettime(CLOCK_MONOTONIC, &begin);
for (size_t index = 0; index < batch_count; ++index) {
if (!process_parent(task, context, &page[index])) {
Lardon3DGeometricVerifierJob jobs
[LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_BATCH];
size_t durable_items = 0;
/* Queue owns this one callback and Governor owns the admitted width.
* Children only prepare independent immutable Match Results; the callback
* owner joins them all, publishes in parent order, and advances one
* contiguous durable cursor before any sequence_break releases admission. */
if (!prepare_batch(context, page, batch_count, contract.cpu_threads,
jobs)) {
destroy_jobs(jobs, batch_count);
return lardon3d_task_fail(task,
"Création des participants GV impossible.");
}
if (!preflight_batch(task, jobs, batch_count)) {
return false;
}
// Publication/reuse must be durable before the business cursor advances.
// Restart may repeat an already published parent, but exact identity
// reuse makes that retry converge without duplicating or guessing GVRs.
context->durable.after_match_result_id = page[index].match_result_id;
++processed;
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TASK_TESTING
/* This barrier is after the all-slot preflight and before index zero, so
* tests can issue control requests without timing luck or weakening the
* production engaged-batch boundary. */
test_barrier_reach(&prepublication_barrier);
#endif
if (!publish_batch(task, context, page, jobs, batch_count, &processed,
&durable_items)) {
return false;
}
(void)clock_gettime(CLOCK_MONOTONIC, &end);
(void)lardon3d_resource_governor_record_batch(
context->governor, LARDON3D_RESOURCE_TASK_CPU, batch_count,
elapsed_ns(begin, end), 0);
bool exhausted = count <= contract.batch_size;
if (!checkpoint_batch(task, context, exhausted ? 100U : 99U, processed)) {
return lardon3d_task_fail(task,
"Checkpoint Geometric Verifier impossible.");
}
(void)clock_gettime(CLOCK_MONOTONIC, &end);
uint64_t duration_ns = elapsed_ns(begin, end);
/* Feedback covers scientific preparation, owner-only publication, cursor
* advancement, and the durable batch checkpointnot CPU utilization. */
(void)lardon3d_resource_governor_record_batch(
context->governor, LARDON3D_RESOURCE_TASK_CPU, durable_items,
duration_ns, 0);
(void)lardon3d_task_internal_record_sequence(task, duration_ns,
durable_items);
if (exhausted) {
/* The final dispatched batch needs the same post-checkpoint control
* boundary as a non-final sequence. Otherwise a pause arriving during
* preparation/publication could be silently converted to COMPLETED; a
* cancel must likewise win only after the engaged prefix is durable. */
if (!lardon3d_task_checkpoint(task)) {
return false;
}
return lardon3d_task_set_progress(task, 100,
"Vérification géométrique terminée.");
}
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TASK_TESTING
/* Test acknowledgement occurs only after the cursor checkpoint and before
* sequence_break releases admission. Policy changes made at the barrier
* therefore affect the real next Governor decision. */
test_barrier_reach(&sequence_barrier);
#endif
Lardon3DResourceReservation *reservation = NULL;
if (!lardon3d_task_sequence_break(task, context->governor, &reservation,
&contract)) {
@ -199,8 +529,8 @@ make_context(const Lardon3DTaskReconstructionContext *runtime,
if (!lardon3d_geometric_verifier_parameters_valid(&parameters)) {
return NULL;
}
// Project DB v22 intentionally has no duplicate verifier-version column in
// this task payload. The exact versioned fingerprint unambiguously recovers
// Project DB intentionally has no duplicate verifier-version column in this
// task payload. The exact versioned fingerprint unambiguously recovers
// historical v1/v2 or current v3 while keeping all identities immutable.
for (uint32_t candidate = LARDON3D_GEOMETRIC_VERIFIER_VERSION_V1;
candidate <= LARDON3D_GEOMETRIC_VERIFIER_VERSION_V3; ++candidate) {
@ -304,10 +634,15 @@ Lardon3DTask *lardon3d_project_create_geometric_verifier_task(
return NULL;
}
const Lardon3DResourceEstimate estimate = {
.memory_fixed_bytes = GEOMETRIC_VERIFIER_MEMORY_BYTES,
.memory_bytes_per_item = GEOMETRIC_VERIFIER_MEMORY_BYTES_PER_ITEM,
.minimum_batch_size = LARDON3D_GEOMETRIC_VERIFIER_TASK_MINIMUM_BATCH,
.maximum_batch_size = LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_BATCH,
.desired_cpu_threads = GEOMETRIC_VERIFIER_CPU_THREADS,
/* CPU8 is the largest width whose complete preparation + ordered publish
* + checkpoint rate improved materially. CPU12 was exact and safe but
* added only 2.68% over CPU8, below Governor's 5% acceptance threshold;
* the independently safe participant/window bound remains 16. */
.desired_cpu_threads =
LARDON3D_GEOMETRIC_VERIFIER_TASK_VALIDATED_USEFUL_CPU_THREADS,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
};
@ -331,6 +666,9 @@ bool lardon3d_project_enqueue_geometric_verifier_task(
Lardon3DAppState *state,
const Lardon3DGeometricVerifierTaskConfiguration *configuration,
uint64_t *task_id) {
if (task_id) {
*task_id = 0;
}
if (!state || !state->task_queue) {
return false;
}

View file

@ -106,7 +106,8 @@ shared_memory_evidence(
/* WHY: amdgpu exposes a small stolen/dedicated VRAM aperture even for an
* integrated GPU. Treating that positive number as separate free memory
* undercharges host RAM. A low-VRAM uncertain device may conservatively
* become UMA; the reverse error could violate the 3 GiB/2 GiB host floors. */
* become UMA; the reverse error could violate the 3 GiB hard reserve and
* the 3--4 GiB host-caution zone. */
return conservatively_small || system_scale_gtt;
}

View file

@ -12,6 +12,8 @@
#include <lardon3d/image_catalog.h>
#include "image_catalog_persistent_internal.h"
enum { COPY_BUFFER_SIZE = 64 * 1024 };
typedef enum {
@ -230,16 +232,21 @@ lardon3d_image_catalog_publish_asset_file(Lardon3DAppState *state,
return LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED;
}
#if defined(LARDON3D_IMAGE_CATALOG_PERSISTENT_TESTING)
Lardon3DImageCatalogAssetPublishResult
lardon3d_image_catalog_test_copy_hash(int input, int output)
{
unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE];
uint64_t size = 0;
CopyResult result = hash_stream(input, output, hash, &size);
return result == COPY_OK ? LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED
: result == COPY_DESTINATION_ERROR ? LARDON3D_IMAGE_CATALOG_ASSET_PUBLICATION_ERROR
if (result == COPY_OK) {
return LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED;
}
return result == COPY_DESTINATION_ERROR
? LARDON3D_IMAGE_CATALOG_ASSET_PUBLICATION_ERROR
: LARDON3D_IMAGE_CATALOG_ASSET_SOURCE_ERROR;
}
#endif
bool
lardon3d_image_catalog_create_scanset(Lardon3DAppState *state,

View file

@ -0,0 +1,19 @@
#ifndef LARDON3D_IMAGE_CATALOG_PERSISTENT_INTERNAL_H
#define LARDON3D_IMAGE_CATALOG_PERSISTENT_INTERNAL_H
#include <lardon3d/image_catalog.h>
#if defined(LARDON3D_IMAGE_CATALOG_PERSISTENT_TESTING)
/* TEST CONTRACT: this direct descriptor seam exists only in
* test-persistent-image-catalog so its /dev/full fixture can distinguish a
* destination write failure. It owns neither descriptor and is absent from
* every production object and public ABI. */
Lardon3DImageCatalogAssetPublishResult lardon3d_image_catalog_test_copy_hash(
int input,
int output
);
#endif
#endif

File diff suppressed because it is too large Load diff

View file

@ -218,7 +218,8 @@ static bool auto_vulkan_backend_candidate(Lardon3DOrbVulkanBackend *backend) {
static bool auto_vulkan_runtime_candidate(const Lardon3DAppState *state) {
/* AUTO creation is caller-thread metadata work only. Memory sizing does not
* belong here: the Governor owns the exact reconstructed batch/depth, UMA
* charge, current MemAvailable/PSI/swap snapshot, and 3 GiB/2 GiB policy.
* charge, current MemAvailable/PSI/swap snapshot, and the 3 GiB hard reserve
* plus the 3--4 GiB caution policy.
* A caller-side maximum-window guess could suppress a safe depth-1 contract
* before CPU fallback was even considered. Driver initialization remains
* deferred to begin() on Queue's affinity-constrained worker. */

View file

@ -14,7 +14,9 @@ typedef struct {
/* OpenCV owns one process-wide CPU pool. Queue's single callback owner makes
* the change race-free, but configure may mutate before its verification
* fails. Therefore begin attempts rollback on every post-capture failure and
* end restores on every callback result. */
* end restores on every callback result. restore_required is cleared only
* after a verified rollback: a transient OpenCV failure must remain retryable
* rather than silently abandoning ownership of process-global state. */
static inline bool lardon3d_opencv_task_threads_begin(
Lardon3DTask *task, unsigned int validated_maximum,
Lardon3DOpenCvTaskThreadControl *control) {
@ -29,8 +31,9 @@ static inline bool lardon3d_opencv_task_threads_begin(
control->restore_required = true;
if (!lardon3d_feature_opencv_configure_threads(contract.cpu_threads)) {
/* Verification failure is after a possibly successful setNumThreads(). */
(void)lardon3d_feature_opencv_configure_threads(control->previous);
if (lardon3d_feature_opencv_configure_threads(control->previous)) {
control->restore_required = false;
}
return false;
}
return true;
@ -39,8 +42,11 @@ static inline bool lardon3d_opencv_task_threads_begin(
static inline bool lardon3d_opencv_task_threads_end(
Lardon3DOpenCvTaskThreadControl *control) {
if (!control || !control->restore_required) return false;
if (!lardon3d_feature_opencv_configure_threads(control->previous)) {
return false;
}
control->restore_required = false;
return lardon3d_feature_opencv_configure_threads(control->previous);
return true;
}
#endif

View file

@ -42,9 +42,12 @@ class Lardon3DOpenCvTaskThreadGuard {
return false;
try {
cv::setNumThreads(previous_ > 0 ? previous_ : 1);
restored_ = true;
restore_succeeded_ =
cv::getNumThreads() == (previous_ > 0 ? previous_ : 1);
/* Keep ownership live after a verification failure. The explicit Task
* cleanup reports the failure, while the noexcept destructor gets one
* final bounded restoration attempt during stack unwinding. */
restored_ = restore_succeeded_;
return restore_succeeded_;
} catch (...) {
/* C callback boundary: restoration failure cannot escape as C++. */

2064
src/optical_profiles.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -67,6 +67,11 @@ bool run_impl(Lardon3DTask *task, void *value) {
if (lardon3d_project_db_load_photo_quality_task(context->db, lardon3d_task_id(task),
context->encoded.data(), context->encoded.size(), &persisted) != LARDON3D_PROJECT_DB_OK)
return lardon3d_task_fail(task, "Triage photo durable introuvable.");
if (persisted.next_group_id == 0 ||
!lardon3d_task_set_durable_progress(
task, persisted.next_group_id - 1u, persisted.group_count,
"Préfixe durable de triage observé."))
return lardon3d_task_fail(task, "Curseur durable de triage invalide.");
for (uint32_t group_id = persisted.next_group_id; group_id <= context->plan.group_count;
++group_id) {
@ -103,9 +108,11 @@ bool run_impl(Lardon3DTask *task, void *value) {
if (lardon3d_project_db_record_photo_quality_result(context->db, &result, next_group_id) !=
LARDON3D_PROJECT_DB_OK)
return lardon3d_task_fail(task, "Publication du résultat de triage impossible.");
unsigned progress = static_cast<unsigned>((uint64_t(group_id) * 100u) /
context->plan.group_count);
if (!lardon3d_task_set_progress(task, progress, "Groupe photo analysé.") ||
/* The typed result/cursor transaction is already committed. Publish the
* exact count only now, so a live snapshot cannot lead durable science. */
if (!lardon3d_task_set_durable_progress(
task, group_id, context->plan.group_count,
"Groupe photo analysé.") ||
!checkpoint(context, task, next_group_id))
return lardon3d_task_fail(task, "Checkpoint du triage photo impossible.");
if (group_id < context->plan.group_count) {

View file

@ -453,7 +453,11 @@ static bool read_project_ini(Lardon3DAppState *state, const char *path, ProjectM
}
}
if (valid) {
(void)snprintf(metadata->stable_id, sizeof(metadata->stable_id), "%s", line + 10);
/* The v2 project identity is exactly 128 lowercase hexadecimal bits. Copying the
* validated width makes the no-truncation contract explicit to both readers and
* compiler diagnostics; project identity must never be accepted by prefix. */
memcpy(metadata->stable_id, line + 10, id_length);
metadata->stable_id[id_length] = '\0';
}
}
stable_id_found = true;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,58 @@
#include <stdbool.h>
#include <lardon3d/resource_governor.h>
#include "resource_governor_internal.h"
static bool
scratch_operation(
Lardon3DResourceGovernor *governor,
Lardon3DSsdController *controller,
Lardon3DSsdScratchLease *lease,
bool acquire
)
{
if (!lardon3d_resource_governor_internal_begin_scratch_operation(
governor, controller, lease, acquire)) {
return false;
}
/* LOCK ORDER: begin has released the Governor mutex before this physical
* call takes the controller mutex. The controller has no Governor callback.
* A concurrent drain/replacement therefore wins at the controller's exact
* capability check instead of creating a nested-lock cycle. */
bool physical_success = acquire
? lardon3d_ssd_controller_acquire_scratch(controller, lease)
: lardon3d_ssd_controller_release_scratch(controller, lease);
Lardon3DSsdSnapshot snapshot;
Lardon3DResourceExternalStorage storage;
bool storage_valid = lardon3d_ssd_controller_copy_snapshot(
controller, &snapshot)
&& lardon3d_resource_external_storage_from_ssd_snapshot(
&snapshot, &storage);
return lardon3d_resource_governor_internal_finish_scratch_operation(
governor, controller, lease, acquire, physical_success,
storage_valid ? &storage : NULL);
}
bool
lardon3d_resource_governor_acquire_scratch(
Lardon3DResourceGovernor *governor,
Lardon3DSsdController *controller,
Lardon3DSsdScratchLease *lease
)
{
return scratch_operation(governor, controller, lease, true);
}
bool
lardon3d_resource_governor_release_scratch(
Lardon3DResourceGovernor *governor,
Lardon3DSsdController *controller,
Lardon3DSsdScratchLease *lease
)
{
return scratch_operation(governor, controller, lease, false);
}

View file

@ -235,6 +235,11 @@ typedef struct {
bool runtime_thread_policy_active;
bool mesa_shader_cache_disabled;
bool externally_constrained;
/* Counts describe the effective pool after an external allowed mask and
* the requested host reserve are combined. With complete topology, masks
* contain whole (package_id, core_id) groups and reserved_cpu_count may
* minimally exceed the logical request; count-only fallbacks leave masks
* empty rather than fabricating CPU identities. */
unsigned int compute_cpu_count;
unsigned int reserved_cpu_count;
uint64_t allowed_mask[LARDON3D_RESOURCE_CPU_MASK_WORDS];
@ -356,6 +361,36 @@ bool lardon3d_resource_governor_internal_capability_hardware_safe(
const Lardon3DTaskCapability *capability
);
/* Private half of the public scratch wrappers. Begin freezes one bounded
* Governor operation, then releases the Governor mutex before the caller
* enters the SSD controller. Finish reconciles the controller snapshot and
* exact caller-owned lease address. SSD code never calls these functions, so
* there is no reverse lock edge. */
bool lardon3d_resource_governor_internal_begin_scratch_operation(
Lardon3DResourceGovernor *governor,
Lardon3DSsdController *controller,
Lardon3DSsdScratchLease *lease,
bool acquire
);
bool lardon3d_resource_governor_internal_finish_scratch_operation(
Lardon3DResourceGovernor *governor,
Lardon3DSsdController *controller,
Lardon3DSsdScratchLease *lease,
bool acquire,
bool physical_success,
const Lardon3DResourceExternalStorage *storage
);
#ifdef LARDON3D_RESOURCE_EXTERNAL_STORAGE_TESTING
/* Test definition is called with the Governor mutex held immediately before a
* public external-storage update is applied. It may block on test-owned
* synchronization only and must never call back into the Governor. */
void lardon3d_resource_governor_external_update_engaged_for_test(
Lardon3DResourceGovernor *governor,
Lardon3DSsdController *controller
);
#endif
/* Governor owns the bounded topology snapshot and compute mask. Queue calls
* apply only from its sole heavy-compute worker; callers/main threads must not
* be constrained. Failure is observable and leaves Task scientific state
@ -424,6 +459,17 @@ void lardon3d_resource_governor_internal_force_capture_failure(
bool force_failure
);
#if defined(LARDON3D_RESOURCE_GOVERNOR_CAPTURE_TESTING)
/* Test-only admission-capture seam. The Governor copies `snapshot`; NULL
* clears the override. Each use receives a fresh monotonic timestamp, while
* every other field remains exactly caller-controlled. This declaration and
* its implementation are absent from production preprocessing and symbols. */
bool lardon3d_resource_governor_internal_set_capture_snapshot(
Lardon3DResourceGovernor *governor,
const Lardon3DResourceSnapshot *snapshot
);
#endif
#ifdef __cplusplus
}
#endif

View file

@ -223,16 +223,18 @@ lardon3d_resource_snapshot_capture_gpu_at_root(
}
}
bool
lardon3d_resource_snapshot_capture(
static bool
capture_resource_snapshot(
const Lardon3DHardwareProfile *profile,
Lardon3DResourceSnapshot *snapshot,
uint64_t *swap_total_bytes,
char *error_message,
size_t error_message_size
)
{
set_error(error_message, error_message_size, "");
if (!profile || !snapshot || profile->logical_cpu_count == 0
if (!profile || !snapshot || !swap_total_bytes
|| profile->logical_cpu_count == 0
|| profile->memory_total_bytes == 0) {
set_error(error_message, error_message_size, "Profil matériel invalide.");
return false;
@ -246,6 +248,7 @@ lardon3d_resource_snapshot_capture(
&snapshot->memory_available_bytes
)
|| !meminfo_bytes(buffer, "MemFree", &snapshot->memory_free_bytes)
|| !meminfo_bytes(buffer, "SwapTotal", swap_total_bytes)
|| !meminfo_bytes(buffer, "SwapFree", &snapshot->swap_available_bytes)
|| !capture_load(snapshot)
|| clock_gettime(CLOCK_MONOTONIC, &snapshot->captured_at) != 0) {
@ -270,3 +273,44 @@ lardon3d_resource_snapshot_capture(
);
return true;
}
bool
lardon3d_resource_snapshot_capture(
const Lardon3DHardwareProfile *profile,
Lardon3DResourceSnapshot *snapshot,
char *error_message,
size_t error_message_size
)
{
uint64_t ignored_swap_total = 0;
/* CONTRACT: this historical entry point writes exactly the frozen
* ResourceSnapshot size; extended telemetry must use observation_capture.
*/
return capture_resource_snapshot(profile, snapshot, &ignored_swap_total,
error_message, error_message_size);
}
bool
lardon3d_resource_observation_capture(
const Lardon3DHardwareProfile *profile,
Lardon3DResourceObservation *observation,
char *error_message,
size_t error_message_size
)
{
if (observation) {
*observation = (Lardon3DResourceObservation) {0};
}
if (!observation) {
set_error(error_message, error_message_size,
"Instantané système invalide.");
return false;
}
if (!capture_resource_snapshot(profile, &observation->snapshot,
&observation->swap_total_bytes, error_message,
error_message_size)) {
return false;
}
observation->swap_total_known = true;
return true;
}

542
src/runtime_observer.c Normal file
View file

@ -0,0 +1,542 @@
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <lardon3d/runtime_observer.h>
#include "runtime_observer_internal.h"
enum {
RUNTIME_OBSERVER_MINIMUM_INTERVAL_NS = 1000000000ULL,
};
_Static_assert(
LARDON3D_TUI_TASK_CAPACITY
>= LARDON3D_TASK_QUEUE_PRODUCTION_CAPACITY + 1
+ LARDON3D_TASK_QUEUE_HISTORY_CAPACITY,
"runtime observation must cover pending + active + terminal history"
);
typedef struct {
Lardon3DHardwareProfile profile;
Lardon3DTaskQueue *queue;
Lardon3DResourceGovernor *governor;
} ProductionProvider;
struct Lardon3DRuntimeObserver {
Lardon3DHardwareProfile profile;
Lardon3DRuntimeObserverProvider provider;
uint64_t minimum_refresh_interval_ns;
uint64_t last_attempt_ns;
bool last_attempt_known;
bool cached_project_loaded;
bool cached_project_known;
bool cache_known;
bool swap_baseline_known;
uint64_t swap_pages_in;
uint64_t swap_pages_out;
Lardon3DTuiProgressTracker progress_tracker;
Lardon3DRuntimeSnapshot cache;
};
static void
copy_text(char *destination, size_t capacity, const char *source)
{
if (destination && capacity > 0) {
(void)snprintf(destination, capacity, "%s", source ? source : "");
}
}
static bool
production_now(void *context, uint64_t *now_ns)
{
(void)context;
struct timespec now;
if (!now_ns || clock_gettime(CLOCK_MONOTONIC, &now) != 0
|| now.tv_sec < 0 || now.tv_nsec < 0 || now.tv_nsec >= 1000000000L
|| (uint64_t)now.tv_sec > UINT64_MAX / UINT64_C(1000000000)) {
return false;
}
uint64_t seconds = (uint64_t)now.tv_sec * UINT64_C(1000000000);
if (seconds > UINT64_MAX - (uint64_t)now.tv_nsec) {
return false;
}
*now_ns = seconds + (uint64_t)now.tv_nsec;
return true;
}
static size_t
active_task_index(
const Lardon3DTaskObservation *tasks,
size_t count,
bool *known
)
{
static const Lardon3DTaskState priority[] = {
TASK_RUNNING,
TASK_PAUSED,
TASK_PENDING,
};
*known = false;
for (size_t state_index = 0;
state_index < sizeof(priority) / sizeof(priority[0]);
++state_index) {
for (size_t index = 0; index < count; ++index) {
if (tasks[index].state == priority[state_index]) {
*known = true;
return index;
}
}
}
return 0;
}
static bool
production_capture(
void *context,
Lardon3DRuntimeObserverSample *sample,
char reason[LARDON3D_TUI_TEXT_CAPACITY]
)
{
if (!context || !sample || !reason) {
return false;
}
ProductionProvider *provider = context;
*sample = (Lardon3DRuntimeObserverSample) {0};
reason[0] = '\0';
if (clock_gettime(CLOCK_REALTIME, &sample->realtime_now) != 0) {
copy_text(reason, LARDON3D_TUI_TEXT_CAPACITY,
"Cannot read realtime clock for elapsed Task time");
return false;
}
sample->task_count = lardon3d_task_queue_observe(
provider->queue, sample->tasks, LARDON3D_TUI_TASK_CAPACITY,
&sample->task_summary);
if (sample->task_count > LARDON3D_TUI_TASK_CAPACITY) {
copy_text(reason, LARDON3D_TUI_TEXT_CAPACITY,
"Queue returned an invalid bounded snapshot count");
return false;
}
char resource_reason[LARDON3D_TUI_TEXT_CAPACITY] = "";
sample->resource_valid = lardon3d_resource_observation_capture(
&provider->profile, &sample->resource_observation,
resource_reason, sizeof(resource_reason))
&& lardon3d_resource_governor_availability(provider->governor,
&sample->resource_observation.snapshot, &sample->availability)
&& lardon3d_resource_governor_get_policy(provider->governor,
&sample->policy)
&& lardon3d_resource_governor_internal_cpu_policy(
provider->governor, &sample->cpu_policy);
sample->pressure = lardon3d_resource_governor_pressure(
provider->governor);
sample->external_storage_registered =
lardon3d_resource_governor_get_external_storage(
provider->governor, &sample->external_storage);
if (!sample->resource_valid) {
copy_text(reason, LARDON3D_TUI_TEXT_CAPACITY,
resource_reason[0] ? resource_reason
: "Resource observation unavailable");
}
return true;
}
static void
production_destroy(void *context)
{
free(context);
}
static const Lardon3DRuntimeObserverProviderOps production_ops = {
.monotonic_now_ns = production_now,
.capture = production_capture,
.destroy = production_destroy,
};
static void
governor_reason(
const Lardon3DRuntimeObserverSample *sample,
const Lardon3DTuiResourceView *resources,
char reason[LARDON3D_TUI_TEXT_CAPACITY]
)
{
if (resources->swap_delta_known
&& (resources->swap_pages_in_delta > 0
|| resources->swap_pages_out_delta > 0)) {
copy_text(reason, LARDON3D_TUI_TEXT_CAPACITY,
"Active swap delta observed");
} else if (resources->ram_available_bytes
<= resources->ram_reserve_bytes) {
copy_text(reason, LARDON3D_TUI_TEXT_CAPACITY,
"MemAvailable is at or below the hard host reserve");
} else if (sample->resource_observation.snapshot.memory_pressure_known
&& sample->resource_observation.snapshot.memory_pressure_avg10 > 0.0) {
copy_text(reason, LARDON3D_TUI_TEXT_CAPACITY,
"Memory PSI is active");
} else if (sample->pressure == LARDON3D_RESOURCE_PRESSURE_GREEN) {
copy_text(reason, LARDON3D_TUI_TEXT_CAPACITY,
"No active pressure signal observed");
} else {
copy_text(reason, LARDON3D_TUI_TEXT_CAPACITY,
"Governor recovery or conservative slow-start");
}
}
static void
build_resources(
Lardon3DRuntimeObserver *observer,
const Lardon3DRuntimeObserverSample *sample,
uint64_t now_ns,
const Lardon3DTaskObservation *active,
Lardon3DTuiResourceView *resources
)
{
*resources = (Lardon3DTuiResourceView) {0};
if (sample->external_storage_registered) {
const Lardon3DResourceExternalStorage *external =
&sample->external_storage;
resources->external_storage_registered = true;
resources->external_storage_status = external->status;
resources->scratch_new_allocations_allowed =
external->new_scratch_allocations_allowed;
resources->scratch_known = true;
resources->scratch_total_known = external->scratch_total_known;
resources->scratch_free_known = external->scratch_free_known;
resources->scratch_total_bytes = external->scratch_total_bytes;
resources->scratch_free_bytes = external->scratch_free_bytes;
resources->scratch_leases = external->active_scratch_leases;
resources->external_swap_total_known = external->swap_total_known;
resources->external_swap_used_known = external->swap_used_known;
resources->external_swap_total_bytes = external->swap_total_bytes;
resources->external_swap_used_bytes = external->swap_used_bytes;
copy_text(resources->external_storage_identity,
sizeof(resources->external_storage_identity),
external->stable_identity);
copy_text(resources->external_storage_reason,
sizeof(resources->external_storage_reason), external->reason);
}
if (!sample->resource_valid) {
copy_text(resources->governor_reason,
sizeof(resources->governor_reason),
"Resource snapshot unavailable");
return;
}
resources->valid = true;
resources->captured_monotonic_ns = now_ns;
resources->governor_pressure = sample->pressure;
resources->cpu_logical_total = observer->profile.logical_cpu_count;
resources->cpu_active = sample->availability.cpu_reserved;
resources->cpu_available = sample->availability.cpu_available;
resources->cpu_admitted_known = active && active->has_execution_contract;
resources->cpu_admitted = resources->cpu_admitted_known
? active->execution_contract.cpu_threads : 0;
copy_text(resources->cpu_reason, sizeof(resources->cpu_reason),
sample->cpu_policy.reason[0] ? sample->cpu_policy.reason
: "CPU topology policy unavailable");
resources->gpu_present = observer->profile.gpu_available;
resources->gpu_uses_shared_memory =
observer->profile.gpu_uses_shared_memory;
resources->gpu_memory_known = sample->availability.gpu_memory_known;
resources->gpu_memory_reserved_bytes =
sample->availability.gpu_memory_reserved_bytes;
resources->gpu_memory_available_bytes =
sample->availability.gpu_memory_available_bytes;
resources->gpu_slots_active = sample->availability.gpu_slots_reserved;
resources->gpu_slots_available = sample->availability.gpu_slots_available;
resources->ram_total_bytes = observer->profile.memory_total_bytes;
resources->ram_available_bytes =
sample->resource_observation.snapshot.memory_available_bytes;
resources->ram_reserve_bytes =
sample->policy.system_memory_reserve_bytes;
resources->ram_reserved_bytes = sample->availability.memory_reserved_bytes;
resources->swap_total_known = sample->resource_observation.swap_total_known;
resources->swap_total_bytes = sample->resource_observation.swap_total_bytes;
if (sample->resource_observation.swap_total_known
&& sample->resource_observation.snapshot.swap_available_bytes
<= sample->resource_observation.swap_total_bytes) {
resources->swap_used_known = true;
resources->swap_used_bytes = sample->resource_observation.swap_total_bytes
- sample->resource_observation.snapshot.swap_available_bytes;
}
if (sample->resource_observation.snapshot.swap_activity_known) {
if (observer->swap_baseline_known
&& sample->resource_observation.snapshot.swap_pages_in
>= observer->swap_pages_in
&& sample->resource_observation.snapshot.swap_pages_out
>= observer->swap_pages_out) {
resources->swap_delta_known = true;
resources->swap_pages_in_delta =
sample->resource_observation.snapshot.swap_pages_in
- observer->swap_pages_in;
resources->swap_pages_out_delta =
sample->resource_observation.snapshot.swap_pages_out
- observer->swap_pages_out;
}
observer->swap_baseline_known = true;
observer->swap_pages_in =
sample->resource_observation.snapshot.swap_pages_in;
observer->swap_pages_out =
sample->resource_observation.snapshot.swap_pages_out;
} else {
observer->swap_baseline_known = false;
}
resources->io_active = sample->availability.io_slots_reserved;
resources->io_available = sample->availability.io_slots_available;
if (active && active->has_execution_contract) {
resources->batch_known = true;
resources->batch_size = active->execution_contract.batch_size;
}
/* A Governor's last diagnostic is keyed only by kind/version and can
* belong to an earlier Task or sequence. It must never override this
* Task's installed contract. Until an exact Task+sequence observation
* exists, private inflight/helper/backend and utilization values remain
* explicitly unknown. */
resources->gpu_backend = LARDON3D_TUI_GPU_BACKEND_UNKNOWN;
copy_text(resources->gpu_backend_reason,
sizeof(resources->gpu_backend_reason),
"Exact active Task backend association UNKNOWN");
governor_reason(sample, resources, resources->governor_reason);
}
static bool
realtime_elapsed(
const struct timespec *now,
const struct timespec *started,
uint64_t *seconds
)
{
if (!now || !started || !seconds || now->tv_sec < 0 || started->tv_sec <= 0
|| now->tv_nsec < 0 || now->tv_nsec >= 1000000000L
|| started->tv_nsec < 0 || started->tv_nsec >= 1000000000L
|| now->tv_sec < started->tv_sec) {
return false;
}
time_t elapsed = now->tv_sec - started->tv_sec;
if (now->tv_nsec < started->tv_nsec) {
if (elapsed == 0) {
return false;
}
--elapsed;
}
if (elapsed < 0) {
return false;
}
*seconds = (uint64_t)elapsed;
return true;
}
static bool
build_snapshot(
Lardon3DRuntimeObserver *observer,
bool project_loaded,
uint64_t now_ns,
const Lardon3DRuntimeObserverSample *sample,
const char *capture_reason,
Lardon3DRuntimeSnapshot *result
)
{
if (sample->task_count > LARDON3D_TUI_TASK_CAPACITY) {
return false;
}
*result = (Lardon3DRuntimeSnapshot) {0};
result->generation = observer->cache.generation == UINT64_MAX
? UINT64_MAX
: observer->cache.generation + 1;
result->captured_monotonic_ns = now_ns;
result->task_count = project_loaded ? sample->task_count : 0;
result->task_summary = project_loaded
? sample->task_summary : (Lardon3DTaskQueueSummary) {0};
if (result->task_count > 0) {
memcpy(result->tasks, sample->tasks,
result->task_count * sizeof(sample->tasks[0]));
}
result->active_task_index = active_task_index(
result->tasks, result->task_count, &result->active_task_known);
const Lardon3DTaskObservation *active = result->active_task_known
? &result->tasks[result->active_task_index]
: NULL;
Lardon3DResourcePressure pressure = sample->resource_valid
? sample->pressure
: LARDON3D_RESOURCE_PRESSURE_YELLOW;
lardon3d_tui_stage_views_build(project_loaded, result->tasks,
result->task_count, pressure, result->stages);
if (active) {
Lardon3DTuiProgressSample progress = {
.task_id = active->id,
.monotonic_ns = now_ns,
.task_state = active->state,
.typed_task = active->has_task_kind,
.progress_percent = active->progress,
.durable_counts_known = active->durable_progress_known,
.durable_completed = active->durable_completed,
.durable_total = active->durable_total,
.pressure_limited =
pressure != LARDON3D_RESOURCE_PRESSURE_GREEN,
};
if (!lardon3d_tui_progress_update(&observer->progress_tracker,
&progress, &result->active_progress)) {
return false;
}
result->active_progress.elapsed_known = realtime_elapsed(
&sample->realtime_now, &active->started_at,
&result->active_progress.elapsed_seconds);
} else {
observer->progress_tracker = (Lardon3DTuiProgressTracker) {0};
}
build_resources(observer, sample, now_ns, active, &result->resources);
result->ssd_controller_available = sample->ssd_controller_available;
result->ssd = sample->ssd;
copy_text(result->status, sizeof(result->status),
capture_reason && capture_reason[0]
? capture_reason
: (sample->resource_valid
? "Runtime observation updated"
: "Runtime updated; resource telemetry unavailable"));
return true;
}
Lardon3DRuntimeObserver *
lardon3d_runtime_observer_create_with_provider(
const Lardon3DHardwareProfile *profile,
Lardon3DRuntimeObserverProvider provider,
uint64_t minimum_refresh_interval_ns
)
{
if (!profile || profile->logical_cpu_count == 0
|| profile->memory_total_bytes == 0 || !provider.ops
|| !provider.ops->monotonic_now_ns || !provider.ops->capture
|| minimum_refresh_interval_ns < RUNTIME_OBSERVER_MINIMUM_INTERVAL_NS) {
return NULL;
}
Lardon3DRuntimeObserver *observer = calloc(1, sizeof(*observer));
if (!observer) {
return NULL;
}
observer->profile = *profile;
observer->provider = provider;
observer->minimum_refresh_interval_ns = minimum_refresh_interval_ns;
return observer;
}
Lardon3DRuntimeObserver *
lardon3d_runtime_observer_create(
const Lardon3DHardwareProfile *profile,
Lardon3DTaskQueue *queue,
Lardon3DResourceGovernor *governor
)
{
if (!profile || !queue || !governor) {
return NULL;
}
ProductionProvider *context = calloc(1, sizeof(*context));
if (!context) {
return NULL;
}
*context = (ProductionProvider) {
.profile = *profile,
.queue = queue,
.governor = governor,
};
Lardon3DRuntimeObserverProvider provider = {
.ops = &production_ops,
.context = context,
};
Lardon3DRuntimeObserver *observer =
lardon3d_runtime_observer_create_with_provider(
profile, provider, RUNTIME_OBSERVER_MINIMUM_INTERVAL_NS);
if (!observer) {
free(context);
}
return observer;
}
void
lardon3d_runtime_observer_destroy(Lardon3DRuntimeObserver *observer)
{
if (!observer) {
return;
}
if (observer->provider.ops && observer->provider.ops->destroy) {
observer->provider.ops->destroy(observer->provider.context);
}
free(observer);
}
bool
lardon3d_runtime_observer_refresh(
Lardon3DRuntimeObserver *observer,
bool project_loaded,
bool force,
Lardon3DRuntimeSnapshot *snapshot
)
{
if (snapshot) {
*snapshot = (Lardon3DRuntimeSnapshot) {0};
}
if (!observer || !snapshot) {
return false;
}
uint64_t now_ns;
if (!observer->provider.ops->monotonic_now_ns(
observer->provider.context, &now_ns)) {
if (observer->cache_known) {
observer->cache.stale = true;
copy_text(observer->cache.status, sizeof(observer->cache.status),
"Runtime clock unavailable; showing stale observation");
*snapshot = observer->cache;
}
return false;
}
bool project_changed = !observer->cached_project_known
|| observer->cached_project_loaded != project_loaded;
if (!force && !project_changed && observer->cache_known
&& observer->last_attempt_known && now_ns >= observer->last_attempt_ns
&& now_ns - observer->last_attempt_ns
< observer->minimum_refresh_interval_ns) {
*snapshot = observer->cache;
return true;
}
observer->last_attempt_known = true;
observer->last_attempt_ns = now_ns;
Lardon3DRuntimeObserverSample sample;
char reason[LARDON3D_TUI_TEXT_CAPACITY] = "";
if (!observer->provider.ops->capture(
observer->provider.context, &sample, reason)) {
if (observer->cache_known) {
observer->cache.stale = true;
copy_text(observer->cache.status, sizeof(observer->cache.status),
reason[0] ? reason
: "Runtime refresh failed; showing stale observation");
*snapshot = observer->cache;
}
return false;
}
Lardon3DRuntimeSnapshot next;
if (!build_snapshot(observer, project_loaded, now_ns, &sample, reason,
&next)) {
if (observer->cache_known) {
observer->cache.stale = true;
copy_text(observer->cache.status, sizeof(observer->cache.status),
"Invalid runtime sample; showing stale observation");
*snapshot = observer->cache;
}
return false;
}
observer->cache = next;
observer->cache_known = true;
observer->cached_project_known = true;
observer->cached_project_loaded = project_loaded;
*snapshot = observer->cache;
return true;
}

View file

@ -0,0 +1,55 @@
#ifndef LARDON3D_RUNTIME_OBSERVER_INTERNAL_H
#define LARDON3D_RUNTIME_OBSERVER_INTERNAL_H
#include <stdbool.h>
#include <stdint.h>
#include <time.h>
#include <lardon3d/runtime_observer.h>
#include "resource_governor_internal.h"
typedef struct {
struct timespec realtime_now;
Lardon3DTaskObservation tasks[LARDON3D_TUI_TASK_CAPACITY];
size_t task_count;
Lardon3DTaskQueueSummary task_summary;
bool resource_valid;
Lardon3DResourceObservation resource_observation;
Lardon3DResourceAvailability availability;
Lardon3DResourcePolicy policy;
Lardon3DResourceCpuPolicyDiagnostic cpu_policy;
Lardon3DResourcePressure pressure;
bool external_storage_registered;
Lardon3DResourceExternalStorage external_storage;
bool ssd_controller_available;
bool ssd_snapshot_valid;
Lardon3DSsdSnapshot ssd;
} Lardon3DRuntimeObserverSample;
typedef struct {
bool (*monotonic_now_ns)(void *context, uint64_t *now_ns);
bool (*capture)(
void *context,
Lardon3DRuntimeObserverSample *sample,
char reason[LARDON3D_TUI_TEXT_CAPACITY]
);
void (*destroy)(void *context);
} Lardon3DRuntimeObserverProviderOps;
typedef struct {
const Lardon3DRuntimeObserverProviderOps *ops;
void *context;
} Lardon3DRuntimeObserverProvider;
/* Private deterministic seam: the observer owns provider.context only after a
* successful create. Tests inject one complete bounded sample per refresh;
* production uses the same pure model builder after collecting real owners. */
Lardon3DRuntimeObserver *lardon3d_runtime_observer_create_with_provider(
const Lardon3DHardwareProfile *profile,
Lardon3DRuntimeObserverProvider provider,
uint64_t minimum_refresh_interval_ns
);
#endif

35
src/runtime_session.c Normal file
View file

@ -0,0 +1,35 @@
#include <stdio.h>
#include <lardon3d/project.h>
#include <lardon3d/runtime_session.h>
#include <lardon3d/task_queue.h>
bool
lardon3d_runtime_project_boundary(
Lardon3DAppState *state,
size_t queue_capacity
)
{
if (!state || !state->resource_governor || queue_capacity == 0) {
return false;
}
/* INVARIANT: callbacks may dereference Project DB until Queue destroy
* returns. Keeping state->task_queue published during destroy also
* preserves the documented read-only callback reentrancy window. */
lardon3d_task_queue_destroy(state->task_queue);
state->task_queue = NULL;
if (state->project_loaded || state->project_db) {
lardon3d_project_close(state);
}
state->task_queue = lardon3d_task_queue_create(
state->resource_governor, queue_capacity);
if (!state->task_queue) {
(void)snprintf(state->status_message,
sizeof(state->status_message),
"Erreur : impossible de recréer la file de tâches.");
return false;
}
return true;
}

View file

@ -1,5 +1,6 @@
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <openssl/evp.h>
#include <stdio.h>
#include <stdlib.h>
@ -167,12 +168,19 @@ static bool run(Lardon3DTask *task, void *userdata) {
struct timespec end = {0};
bool timing_known = clock_gettime(CLOCK_MONOTONIC, &begin) == 0;
Lardon3DOpenCvTaskThreadControl threads;
if (!lardon3d_opencv_task_threads_begin(task, 12, &threads)) {
if (!lardon3d_opencv_task_threads_begin(task, INT_MAX, &threads)) {
/* A failed configure can still mutate OpenCV. Retain and retry the captured
* restoration obligation once before returning across the C Task seam. */
if (threads.restore_required &&
!lardon3d_opencv_task_threads_end(&threads)) {
return lardon3d_task_fail(task, "Restauration OpenCV SIFT impossible.");
}
return lardon3d_task_fail(task, "Contrat CPU OpenCV SIFT invalide.");
}
/* SIFT/RootSIFT consume the immutable admitted 1..12 OpenCV count. Queue's
* single Task owner makes the process-wide set/restore deterministic and
* race-free without changing extractor identity or output semantics. */
/* EXTERNAL LIBRARY: OpenCV accepts a positive signed-int thread count.
* Governor clamps the portable INT_MAX capability to the host compute pool;
* Queue's single Task owner makes the process-wide set/restore deterministic
* without changing SIFT/RootSIFT identity or output semantics. */
size_t durable_items = 0;
bool result = run_body(task, userdata, &durable_items);
if (!lardon3d_opencv_task_threads_end(&threads)) {
@ -289,9 +297,9 @@ Lardon3DTask *lardon3d_project_create_sift_extract_task(
.memory_bytes_per_item = 1024ULL * 1024 * 1024,
.minimum_batch_size = 1,
.maximum_batch_size = 1,
/* Canonical durable maximum; the admitted OpenCV count may
* adapt within the validated 1..12 range. */
.desired_cpu_threads = 12,
/* EXTERNAL LIBRARY: OpenCV's positive-int API is the safe
* ceiling; Governor supplies the portable host maximum. */
.desired_cpu_threads = INT_MAX,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU};
const char *kind = parameters->rootsift ? LARDON3D_ROOTSIFT_EXTRACT_TASK_KIND

View file

@ -13,6 +13,14 @@ namespace {
using Result = Lardon3DSparseGeometryResult;
/*
* OpenCV exposes these robust-estimator controls as signed int. Keep the
* public fixed-width encoding unchanged, but reject an unrepresentable value
* before allocation, RNG mutation, output mutation, or entry into OpenCV.
*/
constexpr uint32_t kOpenCvSignedMaximum =
static_cast<uint32_t>(std::numeric_limits<int>::max());
struct RngGuard {
uint64_t saved;
explicit RngGuard(uint64_t seed) : saved(cv::theRNG().state) {
@ -187,8 +195,12 @@ extern "C" Lardon3DSparseGeometryResult lardon3d_sparse_geometry_relative_pose(
const Lardon3DSparseGeometryRelativePoseParameters *parameters,
Lardon3DSparseGeometryRelativePoseResult *result) {
if (!calibration_a || !calibration_b || !pixels_a || !pixels_b ||
!parameters || !result || count > static_cast<size_t>(INT_MAX) ||
parameters->max_iterations == 0 || parameters->confidence <= 0.0 ||
!parameters || !result ||
count > static_cast<size_t>(std::numeric_limits<int>::max()) ||
parameters->max_iterations == 0 ||
parameters->max_iterations > kOpenCvSignedMaximum ||
parameters->minimum_inliers > kOpenCvSignedMaximum ||
parameters->confidence <= 0.0 ||
parameters->confidence >= 1.0 || parameters->robust_threshold_px <= 0.0 ||
parameters->minimum_parallax_rad < 0.0 ||
parameters->minimum_cheirality_ratio <= 0.0 ||
@ -200,6 +212,8 @@ extern "C" Lardon3DSparseGeometryResult lardon3d_sparse_geometry_relative_pose(
return LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT;
if (!points_finite(pixels_a, count) || !points_finite(pixels_b, count))
return LARDON3D_SPARSE_GEOMETRY_NONFINITE_INPUT;
const int max_iterations = static_cast<int>(parameters->max_iterations);
const int minimum_inliers = static_cast<int>(parameters->minimum_inliers);
result->inlier_count = 0;
result->inlier_ratio = 0.0;
result->median_parallax_rad = 0.0;
@ -266,14 +280,14 @@ extern "C" Lardon3DSparseGeometryResult lardon3d_sparse_geometry_relative_pose(
cv::Mat essential = cv::findEssentialMat(
points_a, points_b, 1.0, cv::Point2d(0, 0), cv::RANSAC,
parameters->confidence, normalized_threshold,
static_cast<int>(parameters->max_iterations), mask);
max_iterations, mask);
if (essential.empty())
return LARDON3D_SPARSE_GEOMETRY_ESTIMATION_FAILED;
cv::Mat rotation, translation;
int inliers = cv::recoverPose(essential, points_a, points_b, rotation,
translation, 1.0, cv::Point2d(0, 0), mask);
double count_value = static_cast<double>(count);
if (inliers < static_cast<int>(parameters->minimum_inliers) ||
if (inliers < minimum_inliers ||
static_cast<double>(inliers) / count_value <
parameters->minimum_inlier_ratio ||
!rotation_valid(rotation) || cv::norm(translation) < 1e-12)
@ -519,8 +533,12 @@ extern "C" Lardon3DSparseGeometryResult lardon3d_sparse_geometry_pnp(
const Lardon3DSparseGeometryPnPParameters *parameters,
Lardon3DSparseGeometryPnPResult *result) {
if (!calibration || !points || !pixels || !parameters || !result ||
count < 4 || count > static_cast<size_t>(INT_MAX) ||
parameters->max_iterations == 0 || parameters->confidence <= 0.0 ||
count < 4 ||
count > static_cast<size_t>(std::numeric_limits<int>::max()) ||
parameters->max_iterations == 0 ||
parameters->max_iterations > kOpenCvSignedMaximum ||
parameters->minimum_inliers > kOpenCvSignedMaximum ||
parameters->confidence <= 0.0 ||
parameters->confidence >= 1.0 || parameters->reprojection_threshold_px <= 0.0 ||
parameters->minimum_inlier_ratio <= 0.0 ||
parameters->minimum_inlier_ratio > 1.0)
@ -534,6 +552,9 @@ extern "C" Lardon3DSparseGeometryResult lardon3d_sparse_geometry_pnp(
!finite_value(points[index].z) || !finite_value(pixels[index].x) ||
!finite_value(pixels[index].y))
return LARDON3D_SPARSE_GEOMETRY_NONFINITE_INPUT;
const int max_iterations = static_cast<int>(parameters->max_iterations);
const int minimum_inliers =
std::max(static_cast<int>(parameters->minimum_inliers), 4);
try {
std::vector<cv::Point3d> object_points;
std::vector<cv::Point2d> image_points;
@ -561,12 +582,11 @@ extern "C" Lardon3DSparseGeometryResult lardon3d_sparse_geometry_pnp(
RngGuard rng(parameters->deterministic_seed);
bool solved = cv::solvePnPRansac(
object_points, image_points, camera_matrix(*calibration),
distortion(*calibration), rvec, tvec, false, parameters->max_iterations,
distortion(*calibration), rvec, tvec, false, max_iterations,
static_cast<float>(parameters->reprojection_threshold_px),
parameters->confidence, inliers, cv::SOLVEPNP_EPNP);
uint32_t minimum = std::max<uint32_t>(parameters->minimum_inliers, 4);
double count_value = static_cast<double>(count);
if (!solved || inliers.rows < static_cast<int>(minimum) ||
if (!solved || inliers.rows < minimum_inliers ||
static_cast<double>(inliers.rows) / count_value <
parameters->minimum_inlier_ratio)
return LARDON3D_SPARSE_GEOMETRY_ESTIMATION_FAILED;

2636
src/ssd_controller.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,162 @@
#ifndef LARDON3D_SSD_CONTROLLER_INTERNAL_H
#define LARDON3D_SSD_CONTROLLER_INTERNAL_H
#include <stdbool.h>
#include <stdint.h>
#include <lardon3d/ssd_controller.h>
#ifdef LARDON3D_SSD_CONTROLLER_TESTING
#include <glib.h>
#endif
#ifdef __cplusplus
extern "C" {
#endif
enum {
LARDON3D_SSD_OBJECT_PATH_CAPACITY = 256,
};
typedef struct {
bool present;
bool unit_ready;
bool interface_available;
uint64_t size_bytes;
char label[LARDON3D_SSD_TEXT_CAPACITY];
char uuid[LARDON3D_SSD_TEXT_CAPACITY];
char drive_identity[LARDON3D_SSD_IDENTITY_CAPACITY];
char object_path[LARDON3D_SSD_OBJECT_PATH_CAPACITY];
char device[LARDON3D_SSD_PATH_CAPACITY];
/* Required UDisks Swapspace.Active telemetry. `active` has no meaning
* unless active_known is true; missing/wrong D-Bus storage must never be
* narrowed to the unsafe default "inactive". */
bool active_known;
bool active;
bool total_known;
bool used_known;
uint64_t total_bytes;
uint64_t used_bytes;
} Lardon3DSsdProviderSwap;
typedef struct {
bool present;
bool unit_ready;
bool interface_available;
uint64_t size_bytes;
char label[LARDON3D_SSD_TEXT_CAPACITY];
char uuid[LARDON3D_SSD_TEXT_CAPACITY];
char drive_identity[LARDON3D_SSD_IDENTITY_CAPACITY];
char object_path[LARDON3D_SSD_OBJECT_PATH_CAPACITY];
char device[LARDON3D_SSD_PATH_CAPACITY];
bool mounted;
bool total_known;
bool free_known;
uint64_t total_bytes;
uint64_t free_bytes;
char mount_path[LARDON3D_SSD_PATH_CAPACITY];
} Lardon3DSsdProviderScratch;
typedef struct {
bool model_known;
bool serial_known;
bool connection_speed_known;
char model[LARDON3D_SSD_TEXT_CAPACITY];
char serial[LARDON3D_SSD_TEXT_CAPACITY];
uint64_t connection_speed_mbps;
bool ambiguous_labels;
bool invalid_observation;
char invalid_reason[LARDON3D_SSD_REASON_CAPACITY];
Lardon3DSsdProviderSwap swap;
Lardon3DSsdProviderScratch scratch;
bool memory_available_known;
uint64_t memory_available_bytes;
bool memory_pressure_known;
bool memory_pressure_elevated;
bool io_pressure_known;
bool io_pressure_elevated;
bool swap_activity_known;
uint64_t swap_pages_in_delta;
uint64_t swap_pages_out_delta;
} Lardon3DSsdProviderSnapshot;
typedef struct {
bool (*monotonic_now_ns)(void *context, uint64_t *now_ns);
bool (*refresh)(
void *context,
Lardon3DSsdProviderSnapshot *snapshot,
char reason[LARDON3D_SSD_REASON_CAPACITY]
);
bool (*start_swap)(
void *context,
const char *object_path,
char reason[LARDON3D_SSD_REASON_CAPACITY]
);
bool (*stop_swap)(
void *context,
const char *object_path,
char reason[LARDON3D_SSD_REASON_CAPACITY]
);
bool (*mount_scratch)(
void *context,
const char *object_path,
char mount_path[LARDON3D_SSD_PATH_CAPACITY],
char reason[LARDON3D_SSD_REASON_CAPACITY]
);
bool (*unmount_scratch)(
void *context,
const char *object_path,
char reason[LARDON3D_SSD_REASON_CAPACITY]
);
void (*destroy)(void *context);
} Lardon3DSsdProviderOps;
typedef struct {
const Lardon3DSsdProviderOps *ops;
void *context;
} Lardon3DSsdProvider;
/* Private dependency-injection seam. The controller takes ownership of
* `provider.context` on success only. Tests use fixed in-memory providers;
* production constructs the GDBus provider below. This never extends the
* public ABI or permits provider replacement after creation. The interval is
* at least one second; tests use force-refresh instead of weakening caching. */
Lardon3DSsdController *lardon3d_ssd_controller_create_with_provider(
Lardon3DSsdProvider provider,
uint64_t minimum_poll_interval_ns
);
bool lardon3d_ssd_production_provider_create(Lardon3DSsdProvider *provider);
#ifdef LARDON3D_SSD_CONTROLLER_TESTING
/* Parses a caller-owned GetManagedObjects fixture without touching D-Bus,
* /proc, mounts, or swap. This exists solely to validate exact GLib storage
* types and bounded UDisks discovery; production uses the same parser. */
bool lardon3d_ssd_parse_managed_objects_for_test(
GVariant *objects,
Lardon3DSsdProviderSnapshot *snapshot,
char reason[LARDON3D_SSD_REASON_CAPACITY]
);
/* Deterministic saturation seam. It changes only the private source watermark
* and cached copy; later production operations still use the real saturating
* bump path and otherwise retain identical controller behavior. */
bool lardon3d_ssd_controller_set_generation_for_test(
Lardon3DSsdController *controller,
uint64_t generation
);
/* Forces only the cached copy to fail bounded-string validation. Physical
* lease bookkeeping still completes through the production release path. */
bool lardon3d_ssd_controller_corrupt_cached_snapshot_for_test(
Lardon3DSsdController *controller
);
#endif
#ifdef __cplusplus
}
#endif
#endif

View file

@ -14,6 +14,9 @@ struct Lardon3DTask {
uint64_t id;
char name[LARDON3D_TASK_NAME_CAPACITY];
unsigned int progress;
bool durable_progress_known;
uint64_t durable_completed;
uint64_t durable_total;
Lardon3DTaskState state;
char message[LARDON3D_TASK_MESSAGE_CAPACITY];
struct timespec started_at;
@ -112,15 +115,17 @@ kind_has_validated_cpu_range(const char *task_kind, uint32_t task_kind_version)
|| strcmp(task_kind, "features.extract.sift") == 0
|| strcmp(task_kind, "features.extract.rootsift") == 0
|| strcmp(task_kind, "visual_index.update") == 0
|| strcmp(task_kind, "candidate_pair.generate") == 0;
|| strcmp(task_kind, "candidate_pair.generate") == 0
|| strcmp(task_kind, "geometric_verifier.run") == 0;
}
static bool
kind_has_validated_batch_range(const char *task_kind, uint32_t version)
{
/* Candidate generation already executes/publishes at every canonical
* 1..64 sequence size. This private bit changes operational pacing only;
* it does not add a scientific or durable identity dimension. */
/* Candidate generation explicitly co-adapts its sequence window. GV keeps
* its 16-item preparation window while trialling CPU, because CPU2 with a
* one-item batch cannot exercise a second participant; Gate G may still
* reduce that bounded window for capacity or pressure. */
return task_kind && version == 1
&& strcmp(task_kind, "candidate_pair.generate") == 0;
}
@ -1107,6 +1112,62 @@ lardon3d_task_set_progress(
bool accepted = !is_terminal(task->state);
if (accepted) {
task->progress = progress;
task->durable_progress_known = false;
task->durable_completed = 0;
task->durable_total = 0;
if (message) {
copy_text(task->message, sizeof(task->message), message);
}
}
(void)pthread_mutex_unlock(&task->mutex);
return accepted;
}
static unsigned int
durable_percentage(uint64_t completed, uint64_t total)
{
if (completed >= total) {
return 100;
}
/* Compare against ceil(percent * total / 100). Splitting total first
* avoids overflowing uint64_t even at the public input maximum. */
uint64_t quotient = total / 100U;
uint64_t remainder = total % 100U;
for (unsigned int percent = 99; percent > 0; --percent) {
uint64_t threshold = quotient * percent;
uint64_t residual_product = remainder * percent;
threshold += residual_product / 100U;
if (residual_product % 100U != 0) {
++threshold;
}
if (completed >= threshold) {
return percent;
}
}
return 0;
}
bool
lardon3d_task_set_durable_progress(
Lardon3DTask *task,
uint64_t completed,
uint64_t total,
const char *message
)
{
if (!task || total == 0 || completed > total) {
return false;
}
(void)pthread_mutex_lock(&task->mutex);
/* Only a typed Task owner can name a durable business prefix. Generic
* Task completion never fills the remaining count: doing so would invent
* a commit outside the typed transaction that owns this observation. */
bool accepted = !is_terminal(task->state) && task->task_kind[0] != '\0';
if (accepted) {
task->progress = durable_percentage(completed, total);
task->durable_progress_known = true;
task->durable_completed = completed;
task->durable_total = total;
if (message) {
copy_text(task->message, sizeof(task->message), message);
}
@ -1159,6 +1220,48 @@ lardon3d_task_snapshot(
return true;
}
bool
lardon3d_task_observation(
const Lardon3DTask *task,
Lardon3DTaskObservation *observation
)
{
if (observation) {
*observation = (Lardon3DTaskObservation) {0};
}
if (!task || !observation) {
return false;
}
Lardon3DTask *mutable_task = (Lardon3DTask *)task;
(void)pthread_mutex_lock(&mutable_task->mutex);
*observation = (Lardon3DTaskObservation) {
.id = task->id,
.progress = task->progress,
.state = task->state,
.started_at = task->started_at,
.finished_at = task->finished_at,
.has_task_kind = task->task_kind[0] != '\0',
.task_kind_version = task->task_kind_version,
.durable_progress_known = task->durable_progress_known,
.durable_completed = task->durable_progress_known
? task->durable_completed : 0,
.durable_total = task->durable_progress_known
? task->durable_total : 0,
.sequence_count = task->sequence_count,
.has_execution_contract = task->has_contract,
.execution_contract = task->has_contract
? task->contract
: (Lardon3DTaskExecutionContract) {0},
};
copy_text(observation->name, sizeof(observation->name), task->name);
copy_text(observation->task_kind, sizeof(observation->task_kind),
task->task_kind);
copy_text(observation->message, sizeof(observation->message),
task->message);
(void)pthread_mutex_unlock(&mutable_task->mutex);
return true;
}
bool
lardon3d_task_durable_snapshot(
const Lardon3DTask *task,

View file

@ -1,3 +1,4 @@
#include <limits.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
@ -23,11 +24,18 @@ extern bool lardon3d_acquisition_campaign_task_internal_configure_restored(
enum {
CANDIDATE_LEGACY_FIXED_BYTES = 128 * 1024,
CANDIDATE_CURRENT_FIXED_BYTES = 256 * 1024,
CANDIDATE_PER_ITEM_BYTES = 64 * 1024,
CANDIDATE_LEGACY_PER_ITEM_BYTES = 64 * 1024,
CANDIDATE_CURRENT_PER_ITEM_BYTES = 8 * 1024 * 1024,
FEATURE_FIXED_BYTES = 64 * 1024 * 1024,
FEATURE_PER_ITEM_BYTES = 512 * 1024 * 1024,
MATCHER_LEGACY_FIXED_BYTES = 10 * 1024 * 1024,
MATCHER_CURRENT_PER_ITEM_BYTES = 10 * 1024 * 1024,
MATCHER_GPU_FIXED_BYTES = 640 * 1024,
GEOMETRIC_VERIFIER_LEGACY_FIXED_BYTES = 4 * 1024 * 1024,
GEOMETRIC_VERIFIER_CURRENT_PER_ITEM_BYTES = 8 * 1024 * 1024,
SIFT_FIXED_BYTES = 64 * 1024 * 1024,
VISUAL_INDEX_FIXED_BYTES = 8 * 1024 * 1024,
VISUAL_INDEX_PER_ITEM_BYTES = 2 * 1024 * 1024,
};
static bool
@ -58,16 +66,35 @@ normalize_known_legacy_estimate(const char *kind,
if (strcmp(kind, "candidate_pair.generate") == 0) {
current = (Lardon3DResourceEstimate) {
.memory_fixed_bytes = CANDIDATE_CURRENT_FIXED_BYTES,
.memory_bytes_per_item = CANDIDATE_PER_ITEM_BYTES,
.memory_bytes_per_item = CANDIDATE_CURRENT_PER_ITEM_BYTES,
.minimum_batch_size = 1,
.maximum_batch_size = 64,
.desired_cpu_threads = 12,
.desired_cpu_threads = 64,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
};
historical = current;
historical.memory_fixed_bytes = CANDIDATE_LEGACY_FIXED_BYTES;
historical.desired_cpu_threads = 1;
historical.memory_bytes_per_item = CANDIDATE_LEGACY_PER_ITEM_BYTES;
historical.desired_cpu_threads = 12;
oldest = historical;
oldest.memory_fixed_bytes = CANDIDATE_LEGACY_FIXED_BYTES;
oldest.desired_cpu_threads = 1;
has_oldest = true;
} else if (strcmp(kind, "features.extract") == 0) {
current = (Lardon3DResourceEstimate) {
.memory_fixed_bytes = FEATURE_FIXED_BYTES,
.memory_bytes_per_item = FEATURE_PER_ITEM_BYTES,
.minimum_batch_size = 1,
.maximum_batch_size = 1,
.desired_cpu_threads = INT_MAX,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
};
historical = current;
historical.desired_cpu_threads = 12;
oldest = historical;
oldest.desired_cpu_threads = 1;
has_oldest = true;
} else if (strcmp(kind, "matcher.run") == 0) {
const bool gpu = durable->desired_gpu_slots == 1;
current = (Lardon3DResourceEstimate) {
@ -108,12 +135,53 @@ normalize_known_legacy_estimate(const char *kind,
.memory_bytes_per_item = UINT64_C(1024) * 1024 * 1024,
.minimum_batch_size = 1,
.maximum_batch_size = 1,
.desired_cpu_threads = 12,
.desired_cpu_threads = INT_MAX,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
};
historical = current;
historical.desired_cpu_threads = 1;
historical.desired_cpu_threads = 12;
oldest = historical;
oldest.desired_cpu_threads = 1;
has_oldest = true;
} else if (strcmp(kind, "visual_index.update") == 0) {
current = (Lardon3DResourceEstimate) {
.memory_fixed_bytes = VISUAL_INDEX_FIXED_BYTES,
.memory_bytes_per_item = VISUAL_INDEX_PER_ITEM_BYTES,
.minimum_batch_size = 1,
.maximum_batch_size = 16,
.desired_cpu_threads = 16,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
};
historical = current;
historical.desired_cpu_threads = 12;
/* An exact serial Visual Index Task signature predates bounded
* internal fan-out. Accept only that complete CPU1 durable shape. */
oldest = historical;
oldest.desired_cpu_threads = 1;
has_oldest = true;
} else if (strcmp(kind, "geometric_verifier.run") == 0) {
current = (Lardon3DResourceEstimate) {
.memory_bytes_per_item =
GEOMETRIC_VERIFIER_CURRENT_PER_ITEM_BYTES,
.minimum_batch_size = 1,
.maximum_batch_size = 16,
.desired_cpu_threads = 8,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
};
historical = (Lardon3DResourceEstimate) {
.memory_fixed_bytes = GEOMETRIC_VERIFIER_LEGACY_FIXED_BYTES,
.minimum_batch_size = 1,
.maximum_batch_size = 8,
.desired_cpu_threads = 1,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
};
/* CPU/batch/memory admission is operational, not GVR identity. Accept
* only the complete frozen serial envelope, normalize it in memory,
* and never publish an estimate-only checkpoint during recovery. */
} else {
*effective = *durable;
return true;

File diff suppressed because it is too large Load diff

35
src/task_queue_internal.h Normal file
View file

@ -0,0 +1,35 @@
#ifndef LARDON3D_TASK_QUEUE_INTERNAL_H
#define LARDON3D_TASK_QUEUE_INTERNAL_H
#include <lardon3d/task_queue.h>
#if defined(LARDON3D_TASK_QUEUE_TESTING)
/* TEST CONTRACT: these events expose exact Queue lifetime linearization points
* only to test-task-queue and test-resource-external-storage. Production must
* not declare, reference or call this seam. Both test binaries provide one
* strong callback definition; task_queue.c keeps a weak reference so the seam
* cannot become a required library/public ABI symbol. */
typedef enum {
LARDON3D_TASK_QUEUE_TEST_CALL_REGISTERED = 1,
LARDON3D_TASK_QUEUE_TEST_PRODUCER_WAITING = 2,
LARDON3D_TASK_QUEUE_TEST_CLOSING = 3,
} Lardon3DTaskQueueTestEvent;
#if defined(LARDON3D_TASK_QUEUE_TEST_WEAK_REFERENCE) \
&& (defined(__GNUC__) || defined(__clang__))
#define LARDON3D_TASK_QUEUE_TEST_ATTRIBUTE __attribute__((weak))
#else
#define LARDON3D_TASK_QUEUE_TEST_ATTRIBUTE
#endif
void lardon3d_task_queue_internal_test_event(
Lardon3DTaskQueue *queue,
Lardon3DTaskQueueTestEvent event
) LARDON3D_TASK_QUEUE_TEST_ATTRIBUTE;
#undef LARDON3D_TASK_QUEUE_TEST_ATTRIBUTE
#endif
#endif

844
src/tui.c

File diff suppressed because it is too large Load diff

679
src/tui_model.c Normal file
View file

@ -0,0 +1,679 @@
#include <limits.h>
#include <math.h>
#include <stdio.h>
#include <string.h>
#include <lardon3d/tui_model.h>
enum {
TUI_COMPACT_MINIMUM_ROWS = 15,
TUI_COMPACT_MINIMUM_COLUMNS = 60,
TUI_FULL_MINIMUM_ROWS = 30,
TUI_FULL_MINIMUM_COLUMNS = 100,
};
static void
copy_text(char *destination, size_t capacity, const char *source)
{
if (!destination || capacity == 0) {
return;
}
(void)snprintf(destination, capacity, "%s", source ? source : "");
}
Lardon3DTuiViewport
lardon3d_tui_viewport_classify(int rows, int columns)
{
if (rows < TUI_COMPACT_MINIMUM_ROWS
|| columns < TUI_COMPACT_MINIMUM_COLUMNS) {
return LARDON3D_TUI_VIEWPORT_TOO_SMALL;
}
if (rows >= TUI_FULL_MINIMUM_ROWS
&& columns >= TUI_FULL_MINIMUM_COLUMNS) {
return LARDON3D_TUI_VIEWPORT_FULL;
}
return LARDON3D_TUI_VIEWPORT_COMPACT;
}
void
lardon3d_tui_palette_plan(
bool colors_supported,
int color_pairs,
Lardon3DTuiPalette *palette
)
{
if (!palette) {
return;
}
*palette = (Lardon3DTuiPalette) {0};
palette->attributes[LARDON3D_TUI_SEMANTIC_HEALTHY] =
LARDON3D_TUI_STYLE_BOLD;
palette->attributes[LARDON3D_TUI_SEMANTIC_WARNING] =
LARDON3D_TUI_STYLE_BOLD;
palette->attributes[LARDON3D_TUI_SEMANTIC_ERROR] =
LARDON3D_TUI_STYLE_BOLD;
palette->attributes[LARDON3D_TUI_SEMANTIC_GPU] =
LARDON3D_TUI_STYLE_BOLD;
palette->attributes[LARDON3D_TUI_SEMANTIC_CPU] =
LARDON3D_TUI_STYLE_BOLD;
palette->attributes[LARDON3D_TUI_SEMANTIC_SSD] =
LARDON3D_TUI_STYLE_BOLD;
palette->attributes[LARDON3D_TUI_SEMANTIC_DIM] =
LARDON3D_TUI_STYLE_DIM;
if (!colors_supported || color_pairs <= 1) {
return;
}
static const Lardon3DTuiSemantic colored[] = {
LARDON3D_TUI_SEMANTIC_HEALTHY,
LARDON3D_TUI_SEMANTIC_WARNING,
LARDON3D_TUI_SEMANTIC_ERROR,
LARDON3D_TUI_SEMANTIC_GPU,
LARDON3D_TUI_SEMANTIC_CPU,
LARDON3D_TUI_SEMANTIC_SSD,
};
palette->color_enabled = true;
for (size_t index = 0; index < sizeof(colored) / sizeof(colored[0]);
++index) {
int pair = (int)index + 1;
if (pair < color_pairs) {
palette->color_pair[colored[index]] = (short)pair;
}
}
}
const char *
lardon3d_tui_stage_name(Lardon3DTuiStage stage)
{
static const char *const names[LARDON3D_TUI_STAGE_COUNT] = {
"Acquisition",
"RAW",
"Quality",
"Features",
"Visual Index",
"Candidate",
"Matcher",
"GV",
"Tracks",
"Sparse SfM",
"Dense (future)",
};
return stage >= LARDON3D_TUI_STAGE_ACQUISITION
&& stage <= LARDON3D_TUI_STAGE_DENSE
? names[stage]
: "Unknown";
}
const char *
lardon3d_tui_stage_state_name(Lardon3DTuiStageState state)
{
switch (state) {
case LARDON3D_TUI_STAGE_NOT_READY:
return "NOT_READY";
case LARDON3D_TUI_STAGE_READY:
return "READY";
case LARDON3D_TUI_STAGE_QUEUED:
return "QUEUED";
case LARDON3D_TUI_STAGE_RUNNING:
return "RUNNING";
case LARDON3D_TUI_STAGE_THROTTLED:
return "THROTTLED";
case LARDON3D_TUI_STAGE_BLOCKED:
return "BLOCKED";
case LARDON3D_TUI_STAGE_COMPLETE:
return "COMPLETE";
case LARDON3D_TUI_STAGE_FAILED:
return "FAILED";
case LARDON3D_TUI_STAGE_NOT_APPLICABLE:
return "NOT_APPLICABLE";
}
return "UNKNOWN";
}
Lardon3DTuiSemantic
lardon3d_tui_stage_semantic(Lardon3DTuiStageState state)
{
switch (state) {
case LARDON3D_TUI_STAGE_READY:
case LARDON3D_TUI_STAGE_COMPLETE:
return LARDON3D_TUI_SEMANTIC_HEALTHY;
case LARDON3D_TUI_STAGE_QUEUED:
case LARDON3D_TUI_STAGE_RUNNING:
return LARDON3D_TUI_SEMANTIC_CPU;
case LARDON3D_TUI_STAGE_THROTTLED:
case LARDON3D_TUI_STAGE_BLOCKED:
return LARDON3D_TUI_SEMANTIC_WARNING;
case LARDON3D_TUI_STAGE_FAILED:
return LARDON3D_TUI_SEMANTIC_ERROR;
case LARDON3D_TUI_STAGE_NOT_READY:
case LARDON3D_TUI_STAGE_NOT_APPLICABLE:
return LARDON3D_TUI_SEMANTIC_DIM;
}
return LARDON3D_TUI_SEMANTIC_NORMAL;
}
static bool
kind_to_stage(
const Lardon3DTaskObservation *task,
Lardon3DTuiStage *stage
)
{
if (!task || !stage || !task->has_task_kind
|| task->task_kind_version != 1) {
return false;
}
const char *kind = task->task_kind;
if (strcmp(kind, "import.images") == 0
|| strcmp(kind, "acquisition_campaign.run") == 0) {
*stage = LARDON3D_TUI_STAGE_ACQUISITION;
} else if (strcmp(kind, "raw.develop") == 0) {
*stage = LARDON3D_TUI_STAGE_RAW;
} else if (strcmp(kind, "photo_quality.triage") == 0) {
*stage = LARDON3D_TUI_STAGE_QUALITY;
} else if (strcmp(kind, "features.extract") == 0
|| strcmp(kind, "features.extract.sift") == 0
|| strcmp(kind, "features.extract.rootsift") == 0) {
*stage = LARDON3D_TUI_STAGE_FEATURES;
} else if (strcmp(kind, "visual_index.update") == 0) {
*stage = LARDON3D_TUI_STAGE_VISUAL_INDEX;
} else if (strcmp(kind, "candidate_pair.generate") == 0) {
*stage = LARDON3D_TUI_STAGE_CANDIDATE;
} else if (strcmp(kind, "matcher.run") == 0) {
*stage = LARDON3D_TUI_STAGE_MATCHER;
} else if (strcmp(kind, "geometric_verifier.run") == 0) {
*stage = LARDON3D_TUI_STAGE_GV;
} else if (strcmp(kind, "track_builder.run") == 0) {
*stage = LARDON3D_TUI_STAGE_TRACKS;
} else if (strcmp(kind, "sparse_sfm.run") == 0
|| strcmp(kind, "incremental_reconstruction.run") == 0) {
*stage = LARDON3D_TUI_STAGE_SPARSE_SFM;
} else {
return false;
}
return true;
}
static Lardon3DTuiStageState
task_stage_state(
const Lardon3DTaskObservation *task,
Lardon3DResourcePressure pressure
)
{
switch (task->state) {
case TASK_PENDING:
return pressure == LARDON3D_RESOURCE_PRESSURE_GREEN
? LARDON3D_TUI_STAGE_QUEUED
: LARDON3D_TUI_STAGE_THROTTLED;
case TASK_RUNNING:
return LARDON3D_TUI_STAGE_RUNNING;
case TASK_PAUSED:
case TASK_CANCELLED:
return LARDON3D_TUI_STAGE_BLOCKED;
case TASK_FAILED:
return LARDON3D_TUI_STAGE_FAILED;
case TASK_COMPLETED:
if (task->durable_progress_known
&& task->durable_completed < task->durable_total) {
return LARDON3D_TUI_STAGE_BLOCKED;
}
return LARDON3D_TUI_STAGE_COMPLETE;
}
return LARDON3D_TUI_STAGE_NOT_READY;
}
static unsigned int
task_stage_relevance(Lardon3DTaskState state)
{
/* Queue snapshots are newest-first, but a newly submitted pending Task
* must not hide an older Task that is actually executing in the same
* stage. Equal relevance retains the first/newest observation. */
switch (state) {
case TASK_RUNNING:
return 3;
case TASK_PAUSED:
return 2;
case TASK_PENDING:
return 1;
case TASK_CANCELLED:
case TASK_FAILED:
case TASK_COMPLETED:
return 0;
}
return 0;
}
void
lardon3d_tui_stage_views_build(
bool project_loaded,
const Lardon3DTaskObservation *tasks,
size_t task_count,
Lardon3DResourcePressure pressure,
Lardon3DTuiStageView stages[LARDON3D_TUI_STAGE_COUNT]
)
{
if (!stages) {
return;
}
for (size_t index = 0; index < LARDON3D_TUI_STAGE_COUNT; ++index) {
stages[index] = (Lardon3DTuiStageView) {
.stage = (Lardon3DTuiStage)index,
.state = LARDON3D_TUI_STAGE_NOT_READY,
};
copy_text(stages[index].reason, sizeof(stages[index].reason),
project_loaded ? "No eligible runtime Task observed"
: "Load a project first");
}
stages[LARDON3D_TUI_STAGE_DENSE].state =
LARDON3D_TUI_STAGE_NOT_APPLICABLE;
copy_text(stages[LARDON3D_TUI_STAGE_DENSE].reason,
sizeof(stages[LARDON3D_TUI_STAGE_DENSE].reason),
"Future stage; no production execution is exposed");
if (project_loaded) {
stages[LARDON3D_TUI_STAGE_ACQUISITION].state =
LARDON3D_TUI_STAGE_READY;
copy_text(stages[LARDON3D_TUI_STAGE_ACQUISITION].reason,
sizeof(stages[LARDON3D_TUI_STAGE_ACQUISITION].reason),
"Project is ready for acquisition/import work");
}
if (!tasks || task_count == 0) {
return;
}
bool observed[LARDON3D_TUI_STAGE_COUNT] = {0};
unsigned int relevance[LARDON3D_TUI_STAGE_COUNT] = {0};
for (size_t index = 0; index < task_count; ++index) {
Lardon3DTuiStage stage;
if (!kind_to_stage(&tasks[index], &stage)) {
continue;
}
unsigned int candidate_relevance = task_stage_relevance(
tasks[index].state);
if (observed[stage] && candidate_relevance <= relevance[stage]) {
continue;
}
observed[stage] = true;
relevance[stage] = candidate_relevance;
stages[stage].state = task_stage_state(&tasks[index], pressure);
if (tasks[index].state == TASK_COMPLETED
&& tasks[index].durable_progress_known
&& tasks[index].durable_completed < tasks[index].durable_total) {
(void)snprintf(stages[stage].reason,
sizeof(stages[stage].reason),
"Durable progress incomplete: %llu/%llu",
(unsigned long long)tasks[index].durable_completed,
(unsigned long long)tasks[index].durable_total);
} else if (tasks[index].state == TASK_COMPLETED
&& !tasks[index].durable_progress_known) {
copy_text(stages[stage].reason, sizeof(stages[stage].reason),
"Lifecycle complete; scientific progress indeterminate");
} else {
copy_text(stages[stage].reason, sizeof(stages[stage].reason),
tasks[index].message[0] ? tasks[index].message
: lardon3d_task_state_name(tasks[index].state));
}
}
/* READY is only a workflow hint after an observed successful predecessor;
* it never claims that scientific prerequisites or an absent Task exist. */
for (size_t index = 0; index + 1 < LARDON3D_TUI_STAGE_DENSE; ++index) {
if (stages[index].state == LARDON3D_TUI_STAGE_COMPLETE
&& !observed[index + 1]) {
stages[index + 1].state = LARDON3D_TUI_STAGE_READY;
copy_text(stages[index + 1].reason,
sizeof(stages[index + 1].reason),
"Previous observed stage completed; eligibility not inferred");
}
}
}
static unsigned int
percentage(uint64_t completed, uint64_t total)
{
if (total == 0) {
return 0;
}
if (completed >= total) {
return 100;
}
uint64_t quotient = total / 100U;
uint64_t remainder = total % 100U;
for (unsigned int percent = 99; percent > 0; --percent) {
uint64_t threshold = quotient * percent;
uint64_t residual_product = remainder * percent;
threshold += residual_product / 100U;
if (residual_product % 100U != 0) {
++threshold;
}
if (completed >= threshold) {
return percent;
}
}
return 0;
}
static void
progress_tracker_reset(
Lardon3DTuiProgressTracker *tracker,
const Lardon3DTuiProgressSample *sample,
uint64_t completed
)
{
*tracker = (Lardon3DTuiProgressTracker) {
.task_id = sample->task_id,
.prefix_completed = completed,
.previous_completed = completed,
.previous_sample_ns = sample->monotonic_ns,
.last_positive_ns = sample->monotonic_ns,
.previous_state = sample->task_state,
.initialized = true,
.using_durable_counts = sample->durable_counts_known,
};
}
bool
lardon3d_tui_progress_update(
Lardon3DTuiProgressTracker *tracker,
const Lardon3DTuiProgressSample *sample,
Lardon3DTuiProgressView *view
)
{
if (view) {
*view = (Lardon3DTuiProgressView) {0};
}
if (!tracker || !sample || !view || sample->task_id == 0
|| sample->progress_percent > 100
|| (sample->durable_counts_known
&& (sample->durable_total == 0
|| sample->durable_completed > sample->durable_total))) {
return false;
}
bool measurement_known = sample->durable_counts_known
|| !sample->typed_task;
uint64_t completed = sample->durable_counts_known
? sample->durable_completed
: sample->progress_percent;
uint64_t total = sample->durable_counts_known
? sample->durable_total
: UINT64_C(100);
if (!measurement_known) {
*tracker = (Lardon3DTuiProgressTracker) {0};
*view = (Lardon3DTuiProgressView) {
.present = true,
.task_id = sample->task_id,
.eta_state = LARDON3D_TUI_ETA_INDETERMINATE,
};
return true;
}
if (!tracker->initialized || tracker->task_id != sample->task_id
|| tracker->using_durable_counts != sample->durable_counts_known) {
progress_tracker_reset(tracker, sample, completed);
} else if (sample->task_state == TASK_RUNNING
&& tracker->previous_state != TASK_RUNNING) {
/* Admission/resume starts a fresh rate interval. Pending or paused
* wall time is not processing throughput and must not inflate ETA. */
tracker->previous_completed = completed;
tracker->previous_sample_ns = sample->monotonic_ns;
tracker->last_positive_ns = sample->monotonic_ns;
if (completed > tracker->prefix_completed) {
tracker->prefix_completed = completed;
}
tracker->ewma_units_per_second = 0.0;
tracker->positive_sample_count = 0;
} else if (completed < tracker->previous_completed
|| sample->monotonic_ns < tracker->previous_sample_ns) {
/* A restored Task may expose an earlier durable prefix. Treat that as
* a new baseline; negative work must never create a rate or huge ETA. */
progress_tracker_reset(tracker, sample, completed);
} else if (completed > tracker->previous_completed
&& sample->monotonic_ns > tracker->previous_sample_ns) {
uint64_t delta = completed - tracker->previous_completed;
uint64_t elapsed_ns = sample->monotonic_ns
- tracker->previous_sample_ns;
double rate = (double)delta * 1000000000.0 / (double)elapsed_ns;
if (isfinite(rate) && rate > 0.0) {
tracker->ewma_units_per_second =
tracker->positive_sample_count == 0
? rate
: tracker->ewma_units_per_second * 0.65 + rate * 0.35;
if (tracker->positive_sample_count < UINT_MAX) {
++tracker->positive_sample_count;
}
tracker->last_positive_ns = sample->monotonic_ns;
}
}
tracker->previous_completed = completed;
tracker->previous_sample_ns = sample->monotonic_ns;
tracker->previous_state = sample->task_state;
*view = (Lardon3DTuiProgressView) {
.present = true,
.task_id = sample->task_id,
.durable_counts_known = sample->durable_counts_known,
.runtime_percentage = !sample->durable_counts_known,
.completed = sample->durable_counts_known ? completed : 0,
.total = sample->durable_counts_known ? total : 0,
.percentage_known = true,
.percentage = sample->durable_counts_known
? percentage(completed, total)
: sample->progress_percent,
.throughput_known = tracker->positive_sample_count >= 2,
.units_per_second = tracker->ewma_units_per_second,
.eta_state = LARDON3D_TUI_ETA_CALCULATING,
.resumed_prefix_excluded = tracker->prefix_completed > 0,
};
if (sample->task_state == TASK_COMPLETED
&& sample->durable_counts_known && completed < total) {
view->integrity_error = true;
view->eta_state = LARDON3D_TUI_ETA_INDETERMINATE;
return true;
}
if ((sample->durable_counts_known && completed == total)
|| (!sample->typed_task && sample->task_state == TASK_COMPLETED)) {
view->percentage = 100;
view->eta_state = LARDON3D_TUI_ETA_COMPLETE;
view->eta_seconds = 0;
return true;
}
if (sample->pressure_limited || sample->task_state == TASK_PAUSED) {
view->eta_state = LARDON3D_TUI_ETA_THROTTLED;
return true;
}
if (sample->task_state != TASK_RUNNING) {
view->eta_state = LARDON3D_TUI_ETA_INDETERMINATE;
return true;
}
if (sample->monotonic_ns >= tracker->last_positive_ns
&& sample->monotonic_ns - tracker->last_positive_ns
>= UINT64_C(5000000000)) {
view->eta_state = LARDON3D_TUI_ETA_STALLED;
return true;
}
if (tracker->positive_sample_count < 2
|| !(tracker->ewma_units_per_second > 0.0)) {
return true;
}
uint64_t remaining = total - completed;
long double seconds = (long double)remaining
/ (long double)tracker->ewma_units_per_second;
if (!isfinite((double)seconds) || seconds > (long double)UINT64_MAX) {
view->eta_state = LARDON3D_TUI_ETA_INDETERMINATE;
return true;
}
uint64_t rounded = (uint64_t)seconds;
if ((long double)rounded < seconds) {
++rounded;
}
view->eta_state = LARDON3D_TUI_ETA_KNOWN;
view->eta_seconds = rounded;
return true;
}
Lardon3DTuiKeyContract
lardon3d_tui_key_contract(Lardon3DTuiInteractionMode mode)
{
switch (mode) {
case LARDON3D_TUI_INTERACTION_TEXT_INPUT:
return (Lardon3DTuiKeyContract) {
.enter = true,
.escape = true,
.f10 = true,
};
case LARDON3D_TUI_INTERACTION_IMPORT_RUNNING:
return (Lardon3DTuiKeyContract) {
.f10 = true,
.cancel_import = true,
};
case LARDON3D_TUI_INTERACTION_IDLE:
return (Lardon3DTuiKeyContract) {
.escape = true,
.f10 = true,
.quit = true,
.navigate = true,
};
}
return (Lardon3DTuiKeyContract) {0};
}
const char *
lardon3d_tui_eta_state_name(Lardon3DTuiEtaState state)
{
switch (state) {
case LARDON3D_TUI_ETA_INDETERMINATE:
return "indeterminate";
case LARDON3D_TUI_ETA_CALCULATING:
return "calculating";
case LARDON3D_TUI_ETA_KNOWN:
return "known";
case LARDON3D_TUI_ETA_STALLED:
return "stalled";
case LARDON3D_TUI_ETA_THROTTLED:
return "throttled";
case LARDON3D_TUI_ETA_COMPLETE:
return "complete";
}
return "unknown";
}
const char *
lardon3d_tui_gpu_backend_name(Lardon3DTuiGpuBackendStatus status)
{
switch (status) {
case LARDON3D_TUI_GPU_BACKEND_UNKNOWN:
return "UNKNOWN";
case LARDON3D_TUI_GPU_BACKEND_UNINITIALIZED:
return "UNINITIALIZED";
case LARDON3D_TUI_GPU_BACKEND_AVAILABLE:
return "AVAILABLE";
case LARDON3D_TUI_GPU_BACKEND_UNAVAILABLE:
return "UNAVAILABLE";
case LARDON3D_TUI_GPU_BACKEND_CPU:
return "CPU";
case LARDON3D_TUI_GPU_BACKEND_ORB_VULKAN:
return "ORB_VULKAN";
case LARDON3D_TUI_GPU_BACKEND_MIXED:
return "MIXED";
}
return "UNKNOWN";
}
const char *
lardon3d_tui_pressure_name(Lardon3DResourcePressure pressure)
{
switch (pressure) {
case LARDON3D_RESOURCE_PRESSURE_GREEN:
return "GREEN";
case LARDON3D_RESOURCE_PRESSURE_YELLOW:
return "YELLOW";
case LARDON3D_RESOURCE_PRESSURE_RED:
return "RED";
}
return "UNKNOWN";
}
Lardon3DTuiOpticsStatus
lardon3d_tui_optics_classify(
bool project_loaded,
bool assignment_found,
bool assignment_valid,
bool manual_lens,
size_t compatible_calibration_count,
bool selection_found,
bool selection_compatible
)
{
(void)manual_lens;
if (!project_loaded) {
return LARDON3D_TUI_OPTICS_NO_PROJECT;
}
if (!assignment_found) {
return LARDON3D_TUI_OPTICS_UNRESOLVED;
}
if (!assignment_valid) {
return LARDON3D_TUI_OPTICS_CORRUPT;
}
if (selection_found && !selection_compatible) {
return LARDON3D_TUI_OPTICS_INCOMPATIBLE;
}
if (selection_found) {
return LARDON3D_TUI_OPTICS_SELECTED;
}
if (compatible_calibration_count == 0) {
return LARDON3D_TUI_OPTICS_CONFIGURATION_ONLY;
}
return LARDON3D_TUI_OPTICS_SELECTION_REQUIRED;
}
const char *
lardon3d_tui_optics_status_name(Lardon3DTuiOpticsStatus status)
{
switch (status) {
case LARDON3D_TUI_OPTICS_NO_PROJECT:
return "NO_PROJECT";
case LARDON3D_TUI_OPTICS_UNRESOLVED:
return "UNRESOLVED";
case LARDON3D_TUI_OPTICS_CONFIGURATION_ONLY:
return "MISSING_CALIBRATION";
case LARDON3D_TUI_OPTICS_SELECTION_REQUIRED:
return "SELECTION_REQUIRED";
case LARDON3D_TUI_OPTICS_SELECTED:
return "SELECTED";
case LARDON3D_TUI_OPTICS_INCOMPATIBLE:
return "INCOMPATIBLE";
case LARDON3D_TUI_OPTICS_CORRUPT:
return "CORRUPT";
}
return "UNKNOWN";
}
const char *
lardon3d_tui_optics_status_explanation(
Lardon3DTuiOpticsStatus status,
bool manual_lens
)
{
switch (status) {
case LARDON3D_TUI_OPTICS_NO_PROJECT:
return "Load a project to inspect optical assignments.";
case LARDON3D_TUI_OPTICS_UNRESOLVED:
return "No explicit optical configuration is assigned; nothing is guessed.";
case LARDON3D_TUI_OPTICS_CONFIGURATION_ONLY:
return manual_lens
? "Manual/no-EXIF lens is valid; no compatible calibration exists."
: "Optical configuration is explicit, but no compatible calibration exists.";
case LARDON3D_TUI_OPTICS_SELECTION_REQUIRED:
return "Compatible calibration exists; select one explicitly.";
case LARDON3D_TUI_OPTICS_SELECTED:
return manual_lens
? "Manual/no-EXIF configuration and exact calibration are selected."
: "Exact optical configuration and calibration are selected.";
case LARDON3D_TUI_OPTICS_INCOMPATIBLE:
return "Stored selection is incompatible with the Capture configuration.";
case LARDON3D_TUI_OPTICS_CORRUPT:
return "Malformed durable optical state; no mutation was attempted.";
}
return "Unknown optical status.";
}

717
src/tui_optics.c Normal file
View file

@ -0,0 +1,717 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <lardon3d/tui_optics.h>
struct Lardon3DTuiOptics {
Lardon3DProjectDb *database;
Lardon3DTuiOpticsSnapshot view;
uint64_t body_cursor;
uint64_t lens_cursor;
uint64_t configuration_cursor;
uint64_t calibration_cursor;
};
enum {
/* One extra row is a bounded look-ahead. A page of exactly sixteen must
* not advertise a nonexistent next page, and the seventeenth row is not
* retained until the caller explicitly advances the cursor. */
TUI_OPTICS_FETCH_CAPACITY = LARDON3D_TUI_OPTICS_PAGE_CAPACITY + 1,
};
static void
set_message(Lardon3DTuiOptics *optics, const char *message)
{
(void)snprintf(optics->view.message, sizeof(optics->view.message), "%s",
message ? message : "");
}
static const char *
result_name(Lardon3DProjectDbResult result)
{
switch (result) {
case LARDON3D_PROJECT_DB_OK:
return "OK";
case LARDON3D_PROJECT_DB_INVALID_ARGUMENT:
return "INVALID_ARGUMENT";
case LARDON3D_PROJECT_DB_NOT_FOUND:
return "NOT_FOUND";
case LARDON3D_PROJECT_DB_BUSY:
return "BUSY";
case LARDON3D_PROJECT_DB_UNSUPPORTED_SCHEMA:
return "UNSUPPORTED_SCHEMA";
case LARDON3D_PROJECT_DB_CORRUPT:
return "CORRUPT";
case LARDON3D_PROJECT_DB_CONSTRAINT:
return "CONSTRAINT";
case LARDON3D_PROJECT_DB_IO_ERROR:
return "IO_ERROR";
}
return "UNKNOWN";
}
static bool
report_result(
Lardon3DTuiOptics *optics,
const char *operation,
Lardon3DProjectDbResult result
)
{
if (result == LARDON3D_PROJECT_DB_OK) {
return true;
}
(void)snprintf(optics->view.message, sizeof(optics->view.message),
"%s: %s", operation, result_name(result));
return false;
}
static bool
load_bodies(Lardon3DTuiOptics *optics, uint64_t cursor)
{
Lardon3DOpticalCameraBodyProfile
items[TUI_OPTICS_FETCH_CAPACITY] = {0};
size_t count = 0;
uint64_t next = cursor;
Lardon3DProjectDbResult result = lardon3d_optical_camera_body_list(
optics->database, cursor, items,
TUI_OPTICS_FETCH_CAPACITY, &count, &next);
if (!report_result(optics, "Cannot list camera bodies", result)) {
return false;
}
memcpy(optics->view.bodies, items, sizeof(optics->view.bodies));
optics->body_cursor = cursor;
optics->view.body_count = count > LARDON3D_TUI_OPTICS_PAGE_CAPACITY
? LARDON3D_TUI_OPTICS_PAGE_CAPACITY : count;
optics->view.selected_body = 0;
optics->view.bodies_have_next =
count > LARDON3D_TUI_OPTICS_PAGE_CAPACITY && next > cursor;
return true;
}
static bool
load_lenses(Lardon3DTuiOptics *optics, uint64_t cursor)
{
Lardon3DOpticalLensProfile items[TUI_OPTICS_FETCH_CAPACITY] = {0};
size_t count = 0;
uint64_t next = cursor;
Lardon3DProjectDbResult result = lardon3d_optical_lens_list(
optics->database, cursor, items,
TUI_OPTICS_FETCH_CAPACITY, &count, &next);
if (!report_result(optics, "Cannot list lenses", result)) {
return false;
}
memcpy(optics->view.lenses, items, sizeof(optics->view.lenses));
optics->lens_cursor = cursor;
optics->view.lens_count = count > LARDON3D_TUI_OPTICS_PAGE_CAPACITY
? LARDON3D_TUI_OPTICS_PAGE_CAPACITY : count;
optics->view.selected_lens = 0;
optics->view.lenses_have_next =
count > LARDON3D_TUI_OPTICS_PAGE_CAPACITY && next > cursor;
return true;
}
static bool
load_configurations(Lardon3DTuiOptics *optics, uint64_t cursor)
{
Lardon3DOpticalConfiguration
items[TUI_OPTICS_FETCH_CAPACITY] = {0};
size_t count = 0;
uint64_t next = cursor;
Lardon3DProjectDbResult result = lardon3d_optical_configuration_list(
optics->database, cursor, items,
TUI_OPTICS_FETCH_CAPACITY, &count, &next);
if (!report_result(optics, "Cannot list optical configurations", result)) {
return false;
}
memcpy(optics->view.configurations, items,
sizeof(optics->view.configurations));
optics->configuration_cursor = cursor;
optics->view.configuration_count =
count > LARDON3D_TUI_OPTICS_PAGE_CAPACITY
? LARDON3D_TUI_OPTICS_PAGE_CAPACITY : count;
optics->view.selected_configuration = 0;
optics->view.configurations_have_next =
count > LARDON3D_TUI_OPTICS_PAGE_CAPACITY && next > cursor;
return true;
}
static bool
load_calibrations(Lardon3DTuiOptics *optics, uint64_t cursor)
{
if (!optics->view.capture_configuration_found) {
set_message(optics,
"Inspect a configured Capture before paging calibrations.");
return false;
}
Lardon3DOpticalCalibrationProfile
items[TUI_OPTICS_FETCH_CAPACITY] = {0};
size_t count = 0;
uint64_t next = cursor;
Lardon3DProjectDbResult result =
lardon3d_optical_calibration_profile_list_compatible(
optics->database,
optics->view.capture_configuration.optical_configuration_id,
cursor, items, TUI_OPTICS_FETCH_CAPACITY,
&count, &next);
if (!report_result(optics, "Cannot list compatible calibrations", result)) {
return false;
}
memcpy(optics->view.calibrations, items,
sizeof(optics->view.calibrations));
optics->calibration_cursor = cursor;
optics->view.calibration_count =
count > LARDON3D_TUI_OPTICS_PAGE_CAPACITY
? LARDON3D_TUI_OPTICS_PAGE_CAPACITY : count;
optics->view.selected_calibration = 0;
optics->view.calibrations_have_next =
count > LARDON3D_TUI_OPTICS_PAGE_CAPACITY && next > cursor;
return true;
}
Lardon3DTuiOptics *
lardon3d_tui_optics_create(void)
{
Lardon3DTuiOptics *optics = calloc(1, sizeof(*optics));
if (optics) {
optics->view.capture_status = LARDON3D_TUI_OPTICS_NO_PROJECT;
set_message(optics, "Load a project to inspect optical profiles.");
}
return optics;
}
void
lardon3d_tui_optics_destroy(Lardon3DTuiOptics *optics)
{
free(optics);
}
void
lardon3d_tui_optics_unbind(Lardon3DTuiOptics *optics)
{
if (!optics) {
return;
}
*optics = (Lardon3DTuiOptics) {0};
optics->view.capture_status = LARDON3D_TUI_OPTICS_NO_PROJECT;
set_message(optics, "Load a project to inspect optical profiles.");
}
bool
lardon3d_tui_optics_bind(
Lardon3DTuiOptics *optics,
Lardon3DProjectDb *database
)
{
if (!optics || !database) {
return false;
}
lardon3d_tui_optics_unbind(optics);
optics->database = database;
optics->view.project_bound = true;
optics->view.capture_status = LARDON3D_TUI_OPTICS_UNRESOLVED;
if (!load_bodies(optics, 0) || !load_lenses(optics, 0)
|| !load_configurations(optics, 0)) {
optics->database = NULL;
optics->view.project_bound = false;
optics->view.capture_status = LARDON3D_TUI_OPTICS_CORRUPT;
return false;
}
set_message(optics,
"Profiles are immutable; edit by creating and selecting a new version.");
return true;
}
bool
lardon3d_tui_optics_snapshot(
const Lardon3DTuiOptics *optics,
Lardon3DTuiOpticsSnapshot *snapshot
)
{
if (snapshot) {
*snapshot = (Lardon3DTuiOpticsSnapshot) {0};
}
if (!optics || !snapshot) {
return false;
}
*snapshot = optics->view;
return true;
}
bool
lardon3d_tui_optics_select_pane(
Lardon3DTuiOptics *optics,
Lardon3DTuiOpticsPane pane
)
{
if (!optics || pane < LARDON3D_TUI_OPTICS_PANE_BODY
|| pane > LARDON3D_TUI_OPTICS_PANE_CALIBRATION) {
return false;
}
optics->view.active_pane = pane;
return true;
}
bool
lardon3d_tui_optics_move_selection(
Lardon3DTuiOptics *optics,
int direction
)
{
if (!optics || (direction != -1 && direction != 1)) {
return false;
}
size_t *selection = NULL;
size_t count = 0;
switch (optics->view.active_pane) {
case LARDON3D_TUI_OPTICS_PANE_BODY:
selection = &optics->view.selected_body;
count = optics->view.body_count;
break;
case LARDON3D_TUI_OPTICS_PANE_LENS:
selection = &optics->view.selected_lens;
count = optics->view.lens_count;
break;
case LARDON3D_TUI_OPTICS_PANE_CONFIGURATION:
selection = &optics->view.selected_configuration;
count = optics->view.configuration_count;
break;
case LARDON3D_TUI_OPTICS_PANE_CALIBRATION:
selection = &optics->view.selected_calibration;
count = optics->view.calibration_count;
break;
}
if (!selection || count == 0) {
return false;
}
if (direction < 0) {
if (*selection == 0) {
return false;
}
--*selection;
} else {
if (*selection + 1 >= count) {
return false;
}
++*selection;
}
return true;
}
bool
lardon3d_tui_optics_page(
Lardon3DTuiOptics *optics,
Lardon3DTuiOpticsPane pane,
bool next
)
{
if (!optics || !optics->database) {
return false;
}
uint64_t cursor = 0;
if (next) {
switch (pane) {
case LARDON3D_TUI_OPTICS_PANE_BODY:
if (!optics->view.bodies_have_next || optics->view.body_count == 0)
return false;
cursor = optics->view.bodies[optics->view.body_count - 1]
.camera_body_profile_id;
break;
case LARDON3D_TUI_OPTICS_PANE_LENS:
if (!optics->view.lenses_have_next || optics->view.lens_count == 0)
return false;
cursor = optics->view.lenses[optics->view.lens_count - 1]
.lens_profile_id;
break;
case LARDON3D_TUI_OPTICS_PANE_CONFIGURATION:
if (!optics->view.configurations_have_next
|| optics->view.configuration_count == 0) return false;
cursor = optics->view.configurations[
optics->view.configuration_count - 1].optical_configuration_id;
break;
case LARDON3D_TUI_OPTICS_PANE_CALIBRATION:
if (!optics->view.calibrations_have_next
|| optics->view.calibration_count == 0) return false;
cursor = optics->view.calibrations[
optics->view.calibration_count - 1].calibration_profile_id;
break;
}
}
switch (pane) {
case LARDON3D_TUI_OPTICS_PANE_BODY:
return load_bodies(optics, cursor);
case LARDON3D_TUI_OPTICS_PANE_LENS:
return load_lenses(optics, cursor);
case LARDON3D_TUI_OPTICS_PANE_CONFIGURATION:
return load_configurations(optics, cursor);
case LARDON3D_TUI_OPTICS_PANE_CALIBRATION:
return load_calibrations(optics, cursor);
}
return false;
}
bool
lardon3d_tui_optics_create_body(
Lardon3DTuiOptics *optics,
const Lardon3DOpticalCameraBodyProfile *input
)
{
if (!optics || !optics->database || !input) {
return false;
}
Lardon3DOpticalCameraBodyProfile output;
Lardon3DProjectDbResult result = lardon3d_optical_camera_body_create(
optics->database, input, &output);
if (!report_result(optics, "Cannot create immutable camera body", result)) {
return false;
}
/* The create result is already a validated caller-owned copy. Keep that
* exact immutable identity selected even when its ID lies beyond the
* first bounded list page; `[` remains the explicit return-to-first-page. */
optics->view.bodies[0] = output;
optics->view.body_count = 1;
optics->view.selected_body = 0;
optics->view.bodies_have_next = false;
optics->body_cursor = output.camera_body_profile_id > 0
? output.camera_body_profile_id - 1 : 0;
(void)snprintf(optics->view.message, sizeof(optics->view.message),
"Immutable camera body #%llu selected/created.",
(unsigned long long)output.camera_body_profile_id);
return true;
}
bool
lardon3d_tui_optics_create_lens(
Lardon3DTuiOptics *optics,
const Lardon3DOpticalLensProfile *input
)
{
if (!optics || !optics->database || !input) {
return false;
}
Lardon3DOpticalLensProfile output;
Lardon3DProjectDbResult result = lardon3d_optical_lens_create(
optics->database, input, &output);
if (!report_result(optics, "Cannot create immutable lens", result)) {
return false;
}
optics->view.lenses[0] = output;
optics->view.lens_count = 1;
optics->view.selected_lens = 0;
optics->view.lenses_have_next = false;
optics->lens_cursor = output.lens_profile_id > 0
? output.lens_profile_id - 1 : 0;
(void)snprintf(optics->view.message, sizeof(optics->view.message),
"Immutable lens #%llu selected/created%s.",
(unsigned long long)output.lens_profile_id,
output.interface_kind == LARDON3D_OPTICAL_LENS_MANUAL
? " (manual/no EXIF is normal)" : "");
return true;
}
static bool
selected_body_lens(
const Lardon3DTuiOptics *optics,
uint64_t *body_id,
uint64_t *lens_id
)
{
if (!optics || !body_id || !lens_id
|| optics->view.selected_body >= optics->view.body_count
|| optics->view.selected_lens >= optics->view.lens_count) {
return false;
}
*body_id = optics->view.bodies[optics->view.selected_body]
.camera_body_profile_id;
*lens_id = optics->view.lenses[optics->view.selected_lens]
.lens_profile_id;
return *body_id != 0 && *lens_id != 0;
}
static bool
selected_configuration(
const Lardon3DTuiOptics *optics,
uint64_t *configuration_id
)
{
if (!optics || !configuration_id
|| optics->view.selected_configuration
>= optics->view.configuration_count) {
return false;
}
*configuration_id = optics->view.configurations[
optics->view.selected_configuration].optical_configuration_id;
return *configuration_id != 0;
}
bool
lardon3d_tui_optics_create_configuration(
Lardon3DTuiOptics *optics,
bool has_focal_length,
uint32_t focal_length_um
)
{
uint64_t body_id;
uint64_t lens_id;
if (!optics || !optics->database
|| !selected_body_lens(optics, &body_id, &lens_id)) {
if (optics) set_message(optics, "Select an existing body and lens first.");
return false;
}
Lardon3DOpticalConfiguration input = {
.camera_body_profile_id = body_id,
.lens_profile_id = lens_id,
.has_focal_length = has_focal_length,
.focal_length_um = focal_length_um,
};
Lardon3DOpticalConfiguration output;
Lardon3DProjectDbResult result = lardon3d_optical_configuration_create(
optics->database, &input, &output);
if (!report_result(optics, "Cannot create immutable configuration", result)) {
return false;
}
optics->view.configurations[0] = output;
optics->view.configuration_count = 1;
optics->view.selected_configuration = 0;
optics->view.configurations_have_next = false;
optics->configuration_cursor = output.optical_configuration_id > 0
? output.optical_configuration_id - 1 : 0;
(void)snprintf(optics->view.message, sizeof(optics->view.message),
"Immutable optical configuration #%llu selected/created.",
(unsigned long long)output.optical_configuration_id);
return true;
}
bool
lardon3d_tui_optics_assign_campaign_group(
Lardon3DTuiOptics *optics,
uint64_t campaign_task_id,
uint32_t group_id
)
{
uint64_t configuration_id;
if (!optics || !optics->database
|| !selected_configuration(optics, &configuration_id)) {
if (optics) set_message(optics, "Select an optical configuration first.");
return false;
}
Lardon3DProjectDbResult result = lardon3d_optical_campaign_group_assign(
optics->database, campaign_task_id, group_id, configuration_id);
if (!report_result(optics, "Cannot assign campaign group", result)) {
return false;
}
(void)snprintf(optics->view.message, sizeof(optics->view.message),
"Campaign Task #%llu group %u assigned to configuration #%llu.",
(unsigned long long)campaign_task_id, group_id,
(unsigned long long)configuration_id);
return true;
}
bool
lardon3d_tui_optics_assign_capture(
Lardon3DTuiOptics *optics,
uint64_t capture_id
)
{
uint64_t configuration_id;
if (!optics || !optics->database
|| !selected_configuration(optics, &configuration_id)) {
if (optics) set_message(optics, "Select an optical configuration first.");
return false;
}
Lardon3DProjectDbResult result =
lardon3d_optical_capture_assign_explicit(
optics->database, capture_id, configuration_id);
if (!report_result(optics, "Cannot assign Capture", result)) {
return false;
}
return lardon3d_tui_optics_inspect_capture(optics, capture_id);
}
static void
clear_capture_view(Lardon3DTuiOptics *optics, uint64_t capture_id)
{
optics->view.capture_inspected = true;
optics->view.capture_id = capture_id;
optics->view.capture_status = LARDON3D_TUI_OPTICS_UNRESOLVED;
optics->view.capture_assignment_found = false;
optics->view.capture_assignment = (Lardon3DOpticalCaptureAssignment) {0};
optics->view.capture_configuration_found = false;
optics->view.capture_configuration = (Lardon3DOpticalConfiguration) {0};
optics->view.capture_body_found = false;
optics->view.capture_body = (Lardon3DOpticalCameraBodyProfile) {0};
optics->view.capture_lens_found = false;
optics->view.capture_lens = (Lardon3DOpticalLensProfile) {0};
optics->view.capture_selection_found = false;
optics->view.capture_selection =
(Lardon3DOpticalCaptureCalibrationSelection) {0};
optics->view.calibration_count = 0;
optics->view.selected_calibration = 0;
optics->view.calibrations_have_next = false;
optics->calibration_cursor = 0;
}
bool
lardon3d_tui_optics_inspect_capture(
Lardon3DTuiOptics *optics,
uint64_t capture_id
)
{
if (!optics || !optics->database || capture_id == 0) {
return false;
}
clear_capture_view(optics, capture_id);
Lardon3DProjectDbCapture capture;
Lardon3DProjectDbResult result = lardon3d_project_db_load_capture(
optics->database, capture_id, &capture);
if (!report_result(optics, "Cannot inspect Capture identity", result)) {
optics->view.capture_status = result == LARDON3D_PROJECT_DB_CORRUPT
? LARDON3D_TUI_OPTICS_CORRUPT
: LARDON3D_TUI_OPTICS_UNRESOLVED;
return false;
}
result =
lardon3d_optical_capture_assignment_load(
optics->database, capture_id, &optics->view.capture_assignment);
if (result == LARDON3D_PROJECT_DB_NOT_FOUND) {
set_message(optics, lardon3d_tui_optics_status_explanation(
LARDON3D_TUI_OPTICS_UNRESOLVED, false));
return true;
}
if (!report_result(optics, "Cannot inspect Capture assignment", result)) {
optics->view.capture_status = result == LARDON3D_PROJECT_DB_CORRUPT
? LARDON3D_TUI_OPTICS_CORRUPT
: LARDON3D_TUI_OPTICS_UNRESOLVED;
return false;
}
optics->view.capture_assignment_found = true;
result = lardon3d_optical_configuration_load(
optics->database,
optics->view.capture_assignment.optical_configuration_id,
&optics->view.capture_configuration);
if (!report_result(optics, "Cannot load Capture configuration", result)) {
optics->view.capture_status = LARDON3D_TUI_OPTICS_CORRUPT;
return false;
}
optics->view.capture_configuration_found = true;
result = lardon3d_optical_camera_body_load(
optics->database,
optics->view.capture_configuration.camera_body_profile_id,
&optics->view.capture_body);
if (!report_result(optics, "Cannot load Capture camera body", result)) {
optics->view.capture_status = LARDON3D_TUI_OPTICS_CORRUPT;
return false;
}
optics->view.capture_body_found = true;
result = lardon3d_optical_lens_load(
optics->database, optics->view.capture_configuration.lens_profile_id,
&optics->view.capture_lens);
if (!report_result(optics, "Cannot load Capture lens", result)) {
optics->view.capture_status = LARDON3D_TUI_OPTICS_CORRUPT;
return false;
}
optics->view.capture_lens_found = true;
if (!load_calibrations(optics, 0)) {
optics->view.capture_status = LARDON3D_TUI_OPTICS_CORRUPT;
return false;
}
result = lardon3d_optical_capture_calibration_selection_load(
optics->database, capture_id, &optics->view.capture_selection);
bool selection_compatible = false;
if (result == LARDON3D_PROJECT_DB_OK) {
optics->view.capture_selection_found = true;
selection_compatible =
optics->view.capture_selection.optical_configuration_id
== optics->view.capture_assignment.optical_configuration_id;
} else if (result != LARDON3D_PROJECT_DB_NOT_FOUND) {
report_result(optics, "Cannot load Capture calibration selection", result);
optics->view.capture_status = result == LARDON3D_PROJECT_DB_CORRUPT
? LARDON3D_TUI_OPTICS_CORRUPT
: LARDON3D_TUI_OPTICS_INCOMPATIBLE;
return false;
}
bool manual = optics->view.capture_lens.interface_kind
== LARDON3D_OPTICAL_LENS_MANUAL;
optics->view.capture_status = lardon3d_tui_optics_classify(
true, true, true, manual, optics->view.calibration_count,
optics->view.capture_selection_found, selection_compatible);
set_message(optics, lardon3d_tui_optics_status_explanation(
optics->view.capture_status, manual));
return true;
}
bool
lardon3d_tui_optics_select_capture_calibration(
Lardon3DTuiOptics *optics
)
{
if (!optics || !optics->database || !optics->view.capture_inspected
|| optics->view.selected_calibration
>= optics->view.calibration_count) {
if (optics) set_message(optics,
"Inspect a configured Capture and select a compatible calibration.");
return false;
}
uint64_t profile_id = optics->view.calibrations[
optics->view.selected_calibration].calibration_profile_id;
Lardon3DProjectDbResult result =
lardon3d_optical_capture_calibration_select(
optics->database, optics->view.capture_id, profile_id);
if (!report_result(optics, "Cannot select Capture calibration", result)) {
return false;
}
return lardon3d_tui_optics_inspect_capture(
optics, optics->view.capture_id);
}
bool
lardon3d_tui_optics_lookup_exact_metadata(
Lardon3DTuiOptics *optics,
const char *body_make,
const char *body_model,
const char *lens_make,
const char *lens_model
)
{
if (!optics || !optics->database || !body_make || !body_model
|| !lens_make || !lens_model) {
return false;
}
optics->view.metadata_lookup_performed = true;
optics->view.metadata_body_found = false;
optics->view.metadata_lens_found = false;
optics->view.metadata_body = (Lardon3DOpticalCameraBodyProfile) {0};
optics->view.metadata_lens = (Lardon3DOpticalLensProfile) {0};
Lardon3DProjectDbResult body_result = LARDON3D_PROJECT_DB_NOT_FOUND;
if (body_make[0] && body_model[0]) {
body_result = lardon3d_optical_camera_body_find_exact_alias(
optics->database, body_make, body_model,
&optics->view.metadata_body);
if (body_result == LARDON3D_PROJECT_DB_OK) {
optics->view.metadata_body_found = true;
} else if (body_result != LARDON3D_PROJECT_DB_NOT_FOUND) {
return report_result(optics, "Camera metadata lookup failed",
body_result);
}
}
Lardon3DProjectDbResult lens_result = LARDON3D_PROJECT_DB_NOT_FOUND;
if (lens_model[0]) {
lens_result = lardon3d_optical_lens_find_exact_alias(
optics->database, lens_make, lens_model,
&optics->view.metadata_lens);
if (lens_result == LARDON3D_PROJECT_DB_OK) {
optics->view.metadata_lens_found = true;
} else if (lens_result != LARDON3D_PROJECT_DB_NOT_FOUND) {
return report_result(optics, "Lens metadata lookup failed",
lens_result);
}
}
(void)snprintf(optics->view.message, sizeof(optics->view.message),
"Exact metadata: body %s, lens %s%s.",
optics->view.metadata_body_found ? "MATCH" : "UNRESOLVED",
optics->view.metadata_lens_found ? "MATCH" : "UNRESOLVED",
lens_model[0] ? "" : " (no lens electronics is normal)");
return true;
}

693
src/tui_ssd_async.c Normal file
View file

@ -0,0 +1,693 @@
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <lardon3d/tui_ssd_async.h>
#include "tui_ssd_async_internal.h"
struct Lardon3DTuiSsdAsync {
pthread_mutex_t mutex;
pthread_cond_t condition;
pthread_t thread;
bool thread_started;
bool running;
bool stopping;
Lardon3DTuiSsdAction action;
bool result_known;
Lardon3DSsdControlResult result;
uint64_t generation;
bool refresh_attempt_known;
uint64_t refresh_attempt_ns;
bool controller_snapshot_known;
bool controller_snapshot_actionable;
Lardon3DSsdSnapshot controller_snapshot;
char reason[LARDON3D_SSD_REASON_CAPACITY];
Lardon3DTuiSsdAsyncProvider provider;
Lardon3DResourceGovernor *governor;
Lardon3DSsdController *controller_identity;
bool external_storage_registered;
};
enum {
SSD_TELEMETRY_INTERVAL_NS = 1000000000ULL,
};
static bool
production_now(void *context, uint64_t *now_ns)
{
(void)context;
struct timespec now;
if (!now_ns || clock_gettime(CLOCK_MONOTONIC, &now) != 0
|| now.tv_sec < 0 || now.tv_nsec < 0 || now.tv_nsec >= 1000000000L
|| (uint64_t)now.tv_sec > UINT64_MAX / UINT64_C(1000000000)) {
return false;
}
uint64_t seconds = (uint64_t)now.tv_sec * UINT64_C(1000000000);
if (seconds > UINT64_MAX - (uint64_t)now.tv_nsec) {
return false;
}
*now_ns = seconds + (uint64_t)now.tv_nsec;
return true;
}
static bool
production_snapshot(void *context, Lardon3DSsdSnapshot *snapshot)
{
return lardon3d_ssd_controller_get_snapshot(context, snapshot);
}
static Lardon3DSsdControlResult
production_enable(void *context)
{
return lardon3d_ssd_controller_enable(context);
}
static Lardon3DSsdControlResult
production_disable(void *context)
{
return lardon3d_ssd_controller_disable(context);
}
static bool
production_cancel(void *context)
{
return lardon3d_ssd_controller_cancel_drain(context);
}
static const Lardon3DTuiSsdAsyncProviderOps production_ops = {
.monotonic_now_ns = production_now,
.snapshot = production_snapshot,
.enable = production_enable,
.disable = production_disable,
.cancel_drain = production_cancel,
};
static bool
valid_controller_snapshot(const Lardon3DSsdSnapshot *snapshot)
{
Lardon3DResourceExternalStorage storage;
return lardon3d_resource_external_storage_from_ssd_snapshot(
snapshot, &storage);
}
static Lardon3DSsdSnapshot
invalid_snapshot_error(void)
{
Lardon3DSsdSnapshot snapshot = {.state = LARDON3D_SSD_ERROR};
snapshot.scratch_lease_capacity = LARDON3D_SSD_MAX_SCRATCH_LEASES;
(void)snprintf(snapshot.model, sizeof(snapshot.model), "UNKNOWN");
(void)snprintf(snapshot.serial, sizeof(snapshot.serial), "UNKNOWN");
(void)snprintf(snapshot.drive_identity,
sizeof(snapshot.drive_identity), "UNKNOWN");
(void)snprintf(snapshot.swap_uuid, sizeof(snapshot.swap_uuid), "UNKNOWN");
(void)snprintf(snapshot.scratch_uuid,
sizeof(snapshot.scratch_uuid), "UNKNOWN");
(void)snprintf(snapshot.swap_device,
sizeof(snapshot.swap_device), "UNKNOWN");
(void)snprintf(snapshot.scratch_device,
sizeof(snapshot.scratch_device), "UNKNOWN");
(void)snprintf(snapshot.scratch_mount_path,
sizeof(snapshot.scratch_mount_path), "UNKNOWN");
(void)snprintf(snapshot.reason, sizeof(snapshot.reason),
"Malformed SSD telemetry; control disabled");
return snapshot;
}
static bool
publish_external_error(
Lardon3DTuiSsdAsync *operation,
uint64_t source_generation,
const char *reason
)
{
if (!operation->external_storage_registered) {
return true;
}
Lardon3DResourceExternalStorage storage;
if (!lardon3d_resource_governor_get_external_storage(
operation->governor, &storage)) {
return false;
}
if (source_generation > storage.generation) {
storage.generation = source_generation;
}
storage.status = LARDON3D_RESOURCE_EXTERNAL_STORAGE_ERROR;
storage.new_scratch_allocations_allowed = false;
(void)snprintf(storage.reason, sizeof(storage.reason), "%s",
reason ? reason : "Malformed SSD telemetry; scratch allocation blocked");
return lardon3d_resource_governor_update_external_storage(
operation->governor, operation->controller_identity, &storage);
}
static bool
publish_external_snapshot(
Lardon3DTuiSsdAsync *operation,
const Lardon3DSsdSnapshot *snapshot
)
{
if (!operation->external_storage_registered) {
return true;
}
Lardon3DResourceExternalStorage storage;
if (!lardon3d_resource_external_storage_from_ssd_snapshot(
snapshot, &storage)) {
return publish_external_error(operation,
snapshot ? snapshot->generation : 0,
"Malformed SSD telemetry; scratch allocation blocked");
}
if (lardon3d_resource_governor_update_external_storage(
operation->governor, operation->controller_identity, &storage)) {
return true;
}
Lardon3DResourceExternalStorage current;
if (lardon3d_resource_governor_get_external_storage(
operation->governor, &current)
&& storage.generation < current.generation) {
/* Another serialized controller operation already published stronger
* evidence; a stale worker copy must not revoke or replace it. */
return true;
}
return publish_external_error(operation, storage.generation,
"Inconsistent SSD generation; scratch allocation blocked");
}
const char *
lardon3d_tui_ssd_action_name(Lardon3DTuiSsdAction action)
{
switch (action) {
case LARDON3D_TUI_SSD_ACTION_NONE:
return "NONE";
case LARDON3D_TUI_SSD_ACTION_OBSERVE:
return "OBSERVE";
case LARDON3D_TUI_SSD_ACTION_ENABLE:
return "ENABLE";
case LARDON3D_TUI_SSD_ACTION_DRAIN:
return "DRAIN";
case LARDON3D_TUI_SSD_ACTION_CANCEL_DRAIN:
return "CANCEL_DRAIN";
}
return "UNKNOWN";
}
static bool
valid_control_action(Lardon3DTuiSsdAction action)
{
return action >= LARDON3D_TUI_SSD_ACTION_ENABLE
&& action <= LARDON3D_TUI_SSD_ACTION_CANCEL_DRAIN;
}
static bool
snapshot_authorizes(
const Lardon3DSsdSnapshot *snapshot,
Lardon3DTuiSsdAction action
)
{
return snapshot
&& ((action == LARDON3D_TUI_SSD_ACTION_ENABLE
&& snapshot->can_enable)
|| (action == LARDON3D_TUI_SSD_ACTION_DRAIN
&& snapshot->can_disable)
|| (action == LARDON3D_TUI_SSD_ACTION_CANCEL_DRAIN
&& snapshot->can_cancel_drain));
}
static void
set_result_reason(Lardon3DTuiSsdAsync *operation)
{
const char *result = operation->result == LARDON3D_SSD_CONTROL_OK
? "OK"
: (operation->result == LARDON3D_SSD_CONTROL_PENDING
? "PENDING" : "ERROR");
(void)snprintf(operation->reason, sizeof(operation->reason), "%s: %s",
lardon3d_tui_ssd_action_name(operation->action), result);
}
static void *
run_operation(void *userdata)
{
Lardon3DTuiSsdAsync *operation = userdata;
Lardon3DTuiSsdAction action;
Lardon3DTuiSsdAsyncProvider provider;
(void)pthread_mutex_lock(&operation->mutex);
action = operation->action;
provider = operation->provider;
(void)pthread_mutex_unlock(&operation->mutex);
Lardon3DSsdControlResult result = LARDON3D_SSD_CONTROL_OK;
if (action == LARDON3D_TUI_SSD_ACTION_ENABLE) {
result = provider.ops->enable(provider.context);
} else if (action == LARDON3D_TUI_SSD_ACTION_DRAIN) {
result = provider.ops->disable(provider.context);
} else if (action == LARDON3D_TUI_SSD_ACTION_CANCEL_DRAIN) {
result = provider.ops->cancel_drain(provider.context)
? LARDON3D_SSD_CONTROL_OK
: LARDON3D_SSD_CONTROL_ERROR;
}
Lardon3DSsdSnapshot controller_snapshot = {0};
bool snapshot_result = provider.ops->snapshot(
provider.context, &controller_snapshot);
bool snapshot_valid = valid_controller_snapshot(&controller_snapshot);
bool snapshot_known = snapshot_valid && (snapshot_result
|| controller_snapshot.state == LARDON3D_SSD_ERROR);
bool snapshot_actionable = snapshot_known;
if (!snapshot_valid
|| (!snapshot_result
&& controller_snapshot.state != LARDON3D_SSD_ERROR)) {
controller_snapshot = invalid_snapshot_error();
snapshot_known = true;
snapshot_actionable = false;
}
/* Provider/controller calls have returned and hold no lock here. Publish
* before advertising worker completion so the next F10/render observes a
* registry state at least as recent as this outcome. */
if (!publish_external_snapshot(operation, &controller_snapshot)) {
controller_snapshot = invalid_snapshot_error();
snapshot_known = true;
snapshot_actionable = false;
result = LARDON3D_SSD_CONTROL_ERROR;
}
(void)pthread_mutex_lock(&operation->mutex);
operation->result = result;
operation->result_known = action != LARDON3D_TUI_SSD_ACTION_OBSERVE;
if (snapshot_known) {
operation->controller_snapshot = controller_snapshot;
operation->controller_snapshot_known = true;
operation->controller_snapshot_actionable = snapshot_actionable;
}
operation->running = false;
operation->generation = operation->generation == UINT64_MAX
? UINT64_MAX : operation->generation + 1;
if (action == LARDON3D_TUI_SSD_ACTION_OBSERVE) {
if (!snapshot_known) {
(void)snprintf(operation->reason, sizeof(operation->reason),
"SSD telemetry refresh failed");
} else {
operation->reason[0] = '\0';
}
} else {
set_result_reason(operation);
}
(void)pthread_cond_broadcast(&operation->condition);
(void)pthread_mutex_unlock(&operation->mutex);
return NULL;
}
static bool
join_if_complete(Lardon3DTuiSsdAsync *operation)
{
pthread_t thread;
bool join = false;
(void)pthread_mutex_lock(&operation->mutex);
if (operation->thread_started && !operation->running) {
thread = operation->thread;
operation->thread_started = false;
join = true;
}
(void)pthread_mutex_unlock(&operation->mutex);
return !join || pthread_join(thread, NULL) == 0;
}
static Lardon3DTuiSsdAsync *
create_with_provider_and_binding(
Lardon3DTuiSsdAsyncProvider provider,
Lardon3DResourceGovernor *governor,
Lardon3DSsdController *controller_identity
)
{
if (!provider.ops || !provider.ops->monotonic_now_ns
|| !provider.ops->snapshot || !provider.ops->enable
|| !provider.ops->disable || !provider.ops->cancel_drain
|| ((governor == NULL) != (controller_identity == NULL))) {
return NULL;
}
Lardon3DTuiSsdAsync *operation = calloc(1, sizeof(*operation));
if (!operation || pthread_mutex_init(&operation->mutex, NULL) != 0) {
free(operation);
return NULL;
}
pthread_condattr_t attributes;
bool attributes_ready = pthread_condattr_init(&attributes) == 0;
bool clock_ready = attributes_ready
&& pthread_condattr_setclock(&attributes, CLOCK_MONOTONIC) == 0;
if (!attributes_ready || !clock_ready
|| pthread_cond_init(&operation->condition, &attributes) != 0) {
if (attributes_ready) {
(void)pthread_condattr_destroy(&attributes);
}
(void)pthread_mutex_destroy(&operation->mutex);
free(operation);
return NULL;
}
(void)pthread_condattr_destroy(&attributes);
operation->provider = provider;
operation->governor = governor;
operation->controller_identity = controller_identity;
if (governor) {
Lardon3DResourceExternalStorage storage = {
.status = LARDON3D_RESOURCE_EXTERNAL_STORAGE_ERROR,
};
(void)snprintf(storage.stable_identity,
sizeof(storage.stable_identity), "UNKNOWN");
(void)snprintf(storage.reason, sizeof(storage.reason),
"SSD telemetry has not yet been validated; scratch allocation blocked");
if (!lardon3d_resource_governor_register_external_storage(
governor, controller_identity, &storage)) {
(void)pthread_cond_destroy(&operation->condition);
(void)pthread_mutex_destroy(&operation->mutex);
free(operation);
return NULL;
}
operation->external_storage_registered = true;
}
(void)snprintf(operation->reason, sizeof(operation->reason),
"No SSD control operation requested");
return operation;
}
Lardon3DTuiSsdAsync *
lardon3d_tui_ssd_async_create_with_provider(
Lardon3DTuiSsdAsyncProvider provider
)
{
return create_with_provider_and_binding(provider, NULL, NULL);
}
Lardon3DTuiSsdAsync *
lardon3d_tui_ssd_async_internal_create_with_provider_and_governor(
Lardon3DTuiSsdAsyncProvider provider,
Lardon3DResourceGovernor *governor,
Lardon3DSsdController *controller_identity
)
{
return create_with_provider_and_binding(
provider, governor, controller_identity);
}
Lardon3DTuiSsdAsync *
lardon3d_tui_ssd_async_create(Lardon3DSsdController *controller)
{
if (!controller) {
return NULL;
}
return lardon3d_tui_ssd_async_create_with_provider(
(Lardon3DTuiSsdAsyncProvider) {
.ops = &production_ops,
.context = controller,
});
}
Lardon3DTuiSsdAsync *
lardon3d_tui_ssd_async_create_with_governor(
Lardon3DSsdController *controller,
Lardon3DResourceGovernor *governor
)
{
if (!controller || !governor) {
return NULL;
}
return create_with_provider_and_binding(
(Lardon3DTuiSsdAsyncProvider) {
.ops = &production_ops,
.context = controller,
},
governor,
controller);
}
bool
lardon3d_tui_ssd_async_destroy_checked(Lardon3DTuiSsdAsync **owned_operation)
{
if (!owned_operation || !*owned_operation) {
return false;
}
Lardon3DTuiSsdAsync *operation = *owned_operation;
pthread_t thread;
bool join = false;
(void)pthread_mutex_lock(&operation->mutex);
operation->stopping = true;
if (operation->thread_started) {
thread = operation->thread;
operation->thread_started = false;
join = true;
}
(void)pthread_mutex_unlock(&operation->mutex);
if (join && pthread_join(thread, NULL) != 0) {
return false;
}
if (operation->external_storage_registered) {
/* INVARIANT: a failed unregister leaves every owner and the exact
* controller token alive. The caller retains this stopped object and
* can retry after releasing its Governor-managed leases. */
if (!lardon3d_resource_governor_unregister_external_storage(
operation->governor, operation->controller_identity)) {
return false;
}
operation->external_storage_registered = false;
}
if (operation->provider.ops->destroy) {
operation->provider.ops->destroy(operation->provider.context);
}
(void)pthread_cond_destroy(&operation->condition);
(void)pthread_mutex_destroy(&operation->mutex);
free(operation);
*owned_operation = NULL;
return true;
}
void
lardon3d_tui_ssd_async_destroy(Lardon3DTuiSsdAsync *operation)
{
if (!operation) {
return;
}
Lardon3DTuiSsdAsync *owned = operation;
(void)lardon3d_tui_ssd_async_destroy_checked(&owned);
}
bool
lardon3d_tui_ssd_async_request(
Lardon3DTuiSsdAsync *operation,
Lardon3DTuiSsdAction action
)
{
if (!operation || !valid_control_action(action)
|| !join_if_complete(operation)) {
return false;
}
uint64_t now_ns;
bool now_known = operation->provider.ops->monotonic_now_ns(
operation->provider.context, &now_ns);
(void)pthread_mutex_lock(&operation->mutex);
if (operation->stopping || operation->running
|| operation->thread_started
|| !operation->controller_snapshot_known
|| !operation->controller_snapshot_actionable
|| !snapshot_authorizes(&operation->controller_snapshot, action)) {
(void)pthread_mutex_unlock(&operation->mutex);
return false;
}
operation->action = action;
operation->running = true;
operation->result_known = false;
operation->reason[0] = '\0';
if (now_known) {
operation->refresh_attempt_known = true;
operation->refresh_attempt_ns = now_ns;
}
int created = pthread_create(
&operation->thread, NULL, run_operation, operation);
if (created != 0) {
operation->running = false;
operation->action = LARDON3D_TUI_SSD_ACTION_NONE;
(void)snprintf(operation->reason, sizeof(operation->reason),
"Cannot start bounded SSD operation thread");
(void)pthread_mutex_unlock(&operation->mutex);
return false;
}
operation->thread_started = true;
(void)pthread_mutex_unlock(&operation->mutex);
return true;
}
bool
lardon3d_tui_ssd_async_refresh(Lardon3DTuiSsdAsync *operation)
{
if (!operation || !join_if_complete(operation)) {
return false;
}
(void)pthread_mutex_lock(&operation->mutex);
if (operation->stopping) {
(void)pthread_mutex_unlock(&operation->mutex);
return false;
}
if (operation->running || operation->thread_started) {
(void)pthread_mutex_unlock(&operation->mutex);
return true;
}
(void)pthread_mutex_unlock(&operation->mutex);
uint64_t now_ns;
if (!operation->provider.ops->monotonic_now_ns(
operation->provider.context, &now_ns)) {
return false;
}
(void)pthread_mutex_lock(&operation->mutex);
if (operation->stopping) {
(void)pthread_mutex_unlock(&operation->mutex);
return false;
}
if (operation->running || operation->thread_started) {
(void)pthread_mutex_unlock(&operation->mutex);
return true;
}
if (operation->refresh_attempt_known
&& now_ns >= operation->refresh_attempt_ns
&& now_ns - operation->refresh_attempt_ns
< SSD_TELEMETRY_INTERVAL_NS) {
(void)pthread_mutex_unlock(&operation->mutex);
return true;
}
operation->refresh_attempt_known = true;
operation->refresh_attempt_ns = now_ns;
operation->action = LARDON3D_TUI_SSD_ACTION_OBSERVE;
operation->running = true;
operation->result_known = false;
operation->reason[0] = '\0';
int created = pthread_create(
&operation->thread, NULL, run_operation, operation);
if (created != 0) {
operation->running = false;
operation->action = LARDON3D_TUI_SSD_ACTION_NONE;
(void)snprintf(operation->reason, sizeof(operation->reason),
"Cannot start bounded SSD telemetry thread");
(void)pthread_mutex_unlock(&operation->mutex);
return false;
}
operation->thread_started = true;
(void)pthread_mutex_unlock(&operation->mutex);
return true;
}
bool
lardon3d_tui_ssd_async_poll(
Lardon3DTuiSsdAsync *operation,
Lardon3DTuiSsdAsyncSnapshot *snapshot
)
{
if (snapshot) {
*snapshot = (Lardon3DTuiSsdAsyncSnapshot) {0};
}
if (!operation || !snapshot || !join_if_complete(operation)) {
return false;
}
(void)pthread_mutex_lock(&operation->mutex);
*snapshot = (Lardon3DTuiSsdAsyncSnapshot) {
.running = operation->running,
.action = operation->action,
.result_known = operation->result_known,
.result = operation->result,
.generation = operation->generation,
.controller_snapshot_known = operation->controller_snapshot_known,
.controller_snapshot_actionable =
operation->controller_snapshot_actionable,
.controller_snapshot = operation->controller_snapshot,
};
(void)snprintf(snapshot->reason, sizeof(snapshot->reason), "%s",
operation->reason);
(void)pthread_mutex_unlock(&operation->mutex);
return true;
}
static bool
deadline_after(uint64_t timeout_ns, struct timespec *deadline)
{
if (!deadline || clock_gettime(CLOCK_MONOTONIC, deadline) != 0
|| deadline->tv_sec < 0 || deadline->tv_nsec < 0
|| deadline->tv_nsec >= 1000000000L) {
return false;
}
uint64_t seconds = timeout_ns / UINT64_C(1000000000);
uint64_t nanoseconds = timeout_ns % UINT64_C(1000000000);
if (seconds > (uint64_t)INT64_MAX
|| deadline->tv_sec > (time_t)(INT64_MAX - (int64_t)seconds)) {
return false;
}
deadline->tv_sec += (time_t)seconds;
deadline->tv_nsec += (long)nanoseconds;
if (deadline->tv_nsec >= 1000000000L) {
if (deadline->tv_sec == (time_t)INT64_MAX) {
return false;
}
++deadline->tv_sec;
deadline->tv_nsec -= 1000000000L;
}
return true;
}
bool
lardon3d_tui_ssd_async_wait_idle(
Lardon3DTuiSsdAsync *operation,
uint64_t timeout_ns
)
{
if (!operation) {
return false;
}
struct timespec deadline;
if (!deadline_after(timeout_ns, &deadline)) {
return false;
}
(void)pthread_mutex_lock(&operation->mutex);
while (operation->running) {
int result = pthread_cond_timedwait(
&operation->condition, &operation->mutex, &deadline);
if (result == ETIMEDOUT) {
(void)pthread_mutex_unlock(&operation->mutex);
return false;
}
if (result != 0) {
(void)pthread_mutex_unlock(&operation->mutex);
return false;
}
}
(void)pthread_mutex_unlock(&operation->mutex);
return join_if_complete(operation);
}
bool
lardon3d_tui_ssd_action_for_snapshot(
const Lardon3DSsdSnapshot *snapshot,
Lardon3DTuiSsdAction *action
)
{
if (action) {
*action = LARDON3D_TUI_SSD_ACTION_NONE;
}
if (!snapshot || !action || !valid_controller_snapshot(snapshot)) {
return false;
}
unsigned int action_count = (unsigned int)snapshot->can_enable
+ (unsigned int)snapshot->can_disable
+ (unsigned int)snapshot->can_cancel_drain;
if (action_count != 1) {
return false;
}
if (snapshot->can_enable) {
*action = LARDON3D_TUI_SSD_ACTION_ENABLE;
return true;
}
if (snapshot->can_disable) {
*action = LARDON3D_TUI_SSD_ACTION_DRAIN;
return true;
}
if (snapshot->can_cancel_drain) {
*action = LARDON3D_TUI_SSD_ACTION_CANCEL_DRAIN;
return true;
}
return false;
}

View file

@ -0,0 +1,17 @@
#ifndef LARDON3D_TUI_SSD_ASYNC_INTERNAL_H
#define LARDON3D_TUI_SSD_ASYNC_INTERNAL_H
#include <lardon3d/tui_ssd_async.h>
/* Deterministic provider seam for testing Governor publication without a real
* D-Bus controller. `controller_identity` is an opaque borrowed registry token
* and is never dereferenced by this object. Production uses the public bound
* constructor with the real controller object. */
Lardon3DTuiSsdAsync *
lardon3d_tui_ssd_async_internal_create_with_provider_and_governor(
Lardon3DTuiSsdAsyncProvider provider,
Lardon3DResourceGovernor *governor,
Lardon3DSsdController *controller_identity
);
#endif

View file

@ -24,6 +24,7 @@ enum {
SEGMENT_POSTING_MAX = LARDON3D_VISUAL_INDEX_SEGMENT_FEATURE_SET_MAX * 1024 *
LARDON3D_VISUAL_INDEX_TABLE_COUNT,
FEATURE_SET_POSTING_MAX = 1024 * LARDON3D_VISUAL_INDEX_TABLE_COUNT,
VISUAL_INDEX_CHILD_STACK_BYTES = 512 * 1024,
};
typedef struct {
@ -249,6 +250,17 @@ static Lardon3DVisualIndexResult build_postings(
PostingBuildJob jobs[LARDON3D_VISUAL_INDEX_SEGMENT_FEATURE_SET_MAX];
pthread_t children[LARDON3D_VISUAL_INDEX_SEGMENT_FEATURE_SET_MAX - 1];
bool created[LARDON3D_VISUAL_INDEX_SEGMENT_FEATURE_SET_MAX - 1] = {false};
pthread_attr_t attributes;
if (pthread_attr_init(&attributes) != 0) {
return LARDON3D_VISUAL_INDEX_IO_ERROR;
}
/* RESOURCE: each child only reads one immutable Feature File slice. Bound
* its stack explicitly so the Task's existing 2 MiB per-member reservation
* accounts for the complete 16-participant segment on every host. */
if (pthread_attr_setstacksize(&attributes, VISUAL_INDEX_CHILD_STACK_BYTES) != 0) {
(void)pthread_attr_destroy(&attributes);
return LARDON3D_VISUAL_INDEX_IO_ERROR;
}
for (size_t participant = 0; participant < participants; ++participant) {
jobs[participant] = (PostingBuildJob){.project_path = project_path,
.sets = sets,
@ -262,9 +274,10 @@ static Lardon3DVisualIndexResult build_postings(
}
for (size_t participant = 1; participant < participants; ++participant) {
created[participant - 1] =
pthread_create(&children[participant - 1], NULL, build_posting_slices,
pthread_create(&children[participant - 1], &attributes, build_posting_slices,
&jobs[participant]) == 0;
}
bool attributes_released = pthread_attr_destroy(&attributes) == 0;
(void)build_posting_slices(&jobs[0]);
bool joined = true;
for (size_t participant = 1; participant < participants; ++participant) {
@ -272,7 +285,7 @@ static Lardon3DVisualIndexResult build_postings(
joined = false;
}
}
if (!joined) {
if (!joined || !attributes_released) {
return LARDON3D_VISUAL_INDEX_IO_ERROR;
}
/* A failed pthread_create does not change results: the owner computes only

View file

@ -11,10 +11,8 @@
#include <lardon3d/visual_index.h>
#include <lardon3d/visual_index_task.h>
#include "visual_index_internal.h"
#include "task_internal.h"
enum { VISUAL_INDEX_TASK_CPU_THREADS = 12 };
#include "visual_index_internal.h"
typedef struct {
char project_path[PATH_MAX];
@ -70,7 +68,8 @@ static bool run(Lardon3DTask *task, void *userdata) {
Lardon3DTaskExecutionContract contract;
if (!lardon3d_task_execution_contract(task, &contract) || contract.batch_size == 0 ||
contract.batch_size > LARDON3D_VISUAL_INDEX_SEGMENT_FEATURE_SET_MAX ||
contract.cpu_threads == 0 || contract.cpu_threads > VISUAL_INDEX_TASK_CPU_THREADS) {
contract.cpu_threads == 0 ||
contract.cpu_threads > LARDON3D_VISUAL_INDEX_SEGMENT_FEATURE_SET_MAX) {
return lardon3d_task_fail(task, "Contrat Visual Index invalide.");
}
struct timespec begin = {0};
@ -98,10 +97,11 @@ static bool run(Lardon3DTask *task, void *userdata) {
context->governor, LARDON3D_RESOURCE_TASK_CPU, durable_indexed,
duration_ns, 0);
}
/* Visual Index already consumes the immutable admitted CPU count. A
* segment whose directory publication is not durable remains visible for
* restart semantics but is zero operational work, so it cannot train the
* next sequence's 1/2/4/8/12 CPU trial. */
/* ALGORITHMIC: one participant owns one disjoint Feature Set slice, so the
* immutable admitted CPU count cannot exceed the 16-member segment bound.
* A segment whose directory publication is not durable remains visible for
* restart semantics but is zero operational work and cannot train the next
* sequence's portable CPU trial. */
if (duration_ns > 0) {
(void)lardon3d_task_internal_record_sequence(
task, duration_ns, durable_indexed);
@ -206,10 +206,16 @@ Lardon3DTask *lardon3d_project_create_visual_index_update_task(
return NULL;
}
Lardon3DResourceEstimate estimate = {.memory_fixed_bytes = 8ULL * 1024 * 1024,
/* RESOURCE: includes each bounded 512 KiB reader stack and
* its per-Feature-Set staging/reader state. */
.memory_bytes_per_item = 2ULL * 1024 * 1024,
.minimum_batch_size = 1,
.maximum_batch_size = 16,
.desired_cpu_threads = VISUAL_INDEX_TASK_CPU_THREADS,
.maximum_batch_size =
LARDON3D_VISUAL_INDEX_SEGMENT_FEATURE_SET_MAX,
/* ALGORITHMIC: no segment contains more
* independent work than this bound. */
.desired_cpu_threads =
LARDON3D_VISUAL_INDEX_SEGMENT_FEATURE_SET_MAX,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU};
Lardon3DTask *task = lardon3d_task_create_typed(

View file

@ -1184,11 +1184,14 @@ bool end_geometric_evidence(Runtime &runtime) {
const bool contract_valid =
last_known && last.backend == LARDON3D_RESOURCE_BACKEND_FIXED &&
last.actual_backend == LARDON3D_RESOURCE_BACKEND_FIXED &&
!last.backend_fallback && last.cpu_threads == 1 &&
!last.backend_fallback && last.cpu_threads >= 1 &&
last.cpu_threads <=
LARDON3D_GEOMETRIC_VERIFIER_TASK_VALIDATED_USEFUL_CPU_THREADS &&
last.gpu_slots == 0 && last.io_slots == 1 &&
last.batch_size >= LARDON3D_GEOMETRIC_VERIFIER_TASK_MINIMUM_BATCH &&
last.batch_size <= LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_BATCH &&
last.memory_bytes == UINT64_C(4) * 1024 * 1024 &&
last.memory_bytes ==
UINT64_C(8) * 1024 * 1024 * last.batch_size &&
last.gpu_memory_bytes == 0;
const bool admission_valid =
aggregate_known && !aggregate.saturated &&

View file

@ -1,18 +1,28 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <string>
#include <unistd.h>
#include <vector>
#include <sqlite3.h>
#include <opencv2/core.hpp>
extern "C" {
#include <lardon3d/acquisition_campaign_task.h>
#include <lardon3d/project_db.h>
#include <lardon3d/task_queue.h>
#include "../src/task_internal.h"
}
extern "C" bool
lardon3d_acquisition_campaign_task_internal_configure_restored(
Lardon3DTask *, void *);
extern "C" bool lardon3d_acquisition_campaign_task_test_configure_execution(
void *, const uint64_t *, size_t, uint32_t, unsigned int);
extern "C" bool lardon3d_acquisition_campaign_task_test_observed_threads(
void *, unsigned int *, size_t, size_t *);
#define CHECK(x) \
do { \
@ -27,6 +37,42 @@ lardon3d_task_kind_registry_production(void) {
return nullptr;
}
static bool completed_task_callback(Lardon3DTask *, void *) { return true; }
static bool query_int64(sqlite3 *database, const std::string &sql,
sqlite3_int64 *value) {
sqlite3_stmt *statement = nullptr;
if (!database || !value ||
sqlite3_prepare_v2(database, sql.c_str(), -1, &statement, nullptr) !=
SQLITE_OK)
return false;
const int code = sqlite3_step(statement);
const bool valid = code == SQLITE_ROW &&
sqlite3_column_type(statement, 0) == SQLITE_INTEGER;
if (valid)
*value = sqlite3_column_int64(statement, 0);
return sqlite3_finalize(statement) == SQLITE_OK && valid;
}
static bool query_text(sqlite3 *database, const std::string &sql,
std::string *value) {
sqlite3_stmt *statement = nullptr;
if (!database || !value ||
sqlite3_prepare_v2(database, sql.c_str(), -1, &statement, nullptr) !=
SQLITE_OK)
return false;
const int code = sqlite3_step(statement);
const unsigned char *text = code == SQLITE_ROW
? sqlite3_column_text(statement, 0)
: nullptr;
const int bytes = code == SQLITE_ROW ? sqlite3_column_bytes(statement, 0) : 0;
const bool valid = code == SQLITE_ROW && text && bytes >= 0;
if (valid)
value->assign(reinterpret_cast<const char *>(text),
static_cast<size_t>(bytes));
return sqlite3_finalize(statement) == SQLITE_OK && valid;
}
int main() {
Lardon3DAcquisitionCampaignSource sources[2]{};
std::snprintf(sources[0].path, sizeof(sources[0].path), "/input/a.arw");
@ -53,6 +99,11 @@ int main() {
input.ingest_options.select_representation = 1;
input.ingest_options.imported_at = 123;
input.ingest_options.max_source_bytes = 456;
Lardon3DAcquisitionCampaignPlan input_plan{};
CHECK(lardon3d_acquisition_campaign_plan(
sources, 2, &confirmation, 1, &input_plan) ==
LARDON3D_ACQUISITION_CAMPAIGN_OK &&
input_plan.group_count == 1);
size_t size = 0;
CHECK(
@ -61,6 +112,21 @@ int main() {
std::vector<unsigned char> encoded(size);
CHECK(lardon3d_acquisition_campaign_request_encode(&input, encoded.data(),
encoded.size(), &size));
size_t required_size = 777;
std::vector<unsigned char> insufficient(encoded.size() - 1u);
CHECK(!lardon3d_acquisition_campaign_request_encode(
&input, insufficient.data(), insufficient.size(), &required_size) &&
required_size == encoded.size());
required_size = 777;
CHECK(!lardon3d_acquisition_campaign_request_encode(
&input, nullptr, 1, &required_size) &&
required_size == encoded.size());
required_size = 777;
CHECK(!lardon3d_acquisition_campaign_request_encode(
nullptr, nullptr, 0, &required_size) &&
required_size == 0);
CHECK(!lardon3d_acquisition_campaign_request_encode(
&input, nullptr, 0, nullptr));
Lardon3DAcquisitionCampaignSource decoded_sources[2]{};
Lardon3DAcquisitionCampaignConfirmation decoded_confirmations[2]{};
Lardon3DAcquisitionCampaignTaskRequest decoded{};
@ -93,8 +159,10 @@ int main() {
static_cast<unsigned char>(uint64_t(123) >> (8u * i));
input.ingest_options.imported_at = -1;
CHECK(!lardon3d_acquisition_campaign_request_encode(&input, nullptr, 0,
&size));
required_size = 777;
CHECK(!lardon3d_acquisition_campaign_request_encode(
&input, nullptr, 0, &required_size) &&
required_size == 0);
input.ingest_options.imported_at = 123;
input.ingest_options.max_source_bytes = 0;
CHECK(!lardon3d_acquisition_campaign_request_encode(&input, nullptr, 0,
@ -126,16 +194,33 @@ int main() {
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_create_scanset(database, "other", &other) ==
LARDON3D_PROJECT_DB_OK);
uint64_t allocated_task_id = 0;
CHECK(lardon3d_project_db_allocate_task_id(database, &allocated_task_id) ==
LARDON3D_PROJECT_DB_OK &&
allocated_task_id == 1);
Lardon3DTaskDurableSnapshot snapshot{};
snapshot.id = 1;
std::snprintf(snapshot.name, sizeof(snapshot.name), "campaign");
snapshot.saved_state = snapshot.recovery_state = TASK_PENDING;
Lardon3DProjectDbAcquisitionCampaignTask durable{
1, target.scanset_id, 0, 2, encoded.data(), encoded.size()};
1, target.scanset_id, 0, 1, encoded.data(), encoded.size()};
CHECK(lardon3d_project_db_record_acquisition_campaign_task(
database, &snapshot, LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND,
LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND_VERSION, nullptr, &durable,
1) == LARDON3D_PROJECT_DB_OK);
uint64_t mismatched_task_id = 0;
CHECK(lardon3d_project_db_allocate_task_id(database, &mismatched_task_id) ==
LARDON3D_PROJECT_DB_OK &&
mismatched_task_id == 2);
Lardon3DTaskDurableSnapshot mismatched_snapshot = snapshot;
mismatched_snapshot.id = mismatched_task_id;
Lardon3DProjectDbAcquisitionCampaignTask mismatched_durable = durable;
mismatched_durable.task_id = mismatched_task_id;
/* Typed business persistence must never be recorded under another generic
* dispatch identity, even when every other field is valid. */
CHECK(lardon3d_project_db_record_acquisition_campaign_task(
database, &mismatched_snapshot, "photo_quality.triage", 1, nullptr,
&mismatched_durable, 1) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
std::vector<unsigned char> changed = encoded;
changed.back() ^= 1u;
snapshot.progress = 50;
@ -170,17 +255,37 @@ int main() {
CHECK(lardon3d_project_db_retain_acquisition_campaign_capture(
database, 1, 1, second.capture_id, 1) ==
LARDON3D_PROJECT_DB_CONSTRAINT);
Lardon3DProjectDbAcquisitionCampaignTask measured_task{};
CHECK(lardon3d_project_db_load_acquisition_campaign_task(
database, 1, nullptr, 0, &measured_task) == LARDON3D_PROJECT_DB_OK);
CHECK(measured_task.request == nullptr &&
measured_task.request_size == encoded.size());
std::vector<unsigned char> loaded(encoded.size());
Lardon3DProjectDbAcquisitionCampaignTask loaded_task{};
CHECK(lardon3d_project_db_load_acquisition_campaign_task(
database, 1, loaded.data(), loaded.size(), &loaded_task) ==
LARDON3D_PROJECT_DB_OK);
CHECK(loaded_task.next_group_id == 1 && loaded_task.group_count == 2 &&
CHECK(loaded_task.next_group_id == 1 && loaded_task.group_count == 1 &&
loaded_task.request_size == encoded.size() &&
std::memcmp(loaded.data(), encoded.data(), encoded.size()) == 0);
snapshot.estimate = Lardon3DResourceEstimate{
256 * 1024, 0, 64 * 1024, 0, 1, 1, 1, 0, 1,
LARDON3D_RESOURCE_TASK_IMPORT};
std::vector<unsigned char> short_request(encoded.size() - 1u);
Lardon3DProjectDbAcquisitionCampaignTask short_task{};
CHECK(lardon3d_project_db_load_acquisition_campaign_task(
database, 1, short_request.data(), short_request.size(),
&short_task) == LARDON3D_PROJECT_DB_CONSTRAINT);
CHECK(short_task.task_id == 0 && short_task.request == nullptr &&
short_task.request_size == 0);
Lardon3DProjectDbAcquisitionCampaignCapture loaded_capture{};
CHECK(lardon3d_project_db_load_acquisition_campaign_capture(
database, 1, 1, &loaded_capture) == LARDON3D_PROJECT_DB_OK &&
loaded_capture.capture_id == first.capture_id);
CHECK(lardon3d_project_db_load_acquisition_campaign_capture(
database, 1, 2, &loaded_capture) ==
LARDON3D_PROJECT_DB_CONSTRAINT);
CHECK(lardon3d_project_db_load_acquisition_campaign_capture(
database, 1, 0, &loaded_capture) ==
LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
Lardon3DHardwareProfile profile{16, 4096, UINT64_MAX, false, 0, false,
false, 0, "test", ""};
Lardon3DResourcePolicy policy{};
@ -190,8 +295,309 @@ int main() {
Lardon3DResourceGovernor *governor =
lardon3d_resource_governor_create(&profile, &policy);
CHECK(governor != nullptr);
Lardon3DTaskReconstructionContext valid_reconstruction{
sqlite3 *corruptor = nullptr;
CHECK(sqlite3_open(database_path, &corruptor) == SQLITE_OK);
CHECK(sqlite3_exec(corruptor, "PRAGMA ignore_check_constraints=ON", nullptr,
nullptr, nullptr) == SQLITE_OK);
const auto execute_sql = [&](const std::string &sql) {
return sqlite3_exec(corruptor, sql.c_str(), nullptr, nullptr, nullptr) ==
SQLITE_OK;
};
const auto campaign_sql = [&](const std::string &assignment) {
const std::string sql = "UPDATE acquisition_campaign_tasks SET " +
assignment + " WHERE task_id=1";
return execute_sql(sql);
};
const auto generic_sql = [&](const std::string &assignment) {
return execute_sql("UPDATE tasks SET " + assignment + " WHERE task_id=1");
};
const std::string valid_campaign_empty =
"scanset_id=" + std::to_string(target.scanset_id) +
",next_group_id=0,group_count=1,request=CAST(request AS BLOB)";
const auto restore_empty_prefix = [&]() {
return execute_sql(
"DELETE FROM acquisition_campaign_captures WHERE task_id=1") &&
campaign_sql(valid_campaign_empty) &&
generic_sql("task_kind='acquisition_campaign.run',"
"task_kind_version=1");
};
const auto restore_valid_prefix = [&]() {
return restore_empty_prefix() &&
execute_sql(
"INSERT INTO acquisition_campaign_captures"
"(task_id,group_id,capture_id) VALUES(1,1," +
std::to_string(first.capture_id) + ")") &&
campaign_sql("next_group_id=1");
};
Lardon3DTaskReconstructionContext count_reconstruction{
"/tmp", database, governor, nullptr};
Lardon3DTaskKindBinding count_binding{};
Lardon3DProjectDbAcquisitionCampaignTask corrupt_task{};
/* SQLite dynamic types and signed-wide values must be rejected before the
* retention transaction inserts a mapping or advances the cursor. */
const std::vector<std::string> campaign_corruptions{
"scanset_id=-1",
"scanset_id=9223372036854775807",
"scanset_id=1.5",
"scanset_id='1x'",
"next_group_id=-1",
"next_group_id=9223372036854775807",
"next_group_id=1.5",
"next_group_id='0x'",
"group_count=-1",
"group_count=0",
"group_count=4097",
"group_count=9223372036854775807",
"group_count=1.5",
"group_count='1x'",
"request=CAST(request AS TEXT)",
};
for (const std::string &assignment : campaign_corruptions) {
CHECK(restore_empty_prefix() && campaign_sql(assignment));
std::string cursor_before, cursor_after;
sqlite3_int64 mapping_count_before = -1;
sqlite3_int64 mapping_count_after = -1;
CHECK(query_text(corruptor,
"SELECT quote(next_group_id) FROM "
"acquisition_campaign_tasks WHERE task_id=1",
&cursor_before));
CHECK(query_int64(corruptor,
"SELECT COUNT(*) FROM acquisition_campaign_captures "
"WHERE task_id=1",
&mapping_count_before));
CHECK(lardon3d_project_db_load_acquisition_campaign_task(
database, 1, loaded.data(), loaded.size(), &corrupt_task) ==
LARDON3D_PROJECT_DB_CORRUPT);
CHECK(lardon3d_project_db_retain_acquisition_campaign_capture(
database, 1, 1, first.capture_id, 1) ==
LARDON3D_PROJECT_DB_CORRUPT);
CHECK(query_text(corruptor,
"SELECT quote(next_group_id) FROM "
"acquisition_campaign_tasks WHERE task_id=1",
&cursor_after));
CHECK(query_int64(corruptor,
"SELECT COUNT(*) FROM acquisition_campaign_captures "
"WHERE task_id=1",
&mapping_count_after));
CHECK(cursor_after == cursor_before && mapping_count_before == 0 &&
mapping_count_after == 0);
}
const std::vector<std::string> generic_corruptions{
"task_kind='wrong'",
"task_kind=1",
"task_kind=1.5",
"task_kind=task_kind||char(0)||'tail'",
"task_kind_version=-1",
"task_kind_version=2",
"task_kind_version=9223372036854775807",
"task_kind_version=1.5",
"task_kind_version='1x'",
};
for (const std::string &assignment : generic_corruptions) {
CHECK(restore_empty_prefix() && generic_sql(assignment));
sqlite3_int64 mapping_count = -1;
CHECK(lardon3d_project_db_load_acquisition_campaign_task(
database, 1, loaded.data(), loaded.size(), &corrupt_task) ==
LARDON3D_PROJECT_DB_CORRUPT);
loaded_capture = {99, 99};
CHECK(lardon3d_project_db_load_acquisition_campaign_capture(
database, 1, 1, &loaded_capture) ==
LARDON3D_PROJECT_DB_CORRUPT &&
loaded_capture.group_id == 0 && loaded_capture.capture_id == 0);
CHECK(lardon3d_project_db_retain_acquisition_campaign_capture(
database, 1, 1, first.capture_id, 1) ==
LARDON3D_PROJECT_DB_CORRUPT);
CHECK(query_int64(corruptor,
"SELECT COUNT(*) FROM acquisition_campaign_captures "
"WHERE task_id=1",
&mapping_count) &&
mapping_count == 0);
}
/* A missing prefix and an ahead mapping are corruption, not resumable
* identity. Mapping output stays zero and reconstruction installs nothing. */
CHECK(restore_empty_prefix() && campaign_sql("next_group_id=1"));
CHECK(lardon3d_project_db_load_acquisition_campaign_task(
database, 1, loaded.data(), loaded.size(), &corrupt_task) ==
LARDON3D_PROJECT_DB_CORRUPT);
loaded_capture = {99, 99};
CHECK(lardon3d_project_db_load_acquisition_campaign_capture(
database, 1, 1, &loaded_capture) ==
LARDON3D_PROJECT_DB_CORRUPT &&
loaded_capture.group_id == 0 && loaded_capture.capture_id == 0);
CHECK(lardon3d_project_db_retain_acquisition_campaign_capture(
database, 1, 1, wrong.capture_id, 1) ==
LARDON3D_PROJECT_DB_CORRUPT);
sqlite3_int64 missing_mapping_count = -1;
CHECK(query_int64(corruptor,
"SELECT COUNT(*) FROM acquisition_campaign_captures "
"WHERE task_id=1",
&missing_mapping_count) &&
missing_mapping_count == 0);
count_binding = {};
CHECK(!lardon3d_acquisition_campaign_task_reconstruct(
&snapshot, &count_reconstruction, &count_binding));
CHECK(restore_valid_prefix() && campaign_sql("next_group_id=0"));
CHECK(lardon3d_project_db_load_acquisition_campaign_task(
database, 1, loaded.data(), loaded.size(), &corrupt_task) ==
LARDON3D_PROJECT_DB_CORRUPT);
loaded_capture = {99, 99};
CHECK(lardon3d_project_db_load_acquisition_campaign_capture(
database, 1, 1, &loaded_capture) ==
LARDON3D_PROJECT_DB_CORRUPT &&
loaded_capture.capture_id == 0);
CHECK(restore_valid_prefix() &&
execute_sql(
"INSERT INTO acquisition_campaign_captures"
"(task_id,group_id,capture_id) VALUES(1,2," +
std::to_string(second.capture_id) + ")"));
loaded_capture = {99, 99};
CHECK(lardon3d_project_db_load_acquisition_campaign_capture(
database, 1, 1, &loaded_capture) ==
LARDON3D_PROJECT_DB_CORRUPT &&
loaded_capture.capture_id == 0);
CHECK(restore_valid_prefix() &&
execute_sql("UPDATE acquisition_campaign_captures SET capture_id=" +
std::to_string(wrong.capture_id) +
" WHERE task_id=1 AND group_id=1"));
CHECK(lardon3d_project_db_load_acquisition_campaign_task(
database, 1, loaded.data(), loaded.size(), &corrupt_task) ==
LARDON3D_PROJECT_DB_CORRUPT);
loaded_capture = {99, 99};
CHECK(lardon3d_project_db_load_acquisition_campaign_capture(
database, 1, 1, &loaded_capture) ==
LARDON3D_PROJECT_DB_CORRUPT &&
loaded_capture.capture_id == 0);
CHECK(lardon3d_project_db_retain_acquisition_campaign_capture(
database, 1, 1, first.capture_id, 1) ==
LARDON3D_PROJECT_DB_CORRUPT);
std::string retained_capture;
CHECK(query_text(corruptor,
"SELECT quote(capture_id) FROM "
"acquisition_campaign_captures WHERE task_id=1",
&retained_capture) &&
retained_capture == std::to_string(wrong.capture_id));
const std::vector<std::string> mapping_corruptions{
"group_id=0",
"group_id=-1",
"group_id=1.5",
"group_id='bad'",
"capture_id=-1",
"capture_id=1.5",
"capture_id='bad'",
"capture_id=9223372036854775807",
};
for (const std::string &assignment : mapping_corruptions) {
CHECK(restore_valid_prefix() &&
execute_sql("UPDATE acquisition_campaign_captures SET " +
assignment + " WHERE task_id=1"));
std::string mapping_before, mapping_after;
CHECK(query_text(
corruptor,
"SELECT quote(group_id)||':'||quote(capture_id) FROM "
"acquisition_campaign_captures WHERE task_id=1",
&mapping_before));
CHECK(lardon3d_project_db_load_acquisition_campaign_task(
database, 1, loaded.data(), loaded.size(), &corrupt_task) ==
LARDON3D_PROJECT_DB_CORRUPT);
loaded_capture = {99, 99};
CHECK(lardon3d_project_db_load_acquisition_campaign_capture(
database, 1, 1, &loaded_capture) ==
LARDON3D_PROJECT_DB_CORRUPT &&
loaded_capture.capture_id == 0);
CHECK(lardon3d_project_db_retain_acquisition_campaign_capture(
database, 1, 1, first.capture_id, 1) ==
LARDON3D_PROJECT_DB_CORRUPT);
CHECK(query_text(
corruptor,
"SELECT quote(group_id)||':'||quote(capture_id) FROM "
"acquisition_campaign_captures WHERE task_id=1",
&mapping_after));
CHECK(mapping_after == mapping_before);
}
/* A row-level count can be within v20 bounds yet disagree with the plan
* deterministically decoded from the immutable request. Recovery must reject
* that relation before installing callback ownership. */
CHECK(restore_valid_prefix() && campaign_sql("group_count=2"));
CHECK(lardon3d_project_db_load_acquisition_campaign_task(
database, 1, loaded.data(), loaded.size(), &corrupt_task) ==
LARDON3D_PROJECT_DB_OK);
count_binding = {};
CHECK(!lardon3d_acquisition_campaign_task_reconstruct(
&snapshot, &count_reconstruction, &count_binding));
CHECK(restore_valid_prefix());
CHECK(sqlite3_close(corruptor) == SQLITE_OK);
char project_directory[] = "/tmp/lardon3d-campaign-project-XXXXXX";
CHECK(mkdtemp(project_directory) != nullptr);
CHECK(std::filesystem::create_directories(
std::string(project_directory) + "/.lardon3d/checkpoints"));
Lardon3DAppState app_state{};
lardon3d_app_state_init(&app_state);
app_state.project_loaded = true;
app_state.project_db = database;
app_state.resource_governor = governor;
std::snprintf(app_state.project_path, sizeof(app_state.project_path), "%s",
project_directory);
uint64_t invalid_enqueue_id = UINT64_MAX;
CHECK(!lardon3d_project_enqueue_acquisition_campaign(
&app_state, target.scanset_id, &input, &invalid_enqueue_id) &&
invalid_enqueue_id == 0);
invalid_enqueue_id = UINT64_MAX;
CHECK(!lardon3d_project_enqueue_acquisition_campaign(
nullptr, target.scanset_id, &input, &invalid_enqueue_id) &&
invalid_enqueue_id == 0);
uint64_t raw_task_id = 0;
Lardon3DTask *raw_task =
lardon3d_project_create_acquisition_campaign_task(
&app_state, target.scanset_id, &input, &raw_task_id);
CHECK(raw_task != nullptr && raw_task_id == 3);
Lardon3DResourceEstimate raw_estimate{};
CHECK(lardon3d_task_resource_estimate(raw_task, &raw_estimate));
constexpr uint64_t raw_working_bytes =
UINT64_C(2) * 1024u * 1024u * 1024u;
CHECK(raw_estimate.memory_fixed_bytes > raw_working_bytes + encoded.size() &&
raw_estimate.memory_bytes_per_item == 64u * 1024u &&
raw_estimate.desired_cpu_threads == 1 &&
raw_estimate.task_class == LARDON3D_RESOURCE_TASK_MIXED);
lardon3d_task_destroy(raw_task);
input.ingest_options.representation =
LARDON3D_ACQUISITION_SELECT_JPEG_SOURCE;
uint64_t jpeg_task_id = 0;
Lardon3DTask *jpeg_task =
lardon3d_project_create_acquisition_campaign_task(
&app_state, target.scanset_id, &input, &jpeg_task_id);
CHECK(jpeg_task != nullptr && jpeg_task_id == 4);
Lardon3DResourceEstimate jpeg_estimate{};
CHECK(lardon3d_task_resource_estimate(jpeg_task, &jpeg_estimate));
Lardon3DTaskDurableSnapshot jpeg_snapshot{};
CHECK(lardon3d_task_durable_snapshot(jpeg_task, &jpeg_snapshot));
CHECK(jpeg_estimate.memory_fixed_bytes < raw_working_bytes &&
raw_estimate.memory_fixed_bytes ==
jpeg_estimate.memory_fixed_bytes + raw_working_bytes &&
jpeg_estimate.task_class == LARDON3D_RESOURCE_TASK_IMPORT);
lardon3d_task_destroy(jpeg_task);
input.ingest_options.representation =
LARDON3D_ACQUISITION_SELECT_DEVELOP_RAW;
/* Existing v22 Tasks carry the old operational signature. It is accepted
* exactly, normalized only for admission, and never rewritten as scientific
* or generic Task identity. */
Lardon3DResourceEstimate legacy_estimate = raw_estimate;
legacy_estimate.memory_fixed_bytes -= raw_working_bytes + encoded.size();
legacy_estimate.task_class = LARDON3D_RESOURCE_TASK_IMPORT;
snapshot.estimate = legacy_estimate;
Lardon3DTaskReconstructionContext valid_reconstruction{
project_directory, database, governor, nullptr};
Lardon3DTaskKindBinding recovered{};
CHECK(lardon3d_acquisition_campaign_task_reconstruct(
&snapshot, &valid_reconstruction, &recovered));
@ -209,23 +615,248 @@ int main() {
Lardon3DResourceReservationInfo admitted{};
CHECK(reservation && lardon3d_resource_reservation_get_active(
governor, reservation, &admitted));
CHECK(admitted.memory_bytes > 320 * 1024);
CHECK(admitted.memory_bytes > raw_working_bytes);
Lardon3DTaskDurableSnapshot unchanged{};
CHECK(lardon3d_task_durable_snapshot(restored, &unchanged));
CHECK(unchanged.estimate.memory_fixed_bytes == 256 * 1024 &&
unchanged.estimate.memory_bytes_per_item == 64 * 1024);
CHECK(unchanged.estimate.memory_fixed_bytes ==
legacy_estimate.memory_fixed_bytes &&
unchanged.estimate.memory_bytes_per_item ==
legacy_estimate.memory_bytes_per_item &&
unchanged.estimate.task_class == legacy_estimate.task_class);
CHECK(lardon3d_resource_governor_release(governor, reservation));
lardon3d_task_destroy(restored);
lardon3d_resource_governor_destroy(governor);
Lardon3DProjectDbAcquisitionCampaignTask jpeg_persisted{};
CHECK(lardon3d_project_db_load_acquisition_campaign_task(
database, jpeg_task_id, nullptr, 0, &jpeg_persisted) ==
LARDON3D_PROJECT_DB_OK);
CHECK(jpeg_estimate.memory_fixed_bytes > jpeg_persisted.request_size);
jpeg_snapshot.estimate = jpeg_estimate;
jpeg_snapshot.estimate.memory_fixed_bytes -= jpeg_persisted.request_size;
recovered = {};
CHECK(lardon3d_acquisition_campaign_task_reconstruct(
&jpeg_snapshot, &valid_reconstruction, &recovered));
restored = lardon3d_task_restore_typed(
&jpeg_snapshot, LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND,
LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND_VERSION, recovered.callback,
recovered.userdata, recovered.userdata_destroy);
CHECK(restored &&
lardon3d_acquisition_campaign_task_internal_configure_restored(
restored, recovered.userdata));
unchanged = {};
CHECK(lardon3d_task_durable_snapshot(restored, &unchanged) &&
unchanged.estimate.memory_fixed_bytes ==
jpeg_snapshot.estimate.memory_fixed_bytes &&
unchanged.estimate.task_class == LARDON3D_RESOURCE_TASK_IMPORT);
lardon3d_task_destroy(restored);
snapshot.estimate = raw_estimate;
++snapshot.estimate.memory_fixed_bytes;
recovered = {};
CHECK(lardon3d_acquisition_campaign_task_reconstruct(
&snapshot, &valid_reconstruction, &recovered));
restored = lardon3d_task_restore_typed(
&snapshot, LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND,
LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND_VERSION, recovered.callback,
recovered.userdata, recovered.userdata_destroy);
CHECK(restored &&
!lardon3d_acquisition_campaign_task_internal_configure_restored(
restored, recovered.userdata));
lardon3d_task_destroy(restored);
snapshot.estimate = raw_estimate;
Lardon3DAcquisitionCampaignSource execution_sources[2]{};
std::snprintf(execution_sources[0].path,
sizeof(execution_sources[0].path), "/input/group-a.jpg");
std::snprintf(execution_sources[1].path,
sizeof(execution_sources[1].path), "/input/group-b.jpg");
for (auto &source : execution_sources) {
source.source_kind = source.metadata.source_kind =
LARDON3D_ACQUISITION_SOURCE_JPEG;
source.metadata_result = LARDON3D_ACQUISITION_OK;
source.metadata.policy_version =
LARDON3D_ACQUISITION_PAIRING_POLICY_VERSION;
}
Lardon3DAcquisitionCampaignTaskRequest execution_request{};
execution_request.sources = execution_sources;
execution_request.source_count = 2;
execution_request.ingest_options.representation =
LARDON3D_ACQUISITION_SELECT_JPEG_SOURCE;
execution_request.ingest_options.select_representation = 1;
execution_request.ingest_options.imported_at = 321;
execution_request.ingest_options.max_source_bytes = 1024;
Lardon3DAcquisitionCampaignPlan execution_plan{};
CHECK(lardon3d_acquisition_campaign_plan(
execution_sources, 2, nullptr, 0, &execution_plan) ==
LARDON3D_ACQUISITION_CAMPAIGN_OK &&
execution_plan.group_count == 2);
const uint64_t fixture_captures[2]{first.capture_id, second.capture_id};
const int previous_opencv_threads = cv::getNumThreads();
cv::setNumThreads(3);
CHECK(cv::getNumThreads() == 3);
uint64_t execution_task_id = 0;
Lardon3DTask *execution_task =
lardon3d_project_create_acquisition_campaign_task(
&app_state, target.scanset_id, &execution_request,
&execution_task_id);
CHECK(execution_task && execution_task_id != 0);
Lardon3DTaskDurableSnapshot execution_snapshot{};
CHECK(lardon3d_task_durable_snapshot(execution_task, &execution_snapshot));
lardon3d_task_destroy(execution_task);
recovered = {};
CHECK(lardon3d_acquisition_campaign_task_reconstruct(
&execution_snapshot, &valid_reconstruction, &recovered));
void *execution_userdata = recovered.userdata;
restored = lardon3d_task_restore_typed(
&execution_snapshot, LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND,
LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND_VERSION, recovered.callback,
recovered.userdata, recovered.userdata_destroy);
CHECK(restored &&
lardon3d_acquisition_campaign_task_internal_configure_restored(
restored, execution_userdata) &&
lardon3d_acquisition_campaign_task_test_configure_execution(
execution_userdata, fixture_captures, 2, 0, 3));
Lardon3DResourceDecision execution_decision{};
Lardon3DResourceReservation *execution_reservation = nullptr;
CHECK(lardon3d_task_internal_reserve_available(
restored, governor, &execution_decision, &execution_reservation));
Lardon3DResourceReservationInfo execution_admission{};
CHECK(execution_reservation &&
lardon3d_resource_reservation_get_active(
governor, execution_reservation, &execution_admission) &&
execution_admission.cpu_threads == 1);
CHECK(lardon3d_task_start(restored, governor, execution_reservation));
Lardon3DTaskObservation execution_completed{};
CHECK(lardon3d_task_observation(restored, &execution_completed) &&
execution_completed.state == TASK_COMPLETED &&
execution_completed.durable_progress_known &&
execution_completed.durable_completed == 2 &&
execution_completed.durable_total == 2 &&
lardon3d_task_sequence_count(restored) == 1 &&
cv::getNumThreads() == 3);
unsigned int observed_threads[2]{};
size_t observed_count = 0;
CHECK(lardon3d_acquisition_campaign_task_test_observed_threads(
execution_userdata, observed_threads, 2, &observed_count) &&
observed_count == 2 && observed_threads[0] == 1 &&
observed_threads[1] == 1);
Lardon3DProjectDbAcquisitionCampaignTask executed_persisted{};
CHECK(lardon3d_project_db_load_acquisition_campaign_task(
database, execution_task_id, nullptr, 0, &executed_persisted) ==
LARDON3D_PROJECT_DB_OK &&
executed_persisted.next_group_id == 2 &&
executed_persisted.group_count == 2);
Lardon3DProjectDbAcquisitionCampaignCapture execution_capture{};
CHECK(lardon3d_project_db_load_acquisition_campaign_capture(
database, execution_task_id, 1, &execution_capture) ==
LARDON3D_PROJECT_DB_OK &&
execution_capture.capture_id == first.capture_id);
CHECK(lardon3d_project_db_load_acquisition_campaign_capture(
database, execution_task_id, 2, &execution_capture) ==
LARDON3D_PROJECT_DB_OK &&
execution_capture.capture_id == second.capture_id);
lardon3d_task_destroy(restored);
uint64_t failure_task_id = 0;
execution_task = lardon3d_project_create_acquisition_campaign_task(
&app_state, target.scanset_id, &execution_request, &failure_task_id);
CHECK(execution_task && failure_task_id != 0 &&
failure_task_id != execution_task_id);
Lardon3DTaskDurableSnapshot failure_snapshot{};
CHECK(lardon3d_task_durable_snapshot(execution_task, &failure_snapshot));
lardon3d_task_destroy(execution_task);
recovered = {};
CHECK(lardon3d_acquisition_campaign_task_reconstruct(
&failure_snapshot, &valid_reconstruction, &recovered));
void *failure_userdata = recovered.userdata;
restored = lardon3d_task_restore_typed(
&failure_snapshot, LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND,
LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND_VERSION, recovered.callback,
recovered.userdata, recovered.userdata_destroy);
CHECK(restored &&
lardon3d_acquisition_campaign_task_internal_configure_restored(
restored, failure_userdata) &&
lardon3d_acquisition_campaign_task_test_configure_execution(
failure_userdata, fixture_captures, 2, 2, 3));
execution_reservation = nullptr;
CHECK(lardon3d_task_internal_reserve_available(
restored, governor, &execution_decision, &execution_reservation));
CHECK(lardon3d_task_start(restored, governor, execution_reservation));
CHECK(lardon3d_task_observation(restored, &execution_completed) &&
execution_completed.state == TASK_FAILED &&
execution_completed.durable_progress_known &&
execution_completed.durable_completed == 1 &&
execution_completed.durable_total == 2 &&
lardon3d_task_sequence_count(restored) == 1 &&
cv::getNumThreads() == 3);
observed_count = 0;
observed_threads[0] = observed_threads[1] = 0;
CHECK(lardon3d_acquisition_campaign_task_test_observed_threads(
failure_userdata, observed_threads, 2, &observed_count) &&
observed_count == 2 && observed_threads[0] == 1 &&
observed_threads[1] == 1);
CHECK(lardon3d_project_db_load_acquisition_campaign_task(
database, failure_task_id, nullptr, 0, &executed_persisted) ==
LARDON3D_PROJECT_DB_OK &&
executed_persisted.next_group_id == 1);
CHECK(lardon3d_project_db_load_acquisition_campaign_capture(
database, failure_task_id, 1, &execution_capture) ==
LARDON3D_PROJECT_DB_OK &&
execution_capture.capture_id == first.capture_id);
execution_capture = {};
CHECK(lardon3d_project_db_load_acquisition_campaign_capture(
database, failure_task_id, 2, &execution_capture) ==
LARDON3D_PROJECT_DB_NOT_FOUND &&
execution_capture.capture_id == 0);
lardon3d_task_destroy(restored);
cv::setNumThreads(previous_opencv_threads > 0 ? previous_opencv_threads : 1);
sqlite3 *id_reader = nullptr;
CHECK(sqlite3_open(database_path, &id_reader) == SQLITE_OK);
sqlite3_int64 collision_id = 0;
CHECK(query_int64(id_reader,
"SELECT value FROM metadata WHERE key='next_task_id'",
&collision_id) &&
collision_id > 0 && sqlite3_close(id_reader) == SQLITE_OK);
Lardon3DResourceEstimate collision_estimate{
1024, 0, 0, 0, 1, 1, 1, 0, 1, LARDON3D_RESOURCE_TASK_IMPORT};
Lardon3DTask *collision_task = lardon3d_task_create(
"queue collision", &collision_estimate, completed_task_callback, nullptr);
CHECK(collision_task &&
lardon3d_task_assign_id(collision_task,
static_cast<uint64_t>(collision_id)));
app_state.task_queue = lardon3d_task_queue_create(governor, 4);
CHECK(app_state.task_queue &&
lardon3d_task_queue_add(app_state.task_queue, collision_task, nullptr));
uint64_t rejected_enqueue_id = UINT64_MAX;
CHECK(!lardon3d_project_enqueue_acquisition_campaign(
&app_state, UINT64_MAX, &input, &rejected_enqueue_id) &&
rejected_enqueue_id == 0);
uint64_t durable_transfer_failure_id = UINT64_MAX;
CHECK(!lardon3d_project_enqueue_acquisition_campaign(
&app_state, target.scanset_id, &input,
&durable_transfer_failure_id) &&
durable_transfer_failure_id == static_cast<uint64_t>(collision_id));
Lardon3DProjectDbAcquisitionCampaignTask transfer_failure{};
CHECK(lardon3d_project_db_load_acquisition_campaign_task(
database, durable_transfer_failure_id, nullptr, 0,
&transfer_failure) == LARDON3D_PROJECT_DB_OK &&
transfer_failure.next_group_id == 0);
lardon3d_task_queue_destroy(app_state.task_queue);
app_state.task_queue = nullptr;
std::vector<char> overlong_project_path(LARDON3D_APP_STATE_PATH_CAPACITY + 1,
'x');
overlong_project_path.back() = '\0';
Lardon3DTaskReconstructionContext reconstruction{
overlong_project_path.data(), database, nullptr, nullptr};
overlong_project_path.data(), database, governor, nullptr};
Lardon3DTaskKindBinding binding{};
CHECK(!lardon3d_acquisition_campaign_task_reconstruct(
&snapshot, &reconstruction, &binding));
lardon3d_resource_governor_destroy(governor);
lardon3d_project_db_close(database);
CHECK(unlink(database_path) == 0);
CHECK(std::filesystem::remove_all(project_directory) > 0);
return 0;
}

View file

@ -289,6 +289,23 @@ static bool run_test(void) {
lardon3d_candidate_pair_generation_fingerprint(index_id, feature_sets[0].feature_set_id,
&options, fp2);
CHECK(memcmp(fp1, fp2, 32) == 0);
static const unsigned char canonical_fingerprint[32] = {
0x60, 0x61, 0x4b, 0x22, 0xa6, 0x2f, 0xe5, 0x10,
0x8a, 0x63, 0x07, 0xb1, 0xa1, 0x69, 0x11, 0x29,
0xd2, 0xee, 0xc4, 0xa4, 0x0e, 0x4f, 0x6c, 0x50,
0x83, 0x78, 0xc0, 0xfa, 0x23, 0x6d, 0x7b, 0x09,
};
Lardon3DVisualIndexQueryOptions canonical_options = {
.top_k = 4,
.minimum_evidence_count = 3,
.scanset_filter = LARDON3D_VISUAL_INDEX_OTHER_SCANSETS,
.exclude_same_asset = true,
};
lardon3d_candidate_pair_generation_fingerprint(
1, 2, &canonical_options, fp2);
/* This vector equals the acquired x86 v1 output and now also proves that
* native integer/enum/bool representation cannot enter scientific identity. */
CHECK(memcmp(fp2, canonical_fingerprint, sizeof(fp2)) == 0);
options.top_k = 8;
lardon3d_candidate_pair_generation_fingerprint(index_id, feature_sets[0].feature_set_id,
&options, fp2);

View file

@ -159,6 +159,79 @@ static bool same_pair_order(const Lardon3DProjectDbCandidatePair *a, size_t a_co
return true;
}
static bool same_computation(const Lardon3DCandidatePairComputation *left,
const Lardon3DCandidatePairComputation *right) {
if (left->source_feature_set_id != right->source_feature_set_id ||
left->queried_count != right->queried_count ||
left->proposal_count != right->proposal_count) {
return false;
}
for (size_t i = 0; i < left->proposal_count; ++i) {
if (left->proposals[i].image_id_a != right->proposals[i].image_id_a ||
left->proposals[i].image_id_b != right->proposals[i].image_id_b) {
return false;
}
}
return true;
}
static bool run_portable_compute_contracts(
Lardon3DAppState *state, uint64_t visual_index_id,
const Lardon3DVisualIndexQueryOptions *options,
const uint64_t fixture_source_ids[3]) {
enum { SOURCE_COUNT = 64 };
uint64_t sources[SOURCE_COUNT];
Lardon3DCandidatePairComputation baseline[SOURCE_COUNT];
Lardon3DCandidatePairComputation parallel[SOURCE_COUNT];
Lardon3DVisualIndexResult baseline_results[SOURCE_COUNT];
Lardon3DVisualIndexResult parallel_results[SOURCE_COUNT];
for (size_t i = 0; i < SOURCE_COUNT; ++i) {
sources[i] = fixture_source_ids[i % 3];
}
lardon3d_candidate_pair_task_test_fail_thread_create_after(SIZE_MAX);
lardon3d_candidate_pair_task_test_reset_parallel_counters();
CHECK(lardon3d_candidate_pair_task_test_compute_window(
state->project_path, state->project_db, visual_index_id, options,
sources, SOURCE_COUNT, 1, baseline, baseline_results) &&
lardon3d_candidate_pair_task_test_started_participants() == 1 &&
lardon3d_candidate_pair_task_test_computed_work_items() == SOURCE_COUNT &&
lardon3d_candidate_pair_task_test_active_private_databases() == 0);
const unsigned int portable_counts[] = {16, 32, 64};
for (size_t run = 0;
run < sizeof(portable_counts) / sizeof(portable_counts[0]); ++run) {
lardon3d_candidate_pair_task_test_reset_parallel_counters();
CHECK(lardon3d_candidate_pair_task_test_compute_window(
state->project_path, state->project_db, visual_index_id,
options, sources, SOURCE_COUNT, portable_counts[run],
parallel, parallel_results) &&
lardon3d_candidate_pair_task_test_started_participants() ==
portable_counts[run] &&
lardon3d_candidate_pair_task_test_computed_work_items() ==
SOURCE_COUNT &&
lardon3d_candidate_pair_task_test_active_private_databases() == 0);
for (size_t i = 0; i < SOURCE_COUNT; ++i) {
CHECK(baseline_results[i] == LARDON3D_VISUAL_INDEX_OK &&
parallel_results[i] == baseline_results[i] &&
same_computation(&parallel[i], &baseline[i]));
}
}
/* Fail after two children have started. The Queue-owner participant and
* those children may compute, but every child is joined and every one of
* the 16 preopened private DB handles is closed before failure returns. */
lardon3d_candidate_pair_task_test_reset_parallel_counters();
lardon3d_candidate_pair_task_test_fail_thread_create_after(2);
CHECK(!lardon3d_candidate_pair_task_test_compute_window(
state->project_path, state->project_db, visual_index_id, options,
sources, SOURCE_COUNT, 16, parallel, parallel_results) &&
lardon3d_candidate_pair_task_test_started_participants() == 3 &&
lardon3d_candidate_pair_task_test_active_private_databases() == 0);
lardon3d_candidate_pair_task_test_fail_thread_create_after(SIZE_MAX);
return true;
}
static bool wait_saved_cursor(Lardon3DProjectDb *database, uint64_t task_id,
uint64_t expected) {
struct timespec deadline;
@ -315,13 +388,17 @@ static bool run_test(void) {
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 4);
CHECK(state.task_queue != NULL);
/* --- Test 1: déterminisme exact à 1/2/4 threads admis --- */
/* --- Test 1: déterminisme exact jusqu'au plafond portable de 64 --- */
Lardon3DVisualIndexQueryOptions qopts = {
.top_k = 16,
.minimum_evidence_count = 10,
.scanset_filter = LARDON3D_VISUAL_INDEX_ANY_SCANSET,
.exclude_same_asset = false,
};
const uint64_t portable_fixture_sources[3] = {
fs_a.feature_set_id, fs_b.feature_set_id, fs_c.feature_set_id};
CHECK(run_portable_compute_contracts(&state, visual_index_id, &qopts,
portable_fixture_sources));
/* A healthy Governor admits two bounded sources per sequence. The six
* durable memberships create three productive windows: initial admission
@ -355,11 +432,11 @@ static bool run_test(void) {
CHECK(unsetenv("LARDON3D_TEST_CANDIDATE_PAIR_MAXIMUM_BATCH") == 0 &&
unsetenv("LARDON3D_TEST_CANDIDATE_PAIR_THREADS") == 0);
/* CPU admission does not authorize extra work. With twelve available
* CPUs but an admitted one-item batch, each of the six sequence breaks
/* CPU admission does not authorize extra work. With 64 CPUs requested but
* an admitted one-item batch, each of the six sequence breaks
* has exactly one independent source and therefore one useful worker. */
CHECK(reset_candidate_pairs(state.project_db) &&
setenv("LARDON3D_TEST_CANDIDATE_PAIR_THREADS", "12", 1) == 0 &&
setenv("LARDON3D_TEST_CANDIDATE_PAIR_THREADS", "64", 1) == 0 &&
setenv("LARDON3D_TEST_CANDIDATE_PAIR_MAXIMUM_BATCH", "1", 1) == 0);
lardon3d_candidate_pair_task_test_reset_parallel_counters();
uint64_t pressure_task_id = 0;
@ -375,11 +452,12 @@ static bool run_test(void) {
CHECK(unsetenv("LARDON3D_TEST_CANDIDATE_PAIR_MAXIMUM_BATCH") == 0 &&
unsetenv("LARDON3D_TEST_CANDIDATE_PAIR_THREADS") == 0);
const char *thread_counts[] = {"1", "2", "4"};
const char *thread_counts[] = {"1", "2", "4", "16", "32", "64"};
uint64_t cp_task_id = 0;
Lardon3DProjectDbCandidatePair baseline[32];
size_t baseline_count = 0;
for (size_t run = 0; run < 3; ++run) {
for (size_t run = 0;
run < sizeof(thread_counts) / sizeof(thread_counts[0]); ++run) {
CHECK(reset_candidate_pairs(state.project_db) &&
setenv("LARDON3D_TEST_CANDIDATE_PAIR_THREADS", thread_counts[run], 1) == 0);
uint64_t current_task_id = 0;
@ -553,6 +631,30 @@ static bool run_test(void) {
};
Lardon3DTask *recovered = NULL;
/* The immediately preceding parallel form used fixed256/per-item64KiB
* with CPU12. It is independent from the oldest fixed128/CPU1 signature
* below and both normalize to the portable, fully accounted estimate. */
Lardon3DTaskDurableSnapshot historical_parallel =
candidate_recovery->snapshot;
historical_parallel.estimate.memory_fixed_bytes = 256 * 1024;
historical_parallel.estimate.memory_bytes_per_item = 64 * 1024;
historical_parallel.estimate.desired_cpu_threads = 12;
Lardon3DTask *historical_parallel_task = NULL;
CHECK(lardon3d_task_kind_registry_restore(
lardon3d_task_kind_registry_production(),
candidate_recovery->task_kind,
candidate_recovery->task_kind_version, &historical_parallel,
&reconstruction, &historical_parallel_task) ==
LARDON3D_TASK_KIND_OK &&
historical_parallel_task);
Lardon3DResourceEstimate historical_parallel_effective;
CHECK(lardon3d_task_resource_estimate(
historical_parallel_task, &historical_parallel_effective) &&
historical_parallel_effective.memory_bytes_per_item ==
8 * 1024 * 1024 &&
historical_parallel_effective.desired_cpu_threads == 64);
lardon3d_task_destroy(historical_parallel_task);
/* Every field belongs to the historical signature. Neighboring estimates
* are malformed rather than evidence for legacy operational policy. */
Lardon3DTaskDurableSnapshot near_snapshots[4];
@ -582,11 +684,11 @@ static bool run_test(void) {
const Lardon3DResourceEstimate current_estimate = {
.memory_fixed_bytes = 256 * 1024,
.gpu_memory_fixed_bytes = 0,
.memory_bytes_per_item = 64 * 1024,
.memory_bytes_per_item = 8 * 1024 * 1024,
.gpu_memory_bytes_per_item = 0,
.minimum_batch_size = 1,
.maximum_batch_size = 64,
.desired_cpu_threads = 12,
.desired_cpu_threads = 64,
.desired_gpu_slots = 0,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
@ -604,7 +706,7 @@ static bool run_test(void) {
lardon3d_resource_governor_reserve(
state.resource_governor, &resources, &recovered_estimate,
&decision, &reservation) &&
decision.cpu_threads == 12 && decision.batch_size >= 1 &&
decision.cpu_threads >= 1 && decision.cpu_threads <= 64 &&
decision.batch_size <= 64 && reservation);
lardon3d_resource_governor_release(state.resource_governor, reservation);

View file

@ -5,6 +5,7 @@
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <math.h>
#include <pthread.h>
#include <sched.h>
@ -153,7 +154,7 @@ typedef struct {
static bool adaptive_extract_callback(Lardon3DTask *task, void *userdata) {
AdaptiveExtractWork *work = userdata;
Lardon3DOpenCvTaskThreadControl control;
if (!lardon3d_opencv_task_threads_begin(task, 12, &control) ||
if (!lardon3d_opencv_task_threads_begin(task, INT_MAX, &control) ||
lardon3d_feature_opencv_thread_count() != work->expected_threads) {
return false;
}
@ -175,6 +176,95 @@ static bool adaptive_extract_callback(Lardon3DTask *task, void *userdata) {
lardon3d_task_set_progress(task, 100, "Extraction adaptative testée.");
}
static bool retry_failed_opencv_restoration(void) {
unsigned int original = lardon3d_feature_opencv_thread_count();
Lardon3DOpenCvTaskThreadControl control = {
.previous = original,
.restore_required = true,
};
unsigned int temporary = original == 1 ? 2 : 1;
if (!lardon3d_feature_opencv_configure_threads(temporary)) return false;
char failed_threads[32];
int written = snprintf(failed_threads, sizeof(failed_threads), "%u", original);
bool ok = written > 0 && (size_t)written < sizeof(failed_threads) &&
setenv("LARDON3D_TEST_OPENCV_CONFIGURE_FAILURE_THREADS", failed_threads, 1) == 0;
/* The injected failure occurs after setNumThreads(), so verification must
* retain the restoration obligation even though the observed value changed. */
ok = ok && !lardon3d_opencv_task_threads_end(&control) && control.restore_required;
ok = unsetenv("LARDON3D_TEST_OPENCV_CONFIGURE_FAILURE_THREADS") == 0 && ok;
ok = lardon3d_opencv_task_threads_end(&control) && !control.restore_required &&
lardon3d_feature_opencv_thread_count() == original && ok;
return ok;
}
static Lardon3DResourceSnapshot deterministic_resource_snapshot(double cpu_load_1m) {
return (Lardon3DResourceSnapshot) {
.memory_available_bytes = UINT64_C(12) * 1024 * 1024 * 1024,
.memory_free_bytes = UINT64_C(8) * 1024 * 1024 * 1024,
.cpu_load_1m = cpu_load_1m,
.cpu_pressure_known = true,
.cpu_pressure_avg10 = 0.0,
.memory_pressure_known = true,
.memory_pressure_avg10 = 0.0,
.io_pressure_known = true,
.io_pressure_avg10 = 0.0,
.swap_activity_known = true,
.swap_pages_in = 0,
.swap_pages_out = 0,
};
}
static bool deterministic_capture_preserves_load_gate(void) {
Lardon3DHardwareProfile profile = {
.logical_cpu_count = 5,
.page_size_bytes = 4096,
.memory_total_bytes = UINT64_C(16) * 1024 * 1024 * 1024,
.cpu_architecture = "test",
};
Lardon3DResourcePolicy policy = {
.system_cpu_reserve = 4,
.maximum_cpu_load_ratio = 1.0,
.maximum_io_pressure_avg10 = 100.0,
.io_slot_capacity = 1,
};
Lardon3DResourceGovernor *governor =
lardon3d_resource_governor_create(&profile, &policy);
if (!governor) return false;
Lardon3DResourceEstimate estimate = {
.minimum_batch_size = 1,
.maximum_batch_size = 1,
.desired_cpu_threads = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
};
Lardon3DResourceSnapshot snapshot = deterministic_resource_snapshot(5.0);
Lardon3DResourceDecision decision;
Lardon3DResourceReservation *reservation = NULL;
/* Equality is intentional: production rejects load >= logical CPUs times
* the configured ratio. The seam controls telemetry, not policy. */
bool ok = lardon3d_resource_governor_internal_set_capture_snapshot(
governor, &snapshot)
&& lardon3d_resource_governor_reserve_available(
governor, &estimate, &decision, &reservation)
&& decision.kind == LARDON3D_RESOURCE_WAIT
&& reservation == NULL
&& strcmp(decision.reason, "Charge CPU trop élevée.") == 0;
snapshot = deterministic_resource_snapshot(0.0);
ok = lardon3d_resource_governor_internal_set_capture_snapshot(
governor, &snapshot)
&& lardon3d_resource_governor_reserve_available(
governor, &estimate, &decision, &reservation)
&& decision.kind == LARDON3D_RESOURCE_START
&& reservation != NULL
&& ok;
if (reservation) {
ok = lardon3d_resource_governor_release(governor, reservation) && ok;
}
ok = lardon3d_resource_governor_internal_set_capture_snapshot(
governor, NULL) && ok;
lardon3d_resource_governor_destroy(governor);
return ok;
}
static bool run_adaptive_output_equivalence(const char *path) {
cpu_set_t allowed_mask;
CPU_ZERO(&allowed_mask);
@ -190,7 +280,9 @@ static bool run_adaptive_output_equivalence(const char *path) {
}
}
if (allowed_count == 0) return true;
const unsigned int requested[] = {1, 2, 4, 8, 12};
/* Counts above the historical host ceiling execute only when the process
* affinity genuinely exposes them; unavailable CPUs are never fabricated. */
const unsigned int requested[] = {1, 2, 4, 8, 12, 16, 32, 64};
Lardon3DExtractedFeatures baseline[TEST_EXTRACT_COUNT] = {0};
bool have_baseline[TEST_EXTRACT_COUNT] = {false};
unsigned int previous_threads = lardon3d_feature_opencv_thread_count();
@ -222,10 +314,13 @@ static bool run_adaptive_output_equivalence(const char *path) {
for (unsigned int index = 0; index < threads; ++index) {
topology.allowed_cpu_ids[index] = allowed_ids[index];
}
Lardon3DResourceSnapshot resources = deterministic_resource_snapshot(0.0);
Lardon3DTaskQueue *queue = NULL;
if (!governor ||
!lardon3d_resource_governor_internal_configure_cpu_topology(
governor, &topology) ||
!lardon3d_resource_governor_internal_set_capture_snapshot(
governor, &resources) ||
!(queue = lardon3d_task_queue_create(governor, TEST_EXTRACT_COUNT))) {
lardon3d_task_queue_destroy(queue);
lardon3d_resource_governor_destroy(governor);
@ -237,7 +332,7 @@ static bool run_adaptive_output_equivalence(const char *path) {
Lardon3DResourceEstimate estimate = {
.minimum_batch_size = 1,
.maximum_batch_size = 1,
.desired_cpu_threads = 12,
.desired_cpu_threads = INT_MAX,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
};
static const char *const task_kinds[TEST_EXTRACT_COUNT] = {
@ -286,6 +381,10 @@ static bool run_adaptive_output_equivalence(const char *path) {
lardon3d_extracted_features_destroy(&work[kind].output);
}
lardon3d_task_queue_destroy(queue);
if (!lardon3d_resource_governor_internal_set_capture_snapshot(
governor, NULL)) {
ok = false;
}
lardon3d_resource_governor_destroy(governor);
}
for (size_t kind = 0; kind < TEST_EXTRACT_COUNT; ++kind) {
@ -301,15 +400,29 @@ static bool runtime(Lardon3DAppState *s) {
Lardon3DResourcePolicy p = {
.maximum_cpu_load_ratio = 1, .maximum_io_pressure_avg10 = 100, .io_slot_capacity = 2};
s->resource_governor = lardon3d_resource_governor_create(&s->hardware_profile, &p);
s->task_queue = s->resource_governor ? lardon3d_task_queue_create(s->resource_governor, 4) : NULL;
return s->task_queue != NULL;
if (!s->resource_governor) return false;
Lardon3DResourceSnapshot resources = deterministic_resource_snapshot(0.0);
if (!lardon3d_resource_governor_internal_set_capture_snapshot(
s->resource_governor, &resources)) {
lardon3d_resource_governor_destroy(s->resource_governor);
s->resource_governor = NULL;
s->task_queue = NULL;
return false;
}
s->task_queue = lardon3d_task_queue_create(s->resource_governor, 4);
if (!s->task_queue) {
lardon3d_resource_governor_destroy(s->resource_governor);
s->resource_governor = NULL;
return false;
}
return true;
}
static bool wait_state(Lardon3DTaskQueue *q, uint64_t id, Lardon3DTaskState wanted,
Lardon3DTaskSnapshot *out) {
struct timespec deadline;
if (clock_gettime(CLOCK_MONOTONIC, &deadline) != 0) return false;
/* The equality fixture deliberately executes ORB/SIFT/RootSIFT at five CPU
* contracts. Instrumented OpenCV is slower than the normal build; keep the
/* The equality fixture executes every genuinely available requested CPU
* contract. Instrumented OpenCV is slower than the normal build; keep the
* wait bounded without mistaking sanitizer overhead for scientific drift. */
deadline.tv_sec += 30;
for (;;) {
@ -354,10 +467,12 @@ static void *consolidate_thread(void *userdata) {
static bool run_test(void) {
char root[] = "/tmp/lardon3d-feature-task-XXXXXX";
CHECK(deterministic_capture_preserves_load_gate());
CHECK(mkdtemp(root) && setenv("LARDON3D_PROJECTS_ROOT", root, 1) == 0);
char source[PATH_MAX];
CHECK(join_path(source, root, "source.pgm") && write_pgm(source));
CHECK(run_adaptive_output_equivalence(source));
CHECK(retry_failed_opencv_restoration());
Lardon3DAppState state;
lardon3d_app_state_init(&state);
CHECK(runtime(&state) && lardon3d_project_create(&state, "Features"));
@ -577,8 +692,8 @@ static bool run_test(void) {
wait_state(state.task_queue, nondurable_rootsift_task, TASK_COMPLETED, &snapshot) &&
last_sequence_is_no_work(&state, LARDON3D_ROOTSIFT_EXTRACT_TASK_KIND) &&
unsetenv("LARDON3D_TEST_FEATURE_FAIL_DIRECTORY_SYNC") == 0);
/* SIFT and RootSIFT CPU1 checkpoints are exact historical operational
* signatures. Recovery uses CPU12 in memory without publishing an
/* SIFT and RootSIFT CPU12/CPU1 checkpoints are exact historical operational
* signatures. Recovery uses the portable OpenCV ceiling in memory without publishing an
* estimate-only checkpoint; a neighboring CPU2 shape is corruption. */
const uint64_t precision_task_ids[2] = {sift_task_id, rootsift_task_id};
const char *precision_kinds[2] = {LARDON3D_SIFT_EXTRACT_TASK_KIND,
@ -589,6 +704,53 @@ static bool run_test(void) {
.resource_governor = state.resource_governor,
.orb_vulkan_backend = state.orb_vulkan_backend,
};
char orb_checkpoint[PATH_MAX];
CHECK(snprintf(orb_checkpoint, sizeof(orb_checkpoint),
"%s/.lardon3d/checkpoints/%lu.chk", state.project_path,
(unsigned long)task_id) > 0);
Lardon3DTaskDurableSnapshot current_orb;
CHECK(lardon3d_task_checkpoint_load(orb_checkpoint, &current_orb, NULL) ==
LARDON3D_TASK_CHECKPOINT_OK &&
current_orb.estimate.desired_cpu_threads == INT_MAX);
const unsigned int legacy_orb_counts[] = {12, 1};
for (size_t legacy = 0;
legacy < sizeof(legacy_orb_counts) / sizeof(legacy_orb_counts[0]);
++legacy) {
Lardon3DTaskDurableSnapshot historical_orb = current_orb;
historical_orb.estimate.desired_cpu_threads = legacy_orb_counts[legacy];
historical_orb.progress = 50;
historical_orb.saved_state = TASK_RUNNING;
historical_orb.recovery_state = TASK_PENDING;
historical_orb.finished_at = (struct timespec){0};
Lardon3DTask *restored_orb = NULL;
CHECK(lardon3d_task_kind_registry_restore(
lardon3d_task_kind_registry_production(),
LARDON3D_FEATURE_EXTRACT_TASK_KIND,
LARDON3D_FEATURE_EXTRACT_TASK_KIND_VERSION, &historical_orb,
&precision_reconstruction, &restored_orb) ==
LARDON3D_TASK_KIND_OK &&
restored_orb);
Lardon3DResourceEstimate effective_orb;
CHECK(lardon3d_task_resource_estimate(restored_orb, &effective_orb) &&
effective_orb.desired_cpu_threads == INT_MAX);
lardon3d_task_destroy(restored_orb);
}
Lardon3DTaskDurableSnapshot malformed_orb = current_orb;
malformed_orb.estimate.desired_cpu_threads = 2;
Lardon3DTask *restored_orb = NULL;
CHECK(lardon3d_task_kind_registry_restore(
lardon3d_task_kind_registry_production(),
LARDON3D_FEATURE_EXTRACT_TASK_KIND,
LARDON3D_FEATURE_EXTRACT_TASK_KIND_VERSION, &malformed_orb,
&precision_reconstruction, &restored_orb) ==
LARDON3D_TASK_KIND_RECONSTRUCTION_FAILED &&
restored_orb == NULL);
Lardon3DTaskDurableSnapshot still_current_orb;
CHECK(lardon3d_task_checkpoint_load(orb_checkpoint, &still_current_orb,
NULL) ==
LARDON3D_TASK_CHECKPOINT_OK &&
still_current_orb.estimate.desired_cpu_threads == INT_MAX);
for (size_t precision_index = 0; precision_index < 2; ++precision_index) {
char precision_checkpoint[PATH_MAX];
char precision_staged[PATH_MAX];
@ -601,6 +763,25 @@ static bool run_test(void) {
CHECK(lardon3d_task_checkpoint_load(precision_checkpoint,
&current_precision, NULL) ==
LARDON3D_TASK_CHECKPOINT_OK);
Lardon3DTaskDurableSnapshot cpu12_precision = current_precision;
cpu12_precision.estimate.desired_cpu_threads = 12;
cpu12_precision.progress = 50;
cpu12_precision.saved_state = TASK_RUNNING;
cpu12_precision.recovery_state = TASK_PENDING;
cpu12_precision.finished_at = (struct timespec){0};
Lardon3DTask *cpu12_restored = NULL;
CHECK(lardon3d_task_kind_registry_restore(
lardon3d_task_kind_registry_production(),
precision_kinds[precision_index],
LARDON3D_SIFT_EXTRACT_TASK_KIND_VERSION, &cpu12_precision,
&precision_reconstruction, &cpu12_restored) ==
LARDON3D_TASK_KIND_OK &&
cpu12_restored);
Lardon3DResourceEstimate cpu12_effective;
CHECK(lardon3d_task_resource_estimate(cpu12_restored,
&cpu12_effective) &&
cpu12_effective.desired_cpu_threads == INT_MAX);
lardon3d_task_destroy(cpu12_restored);
Lardon3DTaskDurableSnapshot historical_precision = current_precision;
historical_precision.estimate.desired_cpu_threads = 1;
historical_precision.progress = 50;
@ -627,7 +808,7 @@ static bool run_test(void) {
Lardon3DResourceEstimate effective_precision;
CHECK(lardon3d_task_resource_estimate(restored_precision,
&effective_precision) &&
effective_precision.desired_cpu_threads == 12);
effective_precision.desired_cpu_threads == INT_MAX);
lardon3d_task_destroy(restored_precision);
Lardon3DTaskDurableSnapshot durable_precision;
CHECK(lardon3d_task_checkpoint_load(precision_checkpoint,
@ -831,6 +1012,19 @@ static bool run_test(void) {
uint64_t visual_task_id = 0;
CHECK(lardon3d_project_enqueue_visual_index_update(&state, visual_index_id, &visual_task_id));
CHECK(wait_state(state.task_queue, visual_task_id, TASK_PAUSED, &snapshot));
char visual_checkpoint[PATH_MAX];
Lardon3DTaskDurableSnapshot historical_visual;
CHECK(snprintf(visual_checkpoint, sizeof(visual_checkpoint),
"%s/.lardon3d/checkpoints/%lu.chk", state.project_path,
(unsigned long)visual_task_id) > 0 &&
lardon3d_task_checkpoint_load(visual_checkpoint, &historical_visual,
NULL) ==
LARDON3D_TASK_CHECKPOINT_OK &&
historical_visual.estimate.desired_cpu_threads == 16);
historical_visual.estimate.desired_cpu_threads = 12;
CHECK(lardon3d_task_checkpoint_save(visual_checkpoint,
&historical_visual) ==
LARDON3D_TASK_CHECKPOINT_OK);
lardon3d_task_queue_destroy(state.task_queue);
state.task_queue = NULL;
lardon3d_project_close(&state);

Some files were not shown because too many files have changed in this diff Show more