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:
parent
96d0791ffb
commit
6b894a24df
126 changed files with 27499 additions and 1732 deletions
78
AGENTS.md
78
AGENTS.md
|
|
@ -34,7 +34,7 @@ through an explicitly authorized, explicitly scoped human ticket:
|
||||||
- F0 — PASS/FROZEN
|
- F0 — PASS/FROZEN
|
||||||
- Phase H v1 — PASS/FROZEN
|
- Phase H v1 — PASS/FROZEN
|
||||||
- MVS-M1 — PASS/FROZEN
|
- MVS-M1 — PASS/FROZEN
|
||||||
- Project DB v22 — PASS/FROZEN
|
- Project DB v22 scientific/persistence foundation — PASS/FROZEN
|
||||||
- Calibration Bootstrap v1 — PASS/FROZEN
|
- Calibration Bootstrap v1 — PASS/FROZEN
|
||||||
- Selected Scientific Execution — PASS/FROZEN
|
- Selected Scientific Execution — PASS/FROZEN
|
||||||
- Photo Quality Triage / Acquisition Selection — 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
|
- S2 Capture-safe Standard Ingestion — PASS/FROZEN
|
||||||
- S3 Capture / Acquisition Ingestion — PASS/FROZEN
|
- S3 Capture / Acquisition Ingestion — PASS/FROZEN
|
||||||
- Durable Acquisition-Campaign Execution — PASS/FROZEN
|
- Durable Acquisition-Campaign Execution — PASS/FROZEN
|
||||||
|
- Global Maintenance Audit — PASS/FROZEN
|
||||||
|
|
||||||
Detailed subcontracts remain defined by their canonical documents. This file
|
Detailed subcontracts remain defined by their canonical documents. This file
|
||||||
does not duplicate every S3 substage, scientific threshold, migration detail,
|
does not duplicate every S3 substage, scientific threshold, migration detail,
|
||||||
or persistence format.
|
or persistence format.
|
||||||
|
|
||||||
Historical references to older Project DB versions remain valid when they
|
Project DB v23 is the current additive optical-context overlay. It preserves
|
||||||
describe the actual historical contract or migration path. Do not rewrite
|
the v22 scientific/persistence foundation and must not infer or backfill optical
|
||||||
legitimate v16/v17/v18/v19 history merely because v22 is current.
|
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 v16–v22 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:
|
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.
|
termination semantics.
|
||||||
- If a public function may be retried, its idempotency or conflict behavior
|
- If a public function may be retried, its idempotency or conflict behavior
|
||||||
must be explicit where non-obvious.
|
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
|
Compiler success alone is not proof of API, ABI, persistence, or scientific
|
||||||
contract correctness.
|
contract correctness.
|
||||||
|
|
@ -173,6 +190,21 @@ explicitly defines such an identity.
|
||||||
to canonical architecture.
|
to canonical architecture.
|
||||||
- Keep ncurses on its designated/main thread wherever the canonical contract
|
- Keep ncurses on its designated/main thread wherever the canonical contract
|
||||||
requires it.
|
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.
|
- Extend validated abstractions rather than rewriting validated modules.
|
||||||
- Reuse the existing Task / Queue / Scheduler / Resource Governor ownership
|
- Reuse the existing Task / Queue / Scheduler / Resource Governor ownership
|
||||||
model rather than creating parallel runtime infrastructure.
|
model rather than creating parallel runtime infrastructure.
|
||||||
|
|
@ -203,9 +235,29 @@ Resource-sensitive work must identify, where relevant:
|
||||||
- cancellation cleanup;
|
- cancellation cleanup;
|
||||||
- whether the bound is operational or scientific.
|
- whether the bound is operational or scientific.
|
||||||
|
|
||||||
External USB SSD scratch/swap support remains a planned resource-management
|
The reviewed external USB SSD controller is the authorized physical-lifecycle
|
||||||
capability. Do not implement ad-hoc mounting, formatting, `swapon`, scratch
|
boundary for the exact UDisks Drive/label/UUID contract. Its validated snapshot
|
||||||
ownership, or device cleanup outside an explicitly authorized ticket.
|
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
|
## 7. Code quality and readability
|
||||||
|
|
||||||
|
|
@ -552,8 +604,8 @@ Persistence changes require explicit attention to:
|
||||||
|
|
||||||
For Project DB:
|
For Project DB:
|
||||||
|
|
||||||
- preserve current v22 semantics unless a ticket explicitly authorizes a schema
|
- preserve the FROZEN v22 semantics and the current additive v23 optical
|
||||||
change;
|
overlay unless a ticket explicitly authorizes a later schema change;
|
||||||
- schema-version changes require explicit human authorization;
|
- schema-version changes require explicit human authorization;
|
||||||
- migrations must be additive unless a different migration is explicitly
|
- migrations must be additive unless a different migration is explicitly
|
||||||
authorized;
|
authorized;
|
||||||
|
|
@ -578,9 +630,11 @@ make retries convenient.
|
||||||
behavior.
|
behavior.
|
||||||
- Roadmap documents may describe future behavior, but future capabilities must
|
- Roadmap documents may describe future behavior, but future capabilities must
|
||||||
be clearly marked as planned/later/exploratory.
|
be clearly marked as planned/later/exploratory.
|
||||||
- Never describe future viewer, Capture Guidance, video/keyframe, SSD scratch,
|
- Never describe future viewer, Capture Guidance, video/keyframe, Task scratch
|
||||||
or camera-control capabilities as implemented before they are actually
|
consumption, or camera-control capabilities as implemented before they are
|
||||||
validated.
|
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
|
- Statuses such as `PLANNED`, `IMPLEMENTED`, `VALIDATION PENDING`, and
|
||||||
`PASS/FROZEN` are authoritative lifecycle statements.
|
`PASS/FROZEN` are authoritative lifecycle statements.
|
||||||
- Update lifecycle state only when implementation, validation, and review
|
- Update lifecycle state only when implementation, validation, and review
|
||||||
|
|
|
||||||
93
README.md
93
README.md
|
|
@ -1,6 +1,7 @@
|
||||||
# Lardon3D
|
# 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
|
## Vision
|
||||||
|
|
||||||
|
|
@ -35,9 +36,10 @@ persistante, enrichissable et versionnable.
|
||||||
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences
|
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences
|
||||||
- **Task Checkpoint v1** : snapshot durable, protocole `.chk.next` → SQLite →
|
- **Task Checkpoint v1** : snapshot durable, protocole `.chk.next` → SQLite →
|
||||||
`.chk` sous verrou par tâche, et reprise sûre
|
`.chk` sous verrou par tâche, et reprise sûre
|
||||||
- **Project Database v22** : fondations v20/v21 préservées, avec snapshot
|
- **Project Database v23** : overlay optique additif au-dessus de la fondation
|
||||||
d'exécution sélectionnée, publication bornée des représentations, attache de
|
v22 PASS / FROZEN ; profils de boîtier et d'objectif, configurations
|
||||||
scope de calibration et tâche durable mince `raw.develop` — PASS / FROZEN
|
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
|
- **[Photo Quality Triage](docs/architecture/photo_quality_triage.md)** : métriques JPEG
|
||||||
- **[Calibration Bootstrap v1](docs/architecture/calibration_bootstrap.md)** :
|
- **[Calibration Bootstrap v1](docs/architecture/calibration_bootstrap.md)** :
|
||||||
import borné d'une calibration optimisée et traçable avant le Sparse SfM à
|
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
|
- **Hardware Profile** : détection des capacités matérielles
|
||||||
- **Resource Snapshot** : capture instantanée des ressources
|
- **Resource Snapshot** : capture instantanée des ressources
|
||||||
- **Resource Governor** : arbitrage centralisé des budgets et réservations
|
- **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
|
### 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
|
- **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
|
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
|
(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é
|
### Plus tard / différé
|
||||||
|
|
||||||
- publication durable dense/mesh et scratch SSD externe optionnel gouverné ;
|
- publication durable dense/mesh et consommation Task explicite du scratch
|
||||||
- workflow TUI de confirmation/progression ;
|
SSD optionnel ;
|
||||||
- vidéo/keyframes et **Capture Guidance / Live Coverage** : analyse et viewer de
|
- vidéo/keyframes et **Capture Guidance / Live Coverage** : analyse et viewer de
|
||||||
couverture, suggestions de prises de vue puis assistance live, après
|
couverture, suggestions de prises de vue puis assistance live, après
|
||||||
reconstruction mature ;
|
reconstruction mature ;
|
||||||
|
|
@ -102,11 +116,11 @@ persistante, enrichissable et versionnable.
|
||||||
```text
|
```text
|
||||||
TUI / Projet
|
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
|
Résultats atomiques / persistants
|
||||||
↓
|
↓
|
||||||
|
|
@ -116,12 +130,37 @@ Viewer (consommation passive de snapshots)
|
||||||
### Invariants fondamentaux
|
### Invariants fondamentaux
|
||||||
|
|
||||||
- Aucun callback de tâche sans réservation active validée
|
- 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
|
- Le Resource Governor est l'unique propriétaire des budgets
|
||||||
- ncurses appartient exclusivement au thread principal
|
- ncurses appartient exclusivement au thread principal
|
||||||
- Les estimations de ressources sont immuables
|
- Les estimations de ressources sont immuables
|
||||||
- Les buffers et files sont strictement bornés
|
- 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
|
## Pipeline cible
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|
@ -150,7 +189,7 @@ Acquisitions
|
||||||
- [Resource Governor](docs/architecture/resource_governor.md)
|
- [Resource Governor](docs/architecture/resource_governor.md)
|
||||||
- [Parallélisme interne borné](docs/architecture/internal_parallelism.md)
|
- [Parallélisme interne borné](docs/architecture/internal_parallelism.md)
|
||||||
- [Pipeline sensible aux ressources](docs/architecture/resource_aware_pipeline.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)
|
- [Pipeline de reconstruction](docs/architecture/reconstruction_pipeline.md)
|
||||||
- [Persistance](docs/architecture/persistence.md)
|
- [Persistance](docs/architecture/persistence.md)
|
||||||
- [Base de données projet](docs/architecture/project_database.md)
|
- [Base de données projet](docs/architecture/project_database.md)
|
||||||
|
|
@ -167,7 +206,8 @@ Acquisitions
|
||||||
- [Backend Vulkan ORB](docs/architecture/vulkan_matcher.md)
|
- [Backend Vulkan ORB](docs/architecture/vulkan_matcher.md)
|
||||||
- [Viewer](docs/architecture/viewer.md)
|
- [Viewer](docs/architecture/viewer.md)
|
||||||
- [Resource Boundary — No New Resource Subsystem](docs/architecture/resource_boundary.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
|
### Concepts
|
||||||
- [Scan Sets](docs/concepts/scan_sets.md)
|
- [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,
|
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
|
profondeur, cache ou sortie antérieure. Le DAG, le viewer et les autres étapes
|
||||||
denses restent des tickets séparés planifiés.
|
denses restent des tickets séparés planifiés.
|
||||||
Project DB v22, `raw.develop` et Calibration Bootstrap v1 sont **PASS /
|
La fondation Project DB v22, `raw.develop` et Calibration Bootstrap v1 reste
|
||||||
FROZEN** : la suite normale 53/53, les contrôles syntaxiques C17, `git diff
|
**PASS / FROZEN**. La tête courante v23 ajoute seulement le contexte optique
|
||||||
--check`, la validation ciblée ASan/UBSan et l'audit final ont passé. La suite
|
générique : neuf tables, migration transactionnelle sans backfill, objectifs
|
||||||
ASan/UBSan complète demeure qualifiée par le comportement LSan du pilote tiers
|
manuels sans EXIF, configurations multiples par campagne et sélection de
|
||||||
RADV ; elle n'est pas présentée comme un PASS complet du dépôt. Les campagnes
|
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
|
réelles S21 et A6000 Engine Bay sont
|
||||||
`CALIBRATION_UNAVAILABLE` par non-identifiabilité scientifique des données de
|
`CALIBRATION_UNAVAILABLE` par non-identifiabilité scientifique des données de
|
||||||
calibration connues ; le Sparse SfM réel reste
|
calibration connues ; le Sparse SfM réel reste
|
||||||
`BLOCKED_BY_KNOWN_CALIBRATION_DATA`, sans pseudo-calibration ni import inféré.
|
`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
|
Le Resource Governor ne constitue pas un Resource System générique : il reste
|
||||||
d’architecture.
|
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
|
## Licence
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -238,18 +238,21 @@ registry production et resoumise à la queue.
|
||||||
|
|
||||||
Les anciens snapshots v1 produits avec l'estimation opérationnelle exacte
|
Les anciens snapshots v1 produits avec l'estimation opérationnelle exacte
|
||||||
128 Kio fixes, 64 Kio par item, lot 1–64, CPU 1, IO 1 et GPU 0 sont normalisés
|
128 Kio fixes, 64 Kio par item, lot 1–64, 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
|
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
|
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,
|
pré-terminale répète donc cette normalisation exacte. Aucun autre snapshot,
|
||||||
curseur ou paramètre scientifique n'est réinterprété.
|
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 :
|
La tâche utilise le runtime générique via le pattern standard :
|
||||||
- Estimation opérationnelle (256 Kio fixes, 64 Kio par item, lot 1–64).
|
- Estimation opérationnelle (256 Kio fixes, 8 Mio par item, lot 1–64).
|
||||||
La Queue conserve un callback ; jusqu'à douze participants CPU admis peuvent
|
La Queue conserve un callback ; jusqu'à soixante-quatre participants CPU
|
||||||
calculer une fenêtre interne bornée sans modifier l'identité scientifique.
|
admis peuvent calculer une fenêtre interne bornée sans modifier l'identité
|
||||||
|
scientifique.
|
||||||
- Réservation CPU + IO avant exécution
|
- Réservation CPU + IO avant exécution
|
||||||
- `lardon3d_task_sequence_break()` entre chaque lot pour réadmission Governor
|
- `lardon3d_task_sequence_break()` entre chaque lot pour réadmission Governor
|
||||||
- Callback terminal checkpoint après `COMPLETED`/`FAILED`/`CANCELLED`
|
- Callback terminal checkpoint après `COMPLETED`/`FAILED`/`CANCELLED`
|
||||||
|
|
@ -365,7 +368,7 @@ canonicalisation, idempotence, batch projet, fingerprint et
|
||||||
réutilisation/invalidation.
|
réutilisation/invalidation.
|
||||||
|
|
||||||
**IMPLEMENTED** — tâche durable `candidate_pair.generate` v1
|
**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
|
checkpoint par curseur, reprise idempotente et intégration
|
||||||
dans la registry production.
|
dans la registry production.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
little-endian. Il ne dépend ni du padding, ni de la locale. Le contrat ORB v1
|
||||||
produit 32 octets binaires par point.
|
produit 32 octets binaires par point.
|
||||||
|
|
||||||
Au démarrage, avant la création des workers, Lardon3D configure la limite de
|
Au démarrage, avant la création du worker Queue, Lardon3D configure la baseline
|
||||||
threads interne process-wide d'OpenCV à
|
OpenCV process-wide depuis le compute-pool réel. `features.extract` publie la
|
||||||
`min(12, logical_cpu_count - system_cpu_reserve)`. La borne douze est
|
borne positive `int` acceptée par OpenCV ; le Resource Governor réduit toujours
|
||||||
opérationnelle et correspond au plafond audité. `features.extract` demande
|
l'admission au budget hôte. Douze threads demeure une cohorte de validation,
|
||||||
jusqu'à douze threads au Resource Governor ; celui-ci réduit l'admission au
|
pas un plafond portable. La Queue conserve un callback actif ; aucun pool
|
||||||
budget hôte, donc à la limite OpenCV configurée. La Queue conserve un callback actif ; aucun pool Lardon3D
|
Lardon3D supplémentaire n'est créé pour ORB.
|
||||||
supplémentaire n'est créé pour ORB.
|
|
||||||
|
|
||||||
Cette limite est une configuration opérationnelle, jamais un paramètre
|
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é
|
scientifique : elle n'entre ni dans le fingerprint ORB v1, ni dans l'identité
|
||||||
|
|
|
||||||
|
|
@ -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
|
## Objectif
|
||||||
|
|
||||||
|
|
@ -51,7 +58,7 @@ Documenter la revue technique de la phase de fondation : task, task_queue, hardw
|
||||||
6. Protection mutex unique du gouverneur
|
6. Protection mutex unique du gouverneur
|
||||||
7. Séparation stricte des rôles (scheduler ne décide pas des ressources)
|
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
|
- File à worker unique avec FIFO strict
|
||||||
- Absence de notification automatique de libération externe
|
- 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
|
- Bornage de la mémoire
|
||||||
- Indépendance de la publication atomique
|
- Indépendance de la publication atomique
|
||||||
|
|
||||||
## Feuille de route
|
## Feuille de route historique (désormais supersédée)
|
||||||
|
|
||||||
### Prochains tickets recommandés
|
### Prochains tickets recommandés
|
||||||
1. Sélectionner une tâche admissible sans blocage par la tête de file ✓
|
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é
|
- TSan passé
|
||||||
- git diff --check propre
|
- git diff --check propre
|
||||||
|
|
||||||
## Statut : DOCUMENTATION DE L'IMPLÉMENTATION ACTUELLE
|
## Statut : ARCHIVE D'ÉVIDENCE HISTORIQUE
|
||||||
|
|
|
||||||
|
|
@ -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 :
|
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
|
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
|
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
|
## 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
|
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
|
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.
|
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
|
## CPU policy
|
||||||
|
|
||||||
Le parallélisme OpenCV reste configuré process-wide. Le benchmark mesurera les threads réellement
|
Le parallélisme scientifique interne OpenCV reste explicitement désactivé :
|
||||||
consommés ; le verifier ne change pas `cv::setNumThreads()` par paire.
|
`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
|
## 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.
|
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
|
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
|
strictement croissant avec une page de `batch + 1`, et traite des lots Governor
|
||||||
parents autres que `MATCHED` avec `match_count > 0` sont seulement traversés par le curseur. Une
|
1..16 avec CPU utile 1..8. Les préparations éligibles peuvent s'exécuter en
|
||||||
unité éligible appelle le core, qui reuse l'identité exacte avant toute lecture d'asset.
|
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.
|
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,
|
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
|
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
|
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
|
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,
|
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,
|
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.
|
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
|
## Out of scope
|
||||||
|
|
||||||
Tracks, model competition, classification planaire ou faible parallaxe, Essential, calibration,
|
Tracks, model competition, classification planaire ou faible parallaxe, Essential, calibration,
|
||||||
|
|
|
||||||
490
docs/architecture/global_maintenance_audit.md
Normal file
490
docs/architecture/global_maintenance_audit.md
Normal 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 v16–v22 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 3–4 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.
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
|
|
||||||
Ce document décrit le contrat validé pour `features.extract`,
|
Ce document décrit le contrat validé pour `features.extract`,
|
||||||
`features.extract.sift`, `visual_index.update`, `candidate_pair.generate` et
|
`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
|
réservations Governor et les sorties canoniques. Il ne préjuge pas du choix
|
||||||
opérationnel CPU/Vulkan ajouté par la tranche v2.
|
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 à
|
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
|
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
|
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
|
scheduler, ni file de travail persistante. Le Governor dérive un masque privé
|
||||||
masque privé depuis l'affinité permise et la topologie package/core Linux. Il
|
depuis l'affinité permise et la topologie package/core Linux. La cible par
|
||||||
réserve des coeurs physiques complets, frères SMT inclus, en ordre décroissant
|
défaut réserve quatre CPU logiques lorsque praticable et conserve au moins un
|
||||||
package/core. Sur l'hôte unrestricted validé, cela donne `0-5,8-13` pour le
|
CPU de calcul sur les petits hôtes. Avec une topologie fiable, un sous-ensemble
|
||||||
calcul lourd et `6,7,14,15` pour le desktop, l'audio et l'interaction. Un masque
|
déterministe de coeurs physiques complets, frères SMT inclus, satisfait cette
|
||||||
caller déjà borné à la capacité de calcul est repris sans seconde réserve ; si
|
cible avec le dépassement minimal. Les CPU exclus par une affinité externe
|
||||||
topologie ou affinité manque, seul le budget portable est conservé et aucun ID
|
comptent déjà dans la réserve. Si topologie ou affinité manque, seul le budget
|
||||||
n'est inventé.
|
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
|
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
|
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
|
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
|
pas la Queue ; seuls leurs `main` autonomes peuvent prendre le défaut sûr avant
|
||||||
tout pthread.
|
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
|
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
|
est **PASS / FROZEN**, sans persistance ni ABI publique. Elle complète le gel
|
||||||
v1 sans le redéfinir.
|
v1 sans le redéfinir.
|
||||||
|
|
@ -157,9 +163,10 @@ CPU/Vulkan, le fallback CPU complet et les diagnostics bornés sont
|
||||||
## Extraction OpenCV
|
## Extraction OpenCV
|
||||||
|
|
||||||
Le processus conserve une limite OpenCV globale. Sous l'unique callback Queue,
|
Le processus conserve une limite OpenCV globale. Sous l'unique callback Queue,
|
||||||
RAW et Photo Quality appliquent/restaurent CPU1. ORB, SIFT et RootSIFT
|
RAW, Photo Quality et la campagne appliquent/restaurent leur CPU admis (CPU1
|
||||||
Extraction appliquent/restaurent exactement le `cpu_threads` immutable admis
|
dans les estimations courantes). ORB, SIFT et RootSIFT Extraction déclarent le
|
||||||
dans `1..min(12, compute_pool)`. Matcher applique OpenCV1 à l'intérieur de sa
|
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
|
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
|
primitive. La réservation vit pendant l'exécution bornée et aucun second pool
|
||||||
runtime n'est créé.
|
runtime n'est créé.
|
||||||
|
|
@ -167,12 +174,14 @@ runtime n'est créé.
|
||||||
Le nombre admis est une politique de ressources, pas une identité scientifique.
|
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,
|
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
|
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
|
varier cette configuration process-wide pendant une extraction. Le callback
|
||||||
assure donc qu'une admission CPU est réellement consommée, sans confondre cette
|
assure donc qu'une admission CPU est réellement consommée, sans confondre cette
|
||||||
dimension avec le lot admis.
|
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.
|
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 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
|
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
|
segment, tandis que Candidate et Matcher observent chaque séquence. Les formes
|
||||||
fixes restent fixes mais conservent un diagnostic d'admission Governor-owned.
|
fixes restent fixes mais conservent un diagnostic d'admission Governor-owned.
|
||||||
|
|
||||||
Pour Candidate Generation, l'estimation demande jusqu'à douze threads CPU et
|
Pour Candidate Generation, l'estimation demande jusqu'à soixante-quatre threads
|
||||||
un slot I/O. Le callback de Queue compte comme un de ces threads ; il crée donc
|
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
|
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
|
la fin de la séquence, la libération de réservation ou
|
||||||
`lardon3d_task_sequence_break()`.
|
`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
|
## Calcul et publication Visual Index
|
||||||
|
|
||||||
Une séquence sélectionne le même préfixe durable d'au plus seize Feature Sets
|
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
|
Feature Files immuables ; chaque reader, son descripteur de fichier et sa
|
||||||
tranche de 256 descripteurs appartiennent à un seul participant. Aucun enfant
|
tranche de 256 descripteurs appartiennent à un seul participant. Aucun enfant
|
||||||
n'utilise le handle Project DB partagé.
|
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
|
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
|
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
|
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
|
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,
|
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
|
exécute exactement la requête Visual Index existante et forme les paires
|
||||||
canoniques en mémoire. Il ne publie rien.
|
canoniques en mémoire. Il ne publie rien.
|
||||||
|
|
||||||
La reprise normalise uniquement l'estimation Candidate v1 sérielle historique
|
La reprise normalise uniquement les estimations Candidate historiques
|
||||||
de forme exacte vers la demande courante de douze participants. Cette
|
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
|
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
|
toujours la réduire à son budget disponible. Le Task ID, le curseur typé, le
|
||||||
lot 1–64 et les paramètres scientifiques restent inchangés. Les autres kinds
|
lot 1–64 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,
|
ne peut donc modifier ni les identités persistées, ni leur ordre de publication,
|
||||||
ni leur cardinalité.
|
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
|
## Échec, reprise et progression
|
||||||
|
|
||||||
Une erreur de calcul à la source `S` interdit toute publication de `S` et de
|
Une erreur de calcul à la source `S` interdit toute publication de `S` et de
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,11 @@ Task
|
||||||
↓
|
↓
|
||||||
Estimate
|
Estimate
|
||||||
↓
|
↓
|
||||||
Governor
|
Task Queue
|
||||||
↓
|
↓
|
||||||
Reservation
|
Resource Governor / Reservation
|
||||||
↓
|
↓
|
||||||
Scheduler
|
callback worker admis
|
||||||
↓
|
|
||||||
Worker
|
|
||||||
↓
|
↓
|
||||||
Résultat atomique
|
Résultat atomique
|
||||||
↓
|
↓
|
||||||
|
|
@ -44,8 +42,9 @@ des fichiers admissibles et maintenance d'un manifeste cohérent.
|
||||||
**Statut :** IMPLEMENTED
|
**Statut :** IMPLEMENTED
|
||||||
|
|
||||||
### Import Task
|
### Import Task
|
||||||
Premier type métier persistant. Il s'exécute par lots bornés dans le scheduler
|
Premier type métier persistant. Il s'exécute par lots bornés dans le runtime et
|
||||||
générique, cible explicitement un ScanSet et peut être reconstruit puis repris.
|
la Queue génériques, cible explicitement un ScanSet et peut être reconstruit
|
||||||
|
puis repris.
|
||||||
|
|
||||||
**Statut :** IMPLEMENTED
|
**Statut :** IMPLEMENTED
|
||||||
|
|
||||||
|
|
@ -56,12 +55,40 @@ mémoire depuis `manifest.tsv` reste une façade legacy pour la TUI.
|
||||||
|
|
||||||
**Statut :** IMPLEMENTED
|
**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
|
### Image View
|
||||||
Vues triées et filtrées du catalogue pour la TUI. Ne modifie pas le
|
Vues triées et filtrées du catalogue pour la TUI. Ne modifie pas le
|
||||||
catalogue, le manifeste ou les images.
|
catalogue, le manifeste ou les images.
|
||||||
|
|
||||||
**Statut :** IMPLEMENTED
|
**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
|
### Feature Store
|
||||||
Extraction ORB réelle par tâche persistante, Feature Sets logiques et assets
|
Extraction ORB réelle par tâche persistante, Feature Sets logiques et assets
|
||||||
binaires content-addressed lisibles par plages bornées.
|
binaires content-addressed lisibles par plages bornées.
|
||||||
|
|
@ -104,10 +131,22 @@ adaptatifs, réservations opaques et historique borné de métriques.
|
||||||
|
|
||||||
**Statut :** IMPLEMENTED
|
**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
|
### Candidate Pair
|
||||||
Sous-système de génération et persistance de paires d'images candidates pour
|
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
|
le matching géométrique. Répond uniquement à « quelles paires valent la peine
|
||||||
peine d'être试探ées ? » sans validation géométrique.
|
d'être explorées ? » sans validation géométrique.
|
||||||
|
|
||||||
**Statut :** IMPLEMENTED
|
**Statut :** IMPLEMENTED
|
||||||
|
|
||||||
|
|
@ -165,7 +204,7 @@ frontière de séquence connue.
|
||||||
## Invariants fondamentaux
|
## Invariants fondamentaux
|
||||||
|
|
||||||
- Aucun callback de tâche n'est lancé sans réservation active validée.
|
- 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.
|
- Le Resource Governor est l'unique propriétaire des budgets.
|
||||||
- Les réservations sont libérées exactement une fois.
|
- Les réservations sont libérées exactement une fois.
|
||||||
- ncurses appartient exclusivement au thread principal.
|
- ncurses appartient exclusivement au thread principal.
|
||||||
|
|
@ -174,7 +213,7 @@ frontière de séquence connue.
|
||||||
|
|
||||||
## Limites actuelles
|
## 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 DAG de dépendances.
|
||||||
- Absence de priorités.
|
- Absence de priorités.
|
||||||
- Absence de pools de workers multiples (CPU/GPU/IO).
|
- Absence de pools de workers multiples (CPU/GPU/IO).
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@
|
||||||
|
|
||||||
## Vision
|
## 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
|
## Principes fondamentaux
|
||||||
|
|
||||||
|
|
@ -37,6 +40,10 @@ Les éléments suivants sont des concepts de domaine, PAS des tables SQL imposé
|
||||||
- track
|
- track
|
||||||
- observation
|
- observation
|
||||||
- camera
|
- camera
|
||||||
|
- camera_body_profile
|
||||||
|
- lens_profile
|
||||||
|
- optical_configuration
|
||||||
|
- optical_calibration_profile
|
||||||
- pose
|
- pose
|
||||||
- point3d
|
- point3d
|
||||||
- reconstruction_layer
|
- 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
|
plateformes uniquement pour les valeurs communes à leurs domaines `size_t` et
|
||||||
`time_t`.
|
`time_t`.
|
||||||
|
|
||||||
## Project Database v7
|
## Project Database v7 — fondation historique
|
||||||
|
|
||||||
SQLite contient l'état logique interrogable et les références aux fichiers ;
|
SQLite contient l'état logique interrogable et les références aux fichiers ;
|
||||||
les checkpoints et artefacts volumineux restent externes. L'enregistrement du
|
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
|
conserve cet avertissement jusqu'au prochain checkpoint durable. Une tâche
|
||||||
terminale n'appartient pas à la requête de reprise.
|
terminale n'appartient pas à la requête de reprise.
|
||||||
|
|
||||||
**NOT_YET_WIRED** — autosave complet, réconciliation des fichiers orphelins et
|
**NOT_YET_WIRED** — réconciliation des fichiers orphelins et retry piloté par
|
||||||
retry piloté par l'utilisateur pour les sources indisponibles.
|
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
|
**NOT_YET_WIRED** — migration de la vue TUI en mémoire vers la pagination
|
||||||
SQLite, scrub des assets et réconciliation globale des orphelins.
|
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
|
**IMPLEMENTED** — Visual Index externe segmenté, memberships transactionnels
|
||||||
et tâche `visual_index.update` récupérable.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
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.
|
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
|
L'estimation SIFT publie la capacité sûre de l'API OpenCV, `INT_MAX`; le
|
||||||
démarrage est une baseline/plafond sûre ; pour chaque séquence, l'unique
|
Governor la borne toujours au compute-pool réel. Le réglage OpenCV du démarrage
|
||||||
callback Queue applique temporairement le compte immuable admis dans `1..12`,
|
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
|
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
|
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 :
|
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.
|
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.
|
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
|
La reprise accepte les formes historiques CPU12 et CPU1 complètes et exactes,
|
||||||
forme CPU1 complète et exacte dans la copie privée restaurée. Elle ne publie
|
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.
|
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
|
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
|
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
|
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
|
propres accès de teardown TLS/`cv::Mat`. Les tests TSan suppriment uniquement
|
||||||
les frames des objets partagés `libopencv_core.so` et
|
les frames des objets partagés `libopencv_core.so` et
|
||||||
`libopencv_features.so`; les frames Lardon3D et toutes les autres bibliothèques
|
`libopencv_features.so`, ainsi que le runtime externe `libtbb.so`; les frames
|
||||||
restent contrôlées.
|
Lardon3D et toutes les autres bibliothèques restent contrôlées.
|
||||||
|
|
||||||
## Consolidation intra-image
|
## 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.
|
images ne sont jamais chargées ensemble.
|
||||||
|
|
||||||
Le Visual Index v1 reste exclusivement ORB-LSH. Candidate Pair Generator,
|
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 C–G 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** :
|
ALIKED est **PLANNED / BLOCKED ON MODEL PROVENANCE + VALIDATED ONNX EXPORT** :
|
||||||
aucun code ALIKED, ONNX ou Python production n'appartient à v1A.
|
aucun code ALIKED, ONNX ou Python production n'appartient à v1A.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,76 @@
|
||||||
# Base de données projet Lardon3D
|
# 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 v16–v22. 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
|
## Snapshot d'exécution scientifique sélectionnée — Project DB v22
|
||||||
|
|
||||||
**PASS / FROZEN.** La migration additive v21→v22 ajoute
|
**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
|
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,
|
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
|
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)
|
## 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
|
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é
|
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
|
et sa reprise sont définies par Project DB v20 ci-dessous ; ils ne modifient pas
|
||||||
le contrat S3.
|
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
|
`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,
|
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
|
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
|
`scanset_id`, le curseur historique `next_group_id`, le nombre de groupes et
|
||||||
immuable : un upsert ne peut faire progresser le curseur que si ScanSet, nombre
|
une requête immuable. Pour cette table de campagne, malgré son nom historique,
|
||||||
de groupes et octets de requête sont identiques.
|
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
|
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
|
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é,
|
Les groupes de plan gardent leurs IDs un-based. Pour chaque groupe achevé,
|
||||||
`acquisition_campaign_captures` conserve la relation unique
|
`acquisition_campaign_captures` conserve la relation unique
|
||||||
`(task_id, group_id) → capture_id` et interdit qu'un même Capture corresponde à
|
`(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
|
deux groupes de la même tâche. L'état durable valide contient exactement le
|
||||||
retient cette relation et avance `next_group_id`, avant la progression générique
|
préfixe de mappings `1..next_group_id`, aucun trou et aucun mapping en avance ;
|
||||||
et le checkpoint. La reprise peut alors transmettre le `capture_id` retenu à
|
chaque Capture existe dans le ScanSet de la campagne. Après le retour de S3-E,
|
||||||
S3-E, sans réhoming ni inférence depuis un chemin, SHA, basename, timestamp,
|
une transaction unique retient la relation du groupe courant et avance ce
|
||||||
métadonnée ou `image_id`.
|
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
|
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,
|
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
|
## Gate F — décision historique Project DB v17
|
||||||
|
|
||||||
**PASS / FROZEN.** La migration historique v15→v16 et le
|
**PASS / FROZEN.** La migration historique v15→v16 et le
|
||||||
modèle de reconstruction Sparse SfM v16 restent inchangés. Gate F fait avancer
|
modèle de reconstruction Sparse SfM v16 restent inchangés. Gate F a fait
|
||||||
la tête de schéma à v17 par une migration strictement additive contenant une
|
avancer la tête de schéma alors courante à v17 par une migration strictement
|
||||||
seule table métier de tâche : `sparse_sfm_tasks`.
|
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
|
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
|
`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
|
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é.
|
effectivement persistée. Aucun placeholder ni table diagnostique n'est ajouté.
|
||||||
|
|
||||||
> Version courante : **v20**. La migration transactionnelle additive v19→v20
|
> Version publiée par cette section historique : **v20** ; la tête actuelle
|
||||||
> ajoute les records de campagne durable décrits ci-dessus sans modifier les
|
> 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
|
> tables ni identités Capture/Asset de v19. La migration transactionnelle
|
||||||
> v18→v19 ajoute la fondation Capture/Asset Provenance sans modifier les tables
|
> v18→v19 ajoute la fondation Capture/Asset Provenance sans modifier les tables
|
||||||
> scientifiques historiques. La migration transactionnelle v17→v18 ajoute le
|
> scientifiques historiques. La migration transactionnelle v17→v18 ajoute le
|
||||||
|
|
@ -924,23 +1006,27 @@ fermeture/réouverture et payload corrompu.
|
||||||
|
|
||||||
## Ouverture et migrations
|
## 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
|
`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
|
`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
|
`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
|
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
|
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
|
`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
|
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'à
|
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 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` ;
|
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
|
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
|
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
|
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` :
|
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
|
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`
|
`.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
|
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
|
contenant seulement `.chk`; toute autre absence, corruption, version future ou
|
||||||
divergence classe seulement la tâche comme non récupérable.
|
divergence classe seulement la tâche comme non récupérable.
|
||||||
|
|
||||||
|
|
@ -1089,9 +1175,17 @@ ouvert.
|
||||||
|
|
||||||
## Statut
|
## Statut
|
||||||
|
|
||||||
**PASS / FROZEN** — SQLite système, schéma v20 et migrations séquentielles
|
**IMPLEMENTED / VALIDATED / REVIEWED** — SQLite système, tête de schéma v23 et
|
||||||
v1→v2→v3→v4→v5→v6→v7→v8→v9→v10→v11→v12→v13→v14→v15→v16→v17→v18→v19→v20, identité projet, transactions
|
migrations séquentielles v1→…→v23, identité projet, transactions
|
||||||
tâche+checkpoint, pagination de reprise et artefacts génériques.
|
tâche+checkpoint, pagination de reprise, artefacts génériques et overlay
|
||||||
|
optique additif. Les contrats scientifiques et de persistance v16–v22 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
|
**PASS / FROZEN** — Project DB v20 : migration additive
|
||||||
v19→v20, record typé de campagne lié au Task ID, requête immuable bornée,
|
v19→v20, record typé de campagne lié au Task ID, requête immuable bornée,
|
||||||
|
|
|
||||||
|
|
@ -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. |
|
| **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
|
**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. |
|
| **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,
|
**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
|
points 3D existants ne jamais supprimés implicitement. Toute
|
||||||
suppression est une action explicite et traçable.
|
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
|
Seul le Resource Governor arbitre les budgets, les lots et les
|
||||||
réservations.
|
réservations.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
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.
|
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
|
Le Governor maintient trois zones. GREEN emploie le lot adapté normal. La zone
|
||||||
floor RAM, un PSI au seuil ou un premier intervalle avec swap actif produit
|
de prudence RAM entre 3 et 4 Gio, un PSI au seuil ou un premier intervalle avec
|
||||||
YELLOW et interdit toute croissance. Deux observations de pression
|
swap actif produit YELLOW et interdit toute croissance. Deux observations de
|
||||||
consécutives, ou `MemAvailable` sous la hard floor, produisent RED et suspendent
|
pression consécutives, ou `MemAvailable` sous la réserve dure de 3 Gio,
|
||||||
toute admission. Le premier snapshot swap établit seulement la baseline.
|
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
|
La récupération possède deux phases distinctes : trois observations saines
|
||||||
font `RED → YELLOW`, puis trois nouvelles observations saines font
|
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
|
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,
|
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,
|
limites systemd/RLIMIT, multi-GPU, historique/monitoring RSS long terme,
|
||||||
redimensionnement d'admission depuis le RSS, scratch et stockage externe restent
|
redimensionnement d'admission depuis le RSS et consommation Task du scratch
|
||||||
différés.
|
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
|
## 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.
|
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 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
|
Le démarrage configure une baseline OpenCV issue du compute-pool réellement
|
||||||
de Queue. L'unique callback lourd applique ensuite temporairement le compte CPU
|
disponible avant la création de Queue. L'unique callback lourd applique ensuite
|
||||||
immuable admis pour sa séquence, dans `1..12`, et restaure la baseline sur toute
|
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
|
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
|
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
|
un thread pendant qu'une primitive interne en utilise davantage. Une mutation
|
||||||
|
|
@ -83,11 +88,15 @@ recalcule pas les descripteurs.
|
||||||
## Geometric Verification
|
## Geometric Verification
|
||||||
|
|
||||||
Project DB v12 stocke un résultat borné à 1024 octets de masque et neuf
|
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
|
binary64. `geometric_verifier.run` v1 traite chaque Match Result comme unité
|
||||||
par une transaction courte puis checkpoint son curseur par lots 1/2/4/8 avant
|
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
|
`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
|
peut employer jusqu'à huit participants utiles et seize participants sûrs, par
|
||||||
slow-start restent exclusivement décidés par Runtime et Governor.
|
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
|
## GPU et files
|
||||||
|
|
||||||
|
|
@ -110,12 +119,13 @@ reste inchangée.
|
||||||
|
|
||||||
## Profil interactif 8845HS mesuré
|
## Profil interactif 8845HS mesuré
|
||||||
|
|
||||||
- budget CPU Lardon3D : 12 threads logiques, 4 réservés au desktop ;
|
- budget CPU Lardon3D observé : 12 threads logiques, 4 réservés au desktop ;
|
||||||
- réserve `MemAvailable` : un quart de la RAM, environ 3,8 Gio ;
|
- réserve dure `MemAvailable` : 3 Gio ;
|
||||||
- hard floor `MemAvailable` : un huitième, environ 1,9 Gio ;
|
- zone de prudence `MemAvailable` : de 3 à 4 Gio ;
|
||||||
- Feature workers : 1 ; batch : 1 image ;
|
- Feature workers : 1 ; batch : 1 image ;
|
||||||
- Matcher workers : 1 ; lots adaptatifs 1, 2, 4 ou 8 Candidate Pairs ;
|
- 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 ;
|
- profondeur de la Task Queue : 64 tâches légères, un seul callback actif ;
|
||||||
- PSI CPU avg10 : nouvelle admission suspendue à 20 % ;
|
- PSI CPU avg10 : nouvelle admission suspendue à 20 % ;
|
||||||
- PSI mémoire avg10 : nouvelle admission suspendue à 1 % ;
|
- PSI mémoire avg10 : nouvelle admission suspendue à 1 % ;
|
||||||
|
|
@ -134,10 +144,13 @@ une distribution de latence estimator-only.
|
||||||
|
|
||||||
## Limites
|
## 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.
|
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
|
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
|
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
|
fixture synthétique de 100 000 landmarks et 500 000 observations, cinq passes
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,14 @@
|
||||||
**ACCEPTED** — architecture decision for the post-Gate C documentation freeze.
|
**ACCEPTED** — architecture decision for the post-Gate C documentation freeze.
|
||||||
|
|
||||||
Current Sparse SfM gates A through G are **PASS / FROZEN**.
|
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
|
This record is normative for the separation of responsibilities. Its original
|
||||||
an implementation, a public API, a persistence format or a roadmap commitment.
|
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
|
## Context
|
||||||
|
|
||||||
|
|
@ -24,7 +28,8 @@ resolver, runtime cache, resource handle or runtime dependency graph.
|
||||||
|
|
||||||
### 1. Persistent identity and metadata
|
### 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
|
projects, ScanSets, images, assets, Feature Sets, results, tracks and Sparse
|
||||||
SfM records. SHA-256 values identify published file contents. Metadata stores
|
SfM records. SHA-256 values identify published file contents. Metadata stores
|
||||||
sizes, states, provenance and project-relative artifact paths.
|
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.
|
The Resource Governor owns policy decisions for RAM, GPU, CPU and IO budgets.
|
||||||
It consumes immutable task estimates, samples system pressure, chooses bounded
|
It consumes immutable task estimates, samples system pressure, chooses bounded
|
||||||
lots and creates opaque transient reservations. A task must hold an active
|
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,
|
The Governor does not own persistent IDs, resolve paths, validate file formats,
|
||||||
load application objects, cache data or manage semantic dependencies.
|
load application objects, cache data or manage semantic dependencies.
|
||||||
|
|
@ -88,11 +96,13 @@ System by this decision.
|
||||||
|
|
||||||
## Resource Governor boundary
|
## Resource Governor boundary
|
||||||
|
|
||||||
The Governor remains resource-type agnostic. It answers whether an operation
|
The Governor remains independent of scientific/artifact identity. It answers
|
||||||
fits current budgets; it does not answer what an asset is or where it lives.
|
whether an operation fits current budgets; it does not answer what an asset is
|
||||||
Tasks create estimates, request reservations, execute bounded work and release
|
or where it lives. Tasks create estimates, request reservations, execute
|
||||||
reservations. The Governor owns only policy, budgets, pressure, lots and
|
bounded work and release reservations. The additive external-storage registry
|
||||||
reservations.
|
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
|
### Task demand declaration versus admission policy
|
||||||
|
|
||||||
|
|
@ -169,9 +179,12 @@ available_ram = max(
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
The implementation retains its existing checked, saturating subtraction order.
|
The implementation retains its checked, saturating subtraction order. On a
|
||||||
The host reserve remains one quarter of physical RAM and the emergency floor
|
capable host, the default hard/admission reserve is approximately 3 GiB of
|
||||||
remains one eighth of physical RAM.
|
`MemAvailable`; the 3–4 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
|
Possible overlap between `MemAvailable` and already-materialized memory from an
|
||||||
active reservation is accepted. The deliberate bias is false `WAIT`, not
|
active reservation is accepted. The deliberate bias is false `WAIT`, not
|
||||||
overcommit. Gate G adds no RSS tracking, materialization state, allocation
|
overcommit. Gate G adds no RSS tracking, materialization state, allocation
|
||||||
|
|
@ -215,17 +228,43 @@ topologies are deferred.
|
||||||
|
|
||||||
#### Scratch, persistence and science
|
#### Scratch, persistence and science
|
||||||
|
|
||||||
Scratch and external-storage support is excluded from Gate G core. It adds no
|
Scratch demand remains excluded from Gate G core: no scratch field was added to
|
||||||
scratch estimate fields, demand/availability API, manager, allocator, removable
|
`Lardon3DResourceEstimate`, and Sparse SfM still has no scratch consumer, spill
|
||||||
media monitor, mount logic, capacity policy, spill contract, cleanup contract,
|
algorithm or out-of-core path. Swap, zram and external SSD capacity never
|
||||||
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
|
|
||||||
enlarge scientific RAM admission.
|
enlarge scientific RAM admission.
|
||||||
|
|
||||||
No automatic `swapon`, `swapoff`, mount, unmount, formatting, partitioning or
|
An explicitly authorized, bounded SSD controller owns physical discovery and
|
||||||
destructive cleanup is authorized. A future feature requires a real task
|
lifecycle through UDisks2/GDBus: exact labels, stable Drive+filesystem UUID
|
||||||
consumer, an explicit task-specific contract, user opt-in where appropriate and
|
identity, fixed-capacity low-level scratch capabilities and safe drain. It is
|
||||||
a separate architecture review. No dormant generic API is introduced.
|
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,
|
Gate G core requires no Project DB v18, resource-history, reservation,
|
||||||
telemetry, policy or scratch table. Reservations and snapshots remain ephemeral;
|
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
|
`orb-vulkan-backend` test, `git diff --check`, and the complete human diff review
|
||||||
pass. No Gate G human decision or implementation blocker remains.
|
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
|
## Project DB boundary
|
||||||
|
|
||||||
The historical Project DB v16 migration and Sparse SfM reconstruction model
|
The historical Project DB v16 migration and Sparse SfM reconstruction model
|
||||||
remain unchanged and frozen. Gate F is authorized to advance the current schema
|
remain unchanged and frozen. Gate F advanced that historical head to v17 only
|
||||||
head to v17 only with the dedicated `sparse_sfm_tasks` typed-payload table; that
|
with `sparse_sfm_tasks`; later authorized migrations through v23 likewise add no
|
||||||
task persistence does not add a resource table, handle table, cache table,
|
resource table, handle table, cache table, dependency table or Governor policy
|
||||||
dependency table or resource-policy field.
|
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
|
This resource-boundary decision itself authorizes no future schema expansion,
|
||||||
change or persistence API redesign is part of this resource-boundary scope.
|
bundle redesign, Track Model change, Track Builder change or persistence API
|
||||||
|
redesign.
|
||||||
|
|
||||||
## Options considered
|
## Options considered
|
||||||
|
|
||||||
### A. Governor evolution
|
### A. Governor evolution
|
||||||
|
|
||||||
Rejected for the current ticket. The Governor is already the correct owner of
|
Rejected by the original no-new-subsystem decision when it meant scientific
|
||||||
budgets, estimates, pressure, lots and reservations. Extending it toward
|
identity or artifact ownership. The later explicitly authorized, additive
|
||||||
identity or artifact ownership would violate the current boundary.
|
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
|
### 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 persistent identities are sufficient.
|
||||||
- Existing stores already validate their own artifacts.
|
- Existing stores already validate their own artifacts.
|
||||||
- Existing task and backend ownership is explicit and bounded.
|
- Existing task and backend ownership is explicit and bounded.
|
||||||
- Existing Governor policy is sufficient for resource admission.
|
- Existing Governor policy remains sufficient for RAM/CPU/GPU/I/O admission.
|
||||||
- No new public syntax or ABI is required.
|
- No new persistent syntax or replacement ABI is required; later observation
|
||||||
|
and scratch APIs are additive C17 surfaces.
|
||||||
- Gate B v16 remains untouched.
|
- Gate B v16 remains untouched.
|
||||||
- The decision is reversible if a concrete future requirement appears.
|
- 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.
|
- Generic dependency graph or cycle management.
|
||||||
- Streaming or asynchronous resource loading.
|
- Streaming or asynchronous resource loading.
|
||||||
- CPU/GPU dual-residency manager.
|
- 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 v18–v22 and current v23 migrations remain
|
||||||
|
governed by their own persistence contracts.
|
||||||
- Bundle redesign.
|
- Bundle redesign.
|
||||||
- Governor integration with persistent asset identity.
|
- Governor integration with persistent asset identity.
|
||||||
- BA, new orchestration, Task Runtime redesign or renderer redesign.
|
- BA, new orchestration, Task Runtime redesign or renderer redesign.
|
||||||
|
|
@ -351,20 +409,29 @@ contracts remain authoritative in their own domains.
|
||||||
|
|
||||||
## Persistence impact
|
## Persistence impact
|
||||||
|
|
||||||
The historical Project DB v16 migration remains unchanged. Gate F adds only the
|
The historical Project DB v16 migration remains unchanged. Gate F added only
|
||||||
v17 typed-task payload already described above. Existing IDs, hashes, metadata
|
the v17 typed-task payload described above; the current v23 overlay is additive
|
||||||
and relative paths are not renamed, generalized or duplicated.
|
and creates no backfill. Existing scientific IDs, hashes, metadata and relative
|
||||||
|
paths are not renamed, generalized or duplicated by resource management.
|
||||||
|
|
||||||
## Memory / resource impact
|
## Memory / resource impact
|
||||||
|
|
||||||
None beyond existing task estimates and Governor reservations. No new cache or
|
The original no-new-subsystem decision added none beyond existing task
|
||||||
resident object ownership is introduced. Existing bounded readers and backend
|
estimates and Governor reservations. The later SSD controller owns only its
|
||||||
buffers remain responsible for their own memory.
|
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
|
## Concurrency impact
|
||||||
|
|
||||||
None. No new shared mutable state, worker, callback, lock or lifetime protocol
|
The original decision introduced no shared mutable state, worker, callback,
|
||||||
is introduced. Existing Governor mutex/condition ownership remains unchanged.
|
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
|
## Security impact
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
un autre contrat. Les dimensions retenues, le corpus S21 complet, les suites
|
||||||
portable/Vulkan, les sanitizers et l'audit XHIGH ferment le statut v2.
|
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
|
**PORTABLE_HOST_POLICY_MAINTENANCE — IMPLEMENTED / VALIDATED / REVIEWED.** La
|
||||||
et la topologie package/core Linux. Il réserve des groupes de coeurs physiques
|
maintenance ultérieure retire les plafonds matériels globaux issus de l'hôte
|
||||||
complets, frères SMT inclus, en préférant déterministiquement les plus grands
|
de preuve, sans changer la science, les formats, les fingerprints ni la
|
||||||
IDs package/core. Avec le masque unrestricted 0–15 et une réserve logique de
|
durabilité v2. Son lifecycle global reste suivi par
|
||||||
quatre, il dérive le pool lourd `0-5,8-13` et réserve `6,7,14,15` au desktop
|
[`GLOBAL_MAINTENANCE_AUDIT`](global_maintenance_audit.md).
|
||||||
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
|
Le Governor lit le masque d'affinité permis et la topologie package/core Linux.
|
||||||
pool de calcul sans seconde soustraction. Sans affinité/topologie exploitable,
|
La politique par défaut demande quatre CPU logiques de réserve lorsque c'est
|
||||||
le budget portable `logical_total - reserve` subsiste sans exclusion arbitraire
|
praticable et conserve au moins un CPU de calcul sur un petit hôte. Avec une
|
||||||
de frères SMT et l'affinité est diagnostiquée inactive. Cette politique privée
|
topologie complète, elle réserve des groupes de coeurs physiques complets,
|
||||||
est **PASS / FROZEN** sur le profil validé ; aucun ID CPU n'est persisté ni ne
|
frères SMT inclus, en choisissant le sous-ensemble déterministe dont le
|
||||||
devient une identité scientifique.
|
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 0–15, 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
|
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
|
(`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 Matcher reste validé dans `1..12`, indépendamment du lot :
|
||||||
`cpu_threads=12, batch_size=1` demeure un contrat valide.
|
`cpu_threads=12, batch_size=1` demeure un contrat valide.
|
||||||
|
|
||||||
La cible Compute Governor v2 conserve 3 GiB de `MemAvailable` et ne franchit
|
La politique par défaut conserve environ 3 GiB de `MemAvailable` comme réserve
|
||||||
pas intentionnellement le plancher dur de 2 GiB sur cette classe d'hôte 16 GiB.
|
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 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
|
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
|
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.
|
réécrivent pas rétroactivement les constantes Gate G core gelées ci-dessous.
|
||||||
Les observations saines autorisent une croissance lente. Pour une dimension
|
Les observations saines autorisent une croissance lente. Pour une dimension
|
||||||
CPU réellement réductible, la rampe est exactement `1 → 2 → 4 → 8 → 12`,
|
CPU réellement réductible, la rampe double depuis 1 et inclut toujours le
|
||||||
bornée par l'enveloppe et le compute-pool. Deux séquences établissent d'abord
|
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.
|
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
|
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
|
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
|
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
|
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
|
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 3–4 Gio,
|
||||||
|
ce qui est interdit. La capacité
|
||||||
rapportée ne forme donc aucun second budget VRAM indépendant sur UMA.
|
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é,
|
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
|
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é.
|
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. |
|
| `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. Un garde Queue applique/restaure CPU1 ; lot et contexte sont honnêtes. |
|
| `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` | 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é. |
|
| `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 | Le callback consomme exactement le lot admis et réadmet entre lots. |
|
| `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..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` | 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..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.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..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. |
|
| `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..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. |
|
| `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..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. |
|
| `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 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. |
|
| `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 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`. |
|
| `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 atomique. La reprise valide le scope mais ne recalcule pas l'estimation avant admission. |
|
| `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; BA à un thread. La forme est redérivée à la reprise sans réconciliation de l'estimation persistée. |
|
| `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 | Exécution atomique; 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 | 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
|
Ces bornes sont opérationnelles, externes-library, algorithmiques, mesurées ou
|
||||||
créent ni nouvelle limite scientifique, ni nouvelle identité, ni modification
|
scientifiques selon la dernière colonne. Elles ne créent aucune limite de
|
||||||
du schéma Project DB. Une enveloppe privée de capacités peut corriger la
|
cardinalité scientifique, identité ou migration Project DB. Les formes
|
||||||
sélection/réconciliation sans modifier l'ABI C public du Task Kind Registry.
|
historiques CPU1/CPU8/CPU12 citées sont des signatures exactes de reprise, pas
|
||||||
|
des plafonds portables de production.
|
||||||
|
|
||||||
## SIFT v1A
|
## SIFT v1A
|
||||||
|
|
||||||
Une extraction SIFT demande jusqu'à douze threads CPU, un slot IO, aucun GPU,
|
Une extraction SIFT demande jusqu'au compute-pool hôte, un slot IO, aucun GPU,
|
||||||
pour une image. L'estimation structurelle conservatrice est environ 1,06 Gio
|
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.
|
(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
|
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
|
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
|
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
|
métriques ; 12 reste une preuve de l'hôte courant, pas un maximum portable.
|
||||||
Set.
|
Cette dimension opérationnelle ne change ni fingerprint ni Feature Set.
|
||||||
|
|
||||||
## Responsabilité
|
## 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
|
IO). Il arbitre les ressources disponibles et calcule les lots adaptatifs pour
|
||||||
chaque tâche.
|
chaque tâche.
|
||||||
|
|
||||||
Le profil interactif par défaut conserve un quart de la RAM détectée et un
|
Le profil interactif par défaut conserve quatre threads logiques lorsque
|
||||||
quart des threads logiques pour le système hôte. Sur 16 Gio/16 threads, cela
|
praticable et au moins un thread de calcul sur un petit hôte. Sur 16 threads,
|
||||||
donne environ 3,8 Gio et 4 threads de headroom. Une nouvelle admission attend
|
cela donne 4 threads de headroom ; avec une topologie fiable, la sélection porte
|
||||||
également lorsque PSI CPU `some avg10` atteint 20 %, ou PSI mémoire 1 %. Ces
|
sur des coeurs physiques complets et peut dépasser minimalement la cible
|
||||||
signaux n'interrompent jamais le petit job déjà réservé. Ces valeurs décrivent
|
logique. Une nouvelle admission attend également lorsque PSI CPU `some avg10`
|
||||||
le profil Gate G core gelé; la cible opérationnelle v2 active est le couple
|
atteint 20 %, ou PSI mémoire 1 %. Ces signaux n'interrompent jamais le petit job
|
||||||
3 GiB/2 GiB et les signaux différentiels définis en tête de document.
|
déjà réservé.
|
||||||
|
|
||||||
La soft floor vaut un quart et la hard floor un huitième de la RAM détectée. La
|
La réserve dure/admission RAM vaut environ 3 GiB sur un hôte capable. La bande
|
||||||
soft floor place le Governor au minimum en YELLOW ; la hard floor le place
|
3–4 GiB place le Governor au minimum en YELLOW et remet la croissance à 1 sans
|
||||||
immédiatement en RED. Le premier delta swap entre deux snapshots produit
|
soustraire 4 GiB de la capacité. La hard floor place immédiatement en RED. Le
|
||||||
YELLOW. Un second delta consécutif produit RED. Le premier snapshot ne constitue
|
premier snapshot swap constitue seulement une baseline ; un delta actif produit
|
||||||
qu'une baseline et n'est jamais interprété comme une activité récente.
|
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
|
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
|
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é
|
## Contrat Gate G gelé
|
||||||
|
|
||||||
**PASS / FROZEN.** Les constantes existantes ci-dessus
|
**PASS / FROZEN.** Le contrat d'admission conserve le modèle conservateur
|
||||||
restent inchangées. La RAM disponible conserve le modèle conservateur
|
|
||||||
`min(MemAvailable, RAM physique) - réserve hôte - réservations actives`, borné à
|
`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
|
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.
|
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
|
RSS ou monitoring live n'appartient à Gate G core. L'observation courante
|
||||||
bornée RSS/HWM de Compute Governor v2 reste strictement diagnostique.
|
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
|
## API principale
|
||||||
|
|
||||||
### Création et destruction
|
### 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_wait_for_change()` - Attendre un changement
|
||||||
- `lardon3d_resource_governor_pressure()` - Lire GREEN, YELLOW ou RED
|
- `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
|
## 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
|
2. Le Resource Governor est l'unique propriétaire des budgets
|
||||||
3. Les réservations sont obligatoires avant toute exécution
|
3. Les réservations sont obligatoires avant toute exécution
|
||||||
4. Les réservations sont libérées exactement une fois
|
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)
|
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
|
7. Un contrat de séquence est immutable jusqu'à sa libération; seule la
|
||||||
séquence suivante peut être adaptée
|
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
|
## 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
|
l'échantillon peut conserver taille/durée mais n'alimente jamais l'adaptation
|
||||||
mémoire.
|
mémoire.
|
||||||
- Pas de communication inter-classes de tâches
|
- 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
|
un slot I/O, avec
|
||||||
64 Mio fixes et 512 Mio par image. Cette estimation conservatrice couvre le
|
64 Mio fixes et 512 Mio par image. Cette estimation conservatrice couvre le
|
||||||
chemin actuel sans prétendre mesurer les allocations internes d'OpenCV.
|
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,
|
nombre immutable au pool OpenCV. `record_batch` couvre la validation source,
|
||||||
le décodage, ORB, la publication
|
le décodage, ORB, la publication
|
||||||
et la finalisation DB ; `peak_memory_bytes == 0` signifie « mesure inconnue ».
|
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
|
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,
|
callback compte comme participant et crée au plus `cpu_threads - 1` enfants,
|
||||||
tous joints avant publication et rupture de séquence. Chaque participant
|
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
|
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
|
partitionnent le buffer borné du segment. `record_batch` compte uniquement
|
||||||
les memberships commités et conserve la mémoire inconnue à zéro.
|
les memberships commités et conserve la mémoire inconnue à zéro.
|
||||||
- `candidate_pair.generate` demande jusqu'à douze threads CPU, un slot I/O,
|
- `candidate_pair.generate` demande jusqu'à soixante-quatre threads CPU, un
|
||||||
256 Kio fixes et 64 Kio par Feature Set, par lots de 1 à 64. Le GPU vaut
|
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 uniquement l'ancienne estimation exacte
|
zéro. La reconstruction reconnaît la forme immédiatement antérieure exacte
|
||||||
(128 Kio fixes, 64 Kio par item, un thread CPU, un slot I/O, aucun GPU, lots
|
(256 Kio fixes, 64 Kio par item, CPU12) et la plus ancienne forme sérielle
|
||||||
1 à 64, classe CPU) et la normalise éphémèrement vers l'estimation courante
|
exacte (128 Kio fixes, 64 Kio par item, CPU1), toutes deux à lot 1..64,
|
||||||
complète ; aucun checkpoint d'estimation seule n'est publié et une forme
|
I/O1, GPU0, classe CPU. Elle les normalise éphémèrement vers la forme courante
|
||||||
voisine n'est jamais réinterprétée comme legacy.
|
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
|
`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 ».
|
réelle du lot ; `peak_memory_bytes == 0` signifie « mesure inconnue ».
|
||||||
Chaque séquence interroge le Visual Index pour jusqu'à 64 memberships. Le
|
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
|
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
|
ordre canonique. Cette estimation opérationnelle ne limite pas la taille
|
||||||
scientifique du dataset.
|
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
|
- `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 à
|
Candidate Pair admise, correspondant au working set contrôlé inférieur à
|
||||||
environ 10 Mio par paire au
|
environ 10 Mio par paire au
|
||||||
|
|
@ -554,3 +653,10 @@ sont gelées ; il ne reste aucune décision humaine Gate G.
|
||||||
## Statut
|
## Statut
|
||||||
|
|
||||||
**GATE G — PASS / FROZEN.**
|
**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.
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,12 @@
|
||||||
## Modèle d'exécution
|
## Modèle d'exécution
|
||||||
|
|
||||||
### Threads
|
### Threads
|
||||||
- Thread principal : TUI ncursesw (exclusif)
|
- Thread principal : entrée, modèle de vue et rendu TUI ncursesw (exclusif)
|
||||||
- Thread worker : exécution des tâches métier
|
- 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
|
### Synchronisation
|
||||||
- Mutex pour les accès partagés
|
- Mutex pour les accès partagés
|
||||||
|
|
@ -16,7 +20,7 @@
|
||||||
```text
|
```text
|
||||||
1. Création (PENDING)
|
1. Création (PENDING)
|
||||||
2. Soumission à la file
|
2. Soumission à la file
|
||||||
3. Sélection par le scheduler
|
3. Sélection FIFO/adaptative par la Queue
|
||||||
4. Réservation obligatoire
|
4. Réservation obligatoire
|
||||||
5. Exécution (RUNNING)
|
5. Exécution (RUNNING)
|
||||||
- Pause/reprise coopérative
|
- Pause/reprise coopérative
|
||||||
|
|
@ -35,7 +39,7 @@
|
||||||
### Variables de condition
|
### Variables de condition
|
||||||
- Coordination entre threads
|
- Coordination entre threads
|
||||||
- Notification de changement d'état
|
- Notification de changement d'état
|
||||||
- Attente passive (pas de polling)
|
- Attente passive ; timeout borné seulement pour réévaluer un `WAIT` ressources
|
||||||
|
|
||||||
### Atomicité
|
### Atomicité
|
||||||
- Opérations indivisibles
|
- Opérations indivisibles
|
||||||
|
|
@ -44,9 +48,10 @@
|
||||||
## Gestion des erreurs
|
## Gestion des erreurs
|
||||||
|
|
||||||
### Rollback
|
### Rollback
|
||||||
- Retour à l'état précédent en cas d'échec
|
- Rollback des transactions locales avant publication
|
||||||
- Nettoyage complet des ressources
|
- Nettoyage complet des ressources possédées par l'opération
|
||||||
- Aucun état partiellement appliqué
|
- 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
|
### Récupération
|
||||||
- Reprise à la dernière frontière connue
|
- Reprise à la dernière frontière connue
|
||||||
|
|
@ -55,8 +60,9 @@
|
||||||
|
|
||||||
## Limites actuelles
|
## Limites actuelles
|
||||||
|
|
||||||
- Worker unique (pas de pools multiples)
|
- Worker Queue unique (pas de pools inter-Tasks multiples)
|
||||||
- Pas de parallélisme inter-tâches
|
- 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 automatique limitée aux tâches indépendantes reconstructibles
|
||||||
|
|
||||||
## Reprise durable
|
## 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
|
resoumet automatiquement les tâches production valides. Il retourne après
|
||||||
l'enqueue et n'attend jamais leur terminaison.
|
l'enqueue et n'attend jamais leur terminaison.
|
||||||
|
|
||||||
L'ordre d'initialisation production est : profil matériel, governor, queue et
|
L'ordre d'initialisation production est : politique driver, profil matériel,
|
||||||
worker, TUI, puis ouverture DB/projet et reprise synchrone. Une fermeture ne
|
Governor, backend, Queue/worker, contrôleur SSD optionnel et binding
|
||||||
peut commencer qu'après le retour de `project_open()`. Le worker peut consommer
|
Governor, puis TUI. L'ouverture DB/projet et la reprise synchrone sont pilotées
|
||||||
pendant le scan ; chaque tâche exécutée est néanmoins réadmise normalement.
|
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
|
**NOT_YET_WIRED** — reprise ordonnée par dépendances/DAG. Les kinds de
|
||||||
dépendances.
|
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
|
## Accès Project Database
|
||||||
|
|
||||||
|
|
@ -100,8 +111,14 @@ dépendances.
|
||||||
les opérations multi-tables sont transactionnelles et bornées.
|
les opérations multi-tables sont transactionnelles et bornées.
|
||||||
|
|
||||||
**IMPLEMENTED** — le cycle de vie projet ouvre/crée `project.db`, vérifie
|
**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
|
l'identité et ferme la connexion. Ouvrir, fermer ou changer de projet est une
|
||||||
fermeture finale du projet.
|
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
|
**IMPLEMENTED** — la registry reconstruit explicitement callback/userdata hors
|
||||||
mutex DB pour un kind connu ; elle ne soumet aucune tâche.
|
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
|
commitée. Un segment temporaire n'est jamais visible et un rejeu exclut les
|
||||||
Feature Sets déjà membres.
|
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
|
## Invariants
|
||||||
|
|
||||||
- ncurses appartient exclusivement au thread principal
|
- ncurses appartient exclusivement au thread principal
|
||||||
- Aucune tâche ne démarre sans réservation active
|
- Aucune tâche ne démarre sans réservation active
|
||||||
- Les réservations sont libérées exactement une fois
|
- Les réservations sont libérées exactement une fois
|
||||||
- Les buffers sont strictement bornés
|
- 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.
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
# Intégration Scheduler ↔ Resource Governor
|
# Intégration Task Queue ↔ Resource Governor
|
||||||
|
|
||||||
## Responsabilité
|
## 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
|
## Frontières de responsabilités
|
||||||
|
|
||||||
### Scheduler
|
### Task Queue / runtime
|
||||||
- Ordonnancement FIFO
|
- Ordonnancement FIFO stable avec bypass des seuls `WAIT`
|
||||||
- Exécution des callbacks
|
- Exécution des callbacks
|
||||||
- Gestion des états de tâche
|
- Gestion des états de tâche
|
||||||
- Sélection de la première tâche admissible
|
- 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
|
- Calcul des lots adaptatifs
|
||||||
- Réservations opaques
|
- Réservations opaques
|
||||||
- Historique de métriques
|
- 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
|
## Cycle d'exécution
|
||||||
|
|
||||||
```text
|
```text
|
||||||
1. Task → Estimate (estimation des ressources)
|
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)
|
3. Governor → Reservation (réservation opaque)
|
||||||
4. Scheduler → Worker (exécution)
|
4. Queue → Worker (exécution)
|
||||||
5. Worker → Governor → record_batch() (métriques)
|
5. Worker → Governor → record_batch() (métriques)
|
||||||
6. Governor → release() (libération)
|
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
|
- `REJECT` : rejeter la tâche
|
||||||
|
|
||||||
### Comportement en cas de WAIT
|
### 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
|
- Il évalue la tâche suivante
|
||||||
- Pas de blocage de la file
|
- Pas de blocage de la file
|
||||||
- Si aucune tâche n'est admissible mais qu'un `WAIT` PENDING subsiste, le worker
|
- 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
|
## Invariants
|
||||||
|
|
||||||
1. Aucune tâche ne passe de PENDING à RUNNING sans réservation active
|
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
|
3. La libération des réservations s'effectue exactement une fois par cycle/séquence
|
||||||
4. Les atomicités sont garanties sous mutex
|
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)
|
- Worker unique (pas de pools multiples)
|
||||||
- Pas de DAG de dépendances
|
- Pas de DAG de dépendances
|
||||||
- Pas de priorités
|
- 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
|
## Statut : GATE G — PASS / FROZEN
|
||||||
|
|
|
||||||
|
|
@ -725,7 +725,11 @@ translations are exhaustive:
|
||||||
|
|
||||||
Every row is fingerprinted. Integer fields require no normalization beyond
|
Every row is fingerprinted. Integer fields require no normalization beyond
|
||||||
their fixed-width little-endian translation; every floating field uses the
|
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,
|
The policy IDs above freeze the full named Gate D and Gate E v1 semantics,
|
||||||
including canonical order and tie breaks,
|
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
|
### Gate F durable task payload
|
||||||
|
|
||||||
**FROZEN.** Gate F advances the current Project Database schema head from v16
|
**FROZEN.** Gate F advanced the then-current Project Database schema head from
|
||||||
to v17 with one strictly additive `sparse_sfm_tasks` table. The historical v16
|
v16 to v17 with one strictly additive `sparse_sfm_tasks` table. The historical v16
|
||||||
migration and its immutable reconstruction model remain unchanged. The new
|
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
|
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
|
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
|
Gate C ground-truth and degeneracy evidence; future orchestration may choose
|
||||||
other explicitly fingerprinted configurations.
|
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
|
### Gate C tested threshold set
|
||||||
|
|
||||||
The pure API has no hidden defaults; callers provide all acceptance settings.
|
The pure API has no hidden defaults; callers provide all acceptance settings.
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
| `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 |
|
| `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 |
|
| `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` | `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` | CPU1 historique exact → durable CPU12; runtime 1..12 |
|
| `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` | CPU1 historique exact → durable CPU12; runtime 1..12 |
|
| `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` | CPU runtime 1..12 ; feedback par segment durable |
|
| `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` | Forme sérielle historique exacte → CPU12 ; runtime CPU 1..12 et lot 1..64 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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
|
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
|
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
|
compute-pool borne l'admission de chaque kind. Feature/SIFT/RootSIFT utilisent
|
||||||
CPU12. Feature/SIFT/RootSIFT consomment le compte immutable 1..12 dans OpenCV ;
|
le maximum `int` positif comme borne de l'API OpenCV, puis consomment le compte
|
||||||
les kinds CPU1 restent fixes. Aucun ID CPU ou choix d'affinité n'entre dans le
|
immutable réellement admis ; les CPU12 durables ne sont plus que des signatures
|
||||||
descriptor, le checkpoint ou le Project DB.
|
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
|
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
|
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`.
|
`PUBLISHED_NOT_DURABLE`.
|
||||||
|
|
||||||
L'état privé par kind/backend coordonne désormais une seule dimension d'essai.
|
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,
|
Les CPU réductibles slow-startent par doubles successifs depuis 1, puis le
|
||||||
deux observations à au moins +5 % sont nécessaires pour accepter le palier.
|
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
|
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
|
son lot peuvent ouvrir un essai de lot. `features.extract`, SIFT et RootSIFT
|
||||||
enregistrent une observation atomique réussie partagée entre Tasks ; Visual
|
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
|
**IMPLEMENTED** — `project_open()` utilise la registry production immutable
|
||||||
pour restaurer hors mutex DB et transférer chaque tâche acceptée à la queue.
|
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
|
**IMPLEMENTED** — `features.extract` version 1 reconstruit une extraction ORB
|
||||||
depuis `image_id` et ses paramètres bornés.
|
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
|
**IMPLEMENTED** — `candidate_pair.generate`, version 1, recharge
|
||||||
`visual_index_id + after_feature_set_id + top_k + minimum_evidence_count
|
`visual_index_id + after_feature_set_id + top_k + minimum_evidence_count
|
||||||
+ scanset_filter + exclude_same_asset` depuis `candidate_pair_generate_tasks`
|
+ scanset_filter + exclude_same_asset` depuis `candidate_pair_generate_tasks`
|
||||||
et reconstruit un contexte boundé. La restauration reconnaît uniquement le
|
et reconstruit un contexte boundé. La restauration reconnaît le snapshot
|
||||||
snapshot opérationnel sériel historique v1 exact et remplace éphémèrement sa
|
opérationnel sériel historique v1 exact et la forme CPU12 immédiatement
|
||||||
demande d'un thread CPU par douze avant admission. Le checkpoint historique et
|
antérieure, puis les remplace éphémèrement par la demande courante CPU64 avec
|
||||||
le curseur typé restent inchangés. Les snapshots Candidate courants et tous les
|
8 Mio par item avant admission. Le checkpoint historique et le curseur typé
|
||||||
autres kinds restent inchangés.
|
restent inchangés ; une forme voisine est rejetée.
|
||||||
|
|
||||||
**PASS / FROZEN — Compute Governor v2.** `matcher.run`, version 1,
|
**PASS / FROZEN — Compute Governor v2.** `matcher.run`, version 1,
|
||||||
recharge la configuration Matcher, l'identité Feature Set et le curseur
|
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
|
uniquement la politique AUTO/fixe déduite de la signature durable, jamais un
|
||||||
pipeline de benchmark.
|
pipeline de benchmark.
|
||||||
|
|
||||||
**IMPLEMENTED** — `features.extract.sift` et `features.extract.rootsift`
|
**IMPLEMENTED** — ORB, SIFT et RootSIFT acceptent leurs formes CPU12/CPU1
|
||||||
acceptent leur forme CPU12 courante et normalisent uniquement leur forme CPU1
|
historiques complètes et les normalisent vers la demande OpenCV portable
|
||||||
historique exacte. ORB/SIFT/RootSIFT appliquent ensuite le compte CPU admis dans
|
`INT_MAX`. Le Governor borne l'exécution au compute-pool ; les sorties testées
|
||||||
`1..12`; les sorties testées à 1/2/4/8/12 restent égales. Cette compatibilité
|
à 1/2/4/8/12 restent égales. Cette compatibilité opérationnelle n'altère ni
|
||||||
opérationnelle n'altère ni fingerprint, Feature Set, checkpoint durable, ni
|
fingerprint, Feature Set, checkpoint durable, ni politique scientifique.
|
||||||
politique scientifique.
|
|
||||||
|
|
||||||
**IMPLEMENTED** — `geometric_verifier.run`, version 1, recharge la configuration
|
**IMPLEMENTED** — `geometric_verifier.run`, version 1, recharge la configuration
|
||||||
Fundamental immuable, en revalide le fingerprint et reprend `after_match_result_id`.
|
Fundamental immuable, en revalide le fingerprint et reprend `after_match_result_id`.
|
||||||
Project DB v13 ajoute uniquement `geometric_verifier_tasks`, car le checkpoint
|
Project DB v13 ajoute uniquement `geometric_verifier_tasks`, car le checkpoint
|
||||||
générique v1 ne possède aucun payload propre au kind.
|
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
|
**IMPLEMENTED** — `track_builder.run`, version 1, reconstruit un scope explicite
|
||||||
depuis son payload Project DB v15 et son asset little-endian validé. Le callback
|
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,
|
réutilise l'orchestration Gate C et le reconstructeur refuse toute corruption,
|
||||||
|
|
|
||||||
|
|
@ -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
|
unique, gère la sélection de la prochaine tâche admissible et transmet les
|
||||||
callbacks de résultat.
|
callbacks de résultat.
|
||||||
|
|
||||||
La file est le point central entre le scheduler (soumission) et les workers
|
La file est le point central entre les producteurs et le worker d'exécution.
|
||||||
(exécution). Elle ne décide jamais des ressources — elle applique uniquement
|
Les responsabilités historiques de scheduler sont portées par cette Queue et
|
||||||
l'ordre FIFO et consulte le gouverneur via le scheduler.
|
le runtime existants : aucun second scheduler n'existe. La Queue ne décide
|
||||||
|
jamais des ressources ; elle demande l'admission au Governor.
|
||||||
|
|
||||||
## Fichiers
|
## 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_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_pause()` / `resume()` | Contrôle une tâche par son ID stable |
|
||||||
| `lardon3d_task_queue_cancel()` | Demande l'annulation par ID |
|
| `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_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.
|
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.
|
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
|
3. La première tâche `PENDING` admissible (non terminale) est
|
||||||
retournée.
|
retournée.
|
||||||
4. Si aucune tâche n'est admissible, le worker attend un changement.
|
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
|
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
|
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
|
1. **Worker unique** : une seule tâche s'exécute à la fois. Pas de parallélisme
|
||||||
interne à la file.
|
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
|
3. **Réservation obligatoire** : aucune tâche n'est exécutée sans réservation
|
||||||
validée par le gouverneur.
|
validée par le gouverneur.
|
||||||
4. **Callback unique** : chaque tâche reçoit exactement un callback (succès,
|
4. **Notification terminale unique** : le callback terminé de la Task retourne
|
||||||
échec ou annulation).
|
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
|
5. **Annulation sûre** : annuler une tâche en cours la met en état
|
||||||
`CANCELLED` sans interrompre brutalement le worker.
|
`CANCELLED` sans interrompre brutalement le worker.
|
||||||
6. **Nettoyage complet** : `task_queue_destroy()` libère toutes les tâches
|
6. **Retraite prompte et bornée** : après retour du callback terminé, la vraie
|
||||||
restantes, y compris celles en cours d'exécution.
|
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
|
## Interactions
|
||||||
|
|
||||||
- **task** : chaque entrée de la file est un `Lardon3DTask` avec son état et sa
|
- **task** : chaque entrée de la file est un `Lardon3DTask` avec son état et sa
|
||||||
progression.
|
progression.
|
||||||
- **scheduler** : la file applique la soumission FIFO et orchestre
|
- **runtime / producteurs** : soumettent les Tasks ; la Queue applique FIFO et
|
||||||
l'exécution via le worker.
|
orchestre l'exécution via le worker.
|
||||||
- **resource_governor** : la file consulte le gouverneur (via le scheduler)
|
- **resource_governor** : la file consulte directement le gouverneur avant
|
||||||
avant d'exécuter chaque tâche.
|
d'exécuter chaque tâche.
|
||||||
- **hardware_profile / resource_snapshot** : informations matériel utilisées
|
- **hardware_profile / resource_snapshot** : informations matériel utilisées
|
||||||
par le gouverneur pour les réservations.
|
par le gouverneur pour les réservations.
|
||||||
|
|
||||||
## Statut
|
## 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
|
sélection adaptative, réévaluation autonome des `WAIT`, pause, annulation
|
||||||
coopérative et accueil des tâches restaurées avec ID préassigné.
|
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
|
`PENDING` en DB et seront réévaluées lors d'une ouverture ultérieure. Ce n'est
|
||||||
pas un second scheduler.
|
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
|
## 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 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
|
- 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.
|
une place se libère pendant la session courante.
|
||||||
- Pas de pool de workers CPU/IO/GPU.
|
- Pas de pool de workers CPU/IO/GPU.
|
||||||
|
|
|
||||||
|
|
@ -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_sequence_break()` | Libère puis renouvelle la réservation |
|
||||||
| `lardon3d_task_snapshot()` | Copie l'état d'observation runtime |
|
| `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
|
### API durable
|
||||||
|
|
||||||
| Fonction | Description |
|
| 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
|
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
|
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
|
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
|
résumé est promue sous le même verrou puis reprise. Un ancien projet v22 ou
|
||||||
possède que `.chk` reste ainsi récupérable. Toute autre absence, version non
|
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
|
supportée, corruption ou divergence interdit la reprise de cette tâche sans
|
||||||
affecter les autres entrées de l'inventaire.
|
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.
|
`lardon3d_task_checkpoint()`. Aucun autre thread ne force son arrêt.
|
||||||
4. **Progression bornée** : la progression ne peut jamais dépasser la valeur
|
4. **Progression bornée** : la progression ne peut jamais dépasser la valeur
|
||||||
maximale définie par l'estimation.
|
maximale définie par l'estimation.
|
||||||
5. **Reprise réadmise** : une tâche restaurée non terminale repasse par la file,
|
5. **Reprise réadmise** : une tâche restaurée non terminale repasse par la
|
||||||
le scheduler et le Resource Governor avec une nouvelle réservation.
|
Queue/runtime et le Resource Governor avec une nouvelle réservation.
|
||||||
6. **Snapshot court** : seuls les champs durables sont copiés sous le mutex ;
|
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.
|
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
|
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,
|
(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
|
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
|
**REJECTED_WITH_MEASURED_REASON** pour AUTO normal. Cette décision Matcher est
|
||||||
en cours jusqu'aux réconciliations restantes.
|
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
|
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
|
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.
|
Vulkan partiel.
|
||||||
|
|
||||||
Cette boucle est maintenant `observe → choose → execute → measure → adapt next`.
|
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
|
Les CPU réductibles progressent par paliers de puissances de deux, puis vers la
|
||||||
compute-pool ; CPU et lot ne sont jamais essayés ensemble. Les observations
|
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
|
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
|
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
|
ê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 à
|
**IMPLEMENTED** — resoumission automatique sélective des tâches production à
|
||||||
l'ouverture du projet.
|
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
|
**IMPLEMENTED** — `visual_index.update` traite jusqu'à seize Feature Sets par
|
||||||
séquence, checkpoint après commit de segment et repasse par le Governor.
|
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
|
**PASS / FROZEN — Compute Governor v2.** `matcher.run` traite une
|
||||||
Candidate Pair atomique à la fois, par lots opérationnels bornés jusqu'à 12.
|
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,
|
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
|
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
|
séquence, sans mutation pendant son exécution. Ces dimensions ne changent ni
|
||||||
identité scientifique ni publication. Le callback persiste le curseur
|
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
|
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.
|
reste 0 et le contrôle synchrone force depth 1.
|
||||||
|
|
||||||
**IMPLEMENTED** — `geometric_verifier.run` v1 traite un Match Result atomique à
|
**IMPLEMENTED** — `geometric_verifier.run` v1 traite des Match Results
|
||||||
la fois, par lots adaptatifs de 1, 2, 4 ou 8. Project DB v13 conserve sa
|
indépendants sous un callback propriétaire, avec jusqu'à 16 participants sûrs,
|
||||||
configuration scientifique et `after_match_result_id`. Chaque résultat est
|
8 utiles et 16 parents par lot. Project DB v13 conserve sa configuration
|
||||||
publié avant le curseur ; pause, annulation, checkpoint et rupture de séquence
|
scientifique et `after_match_result_id`. L'USAC interne reste
|
||||||
restent coopératifs aux frontières des parents et des lots.
|
`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
|
**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
|
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
|
donc valide pendant celui-ci et son destructeur n'est appelé qu'ensuite par la
|
||||||
destruction de la tâche.
|
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
|
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
|
localement : son userdata est détruit, sans callback terminal ni écriture
|
||||||
durable d'une fausse annulation. Une annulation explicitement demandée conserve
|
durable d'une fausse annulation. Une annulation explicitement demandée conserve
|
||||||
le contrat de notification terminale.
|
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
|
`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
|
ni estimation sérialisée complète, ni callback, ni réservation, et ne remplace
|
||||||
pas la validation du fichier checkpoint avant `task_restore()`.
|
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
|
appeler `task_restore()` que via un descriptor connu ; aucun pointeur n'est
|
||||||
persistant.
|
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
|
## 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.
|
- Pas encore de DAG pour ordonner des reprises interdépendantes.
|
||||||
- Aucune dépendance inter-tâches (pas de DAG).
|
- 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.
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ valide. La reprise recommence au dernier curseur commité et l'unicité des
|
||||||
memberships rend le rejeu idempotent.
|
memberships rend le rejeu idempotent.
|
||||||
|
|
||||||
**IMPLEMENTED — parallélisme interne borné.** La Queue exécute toujours un seul
|
**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
|
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
|
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
|
exclusivement des Feature Files immuables et écrit une tranche privée ; il ne
|
||||||
|
|
|
||||||
|
|
@ -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
|
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
|
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
|
de Lowe 0,7, aucune divergence Lowe n'a été observée. La comparaison exacte
|
||||||
n'est pas une garantie universelle. FP32 ne reproduit pas les distances bit à
|
compte néanmoins une divergence d'index top-2 et 20 251 distances aux bits
|
||||||
bit et un corpus d'égalités adversariales démontre
|
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
|
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
|
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
|
OpenCV. Le recalcul CPU des distances des deux candidats ne peut corriger une
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,14 @@ Ces contraintes exploitent la géométrie projective des caméras, la structure
|
||||||
|
|
||||||
## Statut
|
## 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 C–G. 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
|
## Place dans le pipeline
|
||||||
|
|
||||||
|
|
@ -128,7 +135,7 @@ typedef struct {
|
||||||
|
|
||||||
### 6. Triangle Quality Constraints
|
### 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 |
|
| Métrique | Seuil recommandé | Description |
|
||||||
|----------|------------------|-------------|
|
|----------|------------------|-------------|
|
||||||
|
|
@ -149,9 +156,12 @@ La qualité des triangles de triangulation影响 la précision de la reconstruct
|
||||||
|
|
||||||
## Contraintes de conception
|
## 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é.
|
- 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 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).
|
- 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).
|
- En cas d'incertitude, les contraintes sont conservatistes (rejeter plutôt qu'accepter).
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,15 @@
|
||||||
> [tracks.md](../architecture/tracks.md). Les différences notables :
|
> [tracks.md](../architecture/tracks.md). Les différences notables :
|
||||||
> le Track Model v1 ne contient aucune coordonnée 3D, aucun statut
|
> le Track Model v1 ne contient aucune coordonnée 3D, aucun statut
|
||||||
> (ACTIVE/OPTIMIZED/REJECTED), aucune matrice de co-visibilité et aucun
|
> (ACTIVE/OPTIMIZED/REJECTED), aucune matrice de co-visibilité et aucun
|
||||||
> plafond de longueur arbitraire. La triangulation, le Sparse SfM et le
|
> plafond de longueur arbitraire. Le Matcher, le Geometric Verifier et le
|
||||||
> Bundle Adjustment sont des étapes ultérieures séparées.
|
> 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
|
> 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
|
> 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
|
> pas des matches multivues et ne créent aucun track. Le Matcher production
|
||||||
> explicitement SIFT ou RootSIFT après génération des paires.
|
> choisit explicitement le Feature kind retenu après génération des paires.
|
||||||
|
|
||||||
## Définition
|
## Définition
|
||||||
|
|
||||||
|
|
@ -22,8 +24,10 @@ Le matching transforme les features individuelles en relations inter-images. Les
|
||||||
|
|
||||||
## Statut
|
## Statut
|
||||||
|
|
||||||
**PLANNED** — Étape critique du pipeline, pas encore implémentée. Visual Index
|
**ARCHIVE HISTORIQUE — SUPERSEDED.** Candidate Pair, Matching v1, Geometric
|
||||||
v1 fournit désormais les candidats, leur score et leurs preuves distinctes.
|
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
|
## 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 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
|
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 :
|
descriptor-descriptor final, ni une preuve épipolaire. La chaîne courante reste :
|
||||||
candidate Visual Index → futur matching → vérification géométrique → tracks.
|
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
|
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
|
pourra servir de signal secondaire ; la proximité dans un dossier et le nom de
|
||||||
fichier ne constituent jamais l'identité principale.
|
fichier ne constituent jamais l'identité principale.
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,11 @@ L'idée centrale est de décomposer la reconstruction en couches de complexité
|
||||||
|
|
||||||
## Statut
|
## 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
|
## 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. |
|
| **Matching & Tracks** | Fournissent l'entrée de la couche 0. |
|
||||||
| **Geometric Constraints** | Utilisées dans toutes les couches pour filtrer et valider. |
|
| **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. |
|
| **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. |
|
| **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. |
|
| **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. |
|
| **Viewer** | Le viewer affiche les snapshots des couches validées. |
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,9 @@ Project
|
||||||
courante proposée au pipeline scientifique ultérieur. La sélection ne modifie
|
courante proposée au pipeline scientifique ultérieur. La sélection ne modifie
|
||||||
ni l'image, ni son asset, ni les résultats existants.
|
ni l'image, ni son asset, ni les résultats existants.
|
||||||
- Une dérivation asset bornée parent→enfant, versionnée par fingerprint, rend
|
- 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
|
possible l'image de travail DERIVED de `raw.develop` sans confondre SOURCE et
|
||||||
RAW ou vidéo sont déjà importés.
|
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
|
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
|
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
|
Les primitives géométriques Sparse SfM Gate C et le noyau incrémental
|
||||||
synchrone en mémoire Gate D sont **IMPLEMENTED / PASS**.
|
synchrone en mémoire Gate D sont **IMPLEMENTED / PASS**.
|
||||||
|
|
||||||
**PLANNED** — BA, orchestration Sparse SfM, MVS et relations géométriques
|
**IMPLEMENTED / PASS / FROZEN aux frontières acquises** — BA finale Gate E,
|
||||||
entre ScanSets. La vérification/scrub des assets et la réconciliation globale
|
orchestration Sparse SfM Gate F, admission Gate G et Phase H v1. MVS-M1 fournit
|
||||||
restent également NOT_YET_WIRED.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -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 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é). |
|
| **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. |
|
| **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). |
|
| **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
|
## Contraintes de conception
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,16 @@
|
||||||
- **OS** : Linux (testé sur distributions récentes)
|
- **OS** : Linux (testé sur distributions récentes)
|
||||||
- **Compilateur** : Clang (recommandé) ou GCC
|
- **Compilateur** : Clang (recommandé) ou GCC
|
||||||
- **Système de build** : Meson + Ninja
|
- **Système de build** : Meson + Ninja
|
||||||
- **Dépendances** : ncursesw (ncurses avec support Unicode)
|
- **Dépendances principales** : ncursesw, SQLite, OpenSSL, GIO/GLib, OpenCV,
|
||||||
- **Langage** : C17
|
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
|
```sh
|
||||||
# Debian / Ubuntu
|
# Debian / Ubuntu
|
||||||
|
|
@ -24,7 +30,11 @@ sudo pacman -S clang meson ninja ncurses pkgconf
|
||||||
## Build standard
|
## Build standard
|
||||||
|
|
||||||
```sh
|
```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
|
meson compile -C build -j8
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -49,8 +59,69 @@ meson test -C build --print-errorlogs
|
||||||
|
|
||||||
# Vérification du style (whitespace)
|
# Vérification du style (whitespace)
|
||||||
git diff --check
|
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)
|
## Build ASan/UBSan (debug mémoire)
|
||||||
|
|
||||||
À exécuter pour tout ticket touchant la mémoire, les durées de vie ou les
|
À exécuter pour tout ticket touchant la mémoire, les durées de vie ou les
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ Worker thread
|
||||||
├── Exécution des tâches
|
├── Exécution des tâches
|
||||||
├── Calculs métier
|
├── Calculs métier
|
||||||
└── Écritures de résultats
|
└── É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
|
## Règles fondamentales
|
||||||
|
|
@ -48,7 +51,7 @@ pthread_mutex_unlock(&queue->mutex);
|
||||||
### 3. Variables de condition pour la synchronisation
|
### 3. Variables de condition pour la synchronisation
|
||||||
|
|
||||||
```c
|
```c
|
||||||
// Producteur (scheduler)
|
// Producteur (caller de la Task Queue)
|
||||||
pthread_mutex_lock(&queue->mutex);
|
pthread_mutex_lock(&queue->mutex);
|
||||||
queue->ready = true;
|
queue->ready = true;
|
||||||
pthread_cond_signal(&queue->cond);
|
pthread_cond_signal(&queue->cond);
|
||||||
|
|
@ -89,7 +92,7 @@ void worker_callback(task_t *task, void *userdata) {
|
||||||
| `pthread_cond_t` | Synchronisation producteur/consommateur |
|
| `pthread_cond_t` | Synchronisation producteur/consommateur |
|
||||||
| `pthread_create()` | Création des workers |
|
| `pthread_create()` | Création des workers |
|
||||||
| `pthread_join()` | Attente de fin 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
|
## Invariants de concurrence
|
||||||
|
|
||||||
|
|
@ -101,7 +104,7 @@ void worker_callback(task_t *task, void *userdata) {
|
||||||
le même ordre pour éviter les deadlocks.
|
le même ordre pour éviter les deadlocks.
|
||||||
|
|
||||||
3. **Annulation coopérative** : les workers vérifient périodiquement un
|
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.
|
4. **Réservation atomique** : la réservation du gouverneur est atomique.
|
||||||
Deux threads ne peuvent pas obtenir la même réservation.
|
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
|
invoqué sans réservation active. Cet invariant est maintenu même en
|
||||||
présence d'erreurs.
|
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
|
## Anti-patterns
|
||||||
|
|
||||||
### Deadlock
|
### Deadlock
|
||||||
|
|
@ -149,7 +192,7 @@ pthread_mutex_unlock(&task->mutex);
|
||||||
task_destroy(task);
|
task_destroy(task);
|
||||||
task_callback(task); // task est libéré
|
task_callback(task); // task est libéré
|
||||||
|
|
||||||
// ✅ Correct
|
// ✅ Correct : le callback est entièrement revenu avant la destruction
|
||||||
task_callback(task);
|
task_callback(task);
|
||||||
task_destroy(task);
|
task_destroy(task);
|
||||||
```
|
```
|
||||||
|
|
@ -171,10 +214,33 @@ meson test -C build-tsan --print-errorlogs
|
||||||
```
|
```
|
||||||
|
|
||||||
TSan détecte automatiquement :
|
TSan détecte automatiquement :
|
||||||
- les race conditions
|
|
||||||
- les deadlocks potentiels
|
- les accès concurrents conflictuels instrumentés ;
|
||||||
- les signaux perdus
|
- certaines utilisations incohérentes des primitives de synchronisation.
|
||||||
- les verrous non libérés
|
|
||||||
|
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
|
## 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 mutex sont toujours libérés (même en cas d'erreur)
|
||||||
- [ ] Les variables de condition sont vérifiées dans une boucle `while`
|
- [ ] Les variables de condition sont vérifiées dans une boucle `while`
|
||||||
- [ ] Aucun appel ncurses depuis un worker
|
- [ ] 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
|
- [ ] TSan ne signale aucune erreur
|
||||||
- [ ] Le build ASan ne signale aucune fuite mémoire liée aux threads
|
- [ ] Le build ASan ne signale aucune fuite mémoire liée aux threads
|
||||||
|
|
|
||||||
|
|
@ -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
|
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.
|
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 ;
|
- AMD Ryzen 7 8845HS, Zen 4, 8 cœurs et 16 threads SMT ;
|
||||||
- Radeon 780M à mémoire système partagée ;
|
- 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
|
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
|
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
|
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
|
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
|
établit au démarrage une baseline OpenCV depuis le compute-pool réellement
|
||||||
threads logiques au desktop. Pour chaque séquence, l'unique callback Queue
|
disponible. Sur cet hôte précis, le Governor réserve quatre threads logiques au
|
||||||
applique temporairement le compte CPU immuable admis dans `1..12`, le vérifie et
|
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
|
restaure la baseline sur toute sortie. `cv::setNumThreads()` reste une
|
||||||
configuration process-wide : sa mutation concurrente par plusieurs workers
|
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
|
n'est pas supportée. Un futur pool multi-worker devrait donc changer ce modèle
|
||||||
explicitement, pas multiplier silencieusement ces mutations globales.
|
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
|
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
|
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
|
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 129–136 ms. Le seuil
|
production. L'initialisation lazy mesurée vaut environ 129–136 ms. Le seuil
|
||||||
`feature_count_a × feature_count_b >= 768²` évite le GPU pour les petits travaux.
|
`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
|
La mémoire directement contrôlée vaut 640 Kio par slot. Rolling AUTO normal
|
||||||
réserver un ou deux slots, soit au plus 1,25 Mio, débités une fois de la RAM sur
|
réserve exactement un slot ; seule la couture privée de sûreté/benchmark peut
|
||||||
la 780M UMA. Le backend mappe exactement cette capacité pendant la séquence et
|
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é
|
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
|
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
|
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
|
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.
|
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
|
La validation scientifique initiale réservait un CPU logique, 4 Mio et un
|
||||||
vraie Task sur 1000 parents, suivi de ses chemins de reprise/configuration, a traversé environ
|
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 ;
|
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
|
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
|
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
|
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.
|
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
|
## Feasibility Vulkan SIFT / RootSIFT
|
||||||
|
|
||||||
Sur le même RADV PHOENIX, `shaderFloat64`, les timestamps compute, un subgroup
|
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².
|
atteint environ 271 ms à 8192².
|
||||||
|
|
||||||
La campagne numérique trouve une divergence top-2 sur des sommes égales
|
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,
|
adversariales pour FP32 comme FP64. Sur 24 160 requêtes SIFT et 24 161 requêtes
|
||||||
mais aucune divergence Lowe sur le corpus testé. Le backend devrait donc porter
|
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
|
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
|
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
|
performance : SIFT et RootSIFT Vulkan sont rejetés pour Lardon3D v1 sur cette
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,10 @@ en RAM ou termine dans une seule vie de processus.
|
||||||
[Project DB](../architecture/project_database.md).
|
[Project DB](../architecture/project_database.md).
|
||||||
- MVS-M1, frontière OpenMVS v2.4.0, identité dense et export COLMAP/PLY borné :
|
- MVS-M1, frontière OpenMVS v2.4.0, identité dense et export COLMAP/PLY borné :
|
||||||
[pipeline de reconstruction](../architecture/reconstruction_pipeline.md).
|
[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)
|
[Task](../architecture/task_system.md), [Queue](../architecture/task_queue.md)
|
||||||
et [Governor](../architecture/resource_governor.md).
|
et [Governor](../architecture/resource_governor.md).
|
||||||
- Project DB v19 et S1–S3 Capture / Acquisition Ingestion : provenance
|
- Project DB v22 gelé, overlay optique additif v23 courant, et S1–S3 Capture / Acquisition Ingestion : provenance
|
||||||
Capture/Asset, import capture-safe, publication dérivée, développement RAW,
|
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 :
|
siblings multi-source, évidence S3-D, orchestration S3-E et campagne bornée :
|
||||||
[Project DB et ingestion](../architecture/project_database.md).
|
[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 buffers d'analyse par groupe ; ces bornes ne sont pas des limites scientifiques
|
||||||
de campagne ou de dataset.
|
de campagne ou de dataset.
|
||||||
Elle doit conserver une voie honnête pour les RAW sans proxy plutôt que de
|
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
|
présumer que chaque RAW a un JPEG sibling. La revue interactive détaillée des
|
||||||
de capture et les keyframes vidéo restent des intégrations ultérieures qui
|
recommandations Photo Quality, la guidance de capture et les keyframes vidéo
|
||||||
réutiliseront ce même chemin de qualité, sans second pipeline.
|
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
|
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
|
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
|
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
|
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.
|
ni Capture, ni Asset, ni `image_id`, ni l'identité scientifique des acquisitions.
|
||||||
|
|
@ -202,20 +204,23 @@ qualité
|
||||||
→ candidats du même ScanSet
|
→ candidats du même ScanSet
|
||||||
→ matching ORB
|
→ matching ORB
|
||||||
→ GV gelée
|
→ GV gelée
|
||||||
→ tracks
|
→ arrêt durable avant Tracks
|
||||||
```
|
```
|
||||||
|
|
||||||
Il rouvre ensuite le Project DB afin de rapporter l'évidence durable produite.
|
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,
|
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
|
distinct : Task, Queue et Resource Governor existants conservent leurs
|
||||||
conservent leurs responsabilités.
|
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
|
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
|
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
|
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
|
ce jalon pré-SfM. Ce milestone est une intégration réelle, pas une nouvelle
|
||||||
de micro-gates S3, et son statut ne revendique encore ni implémentation achevée,
|
série de micro-gates S3. Son statut acquis ne rend pas disponibles les données
|
||||||
ni validation réussie, ni PASS/FROZEN.
|
de calibration physique manquantes et n'autorise pas à devancer la maintenance
|
||||||
|
globale.
|
||||||
|
|
||||||
## INTERNAL PARALLELISM + COMPUTE RESOURCES v1 — PASS / FROZEN
|
## 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
|
reproductibilité. Les dimensions retenues et toutes les validations v2 sont
|
||||||
closes.
|
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
|
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
|
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
|
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
|
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,
|
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.
|
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
|
Creator/main/TUI reste unrestricted. Les CPU déjà exclus de l'affinité du
|
||||||
aucune exclusion
|
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
|
inventée. Cette couture est **PASS / FROZEN** sur le profil validé; ces IDs ne
|
||||||
sont pas une politique portable. La cible RAM conserve
|
sont pas une politique portable. La cible RAM conserve une réserve dure de
|
||||||
3 GiB de `MemAvailable` et ne franchit pas intentionnellement le plancher dur de
|
3 GiB de `MemAvailable`; la zone 3–4 GiB est une prudence qui ne soustrait pas
|
||||||
2 GiB. Les PSI CPU/mémoire/I/O et les deltas swap-in/swap-out sont des signaux
|
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
|
actifs ; l'occupation totale du swap reste historique. Admission CPU et lot
|
||||||
sont indépendantes. Sur la 780M, Hardware Profile classe conservativement comme
|
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
|
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
|
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
|
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,
|
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
|
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
|
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
|
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/
|
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
|
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
|
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
|
histoire persistée. Les CPU réductibles progressent par puissances de deux vers
|
||||||
observations de baseline et deux gains d'au moins 5 % ; CPU et lot ne changent
|
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
|
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
|
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,
|
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
|
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,
|
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
|
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
|
pas exécutés. `REAL_S21_GV_V3=PASS/FROZEN` ; la prochaine tranche scientifique
|
||||||
donc après GV, sans prétendre que Tracks est acquis.
|
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 :
|
Les gates de fermeture sont acquis :
|
||||||
|
|
||||||
|
|
@ -477,34 +488,99 @@ fondation scientifique pré-SfM courante
|
||||||
correction progression/Resource Governor, audit GPU)
|
correction progression/Resource Governor, audit GPU)
|
||||||
→ COMPUTE GOVERNOR v2 / ORB VULKAN ASYNC EXECUTION (PASS / FROZEN)
|
→ COMPUTE GOVERNOR v2 / ORB VULKAN ASYNC EXECUTION (PASS / FROZEN)
|
||||||
→ REAL S21 GV v3 (PASS / FROZEN; arrêt avant Tracks)
|
→ 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
|
→ poursuite pré-SfM réelle de S21 à partir de Tracks
|
||||||
→ acquisition dédiée de calibration
|
→ acquisition dédiée de calibration
|
||||||
→ Sparse SfM réel
|
→ Sparse SfM réel
|
||||||
→ Dense / MVS
|
→ 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 3–4 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
|
## NEAR TERM
|
||||||
|
|
||||||
### Scratch SSD externe et swap optionnel
|
### Scratch SSD externe et swap optionnel
|
||||||
|
|
||||||
Matériel envisagé : SSD externe d'environ 500 Go dans un boîtier USB-C 10 Gb/s.
|
Le contrôleur physique actuel découvre par UDisks2 une paire exacte de labels
|
||||||
La capacité planifiée est : détecter un disque externe adapté, le présenter dans
|
`LARDON_SWAP`/`LARDON_SCRATCH`, exige des UUID stables et la même identité
|
||||||
la configuration TUI/projet et demander si ce projet doit l'utiliser.
|
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,
|
Les usages futurs possibles sont workspace/scratch et intermédiaires
|
||||||
sur activation explicite, swap de sécurité. Le contrat devra être possédé par
|
dense/mesh/texturing. Leur consommation devra être explicitement possédée par
|
||||||
l'exécution Task, le Resource Governor et la politique temporaire du projet ;
|
les Tasks et passer par les wrappers de lease du Resource Governor ; le
|
||||||
il ne doit pas devenir un gestionnaire de stockage ad hoc.
|
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 ;
|
- les leases scratch ont une ownership explicite ; `DRAINING` refuse les
|
||||||
- il peut permettre des jobs bornés plus grands ;
|
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 ;
|
- 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 ;
|
- latence et débit USB restent distincts de la mémoire ;
|
||||||
- l'utilisation reste optionnelle et Governor-controlled ;
|
- l'utilisation reste optionnelle ; le Governor demeure l'unique orchestrateur
|
||||||
- retrait, déconnexion, ownership, nettoyage et publication atomique exigent
|
de ressources et seul owner des leases scratch de production ;
|
||||||
des sémantiques explicites ;
|
- une action UDisks potentiellement appliquée mais non vérifiable verrouille le
|
||||||
- aucun montage, formatage, `swapon` ou nettoyage destructif automatique n'est
|
tuple physique original ; un remplacement n'obtient aucune autorité ;
|
||||||
actuellement implémenté ou autorisé.
|
- 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
|
### Publication durable dense / mesh
|
||||||
|
|
||||||
|
|
@ -517,12 +593,16 @@ dense → mesh → refinement → texturing → consolidation → export
|
||||||
|
|
||||||
Le scratch devient particulièrement pertinent à cette frontière.
|
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,
|
La TUI expose actuellement état projet, Tasks et contrôles disponibles,
|
||||||
progression, pause/reprise/annulation disponible, Governor, choix du scratch et
|
progression durable/ETA, Governor/ressources, profils optiques et SSD F10.
|
||||||
étapes aval. ncurses reste au thread principal. Aucun projet GUI général n'est
|
ncurses reste au thread principal ; les opérations SSD bornées utilisent un
|
||||||
introduit.
|
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
|
### Sources mixtes et multi-ScanSet
|
||||||
|
|
||||||
|
|
@ -795,6 +875,8 @@ CURRENT NEXT
|
||||||
→ COMPUTE GOVERNOR v2 / ORB VULKAN ASYNC EXECUTION
|
→ COMPUTE GOVERNOR v2 / ORB VULKAN ASYNC EXECUTION
|
||||||
(PASS / FROZEN; AUTO GPU-first ORB et preuve S21 Matcher complète)
|
(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)
|
→ 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
|
→ poursuite pré-SfM réelle de S21 à partir de Tracks
|
||||||
→ acquisition physique dédiée de calibration
|
→ acquisition physique dédiée de calibration
|
||||||
→ calibration connue validée → Sparse SfM réel multi-campagne
|
→ 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 ;
|
1. stabilité et intégrité scientifique avant débit ;
|
||||||
2. résultats atomiques et durables avant parallélisme ;
|
2. résultats atomiques et durables avant parallélisme ;
|
||||||
3. lots bornés et reprise avant taille de campagne ;
|
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 ;
|
5. scratch optionnel sans élargissement implicite des budgets RAM ;
|
||||||
6. TUI de contrôle avant visualisation riche ;
|
6. TUI de contrôle avant visualisation riche ;
|
||||||
7. documentation canonique alignée sur le code validé.
|
7. documentation canonique alignée sur le code validé.
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ enum {
|
||||||
128u)
|
128u)
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
/* Arrays are caller-owned and borrowed only for encode/create/enqueue. */
|
||||||
const Lardon3DAcquisitionCampaignSource *sources;
|
const Lardon3DAcquisitionCampaignSource *sources;
|
||||||
size_t source_count;
|
size_t source_count;
|
||||||
const Lardon3DAcquisitionCampaignConfirmation *confirmations;
|
const Lardon3DAcquisitionCampaignConfirmation *confirmations;
|
||||||
|
|
@ -40,23 +41,43 @@ typedef struct {
|
||||||
Lardon3DAcquisitionIngestOptions ingest_options;
|
Lardon3DAcquisitionIngestOptions ingest_options;
|
||||||
} Lardon3DAcquisitionCampaignTaskRequest;
|
} 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(
|
Lardon3DTask *lardon3d_project_create_acquisition_campaign_task(
|
||||||
Lardon3DAppState *state, uint64_t scanset_id,
|
Lardon3DAppState *state, uint64_t scanset_id,
|
||||||
const Lardon3DAcquisitionCampaignTaskRequest *request, uint64_t *task_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(
|
bool lardon3d_project_enqueue_acquisition_campaign(
|
||||||
Lardon3DAppState *state, uint64_t scanset_id,
|
Lardon3DAppState *state, uint64_t scanset_id,
|
||||||
const Lardon3DAcquisitionCampaignTaskRequest *request, uint64_t *task_id);
|
const Lardon3DAcquisitionCampaignTaskRequest *request, uint64_t *task_id);
|
||||||
/* Request payload is treated as immutable durable input: task execution and
|
/* Rebuild bounded callback context from the immutable durable request.
|
||||||
* checkpoint/recovery flow from the encoded blob, not from caller memory after
|
* snapshot, the Lardon3DTaskReconstructionContext passed through context, and
|
||||||
* enqueue. */
|
* 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(
|
bool lardon3d_acquisition_campaign_task_reconstruct(
|
||||||
const Lardon3DTaskDurableSnapshot *snapshot, void *context,
|
const Lardon3DTaskDurableSnapshot *snapshot, void *context,
|
||||||
Lardon3DTaskKindBinding *binding);
|
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(
|
bool lardon3d_acquisition_campaign_request_encode(
|
||||||
const Lardon3DAcquisitionCampaignTaskRequest *request,
|
const Lardon3DAcquisitionCampaignTaskRequest *request,
|
||||||
unsigned char *output, size_t capacity, size_t *size);
|
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(
|
bool lardon3d_acquisition_campaign_request_decode(
|
||||||
const unsigned char *input, size_t size,
|
const unsigned char *input, size_t size,
|
||||||
Lardon3DAcquisitionCampaignSource *sources, size_t source_capacity,
|
Lardon3DAcquisitionCampaignSource *sources, size_t source_capacity,
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ typedef enum {
|
||||||
LARDON3D_SCREEN_VIEWER,
|
LARDON3D_SCREEN_VIEWER,
|
||||||
LARDON3D_SCREEN_HELP,
|
LARDON3D_SCREEN_HELP,
|
||||||
LARDON3D_SCREEN_TASKS,
|
LARDON3D_SCREEN_TASKS,
|
||||||
LARDON3D_SCREEN_RESOURCES
|
LARDON3D_SCREEN_RESOURCES,
|
||||||
|
LARDON3D_SCREEN_OPTICS,
|
||||||
|
LARDON3D_SCREEN_SSD
|
||||||
} Lardon3DScreen;
|
} Lardon3DScreen;
|
||||||
|
|
||||||
typedef struct Lardon3DAppState {
|
typedef struct Lardon3DAppState {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,12 @@ Lardon3DVisualIndexResult lardon3d_candidate_pair_generate_batch(
|
||||||
Lardon3DCandidatePairGenStats *total_stats,
|
Lardon3DCandidatePairGenStats *total_stats,
|
||||||
uint64_t *last_feature_set_id);
|
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(
|
void lardon3d_candidate_pair_generation_fingerprint(
|
||||||
uint64_t visual_index_id, uint64_t source_feature_set_id,
|
uint64_t visual_index_id, uint64_t source_feature_set_id,
|
||||||
const Lardon3DVisualIndexQueryOptions *query_options,
|
const Lardon3DVisualIndexQueryOptions *query_options,
|
||||||
|
|
|
||||||
|
|
@ -13,23 +13,64 @@
|
||||||
enum {
|
enum {
|
||||||
LARDON3D_GEOMETRIC_VERIFIER_TASK_KIND_VERSION = 1,
|
LARDON3D_GEOMETRIC_VERIFIER_TASK_KIND_VERSION = 1,
|
||||||
LARDON3D_GEOMETRIC_VERIFIER_TASK_MINIMUM_BATCH = 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 {
|
typedef struct {
|
||||||
Lardon3DGeometricVerifierParameters verifier;
|
Lardon3DGeometricVerifierParameters verifier;
|
||||||
} Lardon3DGeometricVerifierTaskConfiguration;
|
} 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(
|
Lardon3DTask *lardon3d_project_create_geometric_verifier_task(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
const Lardon3DGeometricVerifierTaskConfiguration *configuration,
|
const Lardon3DGeometricVerifierTaskConfiguration *configuration,
|
||||||
uint64_t *task_id);
|
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(
|
bool lardon3d_project_enqueue_geometric_verifier_task(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
const Lardon3DGeometricVerifierTaskConfiguration *configuration,
|
const Lardon3DGeometricVerifierTaskConfiguration *configuration,
|
||||||
uint64_t *task_id);
|
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(
|
bool lardon3d_geometric_verifier_task_reconstruct(
|
||||||
const Lardon3DTaskDurableSnapshot *snapshot, void *context,
|
const Lardon3DTaskDurableSnapshot *snapshot, void *context,
|
||||||
Lardon3DTaskKindBinding *binding);
|
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
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,19 @@
|
||||||
|
|
||||||
#include <lardon3d/app_state.h>
|
#include <lardon3d/app_state.h>
|
||||||
#include <lardon3d/import_task.h>
|
#include <lardon3d/import_task.h>
|
||||||
#include <lardon3d/task_queue.h>
|
#include <lardon3d/runtime_observer.h>
|
||||||
#include <lardon3d/resource_governor.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(
|
void lardon3d_layout_draw(
|
||||||
const Lardon3DAppState *state,
|
const Lardon3DAppState *state,
|
||||||
const char *input_text,
|
const char *input_text,
|
||||||
|
|
@ -19,4 +29,29 @@ void lardon3d_layout_draw(
|
||||||
int cols
|
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
|
#endif
|
||||||
|
|
|
||||||
254
include/lardon3d/optical_profiles.h
Normal file
254
include/lardon3d/optical_profiles.h
Normal 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
|
||||||
|
|
@ -11,7 +11,10 @@
|
||||||
#include <lardon3d/sparse_sfm_incremental.h>
|
#include <lardon3d/sparse_sfm_incremental.h>
|
||||||
|
|
||||||
enum {
|
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_ID_CAPACITY = 65,
|
||||||
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
|
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
|
||||||
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,
|
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,
|
||||||
|
|
@ -104,6 +107,8 @@ typedef struct {
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint64_t task_id;
|
uint64_t task_id;
|
||||||
uint64_t scanset_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 next_group_id;
|
||||||
uint32_t group_count;
|
uint32_t group_count;
|
||||||
const unsigned char *request;
|
const unsigned char *request;
|
||||||
|
|
@ -691,24 +696,57 @@ Lardon3DProjectDbResult lardon3d_project_db_list_candidate_pairs(
|
||||||
Lardon3DProjectDbResult
|
Lardon3DProjectDbResult
|
||||||
lardon3d_project_db_load_image_import(Lardon3DProjectDb *database, uint64_t task_id,
|
lardon3d_project_db_load_image_import(Lardon3DProjectDb *database, uint64_t task_id,
|
||||||
Lardon3DProjectDbImageImport *parameters);
|
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(
|
Lardon3DProjectDbResult lardon3d_project_db_record_acquisition_campaign_task(
|
||||||
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
|
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
|
||||||
const char *task_kind, uint32_t task_kind_version,
|
const char *task_kind, uint32_t task_kind_version,
|
||||||
const Lardon3DProjectDbCheckpoint *checkpoint,
|
const Lardon3DProjectDbCheckpoint *checkpoint,
|
||||||
const Lardon3DProjectDbAcquisitionCampaignTask *parameters, int64_t updated_at);
|
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(
|
Lardon3DProjectDbResult lardon3d_project_db_load_acquisition_campaign_task(
|
||||||
Lardon3DProjectDb *database, uint64_t task_id, unsigned char *request,
|
Lardon3DProjectDb *database, uint64_t task_id, unsigned char *request,
|
||||||
size_t request_capacity, Lardon3DProjectDbAcquisitionCampaignTask *parameters);
|
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(
|
Lardon3DProjectDbResult lardon3d_project_db_retain_acquisition_campaign_capture(
|
||||||
Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id,
|
Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id,
|
||||||
uint64_t capture_id, uint32_t next_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(
|
Lardon3DProjectDbResult lardon3d_project_db_load_acquisition_campaign_capture(
|
||||||
Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id,
|
Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id,
|
||||||
Lardon3DProjectDbAcquisitionCampaignCapture *capture);
|
Lardon3DProjectDbAcquisitionCampaignCapture *capture);
|
||||||
/* Atomically record generic Task state and the immutable typed request for an
|
/* Atomically record generic Task state and the immutable typed request for an
|
||||||
* existing ScanSet. All pointers are required except checkpoint; request bytes
|
* existing ScanSet. All pointers are required except checkpoint; request bytes
|
||||||
* are borrowed only for the call. The request identifies campaign groups
|
* are borrowed only for the call. task_kind/version must exactly identify
|
||||||
* operationally and never creates or infers Capture/Asset/image identities. */
|
* 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(
|
Lardon3DProjectDbResult lardon3d_project_db_record_photo_quality_task(
|
||||||
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
|
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
|
||||||
const char *task_kind, uint32_t task_kind_version,
|
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);
|
const Lardon3DProjectDbPhotoQualityTask *parameters, int64_t updated_at);
|
||||||
/* On success, parameters receives validated one-based cursor/count fields and
|
/* On success, parameters receives validated one-based cursor/count fields and
|
||||||
* borrows caller-owned request storage containing the immutable request bytes.
|
* 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(
|
Lardon3DProjectDbResult lardon3d_project_db_load_photo_quality_task(
|
||||||
Lardon3DProjectDb *database, uint64_t task_id, unsigned char *request,
|
Lardon3DProjectDb *database, uint64_t task_id, unsigned char *request,
|
||||||
size_t request_capacity, Lardon3DProjectDbPhotoQualityTask *parameters);
|
size_t request_capacity, Lardon3DProjectDbPhotoQualityTask *parameters);
|
||||||
|
|
@ -784,6 +825,9 @@ Lardon3DProjectDbResult lardon3d_project_db_create_selected_execution(
|
||||||
Lardon3DProjectDbResult lardon3d_project_db_load_selected_execution(
|
Lardon3DProjectDbResult lardon3d_project_db_load_selected_execution(
|
||||||
Lardon3DProjectDb *database, uint64_t execution_id,
|
Lardon3DProjectDb *database, uint64_t execution_id,
|
||||||
Lardon3DProjectDbSelectedExecution *execution);
|
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(
|
Lardon3DProjectDbResult lardon3d_project_db_load_selected_execution_item(
|
||||||
Lardon3DProjectDb *database, uint64_t execution_id, uint32_t item_index,
|
Lardon3DProjectDb *database, uint64_t execution_id, uint32_t item_index,
|
||||||
Lardon3DProjectDbSelectedExecutionItem *item);
|
Lardon3DProjectDbSelectedExecutionItem *item);
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,29 @@
|
||||||
|
|
||||||
#include <lardon3d/hardware_profile.h>
|
#include <lardon3d/hardware_profile.h>
|
||||||
#include <lardon3d/resource_snapshot.h>
|
#include <lardon3d/resource_snapshot.h>
|
||||||
|
#include <lardon3d/ssd_controller.h>
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
LARDON3D_RESOURCE_REASON_CAPACITY = 256,
|
LARDON3D_RESOURCE_REASON_CAPACITY = 256,
|
||||||
|
LARDON3D_RESOURCE_EXTERNAL_IDENTITY_CAPACITY = 256,
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef struct Lardon3DResourceGovernor Lardon3DResourceGovernor;
|
typedef struct Lardon3DResourceGovernor Lardon3DResourceGovernor;
|
||||||
typedef struct Lardon3DResourceReservation Lardon3DResourceReservation;
|
typedef struct Lardon3DResourceReservation Lardon3DResourceReservation;
|
||||||
|
|
||||||
typedef struct {
|
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;
|
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 emergency_memory_floor_bytes;
|
||||||
uint64_t gpu_memory_reserve_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;
|
unsigned int system_cpu_reserve;
|
||||||
double maximum_cpu_load_ratio;
|
double maximum_cpu_load_ratio;
|
||||||
double maximum_cpu_pressure_avg10;
|
double maximum_cpu_pressure_avg10;
|
||||||
|
|
@ -120,6 +131,40 @@ typedef struct {
|
||||||
size_t active_reservations;
|
size_t active_reservations;
|
||||||
} Lardon3DResourceAvailability;
|
} 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(
|
bool lardon3d_resource_policy_default(
|
||||||
const Lardon3DHardwareProfile *profile,
|
const Lardon3DHardwareProfile *profile,
|
||||||
Lardon3DResourcePolicy *policy
|
Lardon3DResourcePolicy *policy
|
||||||
|
|
@ -128,6 +173,9 @@ Lardon3DResourceGovernor *lardon3d_resource_governor_create(
|
||||||
const Lardon3DHardwareProfile *profile,
|
const Lardon3DHardwareProfile *profile,
|
||||||
const Lardon3DResourcePolicy *policy
|
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(
|
void lardon3d_resource_governor_destroy(
|
||||||
Lardon3DResourceGovernor *governor
|
Lardon3DResourceGovernor *governor
|
||||||
);
|
);
|
||||||
|
|
@ -135,6 +183,13 @@ bool lardon3d_resource_governor_set_policy(
|
||||||
Lardon3DResourceGovernor *governor,
|
Lardon3DResourceGovernor *governor,
|
||||||
const Lardon3DResourcePolicy *policy
|
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(
|
bool lardon3d_resource_governor_decide(
|
||||||
Lardon3DResourceGovernor *governor,
|
Lardon3DResourceGovernor *governor,
|
||||||
const Lardon3DResourceSnapshot *snapshot,
|
const Lardon3DResourceSnapshot *snapshot,
|
||||||
|
|
@ -185,6 +240,77 @@ bool lardon3d_resource_governor_availability(
|
||||||
const Lardon3DResourceSnapshot *snapshot,
|
const Lardon3DResourceSnapshot *snapshot,
|
||||||
Lardon3DResourceAvailability *availability
|
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(
|
uint64_t lardon3d_resource_governor_generation(
|
||||||
Lardon3DResourceGovernor *governor
|
Lardon3DResourceGovernor *governor
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,16 @@ typedef struct {
|
||||||
uint64_t swap_pages_out;
|
uint64_t swap_pages_out;
|
||||||
} Lardon3DResourceSnapshot;
|
} 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(
|
bool lardon3d_resource_snapshot_capture(
|
||||||
const Lardon3DHardwareProfile *profile,
|
const Lardon3DHardwareProfile *profile,
|
||||||
Lardon3DResourceSnapshot *snapshot,
|
Lardon3DResourceSnapshot *snapshot,
|
||||||
|
|
@ -40,4 +50,18 @@ bool lardon3d_resource_snapshot_capture(
|
||||||
size_t error_message_size
|
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
|
#endif
|
||||||
|
|
|
||||||
72
include/lardon3d/runtime_observer.h
Normal file
72
include/lardon3d/runtime_observer.h
Normal 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 owners—call 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
|
||||||
30
include/lardon3d/runtime_session.h
Normal file
30
include/lardon3d/runtime_session.h
Normal 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
|
||||||
|
|
@ -53,6 +53,14 @@ typedef struct {
|
||||||
typedef struct {
|
typedef struct {
|
||||||
double robust_threshold_px;
|
double robust_threshold_px;
|
||||||
double confidence;
|
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 max_iterations;
|
||||||
uint32_t minimum_inliers;
|
uint32_t minimum_inliers;
|
||||||
double minimum_inlier_ratio;
|
double minimum_inlier_ratio;
|
||||||
|
|
@ -73,6 +81,14 @@ typedef struct {
|
||||||
typedef struct {
|
typedef struct {
|
||||||
double reprojection_threshold_px;
|
double reprojection_threshold_px;
|
||||||
double confidence;
|
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 max_iterations;
|
||||||
uint32_t minimum_inliers;
|
uint32_t minimum_inliers;
|
||||||
double minimum_inlier_ratio;
|
double minimum_inlier_ratio;
|
||||||
|
|
|
||||||
245
include/lardon3d/ssd_controller.h
Normal file
245
include/lardon3d/ssd_controller.h
Normal 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
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#define LARDON3D_TASK_H
|
#define LARDON3D_TASK_H
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
|
|
@ -30,6 +31,17 @@ typedef void (*Lardon3DTaskFinishedCallback)(
|
||||||
void *userdata
|
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 {
|
typedef struct {
|
||||||
uint64_t id;
|
uint64_t id;
|
||||||
char name[LARDON3D_TASK_NAME_CAPACITY];
|
char name[LARDON3D_TASK_NAME_CAPACITY];
|
||||||
|
|
@ -40,14 +52,32 @@ typedef struct {
|
||||||
struct timespec finished_at;
|
struct timespec finished_at;
|
||||||
} Lardon3DTaskSnapshot;
|
} 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 {
|
typedef struct {
|
||||||
size_t batch_size;
|
uint64_t id;
|
||||||
uint64_t memory_bytes;
|
char name[LARDON3D_TASK_NAME_CAPACITY];
|
||||||
uint64_t gpu_memory_bytes;
|
unsigned int progress;
|
||||||
unsigned int cpu_threads;
|
Lardon3DTaskState state;
|
||||||
unsigned int gpu_slots;
|
char message[LARDON3D_TASK_MESSAGE_CAPACITY];
|
||||||
unsigned int io_slots;
|
struct timespec started_at;
|
||||||
} Lardon3DTaskExecutionContract;
|
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 {
|
typedef struct {
|
||||||
uint64_t id;
|
uint64_t id;
|
||||||
|
|
@ -96,11 +126,32 @@ bool lardon3d_task_set_progress(
|
||||||
unsigned int progress,
|
unsigned int progress,
|
||||||
const char *message
|
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_fail(Lardon3DTask *task, const char *message);
|
||||||
bool lardon3d_task_snapshot(
|
bool lardon3d_task_snapshot(
|
||||||
const Lardon3DTask *task,
|
const Lardon3DTask *task,
|
||||||
Lardon3DTaskSnapshot *snapshot
|
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(
|
bool lardon3d_task_durable_snapshot(
|
||||||
const Lardon3DTask *task,
|
const Lardon3DTask *task,
|
||||||
Lardon3DTaskDurableSnapshot *snapshot
|
Lardon3DTaskDurableSnapshot *snapshot
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,17 @@
|
||||||
|
|
||||||
typedef struct Lardon3DTaskQueue Lardon3DTaskQueue;
|
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 {
|
typedef enum {
|
||||||
LARDON3D_TASK_QUEUE_ADD_OK = 0,
|
LARDON3D_TASK_QUEUE_ADD_OK = 0,
|
||||||
LARDON3D_TASK_QUEUE_ADD_FULL,
|
LARDON3D_TASK_QUEUE_ADD_FULL,
|
||||||
|
|
@ -20,21 +31,55 @@ typedef enum {
|
||||||
typedef struct {
|
typedef struct {
|
||||||
size_t running;
|
size_t running;
|
||||||
size_t pending;
|
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;
|
size_t completed;
|
||||||
|
/* Saturating completed + currently non-terminal Tasks. */
|
||||||
size_t total;
|
size_t total;
|
||||||
} Lardon3DTaskQueueSummary;
|
} 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(
|
Lardon3DTaskQueue *lardon3d_task_queue_create(
|
||||||
Lardon3DResourceGovernor *governor,
|
Lardon3DResourceGovernor *governor,
|
||||||
size_t capacity
|
size_t capacity
|
||||||
);
|
);
|
||||||
/* File d'exécution bornée, à ordre d'attente FIFO avec sélection adaptative du
|
/* Atomically closes call ingress, cancels live work, waits for the worker and
|
||||||
* premier travail admissible, et un seul worker: ownership d'ordonnancement et
|
* every call whose registration preceded closing (including blocked
|
||||||
* de backpressure seulement. L'admission des demandes reste au Governneur.
|
* 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);
|
void lardon3d_task_queue_destroy(Lardon3DTaskQueue *queue);
|
||||||
/* La file devient propriétaire de task uniquement en cas de succès.
|
/* 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(
|
bool lardon3d_task_queue_add(
|
||||||
Lardon3DTaskQueue *queue,
|
Lardon3DTaskQueue *queue,
|
||||||
Lardon3DTask *task,
|
Lardon3DTask *task,
|
||||||
|
|
@ -52,22 +97,49 @@ Lardon3DTaskQueueAddResult lardon3d_task_queue_try_add_ex(
|
||||||
Lardon3DTask *task,
|
Lardon3DTask *task,
|
||||||
uint64_t *task_id
|
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);
|
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);
|
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_pause(Lardon3DTaskQueue *queue, uint64_t task_id);
|
||||||
bool lardon3d_task_queue_resume(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);
|
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);
|
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(
|
bool lardon3d_task_queue_get(
|
||||||
Lardon3DTaskQueue *queue,
|
Lardon3DTaskQueue *queue,
|
||||||
uint64_t task_id,
|
uint64_t task_id,
|
||||||
Lardon3DTaskSnapshot *snapshot
|
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(
|
bool lardon3d_task_queue_get_at(
|
||||||
Lardon3DTaskQueue *queue,
|
Lardon3DTaskQueue *queue,
|
||||||
size_t index,
|
size_t index,
|
||||||
Lardon3DTaskSnapshot *snapshot
|
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(
|
size_t lardon3d_task_queue_snapshot(
|
||||||
Lardon3DTaskQueue *queue,
|
Lardon3DTaskQueue *queue,
|
||||||
Lardon3DTaskSnapshot *snapshots,
|
Lardon3DTaskSnapshot *snapshots,
|
||||||
|
|
@ -75,4 +147,17 @@ size_t lardon3d_task_queue_snapshot(
|
||||||
Lardon3DTaskQueueSummary *summary
|
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
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,40 @@
|
||||||
|
|
||||||
#include <lardon3d/app_state.h>
|
#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_init(void);
|
||||||
bool lardon3d_tui_run(Lardon3DAppState *state);
|
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);
|
void lardon3d_tui_shutdown(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
326
include/lardon3d/tui_model.h
Normal file
326
include/lardon3d/tui_model.h
Normal 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
|
||||||
161
include/lardon3d/tui_optics.h
Normal file
161
include/lardon3d/tui_optics.h
Normal 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
|
||||||
142
include/lardon3d/tui_ssd_async.h
Normal file
142
include/lardon3d/tui_ssd_async.h
Normal 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
|
||||||
140
meson.build
140
meson.build
|
|
@ -24,6 +24,7 @@ ncursesw = dependency('ncursesw', required: true)
|
||||||
threads = dependency('threads')
|
threads = dependency('threads')
|
||||||
sqlite3 = dependency('sqlite3', required: true)
|
sqlite3 = dependency('sqlite3', required: true)
|
||||||
openssl = dependency('openssl', required: true)
|
openssl = dependency('openssl', required: true)
|
||||||
|
gio = dependency('gio-2.0', required: true, include_type: 'system')
|
||||||
ceres = dependency(
|
ceres = dependency(
|
||||||
'Ceres',
|
'Ceres',
|
||||||
method: 'cmake',
|
method: 'cmake',
|
||||||
|
|
@ -145,6 +146,11 @@ lardon3d_app = executable(
|
||||||
'src/app_state.c',
|
'src/app_state.c',
|
||||||
'src/tui.c',
|
'src/tui.c',
|
||||||
'src/layout.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.c',
|
||||||
'src/import_task.c',
|
'src/import_task.c',
|
||||||
'src/image_catalog.c',
|
'src/image_catalog.c',
|
||||||
|
|
@ -159,7 +165,6 @@ lardon3d_app = executable(
|
||||||
'src/photo_quality_task.cpp',
|
'src/photo_quality_task.cpp',
|
||||||
'src/feature_extractor_opencv.cpp',
|
'src/feature_extractor_opencv.cpp',
|
||||||
'src/feature_store.c',
|
'src/feature_store.c',
|
||||||
'src/feature_extractor_opencv.cpp',
|
|
||||||
'src/feature_task.c', 'src/sift_task.c', 'src/precision_features.c',
|
'src/feature_task.c', 'src/sift_task.c', 'src/precision_features.c',
|
||||||
'src/visual_index.c',
|
'src/visual_index.c',
|
||||||
'src/visual_index_task.c',
|
'src/visual_index_task.c',
|
||||||
|
|
@ -170,6 +175,7 @@ lardon3d_app = executable(
|
||||||
'src/image_view.c',
|
'src/image_view.c',
|
||||||
'src/project.c',
|
'src/project.c',
|
||||||
'src/project_db.c', 'src/project_db_sparse_sfm.c',
|
'src/project_db.c', 'src/project_db_sparse_sfm.c',
|
||||||
|
'src/optical_profiles.c',
|
||||||
'src/calibration_bootstrap.c',
|
'src/calibration_bootstrap.c',
|
||||||
'src/sparse_sfm_geometry.cpp',
|
'src/sparse_sfm_geometry.cpp',
|
||||||
'src/sparse_sfm_incremental.cpp',
|
'src/sparse_sfm_incremental.cpp',
|
||||||
|
|
@ -187,8 +193,10 @@ lardon3d_app = executable(
|
||||||
'src/task_kinds.c',
|
'src/task_kinds.c',
|
||||||
'src/task_queue.c',
|
'src/task_queue.c',
|
||||||
'src/resource_governor.c',
|
'src/resource_governor.c',
|
||||||
|
'src/resource_governor_external_storage.c',
|
||||||
'src/resource_snapshot.c',
|
'src/resource_snapshot.c',
|
||||||
'src/hardware_profile.c',
|
'src/hardware_profile.c',
|
||||||
|
'src/ssd_controller.c',
|
||||||
'src/match_file.c',
|
'src/match_file.c',
|
||||||
'src/matcher.cpp',
|
'src/matcher.cpp',
|
||||||
'src/geometric_verifier.cpp',
|
'src/geometric_verifier.cpp',
|
||||||
|
|
@ -210,6 +218,7 @@ lardon3d_app = executable(
|
||||||
threads,
|
threads,
|
||||||
sqlite3,
|
sqlite3,
|
||||||
openssl,
|
openssl,
|
||||||
|
gio,
|
||||||
libraw,
|
libraw,
|
||||||
libexif,
|
libexif,
|
||||||
libpng,
|
libpng,
|
||||||
|
|
@ -317,7 +326,10 @@ persistent_image_catalog_test = executable(
|
||||||
'src/resource_governor.c',
|
'src/resource_governor.c',
|
||||||
'src/resource_snapshot.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'),
|
include_directories: include_directories('include'),
|
||||||
dependencies: [threads, sqlite3, openssl],
|
dependencies: [threads, sqlite3, openssl],
|
||||||
)
|
)
|
||||||
|
|
@ -731,6 +743,7 @@ feature_task_test = executable(
|
||||||
'-DLARDON3D_FEATURE_STORE_TESTING',
|
'-DLARDON3D_FEATURE_STORE_TESTING',
|
||||||
'-DLARDON3D_VISUAL_INDEX_TASK_TESTING',
|
'-DLARDON3D_VISUAL_INDEX_TASK_TESTING',
|
||||||
'-DLARDON3D_VISUAL_INDEX_TESTING',
|
'-DLARDON3D_VISUAL_INDEX_TESTING',
|
||||||
|
'-DLARDON3D_RESOURCE_GOVERNOR_CAPTURE_TESTING',
|
||||||
],
|
],
|
||||||
cpp_args: ['-DLARDON3D_FEATURE_TASK_TESTING'],
|
cpp_args: ['-DLARDON3D_FEATURE_TASK_TESTING'],
|
||||||
include_directories: include_directories('include'),
|
include_directories: include_directories('include'),
|
||||||
|
|
@ -864,6 +877,21 @@ calibration_bootstrap_test = executable(
|
||||||
|
|
||||||
test('calibration-bootstrap', calibration_bootstrap_test, timeout: 30)
|
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(
|
sparse_sfm_model_test = executable(
|
||||||
'test-sparse-sfm-model',
|
'test-sparse-sfm-model',
|
||||||
sources: [
|
sources: [
|
||||||
|
|
@ -975,6 +1003,7 @@ project_test = executable(
|
||||||
'tests/test_project.c',
|
'tests/test_project.c',
|
||||||
'src/app_state.c',
|
'src/app_state.c',
|
||||||
'src/project.c',
|
'src/project.c',
|
||||||
|
'src/runtime_session.c',
|
||||||
'src/project_db.c', 'src/project_db_sparse_sfm.c',
|
'src/project_db.c', 'src/project_db_sparse_sfm.c',
|
||||||
'src/task.c',
|
'src/task.c',
|
||||||
'src/task_checkpoint.c',
|
'src/task_checkpoint.c',
|
||||||
|
|
@ -1045,6 +1074,7 @@ task_queue_test = executable(
|
||||||
'src/resource_governor.c',
|
'src/resource_governor.c',
|
||||||
'src/resource_snapshot.c',
|
'src/resource_snapshot.c',
|
||||||
],
|
],
|
||||||
|
c_args: ['-DLARDON3D_TASK_QUEUE_TESTING'],
|
||||||
include_directories: include_directories('include'),
|
include_directories: include_directories('include'),
|
||||||
dependencies: [threads],
|
dependencies: [threads],
|
||||||
)
|
)
|
||||||
|
|
@ -1062,6 +1092,112 @@ hardware_profile_test = executable(
|
||||||
|
|
||||||
test('hardware-profile', hardware_profile_test, timeout: 30)
|
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(
|
resource_snapshot_test = executable(
|
||||||
'test-resource-snapshot',
|
'test-resource-snapshot',
|
||||||
sources: [
|
sources: [
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ extern "C" {
|
||||||
#include "task_internal.h"
|
#include "task_internal.h"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#include "opencv_task_thread_guard.h"
|
||||||
|
|
||||||
|
#include <climits>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
@ -18,6 +21,10 @@ namespace {
|
||||||
constexpr unsigned char magic[8] = {'L', '3', 'D', 'A', 'C', 'T', '1', '\0'};
|
constexpr unsigned char magic[8] = {'L', '3', 'D', 'A', 'C', 'T', '1', '\0'};
|
||||||
constexpr size_t max_request_size =
|
constexpr size_t max_request_size =
|
||||||
LARDON3D_ACQUISITION_CAMPAIGN_TASK_REQUEST_MAX_BYTES;
|
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
|
/* Codec is transport-safe and deterministic: no native struct serialization is
|
||||||
* used, integers are fixed-width with explicit endianness, and strings/counts are
|
* used, integers are fixed-width with explicit endianness, and strings/counts are
|
||||||
|
|
@ -183,6 +190,20 @@ struct Context {
|
||||||
std::vector<unsigned char> encoded;
|
std::vector<unsigned char> encoded;
|
||||||
Lardon3DAcquisitionCampaignPlan plan{};
|
Lardon3DAcquisitionCampaignPlan plan{};
|
||||||
Lardon3DAcquisitionIngestOptions options{};
|
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) {
|
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;
|
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 destroy(void *p) { delete static_cast<Context *>(p); }
|
||||||
void runtime(Context *c, Lardon3DAppState &s) {
|
void runtime(Context *c, Lardon3DAppState &s) {
|
||||||
lardon3d_app_state_init(&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) {
|
bool run_impl(Lardon3DTask *t, void *p) {
|
||||||
auto *c = static_cast<Context *>(p);
|
auto *c = static_cast<Context *>(p);
|
||||||
Lardon3DProjectDbAcquisitionCampaignTask persisted{};
|
Lardon3DProjectDbAcquisitionCampaignTask persisted{};
|
||||||
// Request is reconstructed from persisted blob and treated as immutable durable
|
/* The retained exact encoding is the immutable replay input and also bounds
|
||||||
// input during recovery and replay.
|
* this transient verification buffer. A maximum-capacity allocation would
|
||||||
std::vector<unsigned char> blob(max_request_size);
|
* 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(
|
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)
|
LARDON3D_PROJECT_DB_OK)
|
||||||
return lardon3d_task_fail(t, "Campagne durable introuvable.");
|
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;
|
for (uint32_t cursor = persisted.next_group_id; cursor < c->plan.group_count;
|
||||||
++cursor) {
|
++cursor) {
|
||||||
const uint32_t group_id = cursor + 1u;
|
const uint32_t group_id = cursor + 1u;
|
||||||
if (!lardon3d_task_checkpoint(t))
|
if (!lardon3d_task_checkpoint(t))
|
||||||
return false;
|
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{};
|
Lardon3DProjectDbAcquisitionCampaignCapture retained{};
|
||||||
uint64_t resume = 0;
|
uint64_t resume = 0;
|
||||||
/* Durable replay uses the stable mapping (task_id, group_id) -> capture_id.
|
/* 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;
|
resume = retained.capture_id;
|
||||||
else if (lr != LARDON3D_PROJECT_DB_NOT_FOUND)
|
else if (lr != LARDON3D_PROJECT_DB_NOT_FOUND)
|
||||||
return lardon3d_task_fail(t, "Lecture de rétention impossible.");
|
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;
|
auto options = c->options;
|
||||||
options.grouping = LARDON3D_ACQUISITION_GROUP_CALLER_EXPLICIT;
|
options.grouping = LARDON3D_ACQUISITION_GROUP_CALLER_EXPLICIT;
|
||||||
options.resume_capture_id = resume;
|
options.resume_capture_id = resume;
|
||||||
options.producer_task_id = lardon3d_task_id(t);
|
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{};
|
Lardon3DAcquisitionIngestOutput out{};
|
||||||
Lardon3DAcquisitionIngestResult ir{};
|
Lardon3DAcquisitionIngestResult ir{};
|
||||||
Lardon3DAppState s;
|
Lardon3DAppState s;
|
||||||
|
|
@ -272,6 +385,8 @@ bool run_impl(Lardon3DTask *t, void *p) {
|
||||||
if (cr != LARDON3D_ACQUISITION_CAMPAIGN_OK ||
|
if (cr != LARDON3D_ACQUISITION_CAMPAIGN_OK ||
|
||||||
ir != LARDON3D_ACQUISITION_INGEST_OK || out.group_count != 1)
|
ir != LARDON3D_ACQUISITION_INGEST_OK || out.group_count != 1)
|
||||||
return lardon3d_task_fail(t, "Échec de matérialisation de campagne.");
|
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
|
#ifdef LARDON3D_ACQUISITION_CAMPAIGN_TASK_TESTING
|
||||||
const char *before_retention =
|
const char *before_retention =
|
||||||
std::getenv("LARDON3D_TEST_CAMPAIGN_FAIL_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.");
|
return lardon3d_task_fail(t, "Échec injecté avant rétention.");
|
||||||
#endif
|
#endif
|
||||||
if (lardon3d_project_db_retain_acquisition_campaign_capture(
|
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)
|
group_id) != LARDON3D_PROJECT_DB_OK)
|
||||||
return lardon3d_task_fail(t, "Rétention de Capture impossible.");
|
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
|
/* 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)
|
if (after_retention && std::strcmp(after_retention, "1") == 0)
|
||||||
return lardon3d_task_fail(t, "Échec injecté après rétention.");
|
return lardon3d_task_fail(t, "Échec injecté après rétention.");
|
||||||
#endif
|
#endif
|
||||||
unsigned progress = static_cast<unsigned>(
|
/* The retained mapping/cursor transaction above is the authority for this
|
||||||
(uint64_t(group_id) * 100u / c->plan.group_count));
|
* exact TUI count. Observation may lag it, but must never lead it. */
|
||||||
if (!lardon3d_task_set_progress(t, progress,
|
if (!lardon3d_task_set_durable_progress(
|
||||||
|
t, group_id, c->plan.group_count,
|
||||||
"Groupe de campagne matérialisé.") ||
|
"Groupe de campagne matérialisé.") ||
|
||||||
!checkpoint(c, t, group_id))
|
!checkpoint(c, t, group_id))
|
||||||
return lardon3d_task_fail(t, "Checkpoint de campagne impossible.");
|
return lardon3d_task_fail(t, "Checkpoint de campagne impossible.");
|
||||||
if (group_id < c->plan.group_count) {
|
if (group_id < c->plan.group_count) {
|
||||||
// sequence_break: libère la réservation courante et renégocie l'admission
|
/* Campaign cardinality never extends a reservation lifetime: this
|
||||||
// via le Governor avant le groupe suivant.
|
* boundary releases the current group admission and Task/Queue asks the
|
||||||
|
* Governor for a fresh bounded contract before the next group. */
|
||||||
Lardon3DTaskExecutionContract contract{};
|
Lardon3DTaskExecutionContract contract{};
|
||||||
Lardon3DResourceReservation *reservation = nullptr;
|
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,
|
if (!lardon3d_task_sequence_break(t, c->governor, &reservation,
|
||||||
&contract))
|
&contract))
|
||||||
return false;
|
return false;
|
||||||
|
if (!apply_admitted_opencv_threads(contract))
|
||||||
|
return lardon3d_task_fail(
|
||||||
|
t, "Contrat CPU OpenCV renouvelé de campagne invalide.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
bool run(Lardon3DTask *t, void *p) noexcept {
|
bool run(Lardon3DTask *t, void *p) noexcept {
|
||||||
try {
|
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 &) {
|
} catch (const std::bad_alloc &) {
|
||||||
return lardon3d_task_fail(t, "Mémoire insuffisante pour la campagne.");
|
return lardon3d_task_fail(t, "Mémoire insuffisante pour la campagne.");
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
|
|
@ -333,7 +472,9 @@ Context *make_context(const char *path, Lardon3DProjectDb *db,
|
||||||
Lardon3DResourceGovernor *g, uint64_t scanset,
|
Lardon3DResourceGovernor *g, uint64_t scanset,
|
||||||
const Lardon3DAcquisitionCampaignTaskRequest &r,
|
const Lardon3DAcquisitionCampaignTaskRequest &r,
|
||||||
const unsigned char *encoded, size_t n) {
|
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;
|
return nullptr;
|
||||||
std::unique_ptr<Context> c(new (std::nothrow) Context);
|
std::unique_ptr<Context> c(new (std::nothrow) Context);
|
||||||
if (!c)
|
if (!c)
|
||||||
|
|
@ -346,6 +487,7 @@ Context *make_context(const char *path, Lardon3DProjectDb *db,
|
||||||
c->governor = g;
|
c->governor = g;
|
||||||
c->scanset = scanset;
|
c->scanset = scanset;
|
||||||
c->sources.assign(r.sources, r.sources + r.source_count);
|
c->sources.assign(r.sources, r.sources + r.source_count);
|
||||||
|
if (r.confirmation_count > 0)
|
||||||
c->confirmations.assign(r.confirmations,
|
c->confirmations.assign(r.confirmations,
|
||||||
r.confirmations + r.confirmation_count);
|
r.confirmations + r.confirmation_count);
|
||||||
c->options = r.ingest_options;
|
c->options = r.ingest_options;
|
||||||
|
|
@ -360,26 +502,75 @@ Context *make_context(const char *path, Lardon3DProjectDb *db,
|
||||||
}
|
}
|
||||||
} // namespace
|
} // 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
|
extern "C" bool
|
||||||
lardon3d_acquisition_campaign_task_internal_configure_restored(
|
lardon3d_acquisition_campaign_task_internal_configure_restored(
|
||||||
Lardon3DTask *task, void *userdata) {
|
Lardon3DTask *task, void *userdata) {
|
||||||
try {
|
try {
|
||||||
auto *context = static_cast<Context *>(userdata);
|
auto *context = static_cast<Context *>(userdata);
|
||||||
uint64_t retained = 0;
|
Lardon3DTaskDurableSnapshot snapshot{};
|
||||||
if (!task || !context || !context_owned_bytes(*context, retained) ||
|
Lardon3DResourceEstimate current{};
|
||||||
retained > UINT64_MAX - 256 * 1024)
|
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;
|
return false;
|
||||||
Lardon3DTaskCapability capability{};
|
Lardon3DTaskCapability capability{};
|
||||||
capability.estimate = Lardon3DResourceEstimate{
|
capability.estimate = current;
|
||||||
retained + 256 * 1024, 0, 64 * 1024, 0, 1, 1, 1, 0, 1,
|
|
||||||
LARDON3D_RESOURCE_TASK_IMPORT};
|
|
||||||
capability.backend = LARDON3D_RESOURCE_BACKEND_FIXED;
|
capability.backend = LARDON3D_RESOURCE_BACKEND_FIXED;
|
||||||
capability.inflight_limit = 1;
|
capability.inflight_limit = 1;
|
||||||
Lardon3DTaskCapabilityEnvelope envelope{};
|
Lardon3DTaskCapabilityEnvelope envelope{};
|
||||||
envelope.count = 1;
|
envelope.count = 1;
|
||||||
envelope.capabilities[0] = capability;
|
envelope.capabilities[0] = capability;
|
||||||
/* Recovery may carry the historical under-estimate, but admission uses the
|
/* Exact v22 historical signatures remain recoverable, but admission uses
|
||||||
* exact newly reconstructed retained context. Durable bytes stay untouched. */
|
* 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);
|
return lardon3d_task_internal_set_capability_envelope(task, &envelope);
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -420,7 +611,8 @@ bool request_decode_impl(
|
||||||
uint64_t imported, maxbytes;
|
uint64_t imported, maxbytes;
|
||||||
/* Version 1 is the only accepted codec shape; any mismatch rejects replay. */
|
/* 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) ||
|
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 ||
|
n > LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES || !q.u32(c) || c > cc ||
|
||||||
c > n || !sources || (c > 0 && !confirmations) || !q.u32(rep) ||
|
c > n || !sources || (c > 0 && !confirmations) || !q.u32(rep) ||
|
||||||
rep < 1 || rep > 2 || !q.u32(select) || select > 1 ||
|
rep < 1 || rep > 2 || !q.u32(select) || select > 1 ||
|
||||||
|
|
@ -488,10 +680,16 @@ bool task_reconstruct_impl(
|
||||||
auto *rt = static_cast<Lardon3DTaskReconstructionContext *>(userdata);
|
auto *rt = static_cast<Lardon3DTaskReconstructionContext *>(userdata);
|
||||||
if (!s || !rt || !b)
|
if (!s || !rt || !b)
|
||||||
return false;
|
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{};
|
Lardon3DProjectDbAcquisitionCampaignTask p{};
|
||||||
if (lardon3d_project_db_load_acquisition_campaign_task(
|
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)
|
LARDON3D_PROJECT_DB_OK)
|
||||||
return false;
|
return false;
|
||||||
std::vector<Lardon3DAcquisitionCampaignSource> sources(
|
std::vector<Lardon3DAcquisitionCampaignSource> sources(
|
||||||
|
|
@ -500,17 +698,20 @@ bool task_reconstruct_impl(
|
||||||
LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES);
|
LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES);
|
||||||
Lardon3DAcquisitionCampaignTaskRequest r{};
|
Lardon3DAcquisitionCampaignTaskRequest r{};
|
||||||
if (!lardon3d_acquisition_campaign_request_decode(
|
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))
|
confirmations.data(), confirmations.size(), &r))
|
||||||
return false;
|
return false;
|
||||||
auto *c =
|
auto *c =
|
||||||
make_context(rt->project_path, rt->project_db, rt->resource_governor,
|
make_context(rt->project_path, rt->project_db, rt->resource_governor,
|
||||||
p.scanset_id, r, blob.data(), p.request_size);
|
p.scanset_id, r, blob.get(), p.request_size);
|
||||||
if (!c)
|
if (!c || p.group_count != c->plan.group_count ||
|
||||||
|
p.next_group_id > c->plan.group_count) {
|
||||||
|
delete c;
|
||||||
return false;
|
return false;
|
||||||
// This Task kind has no legacy operational-resource reconciliation. Keep
|
}
|
||||||
// every optional Registry hook explicit so a future binding extension
|
/* Binding owns the reconstructed context. Registry's private post-restore
|
||||||
// cannot accidentally inherit non-null recovery behavior.
|
* hook validates the exact current or historical operational estimate before
|
||||||
|
* Queue admission; the typed request itself is never rewritten. */
|
||||||
*b = {};
|
*b = {};
|
||||||
b->callback = run;
|
b->callback = run;
|
||||||
b->userdata = c;
|
b->userdata = c;
|
||||||
|
|
@ -549,20 +750,19 @@ Lardon3DTask *create_task_impl(
|
||||||
delete c;
|
delete c;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
uint64_t retained = 0;
|
Lardon3DResourceEstimate e{};
|
||||||
if (!context_owned_bytes(*c, retained) || retained > UINT64_MAX - 256 * 1024) {
|
if (!campaign_estimate(*c, false, e)) {
|
||||||
delete c;
|
delete c;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
/* The request vectors and inline campaign plan remain live for the Task
|
/* Admission charges retained context, one exact transient request reload,
|
||||||
* lifetime. Charge them before admission; the per-group working estimate is
|
* and group work. DEVELOP_RAW additionally owns raw.develop's conservative
|
||||||
* separate and this operational bound does not limit scientific cardinality. */
|
* 2 GiB callback allowance. These are per-execution operational bounds, not
|
||||||
Lardon3DResourceEstimate e{
|
* scientific limits on campaign cardinality. */
|
||||||
retained + 256 * 1024, 0, 64 * 1024, 0, 1,
|
|
||||||
1, 1, 0, 1, LARDON3D_RESOURCE_TASK_IMPORT};
|
|
||||||
auto *t = lardon3d_task_create_typed("Campagne d'acquisition", &e,
|
auto *t = lardon3d_task_create_typed("Campagne d'acquisition", &e,
|
||||||
LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND,
|
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) ||
|
if (!t || !lardon3d_task_assign_id(t, task_id) ||
|
||||||
!lardon3d_task_set_finished_callback(t, finished, c) ||
|
!lardon3d_task_set_finished_callback(t, finished, c) ||
|
||||||
!checkpoint(c, t, 0)) {
|
!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(
|
extern "C" bool lardon3d_project_enqueue_acquisition_campaign(
|
||||||
Lardon3DAppState *s, uint64_t scanset,
|
Lardon3DAppState *s, uint64_t scanset,
|
||||||
const Lardon3DAcquisitionCampaignTaskRequest *r, uint64_t *id) {
|
const Lardon3DAcquisitionCampaignTaskRequest *r, uint64_t *id) {
|
||||||
|
if (id)
|
||||||
|
*id = 0;
|
||||||
try {
|
try {
|
||||||
if (!s || !s->task_queue)
|
if (!s || !s->task_queue || !r || !id)
|
||||||
return false;
|
return false;
|
||||||
auto *t =
|
std::unique_ptr<Lardon3DTask, void (*)(Lardon3DTask *)> t(
|
||||||
lardon3d_project_create_acquisition_campaign_task(s, scanset, r, id);
|
lardon3d_project_create_acquisition_campaign_task(s, scanset, r, id),
|
||||||
|
lardon3d_task_destroy);
|
||||||
if (!t)
|
if (!t)
|
||||||
return false;
|
return false;
|
||||||
if (!lardon3d_task_queue_add(s->task_queue, t, nullptr)) {
|
if (!lardon3d_task_queue_add(s->task_queue, t.get(), nullptr))
|
||||||
lardon3d_task_destroy(t);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
(void)t.release();
|
||||||
return true;
|
return true;
|
||||||
} catch (const std::bad_alloc &) {
|
} catch (const std::bad_alloc &) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
78
src/app.c
78
src/app.c
|
|
@ -10,8 +10,10 @@
|
||||||
#include <lardon3d/orb_vulkan_backend.h>
|
#include <lardon3d/orb_vulkan_backend.h>
|
||||||
#include <lardon3d/project.h>
|
#include <lardon3d/project.h>
|
||||||
#include <lardon3d/resource_governor.h>
|
#include <lardon3d/resource_governor.h>
|
||||||
|
#include <lardon3d/ssd_controller.h>
|
||||||
#include <lardon3d/task_queue.h>
|
#include <lardon3d/task_queue.h>
|
||||||
#include <lardon3d/tui.h>
|
#include <lardon3d/tui.h>
|
||||||
|
#include <lardon3d/tui_ssd_async.h>
|
||||||
|
|
||||||
#include "resource_governor_internal.h"
|
#include "resource_governor_internal.h"
|
||||||
|
|
||||||
|
|
@ -48,24 +50,27 @@ lardon3d_app_run(void)
|
||||||
)) {
|
)) {
|
||||||
return EXIT_FAILURE;
|
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.resource_governor = lardon3d_resource_governor_create(
|
||||||
&state.hardware_profile,
|
&state.hardware_profile,
|
||||||
&resource_policy
|
&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;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
state.orb_vulkan_backend = lardon3d_orb_vulkan_backend_create();
|
state.orb_vulkan_backend = lardon3d_orb_vulkan_backend_create();
|
||||||
|
|
@ -73,29 +78,70 @@ lardon3d_app_run(void)
|
||||||
lardon3d_resource_governor_destroy(state.resource_governor);
|
lardon3d_resource_governor_destroy(state.resource_governor);
|
||||||
return EXIT_FAILURE;
|
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) {
|
if (!state.task_queue) {
|
||||||
lardon3d_orb_vulkan_backend_destroy(state.orb_vulkan_backend);
|
lardon3d_orb_vulkan_backend_destroy(state.orb_vulkan_backend);
|
||||||
lardon3d_resource_governor_destroy(state.resource_governor);
|
lardon3d_resource_governor_destroy(state.resource_governor);
|
||||||
return EXIT_FAILURE;
|
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()) {
|
if (!lardon3d_tui_init()) {
|
||||||
lardon3d_task_queue_destroy(state.task_queue);
|
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);
|
lardon3d_orb_vulkan_backend_destroy(state.orb_vulkan_backend);
|
||||||
|
if (storage_released) {
|
||||||
lardon3d_resource_governor_destroy(state.resource_governor);
|
lardon3d_resource_governor_destroy(state.resource_governor);
|
||||||
|
}
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool success = lardon3d_tui_run(&state);
|
bool success = lardon3d_tui_run_with_ssd_operation(
|
||||||
|
&state, ssd_operation);
|
||||||
lardon3d_tui_shutdown();
|
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);
|
lardon3d_task_queue_destroy(state.task_queue);
|
||||||
state.task_queue = NULL;
|
state.task_queue = NULL;
|
||||||
if (state.project_loaded) {
|
if (state.project_loaded) {
|
||||||
lardon3d_project_close(&state);
|
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);
|
lardon3d_orb_vulkan_backend_destroy(state.orb_vulkan_backend);
|
||||||
|
if (storage_released) {
|
||||||
lardon3d_resource_governor_destroy(state.resource_governor);
|
lardon3d_resource_governor_destroy(state.resource_governor);
|
||||||
|
}
|
||||||
|
|
||||||
return success ? EXIT_SUCCESS : EXIT_FAILURE;
|
return success ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
#include <lardon3d/project_db.h>
|
#include <lardon3d/project_db.h>
|
||||||
#include <lardon3d/visual_index.h>
|
#include <lardon3d/visual_index.h>
|
||||||
|
|
||||||
#include <openssl/sha.h>
|
#include <openssl/evp.h>
|
||||||
|
|
||||||
#include "candidate_pair_gen_internal.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,
|
uint64_t visual_index_id, uint64_t source_feature_set_id,
|
||||||
const Lardon3DVisualIndexQueryOptions *query_options,
|
const Lardon3DVisualIndexQueryOptions *query_options,
|
||||||
unsigned char fingerprint[32]) {
|
unsigned char fingerprint[32]) {
|
||||||
SHA256_CTX sha;
|
if (!fingerprint) return;
|
||||||
SHA256_Init(&sha);
|
/* FROZEN SCIENTIFIC CONTRACT: v1 hashes the same 29 bytes historically
|
||||||
SHA256_Update(&sha, &visual_index_id, sizeof(visual_index_id));
|
* produced on validated little-endian hosts, but spells out widths and byte
|
||||||
SHA256_Update(&sha, &source_feature_set_id, sizeof(source_feature_set_id));
|
* order so a new architecture cannot silently change Candidate identity.
|
||||||
if (query_options) {
|
* The optional-query form remains the historical 16-byte ID prefix. */
|
||||||
SHA256_Update(&sha, &query_options->top_k, sizeof(query_options->top_k));
|
unsigned char canonical[29];
|
||||||
SHA256_Update(&sha, &query_options->minimum_evidence_count,
|
for (unsigned int index = 0; index < 8; ++index) {
|
||||||
sizeof(query_options->minimum_evidence_count));
|
canonical[index] = (unsigned char)(visual_index_id >> (8U * index));
|
||||||
SHA256_Update(&sha, &query_options->scanset_filter, sizeof(query_options->scanset_filter));
|
canonical[8 + index] =
|
||||||
bool exclude = query_options->exclude_same_asset;
|
(unsigned char)(source_feature_set_id >> (8U * index));
|
||||||
SHA256_Update(&sha, &exclude, sizeof(exclude));
|
}
|
||||||
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,20 @@ Lardon3DVisualIndexResult lardon3d_candidate_pair_publish(
|
||||||
void lardon3d_candidate_pair_task_test_reset_parallel_counters(void);
|
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_started_participants(void);
|
||||||
size_t lardon3d_candidate_pair_task_test_computed_work_items(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
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,20 @@
|
||||||
enum {
|
enum {
|
||||||
CANDIDATE_PAIR_MINIMUM_BATCH = 1,
|
CANDIDATE_PAIR_MINIMUM_BATCH = 1,
|
||||||
CANDIDATE_PAIR_MAXIMUM_BATCH = 64,
|
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_PER_THREAD = 2,
|
||||||
CANDIDATE_PAIR_WINDOW_MAX =
|
CANDIDATE_PAIR_WINDOW_MAX = CANDIDATE_PAIR_MAXIMUM_BATCH,
|
||||||
CANDIDATE_PAIR_CPU_THREADS * CANDIDATE_PAIR_WINDOW_PER_THREAD,
|
CANDIDATE_PAIR_CHILD_STACK_BYTES = 4 * 1024 * 1024,
|
||||||
CANDIDATE_PAIR_FIXED_MEMORY = 256 * 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 {
|
typedef struct {
|
||||||
|
|
@ -87,6 +95,8 @@ typedef struct {
|
||||||
#ifdef LARDON3D_CANDIDATE_PAIR_TASK_TESTING
|
#ifdef LARDON3D_CANDIDATE_PAIR_TASK_TESTING
|
||||||
static atomic_size_t test_started_participants;
|
static atomic_size_t test_started_participants;
|
||||||
static atomic_size_t test_computed_work_items;
|
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) {
|
void lardon3d_candidate_pair_task_test_reset_parallel_counters(void) {
|
||||||
atomic_store(&test_started_participants, 0);
|
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) {
|
size_t lardon3d_candidate_pair_task_test_computed_work_items(void) {
|
||||||
return atomic_load(&test_computed_work_items);
|
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
|
#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) {
|
static void *compute_worker(void *userdata) {
|
||||||
CandidateComputeWorker *worker = userdata;
|
CandidateComputeWorker *worker = userdata;
|
||||||
#ifdef LARDON3D_CANDIDATE_PAIR_TASK_TESTING
|
#ifdef LARDON3D_CANDIDATE_PAIR_TASK_TESTING
|
||||||
|
|
@ -121,11 +149,27 @@ static void *compute_worker(void *userdata) {
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
lardon3d_project_db_close(worker->database);
|
close_worker_database(worker);
|
||||||
worker->database = NULL;
|
|
||||||
return NULL;
|
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) {
|
static unsigned int membership_progress(uint64_t completed, uint64_t total) {
|
||||||
if (completed >= total && total != 0) return 99U;
|
if (completed >= total && total != 0) return 99U;
|
||||||
if (total == 0) return 0U;
|
if (total == 0) return 0U;
|
||||||
|
|
@ -161,12 +205,29 @@ static bool compute_window(const Lardon3DCandidatePairTaskContext *context,
|
||||||
unsigned int admitted_threads,
|
unsigned int admitted_threads,
|
||||||
Lardon3DCandidatePairComputation *computations,
|
Lardon3DCandidatePairComputation *computations,
|
||||||
Lardon3DVisualIndexResult *results) {
|
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;
|
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;
|
if (worker_count > source_count) worker_count = (unsigned int)source_count;
|
||||||
CandidateComputeWorker workers[CANDIDATE_PAIR_CPU_THREADS];
|
CandidateComputeWorker workers[CANDIDATE_PAIR_CPU_MAX_SAFE];
|
||||||
pthread_t threads[CANDIDATE_PAIR_CPU_THREADS - 1];
|
pthread_t threads[CANDIDATE_PAIR_CPU_MAX_SAFE - 1];
|
||||||
size_t created = 0;
|
size_t created = 0;
|
||||||
bool started = true;
|
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) {
|
for (size_t i = 0; i < source_count; ++i) {
|
||||||
results[i] = LARDON3D_VISUAL_INDEX_IO_ERROR;
|
results[i] = LARDON3D_VISUAL_INDEX_IO_ERROR;
|
||||||
}
|
}
|
||||||
|
|
@ -187,32 +248,59 @@ static bool compute_window(const Lardon3DCandidatePairTaskContext *context,
|
||||||
&workers[i].database, error) !=
|
&workers[i].database, error) !=
|
||||||
LARDON3D_PROJECT_DB_OK) {
|
LARDON3D_PROJECT_DB_OK) {
|
||||||
for (unsigned int opened = 0; opened < i; ++opened) {
|
for (unsigned int opened = 0; opened < i; ++opened) {
|
||||||
lardon3d_project_db_close(workers[opened].database);
|
close_worker_database(&workers[opened]);
|
||||||
workers[opened].database = NULL;
|
|
||||||
}
|
}
|
||||||
|
(void)pthread_attr_destroy(&attributes);
|
||||||
return false;
|
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) {
|
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;
|
started = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
++created;
|
++created;
|
||||||
}
|
}
|
||||||
|
if (pthread_attr_destroy(&attributes) != 0) started = false;
|
||||||
if (worker_count > 0) (void)compute_worker(&workers[0]);
|
if (worker_count > 0) (void)compute_worker(&workers[0]);
|
||||||
for (size_t i = 0; i < created; ++i) {
|
for (size_t i = 0; i < created; ++i) {
|
||||||
if (pthread_join(threads[i], NULL) != 0) started = false;
|
if (pthread_join(threads[i], NULL) != 0) started = false;
|
||||||
}
|
}
|
||||||
for (unsigned int i = (unsigned int)created + 1; i < worker_count; ++i) {
|
for (unsigned int i = (unsigned int)created + 1; i < worker_count; ++i) {
|
||||||
if (workers[i].database) {
|
close_worker_database(&workers[i]);
|
||||||
lardon3d_project_db_close(workers[i].database);
|
|
||||||
workers[i].database = NULL;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return started;
|
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) {
|
static bool run(Lardon3DTask *task, void *userdata) {
|
||||||
Lardon3DCandidatePairTaskContext *context = 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_MINIMUM_BATCH ||
|
||||||
contract.batch_size > CANDIDATE_PAIR_MAXIMUM_BATCH ||
|
contract.batch_size > CANDIDATE_PAIR_MAXIMUM_BATCH ||
|
||||||
contract.cpu_threads == 0 ||
|
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.");
|
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) {
|
while (processed_in_sequence < contract.batch_size) {
|
||||||
if (!lardon3d_task_checkpoint(task)) return false;
|
if (!lardon3d_task_checkpoint(task)) return false;
|
||||||
size_t remaining = contract.batch_size - processed_in_sequence;
|
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;
|
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;
|
if (window_capacity > remaining) window_capacity = remaining;
|
||||||
uint64_t source_ids[CANDIDATE_PAIR_WINDOW_MAX];
|
uint64_t source_ids[CANDIDATE_PAIR_WINDOW_MAX];
|
||||||
size_t source_count = 0;
|
size_t source_count = 0;
|
||||||
|
|
@ -298,10 +387,12 @@ static bool run(Lardon3DTask *task, void *userdata) {
|
||||||
|
|
||||||
Lardon3DCandidatePairComputation computations[CANDIDATE_PAIR_WINDOW_MAX];
|
Lardon3DCandidatePairComputation computations[CANDIDATE_PAIR_WINDOW_MAX];
|
||||||
Lardon3DVisualIndexResult results[CANDIDATE_PAIR_WINDOW_MAX];
|
Lardon3DVisualIndexResult results[CANDIDATE_PAIR_WINDOW_MAX];
|
||||||
/* The Queue callback is one admitted CPU participant. At most
|
/* CONTRACT: the Queue callback is one admitted CPU participant.
|
||||||
* cpu_threads-1 child threads are created, every reader owns a
|
* At most cpu_threads-1 child threads are created, every useful
|
||||||
* private SQLite handle, and all are joined before reservation
|
* participant owns one private SQLite handle, and all are joined
|
||||||
* release or sequence_break. */
|
* 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,
|
if (!compute_window(context, context->parameters.visual_index_id,
|
||||||
&query_options, source_ids, source_count,
|
&query_options, source_ids, source_count,
|
||||||
contract.cpu_threads, computations, results)) {
|
contract.cpu_threads, computations, results)) {
|
||||||
|
|
@ -472,12 +563,12 @@ Lardon3DTask *lardon3d_project_create_candidate_pair_generate_task(
|
||||||
Lardon3DCandidatePairTaskContext *context =
|
Lardon3DCandidatePairTaskContext *context =
|
||||||
make_context(&runtime, ¶meters);
|
make_context(&runtime, ¶meters);
|
||||||
if (!context) return NULL;
|
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
|
#ifdef LARDON3D_CANDIDATE_PAIR_TASK_TESTING
|
||||||
const char *test_threads = getenv("LARDON3D_TEST_CANDIDATE_PAIR_THREADS");
|
const char *test_threads = getenv("LARDON3D_TEST_CANDIDATE_PAIR_THREADS");
|
||||||
if (test_threads) {
|
if (test_threads) {
|
||||||
unsigned long parsed = strtoul(test_threads, NULL, 10);
|
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;
|
desired_cpu_threads = (unsigned int)parsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
#include <limits.h>
|
||||||
#include <openssl/evp.h>
|
#include <openssl/evp.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
@ -206,13 +207,21 @@ static bool run(Lardon3DTask *task, void *userdata) {
|
||||||
struct timespec end = {0};
|
struct timespec end = {0};
|
||||||
bool timing_known = clock_gettime(CLOCK_MONOTONIC, &begin) == 0;
|
bool timing_known = clock_gettime(CLOCK_MONOTONIC, &begin) == 0;
|
||||||
Lardon3DOpenCvTaskThreadControl threads;
|
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.");
|
return lardon3d_task_fail(task, "Contrat CPU OpenCV Features invalide.");
|
||||||
}
|
}
|
||||||
/* Queue has one callback owner, so this process-wide OpenCV setting cannot
|
/* EXTERNAL LIBRARY: OpenCV accepts a positive signed-int thread count.
|
||||||
* race another Task. The immutable admission selects 1..12, and this guard
|
* Queue has one callback owner, so this process-wide setting cannot race
|
||||||
* applies exactly that count before extraction and restores it on every
|
* another Task. Governor clamps the portable INT_MAX capability to the host
|
||||||
* return without changing Feature identity. */
|
* compute pool; the guard applies that immutable admission and restores it
|
||||||
|
* on every return without changing Feature identity. */
|
||||||
size_t durable_items = 0;
|
size_t durable_items = 0;
|
||||||
bool result = run_body(task, userdata, &durable_items);
|
bool result = run_body(task, userdata, &durable_items);
|
||||||
if (!lardon3d_opencv_task_threads_end(&threads)) {
|
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,
|
.memory_bytes_per_item = 512ULL * 1024 * 1024,
|
||||||
.minimum_batch_size = 1,
|
.minimum_batch_size = 1,
|
||||||
.maximum_batch_size = 1,
|
.maximum_batch_size = 1,
|
||||||
/* Canonical durable maximum. Governor admission may select
|
/* EXTERNAL LIBRARY: OpenCV's positive-int API is the safe
|
||||||
* any validated OpenCV count in 1..12 for this execution. */
|
* ceiling; Governor supplies the portable host maximum. */
|
||||||
.desired_cpu_threads = 12,
|
.desired_cpu_threads = INT_MAX,
|
||||||
.desired_io_slots = 1,
|
.desired_io_slots = 1,
|
||||||
.task_class = LARDON3D_RESOURCE_TASK_CPU};
|
.task_class = LARDON3D_RESOURCE_TASK_CPU};
|
||||||
Lardon3DTask *task = lardon3d_task_create_typed("Extraction de features", &estimate,
|
Lardon3DTask *task = lardon3d_task_create_typed("Extraction de features", &estimate,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
#include <climits>
|
#include <climits>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
|
@ -11,6 +12,7 @@
|
||||||
#include <openssl/evp.h>
|
#include <openssl/evp.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
@ -19,10 +21,23 @@ extern "C" {
|
||||||
#include <lardon3d/match_file.h>
|
#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 {
|
namespace {
|
||||||
|
|
||||||
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TESTING
|
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TESTING
|
||||||
uint32_t estimator_calls;
|
std::atomic<uint32_t> estimator_calls{0};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void put_u32(unsigned char *bytes, uint32_t value) {
|
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) !=
|
if (lardon3d_feature_reader_open(project_path, &set, &reader, &metadata) !=
|
||||||
LARDON3D_FEATURE_STORE_OK)
|
LARDON3D_FEATURE_STORE_OK)
|
||||||
return false;
|
return false;
|
||||||
|
try {
|
||||||
points.resize(set.feature_count);
|
points.resize(set.feature_count);
|
||||||
bool ok = metadata.feature_count == set.feature_count;
|
bool ok = metadata.feature_count == set.feature_count;
|
||||||
for (uint32_t start = 0; ok && start < set.feature_count; start += 256) {
|
for (uint32_t start = 0; ok && start < set.feature_count; start += 256) {
|
||||||
size_t count = std::min<size_t>(256, set.feature_count - start);
|
size_t count = std::min<size_t>(256, set.feature_count - start);
|
||||||
ok = lardon3d_feature_reader_keypoints(reader, start, points.data() + start,
|
ok = lardon3d_feature_reader_keypoints(
|
||||||
count) == LARDON3D_FEATURE_STORE_OK;
|
reader, start, points.data() + start, count) ==
|
||||||
|
LARDON3D_FEATURE_STORE_OK;
|
||||||
}
|
}
|
||||||
lardon3d_feature_reader_close(reader);
|
lardon3d_feature_reader_close(reader);
|
||||||
return ok;
|
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,
|
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,
|
const Lardon3DGeometricVerifierParameters *parameters,
|
||||||
uint32_t seed, cv::Mat &model, cv::Mat &mask) {
|
uint32_t seed, cv::Mat &model, cv::Mat &mask) {
|
||||||
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TESTING
|
#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");
|
const char *behavior = std::getenv("LARDON3D_TEST_GEOMETRIC_ESTIMATOR");
|
||||||
if (behavior && std::strcmp(behavior, "error") == 0)
|
if (behavior && std::strcmp(behavior, "error") == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -171,11 +194,11 @@ bool has_minimal_support(const Lardon3DMatchFileEntry *entries, size_t count,
|
||||||
|
|
||||||
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TESTING
|
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TESTING
|
||||||
extern "C" void lardon3d_geometric_verifier_test_reset_estimator_calls(void) {
|
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) {
|
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(
|
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 char *project_path, Lardon3DProjectDb *db, uint64_t match_id,
|
||||||
const Lardon3DGeometricVerifierParameters *p, uint32_t verifier_version,
|
const Lardon3DGeometricVerifierParameters *p, uint32_t verifier_version,
|
||||||
Lardon3DProjectDbGeometricVerificationResult *result, bool *reused) {
|
Lardon3DProjectDbGeometricVerificationResult *result, bool *reused) {
|
||||||
if (!project_path || !db || match_id == 0 || !p || !result || !reused ||
|
Lardon3DGeometricVerifierPrepared *prepared = nullptr;
|
||||||
!lardon3d_geometric_verifier_parameters_valid(p) ||
|
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_V1 &&
|
||||||
verifier_version != LARDON3D_GEOMETRIC_VERIFIER_VERSION_V2 &&
|
verifier_version != LARDON3D_GEOMETRIC_VERIFIER_VERSION_V2 &&
|
||||||
verifier_version != LARDON3D_GEOMETRIC_VERIFIER_VERSION_V3))
|
verifier_version != LARDON3D_GEOMETRIC_VERIFIER_VERSION_V3))
|
||||||
return LARDON3D_GEOMETRIC_VERIFIER_INVALID_ARGUMENT;
|
return LARDON3D_GEOMETRIC_VERIFIER_INVALID_ARGUMENT;
|
||||||
*reused = false;
|
std::memset(result, 0, sizeof(*result));
|
||||||
unsigned char fingerprint[32];
|
unsigned char fingerprint[32];
|
||||||
if (!lardon3d_geometric_verifier_fingerprint_for_version(
|
if (!lardon3d_geometric_verifier_fingerprint_for_version(
|
||||||
p, verifier_version, fingerprint))
|
p, verifier_version, fingerprint))
|
||||||
|
|
@ -419,8 +465,7 @@ lardon3d_geometric_verifier_verify_and_publish_version(
|
||||||
const bool acceptance_feasible =
|
const bool acceptance_feasible =
|
||||||
verifier_version != LARDON3D_GEOMETRIC_VERIFIER_VERSION_V3 ||
|
verifier_version != LARDON3D_GEOMETRIC_VERIFIER_VERSION_V3 ||
|
||||||
count >= p->min_inlier_count;
|
count >= p->min_inlier_count;
|
||||||
const bool sufficient_support = minimal_support && acceptance_feasible;
|
if (minimal_support && acceptance_feasible) {
|
||||||
if (sufficient_support) {
|
|
||||||
uint32_t seed = lardon3d_geometric_verifier_seed(
|
uint32_t seed = lardon3d_geometric_verifier_seed(
|
||||||
parent.match_asset_sha256, fingerprint);
|
parent.match_asset_sha256, fingerprint);
|
||||||
if (!estimate_fundamental(points_a, points_b, p, seed, model, mask))
|
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)
|
if (mask.ptr<unsigned char>()[index] != 0)
|
||||||
return LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR;
|
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;
|
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));
|
int64_t now = static_cast<int64_t>(std::time(nullptr));
|
||||||
if (now < 0)
|
if (now < 0)
|
||||||
return LARDON3D_GEOMETRIC_VERIFIER_DATABASE_ERROR;
|
return LARDON3D_GEOMETRIC_VERIFIER_DATABASE_ERROR;
|
||||||
|
|
@ -467,17 +548,18 @@ lardon3d_geometric_verifier_verify_and_publish_version(
|
||||||
#endif
|
#endif
|
||||||
Lardon3DProjectDbResult created =
|
Lardon3DProjectDbResult created =
|
||||||
lardon3d_project_db_create_geometric_verification_result(
|
lardon3d_project_db_create_geometric_verification_result(
|
||||||
db, match_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL,
|
db, prepared->match_result_id,
|
||||||
verifier_version, fingerprint,
|
LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL,
|
||||||
accepted ? LARDON3D_GEOMETRIC_VERIFIED
|
prepared->verifier_version, prepared->fingerprint, prepared->status,
|
||||||
: LARDON3D_GEOMETRIC_REJECTED,
|
prepared->inlier_count, prepared->inlier_mask.data(),
|
||||||
inliers, bitset.data(), bitset.size(),
|
prepared->inlier_mask.size(),
|
||||||
accepted ? canonical : nullptr, now, result);
|
prepared->has_model ? prepared->model : nullptr, now, result);
|
||||||
if (created == LARDON3D_PROJECT_DB_CONSTRAINT) {
|
if (created == LARDON3D_PROJECT_DB_CONSTRAINT) {
|
||||||
Lardon3DProjectDbResult concurrent =
|
Lardon3DProjectDbResult concurrent =
|
||||||
lardon3d_project_db_find_geometric_verification_result(
|
lardon3d_project_db_find_geometric_verification_result(
|
||||||
db, match_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL,
|
db, prepared->match_result_id,
|
||||||
verifier_version, fingerprint, result);
|
LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL,
|
||||||
|
prepared->verifier_version, prepared->fingerprint, result);
|
||||||
if (concurrent == LARDON3D_PROJECT_DB_OK) {
|
if (concurrent == LARDON3D_PROJECT_DB_OK) {
|
||||||
*reused = true;
|
*reused = true;
|
||||||
return LARDON3D_GEOMETRIC_VERIFIER_OK;
|
return LARDON3D_GEOMETRIC_VERIFIER_OK;
|
||||||
|
|
@ -487,14 +569,9 @@ lardon3d_geometric_verifier_verify_and_publish_version(
|
||||||
return created == LARDON3D_PROJECT_DB_OK
|
return created == LARDON3D_PROJECT_DB_OK
|
||||||
? LARDON3D_GEOMETRIC_VERIFIER_OK
|
? LARDON3D_GEOMETRIC_VERIFIER_OK
|
||||||
: LARDON3D_GEOMETRIC_VERIFIER_DATABASE_ERROR;
|
: 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.
|
extern "C" void lardon3d_geometric_verifier_internal_prepared_destroy(
|
||||||
} catch (const std::bad_alloc &) {
|
Lardon3DGeometricVerifierPrepared *prepared) {
|
||||||
return LARDON3D_GEOMETRIC_VERIFIER_OUT_OF_MEMORY;
|
delete prepared;
|
||||||
} catch (const cv::Exception &) {
|
|
||||||
return LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR;
|
|
||||||
} catch (...) {
|
|
||||||
return LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
52
src/geometric_verifier_internal.h
Normal file
52
src/geometric_verifier_internal.h
Normal 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
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
#include <pthread.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
@ -9,11 +11,18 @@
|
||||||
#include <lardon3d/project.h>
|
#include <lardon3d/project.h>
|
||||||
#include <lardon3d/task_queue.h>
|
#include <lardon3d/task_queue.h>
|
||||||
|
|
||||||
|
#include "geometric_verifier_internal.h"
|
||||||
|
#include "task_internal.h"
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
GEOMETRIC_VERIFIER_PAGE_CAPACITY =
|
GEOMETRIC_VERIFIER_PAGE_CAPACITY =
|
||||||
LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_BATCH + 1,
|
LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_BATCH + 1,
|
||||||
GEOMETRIC_VERIFIER_MEMORY_BYTES = 4 * 1024 * 1024,
|
/* Each participant owns at most the established conservative 4 MiB core
|
||||||
GEOMETRIC_VERIFIER_CPU_THREADS = 1,
|
* 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 {
|
typedef struct {
|
||||||
|
|
@ -24,6 +33,119 @@ typedef struct {
|
||||||
uint32_t verifier_version;
|
uint32_t verifier_version;
|
||||||
} Lardon3DGeometricVerifierTaskContext;
|
} 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 void destroy_context(void *userdata) { free(userdata); }
|
||||||
|
|
||||||
static Lardon3DGeometricVerifierParameters
|
static Lardon3DGeometricVerifierParameters
|
||||||
|
|
@ -74,36 +196,202 @@ static uint64_t elapsed_ns(struct timespec begin, struct timespec end) {
|
||||||
return seconds * 1000000000ULL + (uint64_t)nanoseconds;
|
return seconds * 1000000000ULL + (uint64_t)nanoseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool process_parent(Lardon3DTask *task,
|
static void *prepare_worker(void *userdata) {
|
||||||
Lardon3DGeometricVerifierTaskContext *context,
|
Lardon3DGeometricVerifierBatch *batch = userdata;
|
||||||
const Lardon3DProjectDbMatchResult *parent) {
|
for (;;) {
|
||||||
if (!lardon3d_task_checkpoint(task)) {
|
if (atomic_load_explicit(&batch->stop, memory_order_relaxed)) {
|
||||||
return false;
|
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 ||
|
if (parent->result_status != LARDON3D_MATCH_RESULT_STATUS_MATCHED ||
|
||||||
parent->match_count == 0) {
|
parent->match_count == 0) {
|
||||||
return true;
|
continue;
|
||||||
}
|
}
|
||||||
Lardon3DGeometricVerifierParameters parameters =
|
job->applicable = true;
|
||||||
parameters_from_durable(&context->durable);
|
job->status = lardon3d_geometric_verifier_internal_prepare_version(
|
||||||
Lardon3DProjectDbGeometricVerificationResult result;
|
batch->context->project_path, batch->context->database,
|
||||||
bool reused = false;
|
parent->match_result_id, &batch->parameters,
|
||||||
if (lardon3d_geometric_verifier_verify_and_publish_version(
|
batch->context->verifier_version, &job->prepared, &job->result,
|
||||||
context->project_path, context->database, parent->match_result_id,
|
&job->reused);
|
||||||
¶meters, 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.
|
#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.");
|
(void)lardon3d_task_fail(task, "Vérification géométrique impossible.");
|
||||||
return false;
|
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
|
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TASK_TESTING
|
||||||
const char *pause = getenv("LARDON3D_TEST_GEOMETRIC_PAUSE_AFTER_PUBLICATION");
|
const char *pause =
|
||||||
if (pause && strcmp(pause, "1") == 0) {
|
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);
|
(void)lardon3d_task_pause(task);
|
||||||
return lardon3d_task_checkpoint(task);
|
destroy_jobs(jobs, count);
|
||||||
|
(void)lardon3d_task_checkpoint(task);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
#endif
|
#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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,11 +420,19 @@ static bool run(Lardon3DTask *task, void *userdata) {
|
||||||
}
|
}
|
||||||
Lardon3DTaskExecutionContract contract;
|
Lardon3DTaskExecutionContract contract;
|
||||||
if (!lardon3d_task_execution_contract(task, &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_MINIMUM_BATCH ||
|
||||||
contract.batch_size > LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_BATCH) {
|
contract.batch_size > LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_BATCH) {
|
||||||
return lardon3d_task_fail(task,
|
return lardon3d_task_fail(
|
||||||
"Contrat de lot Geometric Verifier invalide.");
|
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];
|
Lardon3DProjectDbMatchResult page[GEOMETRIC_VERIFIER_PAGE_CAPACITY];
|
||||||
size_t count = 0;
|
size_t count = 0;
|
||||||
size_t capacity = contract.batch_size + 1;
|
size_t capacity = contract.batch_size + 1;
|
||||||
|
|
@ -154,29 +450,63 @@ static bool run(Lardon3DTask *task, void *userdata) {
|
||||||
struct timespec begin;
|
struct timespec begin;
|
||||||
struct timespec end;
|
struct timespec end;
|
||||||
(void)clock_gettime(CLOCK_MONOTONIC, &begin);
|
(void)clock_gettime(CLOCK_MONOTONIC, &begin);
|
||||||
for (size_t index = 0; index < batch_count; ++index) {
|
Lardon3DGeometricVerifierJob jobs
|
||||||
if (!process_parent(task, context, &page[index])) {
|
[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;
|
return false;
|
||||||
}
|
}
|
||||||
// Publication/reuse must be durable before the business cursor advances.
|
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TASK_TESTING
|
||||||
// Restart may repeat an already published parent, but exact identity
|
/* This barrier is after the all-slot preflight and before index zero, so
|
||||||
// reuse makes that retry converge without duplicating or guessing GVRs.
|
* tests can issue control requests without timing luck or weakening the
|
||||||
context->durable.after_match_result_id = page[index].match_result_id;
|
* production engaged-batch boundary. */
|
||||||
++processed;
|
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;
|
bool exhausted = count <= contract.batch_size;
|
||||||
if (!checkpoint_batch(task, context, exhausted ? 100U : 99U, processed)) {
|
if (!checkpoint_batch(task, context, exhausted ? 100U : 99U, processed)) {
|
||||||
return lardon3d_task_fail(task,
|
return lardon3d_task_fail(task,
|
||||||
"Checkpoint Geometric Verifier impossible.");
|
"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 checkpoint—not 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) {
|
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,
|
return lardon3d_task_set_progress(task, 100,
|
||||||
"Vérification géométrique terminée.");
|
"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;
|
Lardon3DResourceReservation *reservation = NULL;
|
||||||
if (!lardon3d_task_sequence_break(task, context->governor, &reservation,
|
if (!lardon3d_task_sequence_break(task, context->governor, &reservation,
|
||||||
&contract)) {
|
&contract)) {
|
||||||
|
|
@ -199,8 +529,8 @@ make_context(const Lardon3DTaskReconstructionContext *runtime,
|
||||||
if (!lardon3d_geometric_verifier_parameters_valid(¶meters)) {
|
if (!lardon3d_geometric_verifier_parameters_valid(¶meters)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
// Project DB v22 intentionally has no duplicate verifier-version column in
|
// Project DB intentionally has no duplicate verifier-version column in this
|
||||||
// this task payload. The exact versioned fingerprint unambiguously recovers
|
// task payload. The exact versioned fingerprint unambiguously recovers
|
||||||
// historical v1/v2 or current v3 while keeping all identities immutable.
|
// historical v1/v2 or current v3 while keeping all identities immutable.
|
||||||
for (uint32_t candidate = LARDON3D_GEOMETRIC_VERIFIER_VERSION_V1;
|
for (uint32_t candidate = LARDON3D_GEOMETRIC_VERIFIER_VERSION_V1;
|
||||||
candidate <= LARDON3D_GEOMETRIC_VERIFIER_VERSION_V3; ++candidate) {
|
candidate <= LARDON3D_GEOMETRIC_VERIFIER_VERSION_V3; ++candidate) {
|
||||||
|
|
@ -304,10 +634,15 @@ Lardon3DTask *lardon3d_project_create_geometric_verifier_task(
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
const Lardon3DResourceEstimate estimate = {
|
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,
|
.minimum_batch_size = LARDON3D_GEOMETRIC_VERIFIER_TASK_MINIMUM_BATCH,
|
||||||
.maximum_batch_size = LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_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,
|
.desired_io_slots = 1,
|
||||||
.task_class = LARDON3D_RESOURCE_TASK_CPU,
|
.task_class = LARDON3D_RESOURCE_TASK_CPU,
|
||||||
};
|
};
|
||||||
|
|
@ -331,6 +666,9 @@ bool lardon3d_project_enqueue_geometric_verifier_task(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
const Lardon3DGeometricVerifierTaskConfiguration *configuration,
|
const Lardon3DGeometricVerifierTaskConfiguration *configuration,
|
||||||
uint64_t *task_id) {
|
uint64_t *task_id) {
|
||||||
|
if (task_id) {
|
||||||
|
*task_id = 0;
|
||||||
|
}
|
||||||
if (!state || !state->task_queue) {
|
if (!state || !state->task_queue) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,8 @@ shared_memory_evidence(
|
||||||
/* WHY: amdgpu exposes a small stolen/dedicated VRAM aperture even for an
|
/* WHY: amdgpu exposes a small stolen/dedicated VRAM aperture even for an
|
||||||
* integrated GPU. Treating that positive number as separate free memory
|
* integrated GPU. Treating that positive number as separate free memory
|
||||||
* undercharges host RAM. A low-VRAM uncertain device may conservatively
|
* 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;
|
return conservatively_small || system_scale_gtt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
|
|
||||||
#include <lardon3d/image_catalog.h>
|
#include <lardon3d/image_catalog.h>
|
||||||
|
|
||||||
|
#include "image_catalog_persistent_internal.h"
|
||||||
|
|
||||||
enum { COPY_BUFFER_SIZE = 64 * 1024 };
|
enum { COPY_BUFFER_SIZE = 64 * 1024 };
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
|
|
@ -230,16 +232,21 @@ lardon3d_image_catalog_publish_asset_file(Lardon3DAppState *state,
|
||||||
return LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED;
|
return LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if defined(LARDON3D_IMAGE_CATALOG_PERSISTENT_TESTING)
|
||||||
Lardon3DImageCatalogAssetPublishResult
|
Lardon3DImageCatalogAssetPublishResult
|
||||||
lardon3d_image_catalog_test_copy_hash(int input, int output)
|
lardon3d_image_catalog_test_copy_hash(int input, int output)
|
||||||
{
|
{
|
||||||
unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE];
|
unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE];
|
||||||
uint64_t size = 0;
|
uint64_t size = 0;
|
||||||
CopyResult result = hash_stream(input, output, hash, &size);
|
CopyResult result = hash_stream(input, output, hash, &size);
|
||||||
return result == COPY_OK ? LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED
|
if (result == COPY_OK) {
|
||||||
: result == COPY_DESTINATION_ERROR ? LARDON3D_IMAGE_CATALOG_ASSET_PUBLICATION_ERROR
|
return LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED;
|
||||||
|
}
|
||||||
|
return result == COPY_DESTINATION_ERROR
|
||||||
|
? LARDON3D_IMAGE_CATALOG_ASSET_PUBLICATION_ERROR
|
||||||
: LARDON3D_IMAGE_CATALOG_ASSET_SOURCE_ERROR;
|
: LARDON3D_IMAGE_CATALOG_ASSET_SOURCE_ERROR;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
bool
|
bool
|
||||||
lardon3d_image_catalog_create_scanset(Lardon3DAppState *state,
|
lardon3d_image_catalog_create_scanset(Lardon3DAppState *state,
|
||||||
|
|
|
||||||
19
src/image_catalog_persistent_internal.h
Normal file
19
src/image_catalog_persistent_internal.h
Normal 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
|
||||||
1494
src/layout.c
1494
src/layout.c
File diff suppressed because it is too large
Load diff
|
|
@ -218,7 +218,8 @@ static bool auto_vulkan_backend_candidate(Lardon3DOrbVulkanBackend *backend) {
|
||||||
static bool auto_vulkan_runtime_candidate(const Lardon3DAppState *state) {
|
static bool auto_vulkan_runtime_candidate(const Lardon3DAppState *state) {
|
||||||
/* AUTO creation is caller-thread metadata work only. Memory sizing does not
|
/* AUTO creation is caller-thread metadata work only. Memory sizing does not
|
||||||
* belong here: the Governor owns the exact reconstructed batch/depth, UMA
|
* 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
|
* A caller-side maximum-window guess could suppress a safe depth-1 contract
|
||||||
* before CPU fallback was even considered. Driver initialization remains
|
* before CPU fallback was even considered. Driver initialization remains
|
||||||
* deferred to begin() on Queue's affinity-constrained worker. */
|
* deferred to begin() on Queue's affinity-constrained worker. */
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@ typedef struct {
|
||||||
/* OpenCV owns one process-wide CPU pool. Queue's single callback owner makes
|
/* OpenCV owns one process-wide CPU pool. Queue's single callback owner makes
|
||||||
* the change race-free, but configure may mutate before its verification
|
* the change race-free, but configure may mutate before its verification
|
||||||
* fails. Therefore begin attempts rollback on every post-capture failure and
|
* 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(
|
static inline bool lardon3d_opencv_task_threads_begin(
|
||||||
Lardon3DTask *task, unsigned int validated_maximum,
|
Lardon3DTask *task, unsigned int validated_maximum,
|
||||||
Lardon3DOpenCvTaskThreadControl *control) {
|
Lardon3DOpenCvTaskThreadControl *control) {
|
||||||
|
|
@ -29,8 +31,9 @@ static inline bool lardon3d_opencv_task_threads_begin(
|
||||||
control->restore_required = true;
|
control->restore_required = true;
|
||||||
if (!lardon3d_feature_opencv_configure_threads(contract.cpu_threads)) {
|
if (!lardon3d_feature_opencv_configure_threads(contract.cpu_threads)) {
|
||||||
/* Verification failure is after a possibly successful setNumThreads(). */
|
/* 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;
|
control->restore_required = false;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -39,8 +42,11 @@ static inline bool lardon3d_opencv_task_threads_begin(
|
||||||
static inline bool lardon3d_opencv_task_threads_end(
|
static inline bool lardon3d_opencv_task_threads_end(
|
||||||
Lardon3DOpenCvTaskThreadControl *control) {
|
Lardon3DOpenCvTaskThreadControl *control) {
|
||||||
if (!control || !control->restore_required) return false;
|
if (!control || !control->restore_required) return false;
|
||||||
|
if (!lardon3d_feature_opencv_configure_threads(control->previous)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
control->restore_required = false;
|
control->restore_required = false;
|
||||||
return lardon3d_feature_opencv_configure_threads(control->previous);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,12 @@ class Lardon3DOpenCvTaskThreadGuard {
|
||||||
return false;
|
return false;
|
||||||
try {
|
try {
|
||||||
cv::setNumThreads(previous_ > 0 ? previous_ : 1);
|
cv::setNumThreads(previous_ > 0 ? previous_ : 1);
|
||||||
restored_ = true;
|
|
||||||
restore_succeeded_ =
|
restore_succeeded_ =
|
||||||
cv::getNumThreads() == (previous_ > 0 ? previous_ : 1);
|
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_;
|
return restore_succeeded_;
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
/* C callback boundary: restoration failure cannot escape as C++. */
|
/* C callback boundary: restoration failure cannot escape as C++. */
|
||||||
|
|
|
||||||
2064
src/optical_profiles.c
Normal file
2064
src/optical_profiles.c
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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),
|
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)
|
context->encoded.data(), context->encoded.size(), &persisted) != LARDON3D_PROJECT_DB_OK)
|
||||||
return lardon3d_task_fail(task, "Triage photo durable introuvable.");
|
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;
|
for (uint32_t group_id = persisted.next_group_id; group_id <= context->plan.group_count;
|
||||||
++group_id) {
|
++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) !=
|
if (lardon3d_project_db_record_photo_quality_result(context->db, &result, next_group_id) !=
|
||||||
LARDON3D_PROJECT_DB_OK)
|
LARDON3D_PROJECT_DB_OK)
|
||||||
return lardon3d_task_fail(task, "Publication du résultat de triage impossible.");
|
return lardon3d_task_fail(task, "Publication du résultat de triage impossible.");
|
||||||
unsigned progress = static_cast<unsigned>((uint64_t(group_id) * 100u) /
|
/* The typed result/cursor transaction is already committed. Publish the
|
||||||
context->plan.group_count);
|
* exact count only now, so a live snapshot cannot lead durable science. */
|
||||||
if (!lardon3d_task_set_progress(task, progress, "Groupe photo analysé.") ||
|
if (!lardon3d_task_set_durable_progress(
|
||||||
|
task, group_id, context->plan.group_count,
|
||||||
|
"Groupe photo analysé.") ||
|
||||||
!checkpoint(context, task, next_group_id))
|
!checkpoint(context, task, next_group_id))
|
||||||
return lardon3d_task_fail(task, "Checkpoint du triage photo impossible.");
|
return lardon3d_task_fail(task, "Checkpoint du triage photo impossible.");
|
||||||
if (group_id < context->plan.group_count) {
|
if (group_id < context->plan.group_count) {
|
||||||
|
|
|
||||||
|
|
@ -453,7 +453,11 @@ static bool read_project_ini(Lardon3DAppState *state, const char *path, ProjectM
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (valid) {
|
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;
|
stable_id_found = true;
|
||||||
|
|
|
||||||
899
src/project_db.c
899
src/project_db.c
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
58
src/resource_governor_external_storage.c
Normal file
58
src/resource_governor_external_storage.c
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -235,6 +235,11 @@ typedef struct {
|
||||||
bool runtime_thread_policy_active;
|
bool runtime_thread_policy_active;
|
||||||
bool mesa_shader_cache_disabled;
|
bool mesa_shader_cache_disabled;
|
||||||
bool externally_constrained;
|
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 compute_cpu_count;
|
||||||
unsigned int reserved_cpu_count;
|
unsigned int reserved_cpu_count;
|
||||||
uint64_t allowed_mask[LARDON3D_RESOURCE_CPU_MASK_WORDS];
|
uint64_t allowed_mask[LARDON3D_RESOURCE_CPU_MASK_WORDS];
|
||||||
|
|
@ -356,6 +361,36 @@ bool lardon3d_resource_governor_internal_capability_hardware_safe(
|
||||||
const Lardon3DTaskCapability *capability
|
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
|
/* Governor owns the bounded topology snapshot and compute mask. Queue calls
|
||||||
* apply only from its sole heavy-compute worker; callers/main threads must not
|
* apply only from its sole heavy-compute worker; callers/main threads must not
|
||||||
* be constrained. Failure is observable and leaves Task scientific state
|
* 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
|
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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -223,16 +223,18 @@ lardon3d_resource_snapshot_capture_gpu_at_root(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
static bool
|
||||||
lardon3d_resource_snapshot_capture(
|
capture_resource_snapshot(
|
||||||
const Lardon3DHardwareProfile *profile,
|
const Lardon3DHardwareProfile *profile,
|
||||||
Lardon3DResourceSnapshot *snapshot,
|
Lardon3DResourceSnapshot *snapshot,
|
||||||
|
uint64_t *swap_total_bytes,
|
||||||
char *error_message,
|
char *error_message,
|
||||||
size_t error_message_size
|
size_t error_message_size
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
set_error(error_message, 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) {
|
|| profile->memory_total_bytes == 0) {
|
||||||
set_error(error_message, error_message_size, "Profil matériel invalide.");
|
set_error(error_message, error_message_size, "Profil matériel invalide.");
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -246,6 +248,7 @@ lardon3d_resource_snapshot_capture(
|
||||||
&snapshot->memory_available_bytes
|
&snapshot->memory_available_bytes
|
||||||
)
|
)
|
||||||
|| !meminfo_bytes(buffer, "MemFree", &snapshot->memory_free_bytes)
|
|| !meminfo_bytes(buffer, "MemFree", &snapshot->memory_free_bytes)
|
||||||
|
|| !meminfo_bytes(buffer, "SwapTotal", swap_total_bytes)
|
||||||
|| !meminfo_bytes(buffer, "SwapFree", &snapshot->swap_available_bytes)
|
|| !meminfo_bytes(buffer, "SwapFree", &snapshot->swap_available_bytes)
|
||||||
|| !capture_load(snapshot)
|
|| !capture_load(snapshot)
|
||||||
|| clock_gettime(CLOCK_MONOTONIC, &snapshot->captured_at) != 0) {
|
|| clock_gettime(CLOCK_MONOTONIC, &snapshot->captured_at) != 0) {
|
||||||
|
|
@ -270,3 +273,44 @@ lardon3d_resource_snapshot_capture(
|
||||||
);
|
);
|
||||||
return true;
|
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
542
src/runtime_observer.c
Normal 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;
|
||||||
|
}
|
||||||
55
src/runtime_observer_internal.h
Normal file
55
src/runtime_observer_internal.h
Normal 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
35
src/runtime_session.c
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
#include <limits.h>
|
||||||
#include <openssl/evp.h>
|
#include <openssl/evp.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
@ -167,12 +168,19 @@ static bool run(Lardon3DTask *task, void *userdata) {
|
||||||
struct timespec end = {0};
|
struct timespec end = {0};
|
||||||
bool timing_known = clock_gettime(CLOCK_MONOTONIC, &begin) == 0;
|
bool timing_known = clock_gettime(CLOCK_MONOTONIC, &begin) == 0;
|
||||||
Lardon3DOpenCvTaskThreadControl threads;
|
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.");
|
return lardon3d_task_fail(task, "Contrat CPU OpenCV SIFT invalide.");
|
||||||
}
|
}
|
||||||
/* SIFT/RootSIFT consume the immutable admitted 1..12 OpenCV count. Queue's
|
/* EXTERNAL LIBRARY: OpenCV accepts a positive signed-int thread count.
|
||||||
* single Task owner makes the process-wide set/restore deterministic and
|
* Governor clamps the portable INT_MAX capability to the host compute pool;
|
||||||
* race-free without changing extractor identity or output semantics. */
|
* 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;
|
size_t durable_items = 0;
|
||||||
bool result = run_body(task, userdata, &durable_items);
|
bool result = run_body(task, userdata, &durable_items);
|
||||||
if (!lardon3d_opencv_task_threads_end(&threads)) {
|
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,
|
.memory_bytes_per_item = 1024ULL * 1024 * 1024,
|
||||||
.minimum_batch_size = 1,
|
.minimum_batch_size = 1,
|
||||||
.maximum_batch_size = 1,
|
.maximum_batch_size = 1,
|
||||||
/* Canonical durable maximum; the admitted OpenCV count may
|
/* EXTERNAL LIBRARY: OpenCV's positive-int API is the safe
|
||||||
* adapt within the validated 1..12 range. */
|
* ceiling; Governor supplies the portable host maximum. */
|
||||||
.desired_cpu_threads = 12,
|
.desired_cpu_threads = INT_MAX,
|
||||||
.desired_io_slots = 1,
|
.desired_io_slots = 1,
|
||||||
.task_class = LARDON3D_RESOURCE_TASK_CPU};
|
.task_class = LARDON3D_RESOURCE_TASK_CPU};
|
||||||
const char *kind = parameters->rootsift ? LARDON3D_ROOTSIFT_EXTRACT_TASK_KIND
|
const char *kind = parameters->rootsift ? LARDON3D_ROOTSIFT_EXTRACT_TASK_KIND
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,14 @@ namespace {
|
||||||
|
|
||||||
using Result = Lardon3DSparseGeometryResult;
|
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 {
|
struct RngGuard {
|
||||||
uint64_t saved;
|
uint64_t saved;
|
||||||
explicit RngGuard(uint64_t seed) : saved(cv::theRNG().state) {
|
explicit RngGuard(uint64_t seed) : saved(cv::theRNG().state) {
|
||||||
|
|
@ -187,8 +195,12 @@ extern "C" Lardon3DSparseGeometryResult lardon3d_sparse_geometry_relative_pose(
|
||||||
const Lardon3DSparseGeometryRelativePoseParameters *parameters,
|
const Lardon3DSparseGeometryRelativePoseParameters *parameters,
|
||||||
Lardon3DSparseGeometryRelativePoseResult *result) {
|
Lardon3DSparseGeometryRelativePoseResult *result) {
|
||||||
if (!calibration_a || !calibration_b || !pixels_a || !pixels_b ||
|
if (!calibration_a || !calibration_b || !pixels_a || !pixels_b ||
|
||||||
!parameters || !result || count > static_cast<size_t>(INT_MAX) ||
|
!parameters || !result ||
|
||||||
parameters->max_iterations == 0 || parameters->confidence <= 0.0 ||
|
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->confidence >= 1.0 || parameters->robust_threshold_px <= 0.0 ||
|
||||||
parameters->minimum_parallax_rad < 0.0 ||
|
parameters->minimum_parallax_rad < 0.0 ||
|
||||||
parameters->minimum_cheirality_ratio <= 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;
|
return LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT;
|
||||||
if (!points_finite(pixels_a, count) || !points_finite(pixels_b, count))
|
if (!points_finite(pixels_a, count) || !points_finite(pixels_b, count))
|
||||||
return LARDON3D_SPARSE_GEOMETRY_NONFINITE_INPUT;
|
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_count = 0;
|
||||||
result->inlier_ratio = 0.0;
|
result->inlier_ratio = 0.0;
|
||||||
result->median_parallax_rad = 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(
|
cv::Mat essential = cv::findEssentialMat(
|
||||||
points_a, points_b, 1.0, cv::Point2d(0, 0), cv::RANSAC,
|
points_a, points_b, 1.0, cv::Point2d(0, 0), cv::RANSAC,
|
||||||
parameters->confidence, normalized_threshold,
|
parameters->confidence, normalized_threshold,
|
||||||
static_cast<int>(parameters->max_iterations), mask);
|
max_iterations, mask);
|
||||||
if (essential.empty())
|
if (essential.empty())
|
||||||
return LARDON3D_SPARSE_GEOMETRY_ESTIMATION_FAILED;
|
return LARDON3D_SPARSE_GEOMETRY_ESTIMATION_FAILED;
|
||||||
cv::Mat rotation, translation;
|
cv::Mat rotation, translation;
|
||||||
int inliers = cv::recoverPose(essential, points_a, points_b, rotation,
|
int inliers = cv::recoverPose(essential, points_a, points_b, rotation,
|
||||||
translation, 1.0, cv::Point2d(0, 0), mask);
|
translation, 1.0, cv::Point2d(0, 0), mask);
|
||||||
double count_value = static_cast<double>(count);
|
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 <
|
static_cast<double>(inliers) / count_value <
|
||||||
parameters->minimum_inlier_ratio ||
|
parameters->minimum_inlier_ratio ||
|
||||||
!rotation_valid(rotation) || cv::norm(translation) < 1e-12)
|
!rotation_valid(rotation) || cv::norm(translation) < 1e-12)
|
||||||
|
|
@ -519,8 +533,12 @@ extern "C" Lardon3DSparseGeometryResult lardon3d_sparse_geometry_pnp(
|
||||||
const Lardon3DSparseGeometryPnPParameters *parameters,
|
const Lardon3DSparseGeometryPnPParameters *parameters,
|
||||||
Lardon3DSparseGeometryPnPResult *result) {
|
Lardon3DSparseGeometryPnPResult *result) {
|
||||||
if (!calibration || !points || !pixels || !parameters || !result ||
|
if (!calibration || !points || !pixels || !parameters || !result ||
|
||||||
count < 4 || count > static_cast<size_t>(INT_MAX) ||
|
count < 4 ||
|
||||||
parameters->max_iterations == 0 || parameters->confidence <= 0.0 ||
|
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->confidence >= 1.0 || parameters->reprojection_threshold_px <= 0.0 ||
|
||||||
parameters->minimum_inlier_ratio <= 0.0 ||
|
parameters->minimum_inlier_ratio <= 0.0 ||
|
||||||
parameters->minimum_inlier_ratio > 1.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(points[index].z) || !finite_value(pixels[index].x) ||
|
||||||
!finite_value(pixels[index].y))
|
!finite_value(pixels[index].y))
|
||||||
return LARDON3D_SPARSE_GEOMETRY_NONFINITE_INPUT;
|
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 {
|
try {
|
||||||
std::vector<cv::Point3d> object_points;
|
std::vector<cv::Point3d> object_points;
|
||||||
std::vector<cv::Point2d> image_points;
|
std::vector<cv::Point2d> image_points;
|
||||||
|
|
@ -561,12 +582,11 @@ extern "C" Lardon3DSparseGeometryResult lardon3d_sparse_geometry_pnp(
|
||||||
RngGuard rng(parameters->deterministic_seed);
|
RngGuard rng(parameters->deterministic_seed);
|
||||||
bool solved = cv::solvePnPRansac(
|
bool solved = cv::solvePnPRansac(
|
||||||
object_points, image_points, camera_matrix(*calibration),
|
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),
|
static_cast<float>(parameters->reprojection_threshold_px),
|
||||||
parameters->confidence, inliers, cv::SOLVEPNP_EPNP);
|
parameters->confidence, inliers, cv::SOLVEPNP_EPNP);
|
||||||
uint32_t minimum = std::max<uint32_t>(parameters->minimum_inliers, 4);
|
|
||||||
double count_value = static_cast<double>(count);
|
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 <
|
static_cast<double>(inliers.rows) / count_value <
|
||||||
parameters->minimum_inlier_ratio)
|
parameters->minimum_inlier_ratio)
|
||||||
return LARDON3D_SPARSE_GEOMETRY_ESTIMATION_FAILED;
|
return LARDON3D_SPARSE_GEOMETRY_ESTIMATION_FAILED;
|
||||||
|
|
|
||||||
2636
src/ssd_controller.c
Normal file
2636
src/ssd_controller.c
Normal file
File diff suppressed because it is too large
Load diff
162
src/ssd_controller_internal.h
Normal file
162
src/ssd_controller_internal.h
Normal 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
|
||||||
111
src/task.c
111
src/task.c
|
|
@ -14,6 +14,9 @@ struct Lardon3DTask {
|
||||||
uint64_t id;
|
uint64_t id;
|
||||||
char name[LARDON3D_TASK_NAME_CAPACITY];
|
char name[LARDON3D_TASK_NAME_CAPACITY];
|
||||||
unsigned int progress;
|
unsigned int progress;
|
||||||
|
bool durable_progress_known;
|
||||||
|
uint64_t durable_completed;
|
||||||
|
uint64_t durable_total;
|
||||||
Lardon3DTaskState state;
|
Lardon3DTaskState state;
|
||||||
char message[LARDON3D_TASK_MESSAGE_CAPACITY];
|
char message[LARDON3D_TASK_MESSAGE_CAPACITY];
|
||||||
struct timespec started_at;
|
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.sift") == 0
|
||||||
|| strcmp(task_kind, "features.extract.rootsift") == 0
|
|| strcmp(task_kind, "features.extract.rootsift") == 0
|
||||||
|| strcmp(task_kind, "visual_index.update") == 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
|
static bool
|
||||||
kind_has_validated_batch_range(const char *task_kind, uint32_t version)
|
kind_has_validated_batch_range(const char *task_kind, uint32_t version)
|
||||||
{
|
{
|
||||||
/* Candidate generation already executes/publishes at every canonical
|
/* Candidate generation explicitly co-adapts its sequence window. GV keeps
|
||||||
* 1..64 sequence size. This private bit changes operational pacing only;
|
* its 16-item preparation window while trialling CPU, because CPU2 with a
|
||||||
* it does not add a scientific or durable identity dimension. */
|
* 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
|
return task_kind && version == 1
|
||||||
&& strcmp(task_kind, "candidate_pair.generate") == 0;
|
&& strcmp(task_kind, "candidate_pair.generate") == 0;
|
||||||
}
|
}
|
||||||
|
|
@ -1107,6 +1112,62 @@ lardon3d_task_set_progress(
|
||||||
bool accepted = !is_terminal(task->state);
|
bool accepted = !is_terminal(task->state);
|
||||||
if (accepted) {
|
if (accepted) {
|
||||||
task->progress = progress;
|
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) {
|
if (message) {
|
||||||
copy_text(task->message, sizeof(task->message), message);
|
copy_text(task->message, sizeof(task->message), message);
|
||||||
}
|
}
|
||||||
|
|
@ -1159,6 +1220,48 @@ lardon3d_task_snapshot(
|
||||||
return true;
|
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
|
bool
|
||||||
lardon3d_task_durable_snapshot(
|
lardon3d_task_durable_snapshot(
|
||||||
const Lardon3DTask *task,
|
const Lardon3DTask *task,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
#include <limits.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
@ -23,11 +24,18 @@ extern bool lardon3d_acquisition_campaign_task_internal_configure_restored(
|
||||||
enum {
|
enum {
|
||||||
CANDIDATE_LEGACY_FIXED_BYTES = 128 * 1024,
|
CANDIDATE_LEGACY_FIXED_BYTES = 128 * 1024,
|
||||||
CANDIDATE_CURRENT_FIXED_BYTES = 256 * 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_LEGACY_FIXED_BYTES = 10 * 1024 * 1024,
|
||||||
MATCHER_CURRENT_PER_ITEM_BYTES = 10 * 1024 * 1024,
|
MATCHER_CURRENT_PER_ITEM_BYTES = 10 * 1024 * 1024,
|
||||||
MATCHER_GPU_FIXED_BYTES = 640 * 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,
|
SIFT_FIXED_BYTES = 64 * 1024 * 1024,
|
||||||
|
VISUAL_INDEX_FIXED_BYTES = 8 * 1024 * 1024,
|
||||||
|
VISUAL_INDEX_PER_ITEM_BYTES = 2 * 1024 * 1024,
|
||||||
};
|
};
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
|
|
@ -58,16 +66,35 @@ normalize_known_legacy_estimate(const char *kind,
|
||||||
if (strcmp(kind, "candidate_pair.generate") == 0) {
|
if (strcmp(kind, "candidate_pair.generate") == 0) {
|
||||||
current = (Lardon3DResourceEstimate) {
|
current = (Lardon3DResourceEstimate) {
|
||||||
.memory_fixed_bytes = CANDIDATE_CURRENT_FIXED_BYTES,
|
.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,
|
.minimum_batch_size = 1,
|
||||||
.maximum_batch_size = 64,
|
.maximum_batch_size = 64,
|
||||||
.desired_cpu_threads = 12,
|
.desired_cpu_threads = 64,
|
||||||
.desired_io_slots = 1,
|
.desired_io_slots = 1,
|
||||||
.task_class = LARDON3D_RESOURCE_TASK_CPU,
|
.task_class = LARDON3D_RESOURCE_TASK_CPU,
|
||||||
};
|
};
|
||||||
historical = current;
|
historical = current;
|
||||||
historical.memory_fixed_bytes = CANDIDATE_LEGACY_FIXED_BYTES;
|
historical.memory_bytes_per_item = CANDIDATE_LEGACY_PER_ITEM_BYTES;
|
||||||
historical.desired_cpu_threads = 1;
|
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) {
|
} else if (strcmp(kind, "matcher.run") == 0) {
|
||||||
const bool gpu = durable->desired_gpu_slots == 1;
|
const bool gpu = durable->desired_gpu_slots == 1;
|
||||||
current = (Lardon3DResourceEstimate) {
|
current = (Lardon3DResourceEstimate) {
|
||||||
|
|
@ -108,12 +135,53 @@ normalize_known_legacy_estimate(const char *kind,
|
||||||
.memory_bytes_per_item = UINT64_C(1024) * 1024 * 1024,
|
.memory_bytes_per_item = UINT64_C(1024) * 1024 * 1024,
|
||||||
.minimum_batch_size = 1,
|
.minimum_batch_size = 1,
|
||||||
.maximum_batch_size = 1,
|
.maximum_batch_size = 1,
|
||||||
.desired_cpu_threads = 12,
|
.desired_cpu_threads = INT_MAX,
|
||||||
.desired_io_slots = 1,
|
.desired_io_slots = 1,
|
||||||
.task_class = LARDON3D_RESOURCE_TASK_CPU,
|
.task_class = LARDON3D_RESOURCE_TASK_CPU,
|
||||||
};
|
};
|
||||||
historical = current;
|
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 {
|
} else {
|
||||||
*effective = *durable;
|
*effective = *durable;
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
784
src/task_queue.c
784
src/task_queue.c
File diff suppressed because it is too large
Load diff
35
src/task_queue_internal.h
Normal file
35
src/task_queue_internal.h
Normal 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
|
||||||
679
src/tui_model.c
Normal file
679
src/tui_model.c
Normal 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
717
src/tui_optics.c
Normal 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
693
src/tui_ssd_async.c
Normal 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, ¤t)
|
||||||
|
&& 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;
|
||||||
|
}
|
||||||
17
src/tui_ssd_async_internal.h
Normal file
17
src/tui_ssd_async_internal.h
Normal 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
|
||||||
|
|
@ -24,6 +24,7 @@ enum {
|
||||||
SEGMENT_POSTING_MAX = LARDON3D_VISUAL_INDEX_SEGMENT_FEATURE_SET_MAX * 1024 *
|
SEGMENT_POSTING_MAX = LARDON3D_VISUAL_INDEX_SEGMENT_FEATURE_SET_MAX * 1024 *
|
||||||
LARDON3D_VISUAL_INDEX_TABLE_COUNT,
|
LARDON3D_VISUAL_INDEX_TABLE_COUNT,
|
||||||
FEATURE_SET_POSTING_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 {
|
typedef struct {
|
||||||
|
|
@ -249,6 +250,17 @@ static Lardon3DVisualIndexResult build_postings(
|
||||||
PostingBuildJob jobs[LARDON3D_VISUAL_INDEX_SEGMENT_FEATURE_SET_MAX];
|
PostingBuildJob jobs[LARDON3D_VISUAL_INDEX_SEGMENT_FEATURE_SET_MAX];
|
||||||
pthread_t children[LARDON3D_VISUAL_INDEX_SEGMENT_FEATURE_SET_MAX - 1];
|
pthread_t children[LARDON3D_VISUAL_INDEX_SEGMENT_FEATURE_SET_MAX - 1];
|
||||||
bool created[LARDON3D_VISUAL_INDEX_SEGMENT_FEATURE_SET_MAX - 1] = {false};
|
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) {
|
for (size_t participant = 0; participant < participants; ++participant) {
|
||||||
jobs[participant] = (PostingBuildJob){.project_path = project_path,
|
jobs[participant] = (PostingBuildJob){.project_path = project_path,
|
||||||
.sets = sets,
|
.sets = sets,
|
||||||
|
|
@ -262,9 +274,10 @@ static Lardon3DVisualIndexResult build_postings(
|
||||||
}
|
}
|
||||||
for (size_t participant = 1; participant < participants; ++participant) {
|
for (size_t participant = 1; participant < participants; ++participant) {
|
||||||
created[participant - 1] =
|
created[participant - 1] =
|
||||||
pthread_create(&children[participant - 1], NULL, build_posting_slices,
|
pthread_create(&children[participant - 1], &attributes, build_posting_slices,
|
||||||
&jobs[participant]) == 0;
|
&jobs[participant]) == 0;
|
||||||
}
|
}
|
||||||
|
bool attributes_released = pthread_attr_destroy(&attributes) == 0;
|
||||||
(void)build_posting_slices(&jobs[0]);
|
(void)build_posting_slices(&jobs[0]);
|
||||||
bool joined = true;
|
bool joined = true;
|
||||||
for (size_t participant = 1; participant < participants; ++participant) {
|
for (size_t participant = 1; participant < participants; ++participant) {
|
||||||
|
|
@ -272,7 +285,7 @@ static Lardon3DVisualIndexResult build_postings(
|
||||||
joined = false;
|
joined = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!joined) {
|
if (!joined || !attributes_released) {
|
||||||
return LARDON3D_VISUAL_INDEX_IO_ERROR;
|
return LARDON3D_VISUAL_INDEX_IO_ERROR;
|
||||||
}
|
}
|
||||||
/* A failed pthread_create does not change results: the owner computes only
|
/* A failed pthread_create does not change results: the owner computes only
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,8 @@
|
||||||
#include <lardon3d/visual_index.h>
|
#include <lardon3d/visual_index.h>
|
||||||
#include <lardon3d/visual_index_task.h>
|
#include <lardon3d/visual_index_task.h>
|
||||||
|
|
||||||
#include "visual_index_internal.h"
|
|
||||||
#include "task_internal.h"
|
#include "task_internal.h"
|
||||||
|
#include "visual_index_internal.h"
|
||||||
enum { VISUAL_INDEX_TASK_CPU_THREADS = 12 };
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char project_path[PATH_MAX];
|
char project_path[PATH_MAX];
|
||||||
|
|
@ -70,7 +68,8 @@ static bool run(Lardon3DTask *task, void *userdata) {
|
||||||
Lardon3DTaskExecutionContract contract;
|
Lardon3DTaskExecutionContract contract;
|
||||||
if (!lardon3d_task_execution_contract(task, &contract) || contract.batch_size == 0 ||
|
if (!lardon3d_task_execution_contract(task, &contract) || contract.batch_size == 0 ||
|
||||||
contract.batch_size > LARDON3D_VISUAL_INDEX_SEGMENT_FEATURE_SET_MAX ||
|
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.");
|
return lardon3d_task_fail(task, "Contrat Visual Index invalide.");
|
||||||
}
|
}
|
||||||
struct timespec begin = {0};
|
struct timespec begin = {0};
|
||||||
|
|
@ -98,10 +97,11 @@ static bool run(Lardon3DTask *task, void *userdata) {
|
||||||
context->governor, LARDON3D_RESOURCE_TASK_CPU, durable_indexed,
|
context->governor, LARDON3D_RESOURCE_TASK_CPU, durable_indexed,
|
||||||
duration_ns, 0);
|
duration_ns, 0);
|
||||||
}
|
}
|
||||||
/* Visual Index already consumes the immutable admitted CPU count. A
|
/* ALGORITHMIC: one participant owns one disjoint Feature Set slice, so the
|
||||||
* segment whose directory publication is not durable remains visible for
|
* immutable admitted CPU count cannot exceed the 16-member segment bound.
|
||||||
* restart semantics but is zero operational work, so it cannot train the
|
* A segment whose directory publication is not durable remains visible for
|
||||||
* next sequence's 1/2/4/8/12 CPU trial. */
|
* restart semantics but is zero operational work and cannot train the next
|
||||||
|
* sequence's portable CPU trial. */
|
||||||
if (duration_ns > 0) {
|
if (duration_ns > 0) {
|
||||||
(void)lardon3d_task_internal_record_sequence(
|
(void)lardon3d_task_internal_record_sequence(
|
||||||
task, duration_ns, durable_indexed);
|
task, duration_ns, durable_indexed);
|
||||||
|
|
@ -206,10 +206,16 @@ Lardon3DTask *lardon3d_project_create_visual_index_update_task(
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
Lardon3DResourceEstimate estimate = {.memory_fixed_bytes = 8ULL * 1024 * 1024,
|
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,
|
.memory_bytes_per_item = 2ULL * 1024 * 1024,
|
||||||
.minimum_batch_size = 1,
|
.minimum_batch_size = 1,
|
||||||
.maximum_batch_size = 16,
|
.maximum_batch_size =
|
||||||
.desired_cpu_threads = VISUAL_INDEX_TASK_CPU_THREADS,
|
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,
|
.desired_io_slots = 1,
|
||||||
.task_class = LARDON3D_RESOURCE_TASK_CPU};
|
.task_class = LARDON3D_RESOURCE_TASK_CPU};
|
||||||
Lardon3DTask *task = lardon3d_task_create_typed(
|
Lardon3DTask *task = lardon3d_task_create_typed(
|
||||||
|
|
|
||||||
|
|
@ -1184,11 +1184,14 @@ bool end_geometric_evidence(Runtime &runtime) {
|
||||||
const bool contract_valid =
|
const bool contract_valid =
|
||||||
last_known && last.backend == LARDON3D_RESOURCE_BACKEND_FIXED &&
|
last_known && last.backend == LARDON3D_RESOURCE_BACKEND_FIXED &&
|
||||||
last.actual_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.gpu_slots == 0 && last.io_slots == 1 &&
|
||||||
last.batch_size >= LARDON3D_GEOMETRIC_VERIFIER_TASK_MINIMUM_BATCH &&
|
last.batch_size >= LARDON3D_GEOMETRIC_VERIFIER_TASK_MINIMUM_BATCH &&
|
||||||
last.batch_size <= LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_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;
|
last.gpu_memory_bytes == 0;
|
||||||
const bool admission_valid =
|
const bool admission_valid =
|
||||||
aggregate_known && !aggregate.saturated &&
|
aggregate_known && !aggregate.saturated &&
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,28 @@
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#include <opencv2/core.hpp>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <lardon3d/acquisition_campaign_task.h>
|
#include <lardon3d/acquisition_campaign_task.h>
|
||||||
#include <lardon3d/project_db.h>
|
#include <lardon3d/project_db.h>
|
||||||
|
#include <lardon3d/task_queue.h>
|
||||||
#include "../src/task_internal.h"
|
#include "../src/task_internal.h"
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" bool
|
extern "C" bool
|
||||||
lardon3d_acquisition_campaign_task_internal_configure_restored(
|
lardon3d_acquisition_campaign_task_internal_configure_restored(
|
||||||
Lardon3DTask *, void *);
|
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) \
|
#define CHECK(x) \
|
||||||
do { \
|
do { \
|
||||||
|
|
@ -27,6 +37,42 @@ lardon3d_task_kind_registry_production(void) {
|
||||||
return nullptr;
|
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() {
|
int main() {
|
||||||
Lardon3DAcquisitionCampaignSource sources[2]{};
|
Lardon3DAcquisitionCampaignSource sources[2]{};
|
||||||
std::snprintf(sources[0].path, sizeof(sources[0].path), "/input/a.arw");
|
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.select_representation = 1;
|
||||||
input.ingest_options.imported_at = 123;
|
input.ingest_options.imported_at = 123;
|
||||||
input.ingest_options.max_source_bytes = 456;
|
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;
|
size_t size = 0;
|
||||||
CHECK(
|
CHECK(
|
||||||
|
|
@ -61,6 +112,21 @@ int main() {
|
||||||
std::vector<unsigned char> encoded(size);
|
std::vector<unsigned char> encoded(size);
|
||||||
CHECK(lardon3d_acquisition_campaign_request_encode(&input, encoded.data(),
|
CHECK(lardon3d_acquisition_campaign_request_encode(&input, encoded.data(),
|
||||||
encoded.size(), &size));
|
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]{};
|
Lardon3DAcquisitionCampaignSource decoded_sources[2]{};
|
||||||
Lardon3DAcquisitionCampaignConfirmation decoded_confirmations[2]{};
|
Lardon3DAcquisitionCampaignConfirmation decoded_confirmations[2]{};
|
||||||
Lardon3DAcquisitionCampaignTaskRequest decoded{};
|
Lardon3DAcquisitionCampaignTaskRequest decoded{};
|
||||||
|
|
@ -93,8 +159,10 @@ int main() {
|
||||||
static_cast<unsigned char>(uint64_t(123) >> (8u * i));
|
static_cast<unsigned char>(uint64_t(123) >> (8u * i));
|
||||||
|
|
||||||
input.ingest_options.imported_at = -1;
|
input.ingest_options.imported_at = -1;
|
||||||
CHECK(!lardon3d_acquisition_campaign_request_encode(&input, nullptr, 0,
|
required_size = 777;
|
||||||
&size));
|
CHECK(!lardon3d_acquisition_campaign_request_encode(
|
||||||
|
&input, nullptr, 0, &required_size) &&
|
||||||
|
required_size == 0);
|
||||||
input.ingest_options.imported_at = 123;
|
input.ingest_options.imported_at = 123;
|
||||||
input.ingest_options.max_source_bytes = 0;
|
input.ingest_options.max_source_bytes = 0;
|
||||||
CHECK(!lardon3d_acquisition_campaign_request_encode(&input, nullptr, 0,
|
CHECK(!lardon3d_acquisition_campaign_request_encode(&input, nullptr, 0,
|
||||||
|
|
@ -126,16 +194,33 @@ int main() {
|
||||||
LARDON3D_PROJECT_DB_OK);
|
LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(lardon3d_project_db_create_scanset(database, "other", &other) ==
|
CHECK(lardon3d_project_db_create_scanset(database, "other", &other) ==
|
||||||
LARDON3D_PROJECT_DB_OK);
|
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{};
|
Lardon3DTaskDurableSnapshot snapshot{};
|
||||||
snapshot.id = 1;
|
snapshot.id = 1;
|
||||||
std::snprintf(snapshot.name, sizeof(snapshot.name), "campaign");
|
std::snprintf(snapshot.name, sizeof(snapshot.name), "campaign");
|
||||||
snapshot.saved_state = snapshot.recovery_state = TASK_PENDING;
|
snapshot.saved_state = snapshot.recovery_state = TASK_PENDING;
|
||||||
Lardon3DProjectDbAcquisitionCampaignTask durable{
|
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(
|
CHECK(lardon3d_project_db_record_acquisition_campaign_task(
|
||||||
database, &snapshot, LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND,
|
database, &snapshot, LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND,
|
||||||
LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND_VERSION, nullptr, &durable,
|
LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND_VERSION, nullptr, &durable,
|
||||||
1) == LARDON3D_PROJECT_DB_OK);
|
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;
|
std::vector<unsigned char> changed = encoded;
|
||||||
changed.back() ^= 1u;
|
changed.back() ^= 1u;
|
||||||
snapshot.progress = 50;
|
snapshot.progress = 50;
|
||||||
|
|
@ -170,17 +255,37 @@ int main() {
|
||||||
CHECK(lardon3d_project_db_retain_acquisition_campaign_capture(
|
CHECK(lardon3d_project_db_retain_acquisition_campaign_capture(
|
||||||
database, 1, 1, second.capture_id, 1) ==
|
database, 1, 1, second.capture_id, 1) ==
|
||||||
LARDON3D_PROJECT_DB_CONSTRAINT);
|
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());
|
std::vector<unsigned char> loaded(encoded.size());
|
||||||
Lardon3DProjectDbAcquisitionCampaignTask loaded_task{};
|
Lardon3DProjectDbAcquisitionCampaignTask loaded_task{};
|
||||||
CHECK(lardon3d_project_db_load_acquisition_campaign_task(
|
CHECK(lardon3d_project_db_load_acquisition_campaign_task(
|
||||||
database, 1, loaded.data(), loaded.size(), &loaded_task) ==
|
database, 1, loaded.data(), loaded.size(), &loaded_task) ==
|
||||||
LARDON3D_PROJECT_DB_OK);
|
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() &&
|
loaded_task.request_size == encoded.size() &&
|
||||||
std::memcmp(loaded.data(), encoded.data(), encoded.size()) == 0);
|
std::memcmp(loaded.data(), encoded.data(), encoded.size()) == 0);
|
||||||
snapshot.estimate = Lardon3DResourceEstimate{
|
std::vector<unsigned char> short_request(encoded.size() - 1u);
|
||||||
256 * 1024, 0, 64 * 1024, 0, 1, 1, 1, 0, 1,
|
Lardon3DProjectDbAcquisitionCampaignTask short_task{};
|
||||||
LARDON3D_RESOURCE_TASK_IMPORT};
|
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,
|
Lardon3DHardwareProfile profile{16, 4096, UINT64_MAX, false, 0, false,
|
||||||
false, 0, "test", ""};
|
false, 0, "test", ""};
|
||||||
Lardon3DResourcePolicy policy{};
|
Lardon3DResourcePolicy policy{};
|
||||||
|
|
@ -190,8 +295,309 @@ int main() {
|
||||||
Lardon3DResourceGovernor *governor =
|
Lardon3DResourceGovernor *governor =
|
||||||
lardon3d_resource_governor_create(&profile, &policy);
|
lardon3d_resource_governor_create(&profile, &policy);
|
||||||
CHECK(governor != nullptr);
|
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};
|
"/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{};
|
Lardon3DTaskKindBinding recovered{};
|
||||||
CHECK(lardon3d_acquisition_campaign_task_reconstruct(
|
CHECK(lardon3d_acquisition_campaign_task_reconstruct(
|
||||||
&snapshot, &valid_reconstruction, &recovered));
|
&snapshot, &valid_reconstruction, &recovered));
|
||||||
|
|
@ -209,23 +615,248 @@ int main() {
|
||||||
Lardon3DResourceReservationInfo admitted{};
|
Lardon3DResourceReservationInfo admitted{};
|
||||||
CHECK(reservation && lardon3d_resource_reservation_get_active(
|
CHECK(reservation && lardon3d_resource_reservation_get_active(
|
||||||
governor, reservation, &admitted));
|
governor, reservation, &admitted));
|
||||||
CHECK(admitted.memory_bytes > 320 * 1024);
|
CHECK(admitted.memory_bytes > raw_working_bytes);
|
||||||
Lardon3DTaskDurableSnapshot unchanged{};
|
Lardon3DTaskDurableSnapshot unchanged{};
|
||||||
CHECK(lardon3d_task_durable_snapshot(restored, &unchanged));
|
CHECK(lardon3d_task_durable_snapshot(restored, &unchanged));
|
||||||
CHECK(unchanged.estimate.memory_fixed_bytes == 256 * 1024 &&
|
CHECK(unchanged.estimate.memory_fixed_bytes ==
|
||||||
unchanged.estimate.memory_bytes_per_item == 64 * 1024);
|
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));
|
CHECK(lardon3d_resource_governor_release(governor, reservation));
|
||||||
lardon3d_task_destroy(restored);
|
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,
|
std::vector<char> overlong_project_path(LARDON3D_APP_STATE_PATH_CAPACITY + 1,
|
||||||
'x');
|
'x');
|
||||||
overlong_project_path.back() = '\0';
|
overlong_project_path.back() = '\0';
|
||||||
Lardon3DTaskReconstructionContext reconstruction{
|
Lardon3DTaskReconstructionContext reconstruction{
|
||||||
overlong_project_path.data(), database, nullptr, nullptr};
|
overlong_project_path.data(), database, governor, nullptr};
|
||||||
Lardon3DTaskKindBinding binding{};
|
Lardon3DTaskKindBinding binding{};
|
||||||
CHECK(!lardon3d_acquisition_campaign_task_reconstruct(
|
CHECK(!lardon3d_acquisition_campaign_task_reconstruct(
|
||||||
&snapshot, &reconstruction, &binding));
|
&snapshot, &reconstruction, &binding));
|
||||||
|
lardon3d_resource_governor_destroy(governor);
|
||||||
lardon3d_project_db_close(database);
|
lardon3d_project_db_close(database);
|
||||||
CHECK(unlink(database_path) == 0);
|
CHECK(unlink(database_path) == 0);
|
||||||
|
CHECK(std::filesystem::remove_all(project_directory) > 0);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -289,6 +289,23 @@ static bool run_test(void) {
|
||||||
lardon3d_candidate_pair_generation_fingerprint(index_id, feature_sets[0].feature_set_id,
|
lardon3d_candidate_pair_generation_fingerprint(index_id, feature_sets[0].feature_set_id,
|
||||||
&options, fp2);
|
&options, fp2);
|
||||||
CHECK(memcmp(fp1, fp2, 32) == 0);
|
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;
|
options.top_k = 8;
|
||||||
lardon3d_candidate_pair_generation_fingerprint(index_id, feature_sets[0].feature_set_id,
|
lardon3d_candidate_pair_generation_fingerprint(index_id, feature_sets[0].feature_set_id,
|
||||||
&options, fp2);
|
&options, fp2);
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,79 @@ static bool same_pair_order(const Lardon3DProjectDbCandidatePair *a, size_t a_co
|
||||||
return true;
|
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(¶llel[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,
|
static bool wait_saved_cursor(Lardon3DProjectDb *database, uint64_t task_id,
|
||||||
uint64_t expected) {
|
uint64_t expected) {
|
||||||
struct timespec deadline;
|
struct timespec deadline;
|
||||||
|
|
@ -315,13 +388,17 @@ static bool run_test(void) {
|
||||||
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 4);
|
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 4);
|
||||||
CHECK(state.task_queue != NULL);
|
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 = {
|
Lardon3DVisualIndexQueryOptions qopts = {
|
||||||
.top_k = 16,
|
.top_k = 16,
|
||||||
.minimum_evidence_count = 10,
|
.minimum_evidence_count = 10,
|
||||||
.scanset_filter = LARDON3D_VISUAL_INDEX_ANY_SCANSET,
|
.scanset_filter = LARDON3D_VISUAL_INDEX_ANY_SCANSET,
|
||||||
.exclude_same_asset = false,
|
.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
|
/* A healthy Governor admits two bounded sources per sequence. The six
|
||||||
* durable memberships create three productive windows: initial admission
|
* 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 &&
|
CHECK(unsetenv("LARDON3D_TEST_CANDIDATE_PAIR_MAXIMUM_BATCH") == 0 &&
|
||||||
unsetenv("LARDON3D_TEST_CANDIDATE_PAIR_THREADS") == 0);
|
unsetenv("LARDON3D_TEST_CANDIDATE_PAIR_THREADS") == 0);
|
||||||
|
|
||||||
/* CPU admission does not authorize extra work. With twelve available
|
/* CPU admission does not authorize extra work. With 64 CPUs requested but
|
||||||
* CPUs but an admitted one-item batch, each of the six sequence breaks
|
* an admitted one-item batch, each of the six sequence breaks
|
||||||
* has exactly one independent source and therefore one useful worker. */
|
* has exactly one independent source and therefore one useful worker. */
|
||||||
CHECK(reset_candidate_pairs(state.project_db) &&
|
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);
|
setenv("LARDON3D_TEST_CANDIDATE_PAIR_MAXIMUM_BATCH", "1", 1) == 0);
|
||||||
lardon3d_candidate_pair_task_test_reset_parallel_counters();
|
lardon3d_candidate_pair_task_test_reset_parallel_counters();
|
||||||
uint64_t pressure_task_id = 0;
|
uint64_t pressure_task_id = 0;
|
||||||
|
|
@ -375,11 +452,12 @@ static bool run_test(void) {
|
||||||
CHECK(unsetenv("LARDON3D_TEST_CANDIDATE_PAIR_MAXIMUM_BATCH") == 0 &&
|
CHECK(unsetenv("LARDON3D_TEST_CANDIDATE_PAIR_MAXIMUM_BATCH") == 0 &&
|
||||||
unsetenv("LARDON3D_TEST_CANDIDATE_PAIR_THREADS") == 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;
|
uint64_t cp_task_id = 0;
|
||||||
Lardon3DProjectDbCandidatePair baseline[32];
|
Lardon3DProjectDbCandidatePair baseline[32];
|
||||||
size_t baseline_count = 0;
|
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) &&
|
CHECK(reset_candidate_pairs(state.project_db) &&
|
||||||
setenv("LARDON3D_TEST_CANDIDATE_PAIR_THREADS", thread_counts[run], 1) == 0);
|
setenv("LARDON3D_TEST_CANDIDATE_PAIR_THREADS", thread_counts[run], 1) == 0);
|
||||||
uint64_t current_task_id = 0;
|
uint64_t current_task_id = 0;
|
||||||
|
|
@ -553,6 +631,30 @@ static bool run_test(void) {
|
||||||
};
|
};
|
||||||
Lardon3DTask *recovered = NULL;
|
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
|
/* Every field belongs to the historical signature. Neighboring estimates
|
||||||
* are malformed rather than evidence for legacy operational policy. */
|
* are malformed rather than evidence for legacy operational policy. */
|
||||||
Lardon3DTaskDurableSnapshot near_snapshots[4];
|
Lardon3DTaskDurableSnapshot near_snapshots[4];
|
||||||
|
|
@ -582,11 +684,11 @@ static bool run_test(void) {
|
||||||
const Lardon3DResourceEstimate current_estimate = {
|
const Lardon3DResourceEstimate current_estimate = {
|
||||||
.memory_fixed_bytes = 256 * 1024,
|
.memory_fixed_bytes = 256 * 1024,
|
||||||
.gpu_memory_fixed_bytes = 0,
|
.gpu_memory_fixed_bytes = 0,
|
||||||
.memory_bytes_per_item = 64 * 1024,
|
.memory_bytes_per_item = 8 * 1024 * 1024,
|
||||||
.gpu_memory_bytes_per_item = 0,
|
.gpu_memory_bytes_per_item = 0,
|
||||||
.minimum_batch_size = 1,
|
.minimum_batch_size = 1,
|
||||||
.maximum_batch_size = 64,
|
.maximum_batch_size = 64,
|
||||||
.desired_cpu_threads = 12,
|
.desired_cpu_threads = 64,
|
||||||
.desired_gpu_slots = 0,
|
.desired_gpu_slots = 0,
|
||||||
.desired_io_slots = 1,
|
.desired_io_slots = 1,
|
||||||
.task_class = LARDON3D_RESOURCE_TASK_CPU,
|
.task_class = LARDON3D_RESOURCE_TASK_CPU,
|
||||||
|
|
@ -604,7 +706,7 @@ static bool run_test(void) {
|
||||||
lardon3d_resource_governor_reserve(
|
lardon3d_resource_governor_reserve(
|
||||||
state.resource_governor, &resources, &recovered_estimate,
|
state.resource_governor, &resources, &recovered_estimate,
|
||||||
&decision, &reservation) &&
|
&decision, &reservation) &&
|
||||||
decision.cpu_threads == 12 && decision.batch_size >= 1 &&
|
decision.cpu_threads >= 1 && decision.cpu_threads <= 64 &&
|
||||||
decision.batch_size <= 64 && reservation);
|
decision.batch_size <= 64 && reservation);
|
||||||
lardon3d_resource_governor_release(state.resource_governor, reservation);
|
lardon3d_resource_governor_release(state.resource_governor, reservation);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
#include <limits.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
|
|
@ -153,7 +154,7 @@ typedef struct {
|
||||||
static bool adaptive_extract_callback(Lardon3DTask *task, void *userdata) {
|
static bool adaptive_extract_callback(Lardon3DTask *task, void *userdata) {
|
||||||
AdaptiveExtractWork *work = userdata;
|
AdaptiveExtractWork *work = userdata;
|
||||||
Lardon3DOpenCvTaskThreadControl control;
|
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) {
|
lardon3d_feature_opencv_thread_count() != work->expected_threads) {
|
||||||
return false;
|
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.");
|
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) {
|
static bool run_adaptive_output_equivalence(const char *path) {
|
||||||
cpu_set_t allowed_mask;
|
cpu_set_t allowed_mask;
|
||||||
CPU_ZERO(&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;
|
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};
|
Lardon3DExtractedFeatures baseline[TEST_EXTRACT_COUNT] = {0};
|
||||||
bool have_baseline[TEST_EXTRACT_COUNT] = {false};
|
bool have_baseline[TEST_EXTRACT_COUNT] = {false};
|
||||||
unsigned int previous_threads = lardon3d_feature_opencv_thread_count();
|
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) {
|
for (unsigned int index = 0; index < threads; ++index) {
|
||||||
topology.allowed_cpu_ids[index] = allowed_ids[index];
|
topology.allowed_cpu_ids[index] = allowed_ids[index];
|
||||||
}
|
}
|
||||||
|
Lardon3DResourceSnapshot resources = deterministic_resource_snapshot(0.0);
|
||||||
Lardon3DTaskQueue *queue = NULL;
|
Lardon3DTaskQueue *queue = NULL;
|
||||||
if (!governor ||
|
if (!governor ||
|
||||||
!lardon3d_resource_governor_internal_configure_cpu_topology(
|
!lardon3d_resource_governor_internal_configure_cpu_topology(
|
||||||
governor, &topology) ||
|
governor, &topology) ||
|
||||||
|
!lardon3d_resource_governor_internal_set_capture_snapshot(
|
||||||
|
governor, &resources) ||
|
||||||
!(queue = lardon3d_task_queue_create(governor, TEST_EXTRACT_COUNT))) {
|
!(queue = lardon3d_task_queue_create(governor, TEST_EXTRACT_COUNT))) {
|
||||||
lardon3d_task_queue_destroy(queue);
|
lardon3d_task_queue_destroy(queue);
|
||||||
lardon3d_resource_governor_destroy(governor);
|
lardon3d_resource_governor_destroy(governor);
|
||||||
|
|
@ -237,7 +332,7 @@ static bool run_adaptive_output_equivalence(const char *path) {
|
||||||
Lardon3DResourceEstimate estimate = {
|
Lardon3DResourceEstimate estimate = {
|
||||||
.minimum_batch_size = 1,
|
.minimum_batch_size = 1,
|
||||||
.maximum_batch_size = 1,
|
.maximum_batch_size = 1,
|
||||||
.desired_cpu_threads = 12,
|
.desired_cpu_threads = INT_MAX,
|
||||||
.task_class = LARDON3D_RESOURCE_TASK_CPU,
|
.task_class = LARDON3D_RESOURCE_TASK_CPU,
|
||||||
};
|
};
|
||||||
static const char *const task_kinds[TEST_EXTRACT_COUNT] = {
|
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_extracted_features_destroy(&work[kind].output);
|
||||||
}
|
}
|
||||||
lardon3d_task_queue_destroy(queue);
|
lardon3d_task_queue_destroy(queue);
|
||||||
|
if (!lardon3d_resource_governor_internal_set_capture_snapshot(
|
||||||
|
governor, NULL)) {
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
lardon3d_resource_governor_destroy(governor);
|
lardon3d_resource_governor_destroy(governor);
|
||||||
}
|
}
|
||||||
for (size_t kind = 0; kind < TEST_EXTRACT_COUNT; ++kind) {
|
for (size_t kind = 0; kind < TEST_EXTRACT_COUNT; ++kind) {
|
||||||
|
|
@ -301,15 +400,29 @@ static bool runtime(Lardon3DAppState *s) {
|
||||||
Lardon3DResourcePolicy p = {
|
Lardon3DResourcePolicy p = {
|
||||||
.maximum_cpu_load_ratio = 1, .maximum_io_pressure_avg10 = 100, .io_slot_capacity = 2};
|
.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->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;
|
if (!s->resource_governor) return false;
|
||||||
return s->task_queue != NULL;
|
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,
|
static bool wait_state(Lardon3DTaskQueue *q, uint64_t id, Lardon3DTaskState wanted,
|
||||||
Lardon3DTaskSnapshot *out) {
|
Lardon3DTaskSnapshot *out) {
|
||||||
struct timespec deadline;
|
struct timespec deadline;
|
||||||
if (clock_gettime(CLOCK_MONOTONIC, &deadline) != 0) return false;
|
if (clock_gettime(CLOCK_MONOTONIC, &deadline) != 0) return false;
|
||||||
/* The equality fixture deliberately executes ORB/SIFT/RootSIFT at five CPU
|
/* The equality fixture executes every genuinely available requested CPU
|
||||||
* contracts. Instrumented OpenCV is slower than the normal build; keep the
|
* contract. Instrumented OpenCV is slower than the normal build; keep the
|
||||||
* wait bounded without mistaking sanitizer overhead for scientific drift. */
|
* wait bounded without mistaking sanitizer overhead for scientific drift. */
|
||||||
deadline.tv_sec += 30;
|
deadline.tv_sec += 30;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
|
@ -354,10 +467,12 @@ static void *consolidate_thread(void *userdata) {
|
||||||
|
|
||||||
static bool run_test(void) {
|
static bool run_test(void) {
|
||||||
char root[] = "/tmp/lardon3d-feature-task-XXXXXX";
|
char root[] = "/tmp/lardon3d-feature-task-XXXXXX";
|
||||||
|
CHECK(deterministic_capture_preserves_load_gate());
|
||||||
CHECK(mkdtemp(root) && setenv("LARDON3D_PROJECTS_ROOT", root, 1) == 0);
|
CHECK(mkdtemp(root) && setenv("LARDON3D_PROJECTS_ROOT", root, 1) == 0);
|
||||||
char source[PATH_MAX];
|
char source[PATH_MAX];
|
||||||
CHECK(join_path(source, root, "source.pgm") && write_pgm(source));
|
CHECK(join_path(source, root, "source.pgm") && write_pgm(source));
|
||||||
CHECK(run_adaptive_output_equivalence(source));
|
CHECK(run_adaptive_output_equivalence(source));
|
||||||
|
CHECK(retry_failed_opencv_restoration());
|
||||||
Lardon3DAppState state;
|
Lardon3DAppState state;
|
||||||
lardon3d_app_state_init(&state);
|
lardon3d_app_state_init(&state);
|
||||||
CHECK(runtime(&state) && lardon3d_project_create(&state, "Features"));
|
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) &&
|
wait_state(state.task_queue, nondurable_rootsift_task, TASK_COMPLETED, &snapshot) &&
|
||||||
last_sequence_is_no_work(&state, LARDON3D_ROOTSIFT_EXTRACT_TASK_KIND) &&
|
last_sequence_is_no_work(&state, LARDON3D_ROOTSIFT_EXTRACT_TASK_KIND) &&
|
||||||
unsetenv("LARDON3D_TEST_FEATURE_FAIL_DIRECTORY_SYNC") == 0);
|
unsetenv("LARDON3D_TEST_FEATURE_FAIL_DIRECTORY_SYNC") == 0);
|
||||||
/* SIFT and RootSIFT CPU1 checkpoints are exact historical operational
|
/* SIFT and RootSIFT CPU12/CPU1 checkpoints are exact historical operational
|
||||||
* signatures. Recovery uses CPU12 in memory without publishing an
|
* signatures. Recovery uses the portable OpenCV ceiling in memory without publishing an
|
||||||
* estimate-only checkpoint; a neighboring CPU2 shape is corruption. */
|
* estimate-only checkpoint; a neighboring CPU2 shape is corruption. */
|
||||||
const uint64_t precision_task_ids[2] = {sift_task_id, rootsift_task_id};
|
const uint64_t precision_task_ids[2] = {sift_task_id, rootsift_task_id};
|
||||||
const char *precision_kinds[2] = {LARDON3D_SIFT_EXTRACT_TASK_KIND,
|
const char *precision_kinds[2] = {LARDON3D_SIFT_EXTRACT_TASK_KIND,
|
||||||
|
|
@ -589,6 +704,53 @@ static bool run_test(void) {
|
||||||
.resource_governor = state.resource_governor,
|
.resource_governor = state.resource_governor,
|
||||||
.orb_vulkan_backend = state.orb_vulkan_backend,
|
.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, ¤t_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) {
|
for (size_t precision_index = 0; precision_index < 2; ++precision_index) {
|
||||||
char precision_checkpoint[PATH_MAX];
|
char precision_checkpoint[PATH_MAX];
|
||||||
char precision_staged[PATH_MAX];
|
char precision_staged[PATH_MAX];
|
||||||
|
|
@ -601,6 +763,25 @@ static bool run_test(void) {
|
||||||
CHECK(lardon3d_task_checkpoint_load(precision_checkpoint,
|
CHECK(lardon3d_task_checkpoint_load(precision_checkpoint,
|
||||||
¤t_precision, NULL) ==
|
¤t_precision, NULL) ==
|
||||||
LARDON3D_TASK_CHECKPOINT_OK);
|
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;
|
Lardon3DTaskDurableSnapshot historical_precision = current_precision;
|
||||||
historical_precision.estimate.desired_cpu_threads = 1;
|
historical_precision.estimate.desired_cpu_threads = 1;
|
||||||
historical_precision.progress = 50;
|
historical_precision.progress = 50;
|
||||||
|
|
@ -627,7 +808,7 @@ static bool run_test(void) {
|
||||||
Lardon3DResourceEstimate effective_precision;
|
Lardon3DResourceEstimate effective_precision;
|
||||||
CHECK(lardon3d_task_resource_estimate(restored_precision,
|
CHECK(lardon3d_task_resource_estimate(restored_precision,
|
||||||
&effective_precision) &&
|
&effective_precision) &&
|
||||||
effective_precision.desired_cpu_threads == 12);
|
effective_precision.desired_cpu_threads == INT_MAX);
|
||||||
lardon3d_task_destroy(restored_precision);
|
lardon3d_task_destroy(restored_precision);
|
||||||
Lardon3DTaskDurableSnapshot durable_precision;
|
Lardon3DTaskDurableSnapshot durable_precision;
|
||||||
CHECK(lardon3d_task_checkpoint_load(precision_checkpoint,
|
CHECK(lardon3d_task_checkpoint_load(precision_checkpoint,
|
||||||
|
|
@ -831,6 +1012,19 @@ static bool run_test(void) {
|
||||||
uint64_t visual_task_id = 0;
|
uint64_t visual_task_id = 0;
|
||||||
CHECK(lardon3d_project_enqueue_visual_index_update(&state, visual_index_id, &visual_task_id));
|
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));
|
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);
|
lardon3d_task_queue_destroy(state.task_queue);
|
||||||
state.task_queue = NULL;
|
state.task_queue = NULL;
|
||||||
lardon3d_project_close(&state);
|
lardon3d_project_close(&state);
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue