feat: freeze calibration bootstrap and selected execution

This commit is contained in:
fy59 2026-08-28 21:39:02 +02:00
parent 892d0c5954
commit 08a45a0724
25 changed files with 2424 additions and 65 deletions

View file

@ -34,7 +34,9 @@ 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 v21 — PASS/FROZEN - Project DB v22 — PASS/FROZEN
- Calibration Bootstrap v1 — PASS/FROZEN
- Selected Scientific Execution — PASS/FROZEN
- Photo Quality Triage / Acquisition Selection — PASS/FROZEN - Photo Quality Triage / Acquisition Selection — PASS/FROZEN
- S1 Capture / Asset Provenance — PASS/FROZEN - S1 Capture / Asset Provenance — PASS/FROZEN
- S2 Capture-safe Standard Ingestion — PASS/FROZEN - S2 Capture-safe Standard Ingestion — PASS/FROZEN

View file

@ -34,10 +34,14 @@ persistante, enrichissable et versionnable.
- **Image View** : vues triées et filtrées pour la TUI - **Image View** : vues triées et filtrées pour la TUI
- **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, fichier atomique et reprise sûre - **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre
- **Project Database v21** : fondations v20 préservées, avec persistance additive - **Project Database v22** : fondations v20/v21 préservées, avec snapshot
des résultats de triage photo d'exécution sélectionnée, publication bornée des représentations, attache de
scope de calibration et tâche durable mince `raw.develop` — PASS / FROZEN
- **[Photo Quality Triage](docs/architecture/photo_quality_triage.md)** : métriques JPEG - **[Photo Quality Triage](docs/architecture/photo_quality_triage.md)** : métriques JPEG
bornées et recommandations non destructives, avec persistance additive v21 - **[Calibration Bootstrap v1](docs/architecture/calibration_bootstrap.md)** :
import borné d'une calibration optimisée et traçable avant le Sparse SfM à
calibration connue — PASS / FROZEN ; ni
auto-calibration interne ni EXIF comme source de calibration scientifique
- **Exécution durable de campagne d'acquisition** : tâche générique à requête - **Exécution durable de campagne d'acquisition** : tâche générique à requête
typée immuable, confirmations `CALLER_EXPLICIT`, curseur et correspondance typée immuable, confirmations `CALLER_EXPLICIT`, curseur et correspondance
tâche/groupe→Capture persistants ; un groupe S3-E par séquence, reprise par tâche/groupe→Capture persistants ; un groupe S3-E par séquence, reprise par
@ -72,7 +76,11 @@ persistante, enrichissable et versionnable.
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
temporaire, avec exécution durable, Governor/Queue et reprise sans Capture temporaire, avec exécution durable, Governor/Queue et reprise sans Capture
dupliqué. La suite reste le pipeline scientifique aval, selon la dupliqué. Les campagnes réelles actuellement évaluées sont
`CALIBRATION_UNAVAILABLE`, donc le Sparse SfM réel est
`BLOCKED_BY_KNOWN_CALIBRATION_DATA` : ce n'est ni un échec logiciel, ni un
rejet de qualité, ni une autorisation d'importer une pseudo-calibration. La
suite reste le pipeline scientifique aval, selon la
[roadmap canonique](docs/roadmap/roadmap.md). [roadmap canonique](docs/roadmap/roadmap.md).
### Plus tard / différé ### Plus tard / différé
@ -210,6 +218,15 @@ 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 /
FROZEN** : la suite normale 53/53, les contrôles syntaxiques C17, `git diff
--check`, la validation ciblée ASan/UBSan et l'audit final ont passé. La suite
ASan/UBSan complète demeure qualifiée par le comportement LSan du pilote tiers
RADV ; elle n'est pas présentée comme un PASS complet du dépôt. Les campagnes
réelles S21 et A6000 Engine Bay sont
`CALIBRATION_UNAVAILABLE` par non-identifiabilité scientifique des données de
calibration connues ; le Sparse SfM réel reste
`BLOCKED_BY_KNOWN_CALIBRATION_DATA`, sans pseudo-calibration ni import inféré.
Le Resource Governor ne constitue pas un Resource System générique : voir la décision Le Resource Governor ne constitue pas un Resource System générique : voir la décision
darchitecture. darchitecture.

View file

@ -0,0 +1,148 @@
# Calibration Bootstrap v1
**PASS / FROZEN.** Project DB v22 implements the
immutable selected-execution snapshot, explicit cross-task mappings, representation cursor and
calibration-scope attachment described below. The bounded artifact validator/importer produces explicit
immutable `IMPORTED_TRUSTED` calibrations for the existing known-calibration
Sparse SfM contract. It is not self-calibration inside Sparse SfM and never
makes EXIF a scientific calibration source. Solver execution remains outside this importer and is not
claimed as implemented by this stage.
## Boundary and identity
Sparse SfM continues to consume an explicit Track Set and immutable calibration
scope. Bootstrap artifacts can support a `Lardon3DSparseCalibration`; external
poses, tracks, matches and points are diagnostics only and never become Lardon3D
scientific reconstruction results. Each artifact identifies a calibration
evidence run, not a Capture, Asset, `image_id`, path, basename, Task ID or
external reconstruction. It records ordered representation hashes/dimensions,
solver executable identity/model/configuration, initialization evidence,
optimized parameters and validation diagnostics. Its SHA-256 is the immutable
`IMPORTED_TRUSTED` provenance fingerprint.
## Artifact format v1
The import API borrows at most 600000 artifact bytes and requires their SHA-256 to equal an
independently supplied expected digest before parsing. The format is fixed-width little-endian; binary64
uses IEEE-754 bits, rejects non-finite values and canonicalizes negative zero before calibration
publication. Native structs, padding, locale text and trailing bytes are never accepted.
```text
magic 8 bytes = ASCII "L3DCALB1"
format_version u32 = 1
model_kind u32 = PINHOLE(1)
model_version u32 = 1
entry_count u32, 1..4096 and equal to selected item_count
solver_executable_sha256 32 bytes, nonzero
solver_configuration_sha256 32 bytes, nonzero
initialization_evidence_sha256 32 bytes, nonzero
validation_evidence_sha256 32 bytes, nonzero
for each selected item in immutable item_index order:
image_id u64
representation_sha256 32 bytes
width, height u32, u32
fx, fy, cx, cy f64, f64, f64, f64
k1, k2, p1, p2 f64, f64, f64, f64
support_images u32, nonzero
support_observations u32, nonzero
reprojection_rmse_px finite nonnegative f64
maximum_parameter_delta finite nonnegative f64
validation_flags u32 = 0x0f
```
The four v1 validation bits attest convergence, non-degenerate support, deterministic stability, and
representative-coordinate equivalence to Lardon3D respectively. The importer requires all four; it does
not silently derive missing evidence or invent a calibration threshold. `image_id` and representation
SHA-256 must exactly match the durable selected item and its image asset. Width/height are artifact-owned
geometry evidence and also enter calibration scientific identity. The artifact contains no external pose,
point, track or match field, so those diagnostics cannot cross the import boundary.
## Exact model requirement
Bootstrap v1 accepts only an exact equivalent of Lardon3D's OpenCV-style
pinhole model with binary64 `fx, fy, cx, cy, k1, k2, p1, p2`:
```text
x_d = x * (1 + k1*r2 + k2*r2*r2) + 2*p1*x*y + p2*(r2 + 2*x*x)
y_d = y * (1 + k1*r2 + k2*r2*r2) + p1*(r2 + 2*y*y) + 2*p2*x*y
u = fx*x_d + cx; v = fy*y_d + cy
```
The imported geometry is exactly the downstream Feature Store geometry. Models
with active extra coefficients are rejected, never truncated. Representative
coordinates must validate origin, axes, pixel convention and projection against
the Lardon3D implementation before import.
## Metadata, representations and failure state
Metadata is initialization/grouping evidence only. Orientation does not identify
a physical camera, though coordinates must match downstream decoding. A6000
calibrates deterministic RAW-derived PNG geometry, never camera-JPEG geometry
without a separately proven exact transform. Candidate A6000 groups use RAW
geometry, lens evidence and exact EXIF focal only as solver inputs; zoom groups
remain separate unless optimization proves equivalence. S21 uses its selected
source JPEG geometry and solver-supported module grouping.
Bootstrap requires convergence, finite values, positive focal lengths, valid
principal points, non-degenerate support, model compatibility and deterministic
stability evidence. Unstable groups are expanded or partitioned; metadata
interpolation and guessed calibration are prohibited. An affected selected image
is `CALIBRATION_UNAVAILABLE`, not a quality rejection or corrupt source.
The real S21 and current A6000 Engine Bay selected campaigns are presently
`CALIBRATION_UNAVAILABLE` because the available evidence is scientifically
non-identifiable for the known-calibration contract. This is neither a source
or quality failure nor a software failure. No pseudo-calibration, metadata
interpolation or inferred import is permitted; real Sparse SfM is consequently
`BLOCKED_BY_KNOWN_CALIBRATION_DATA`. The v22 normal suite passed 53/53, C17
syntax checks and `git diff --check` passed, targeted ASan/UBSan validation
passed, and final review passed. The full ASan/UBSan suite remains qualified by
third-party RADV LSan behavior and is not represented as a repository-wide clean
sanitizer pass. This bootstrap infrastructure is therefore `PASS / FROZEN`; that
lifecycle state applies to the bounded importer and persistence infrastructure,
not to calibration of either real campaign or to real Sparse SfM execution.
A dedicated future physical calibration acquisition, with images and evidence
adequate to establish an exact supported model for each relevant representation
group, is recommended before attempting real known-calibration Sparse SfM.
That acquisition is future field work, not an implemented solver, a change to
the scientific contract, or permission to retrofit the present campaigns.
## Execution relationship and resources
```text
SCIENTIFIC_EXECUTABLE = QUALITY_SELECTED
intersection REPRESENTATION_READY
intersection CALIBRATION_ASSIGNED
```
Quality selection is snapshotted immutably. A durable path must retain explicit
quality-group → campaign-group → Capture mappings: equal numeric group values
from independent Task requests imply no identity. Source photos remain read-only;
RAW bootstrap development processes one image at a time, with no campaign-wide
decoded cache. Ticket-owned artifacts retain source/representation hashes,
output dimensions, policy fingerprint and solver provenance.
Project DB v22 stores the ordered selected rows and advances representation publication one item
at a time. Each row immutably declares either a Capture-owned SOURCE RAW `asset_id` for S3-B1 or
that the representation is a source image and no RAW identity exists. Creation validates
`capture_source_assets.source_kind` as RAW; later execution never resolves it from path, digest, basename,
request source index or group number. An `image_id` is accepted only through the retained Capture's explicit
`capture_images` relation. The calibration scope is attached only after all representations are
durable and only when that existing immutable scope contains every selected image. These DB
transitions are idempotent exact retries; they do not execute a solver or RAW developer and do not
introduce a scheduler, worker pool, sidecar, or parallel resource owner.
Artifact validation completes before any publication. Calibration and scope creation then reuse the
existing short Project DB transactions. A crash or final attachment conflict may therefore retain only
immutable content-addressed calibration evidence; it cannot make the selected execution `READY`. An exact
retry reuses that evidence and converges through the existing scope attachment. The importer has no Task,
Queue or Governor ownership and holds only two arrays bounded by the selected item count; solver execution
and one-at-a-time RAW development must be coordinated by the established runtime when invoked.
The thin durable `raw.develop` Task provides that S3-B1 execution boundary for one exact
`capture_id`/`source_asset_id` pair. It reuses the existing single-active Queue and Governor, never
advances the selected-execution cursor itself, and retains its published `image_id` before terminal
generic Task progress can become durable. Its reservation owns one CPU thread, one I/O slot and a
conservative 2 GiB working allowance for the bounded 40 MP decoder and publication buffers only for
the callback lifetime; this operational admission bound is not a scientific Capture-count limit.
A higher-level selected-execution coordinator remains separate scope.

View file

@ -1,5 +1,71 @@
# Base de données projet Lardon3D # Base de données projet Lardon3D
## Snapshot d'exécution scientifique sélectionnée — Project DB v22
**PASS / FROZEN.** La migration additive v21→v22 ajoute
`capture_source_assets`, `selected_executions`, `selected_execution_items` et
`raw_development_tasks`.
`capture_source_assets` retient l'association explicitement publiée par S3-E entre un Capture,
son asset `SOURCE` et le kind JPEG ou RAW déjà validé. La publication de l'association SOURCE et
du kind est transactionnelle et un retry exact converge ; un autre kind est un conflit. Les
Captures provenant d'une v21 restent honnêtement sans mapping : la migration ne déduit rien de
leur chemin, nom, SHA-256, ordre d'attache ou image logique.
Un snapshot ordonné retient explicitement
la relation `quality_task_id/group_id → campaign_task_id/group_id → capture_id`; les deux espaces
de `group_id` restent indépendants et une égalité numérique n'est jamais une identité. Les deux
tâches doivent viser le même ScanSet, le triage doit être terminé et chaque résultat retenu doit
être effectivement inclus au moment du snapshot. Le snapshot et son ordre deviennent ensuite
immuables.
Chaque item reçoit un `image_id` seulement par association explicite déjà durable dans
`capture_images`. La publication de cette représentation et l'avancement du curseur sont
transactionnels ; un retry exact converge, tandis qu'un autre `image_id` est un conflit. Après le
dernier item, un `calibration_scope_id` existant ne peut être attaché que si chaque image retenue
est membre de ce scope. Le stage terminal `READY` représente donc exactement l'intersection
`QUALITY_SELECTED ∩ REPRESENTATION_READY ∩ CALIBRATION_ASSIGNED` sans inférer Capture depuis
Asset, SHA-256, chemin, basename, Task ID, groupe ou `image_id`.
Le snapshot fixe aussi la source de représentation de chaque item. Un item A6000 retient
explicitement l'`asset_id` RAW déjà associé comme `SOURCE` au Capture ; un item dont la
représentation est une image source retient explicitement l'absence d'identité RAW. Cette paire
discriminant/Asset est immuable et fait partie de l'exact retry du snapshot. La création vérifie
l'association RAW dans `capture_source_assets` ; aucune exécution ultérieure ne retrouve le RAW depuis
un chemin, SHA-256, basename, index source ou numéro de groupe.
Le curseur est borné à 4096 items et ne réserve aucune ressource d'exécution. Cette fondation v22
n'ajoute ni scheduler, worker pool, sidecar, cache décodé, ni nouvelle Queue/Governor. Le
coordinateur scientifique et le développement RAW borné restent des étapes d'exécution séparées.
L'importeur Calibration Bootstrap v1 valide un artifact borné contre ce snapshot, crée/réutilise les
calibrations et le scope immuables, puis emploie cette attache v22 ; il n'étend pas le schéma et n'exécute
ni solveur ni développement RAW.
Le développeur RAW expose aussi une entrée bornée par `source_asset_id` explicite. Elle exige la
relation RAW du Capture, charge le chemin géré depuis cet ID, vérifie les octets contre le SHA-256
persisté, puis réutilise sans modification RAW Policy v1. Le chemin géré n'est donc qu'un accès aux
octets après résolution d'identité explicite, jamais une méthode de découverte d'identité.
La v22 amendée ajoute `raw_development_tasks` pour l'exécution durable mince de cette entrée. Une
ligne retient exactement `task_id`, `capture_id`, `source_asset_id`, une phase monotone
`PENDING → PUBLISHED` et, en phase publiée, l'`image_id` dérivée explicitement associée au même
Capture. Le couple Capture/SOURCE RAW est immuable, doit exister dans `capture_source_assets` avec
le kind RAW, et n'est jamais retrouvé depuis un chemin, digest, nom, image ou ID opérationnel.
Snapshot Task générique, référence du checkpoint et ligne typée sont écrits dans une même
transaction. L'asset et l'image immuables peuvent précéder ce checkpoint si le processus meurt :
le retry exact S3-B1 converge par contenu puis publie la phase et l'`image_id` sans deviner
l'identité. Les lectures de reprise rejettent types SQLite, phases, nullabilité et relations
Capture/RAW/image incohérents comme corruption durable.
La suite normale v22 a passé 53/53, les contrôles syntaxiques C17 et `git diff
--check` ont passé, la validation ciblée ASan/UBSan a passé et l'audit final a
conclu au PASS. La suite ASan/UBSan complète reste qualifiée par le comportement
LSan du pilote tiers RADV ; elle ne constitue pas un PASS complet du dépôt.
Cette évidence gèle la frontière de persistance v22 décrite ici, sans changement
de contrat ni de schéma. Les campagnes réelles peuvent donc conserver un snapshot
et leurs représentations sans être scientifiquement exécutables : une absence de
calibration connue reste `CALIBRATION_UNAVAILABLE`, non une raison d'inférer ou
de créer une identité, une calibration ou une migration supplémentaire.
## Photo Quality Triage — Project DB v21 ## Photo Quality Triage — Project DB v21
**PASS / FROZEN.** La migration additive v20→v21 ajoute **PASS / FROZEN.** La migration additive v20→v21 ajoute

View file

@ -5,7 +5,7 @@
**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 **v20**; historical v16 remains frozen. Project Database: current schema **v22**; historical v16 remains frozen.
This record is normative for the current architecture. It does not introduce This record is normative for the current architecture. It does not introduce
an implementation, a public API, a persistence format or a roadmap commitment. an implementation, a public API, a persistence format or a roadmap commitment.

View file

@ -121,6 +121,26 @@ Queue/Governor, persistance tâche/groupe→Capture et reprise sans duplication.
La capacité bornée des propositions de revue conserve un préfixe déterministe; La capacité bornée des propositions de revue conserve un préfixe déterministe;
elle ne limite jamais l'évaluation complète ni le groupement scientifique. elle ne limite jamais l'évaluation complète ni le groupement scientifique.
### État de calibration des campagnes réelles
L'infrastructure Project DB v22, exécution sélectionnée, `raw.develop` et
Calibration Bootstrap v1 est **PASS / FROZEN** : la suite normale 53/53, les
contrôles syntaxiques C17, `git diff --check`, la validation ciblée ASan/UBSan
et l'audit final ont passé. La suite ASan/UBSan complète reste qualifiée par le
comportement LSan du pilote tiers RADV ; elle n'est pas déclarée PASS complet du
dépôt. Ce gel concerne l'infrastructure de persistance et d'import borné, pas
la calibration des campagnes réelles ni leur Sparse SfM.
Les campagnes réelles S21 et A6000 Engine Bay actuellement évaluées sont
`CALIBRATION_UNAVAILABLE` par non-identifiabilité scientifique des données
disponibles pour le contrat Sparse SfM à calibration connue. Ce statut ne
signifie ni échec logiciel, ni défaut des sources, ni rejet de qualité. Sans
pseudo-calibration, interpolation de métadonnées ou import inféré, le Sparse
SfM réel est `BLOCKED_BY_KNOWN_CALIBRATION_DATA`; l'intégration synthétique à
calibration connue a passé. Une acquisition physique dédiée de calibration est
une étape future, décrite dans [Calibration Bootstrap v1](../architecture/calibration_bootstrap.md),
et non une fonctionnalité déjà réalisée.
## PHOTO QUALITY TRIAGE / ACQUISITION SELECTION — PASS / FROZEN ## PHOTO QUALITY TRIAGE / ACQUISITION SELECTION — PASS / FROZEN
L'étape qualité canonique implémentée se place après la découverte bornée, les L'étape qualité canonique implémentée se place après la découverte bornée, les
@ -484,8 +504,9 @@ L'ordre demeure sans ambiguïté :
```text ```text
CURRENT NEXT CURRENT NEXT
intégration réelle multi-campagne A6000 + S21 acquisition physique dédiée de calibration
→ pipeline scientifique → dense/mesh → publication → calibration connue validée → Sparse SfM réel multi-campagne
→ dense/mesh → publication
→ LATER → LATER
Coverage Analysis → Coverage Viewer → suggestions de points de vue Coverage Analysis → Coverage Viewer → suggestions de points de vue
→ localisation live → intégration HDMI/USB → Capture Guidance / Live Coverage → localisation live → intégration HDMI/USB → Capture Guidance / Live Coverage

View file

@ -0,0 +1,59 @@
#ifndef LARDON3D_CALIBRATION_BOOTSTRAP_H
#define LARDON3D_CALIBRATION_BOOTSTRAP_H
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/project_db.h>
#include <lardon3d/sparse_sfm_model.h>
#ifdef __cplusplus
extern "C" {
#endif
enum {
LARDON3D_CALIBRATION_BOOTSTRAP_ARTIFACT_VERSION = 1,
LARDON3D_CALIBRATION_BOOTSTRAP_MAX_BYTES = 600000,
};
typedef enum {
LARDON3D_CALIBRATION_BOOTSTRAP_OK = 0,
LARDON3D_CALIBRATION_BOOTSTRAP_INVALID_ARGUMENT,
LARDON3D_CALIBRATION_BOOTSTRAP_MALFORMED_ARTIFACT,
LARDON3D_CALIBRATION_BOOTSTRAP_PROVENANCE_MISMATCH,
LARDON3D_CALIBRATION_BOOTSTRAP_SELECTION_CONFLICT,
LARDON3D_CALIBRATION_BOOTSTRAP_DB_ERROR,
LARDON3D_CALIBRATION_BOOTSTRAP_OUT_OF_MEMORY,
} Lardon3DCalibrationBootstrapResult;
typedef struct {
unsigned char artifact_sha256[LARDON3D_PROJECT_DB_SHA256_SIZE];
Lardon3DSparseCalibrationScope scope;
uint32_t calibration_count;
} Lardon3DCalibrationBootstrapOutput;
/* Import a complete Calibration Bootstrap v1 artifact for an existing selected
* execution. All pointers are required and artifact bytes are borrowed only
* for the call. The artifact is bounded by MAX_BYTES, uses the documented
* fixed-width little-endian format, and must hash to expected_artifact_sha256.
* Its ordered image IDs and representation SHA-256 values must exactly match
* the execution's durable selected images. Only the exact OpenCV-compatible
* eight-parameter pinhole model is accepted.
*
* Successful import creates/reuses immutable IMPORTED_TRUSTED calibrations,
* creates/reuses their immutable scope, then attaches that scope through the
* selected-execution contract. Exact retries converge. A failure after an
* immutable calibration is created may retain that content-addressed evidence,
* but cannot make the selected execution READY. The function imports no poses,
* points, tracks, matches, EXIF calibration, or Capture identity. */
Lardon3DCalibrationBootstrapResult lardon3d_calibration_bootstrap_import(
Lardon3DProjectDb *database, uint64_t execution_id,
const unsigned char *artifact, size_t artifact_size,
const unsigned char expected_artifact_sha256[LARDON3D_PROJECT_DB_SHA256_SIZE],
Lardon3DCalibrationBootstrapOutput *output);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -67,6 +67,12 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_acquisition_camp
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_photo_quality_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_photo_quality_task(
Lardon3DAppState *state, const Lardon3DTask *task, Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbPhotoQualityTask *parameters); const Lardon3DProjectDbPhotoQualityTask *parameters);
/* Persist one RAW Task snapshot and its exact Capture/SOURCE RAW phase in the
* same DB transaction. The checkpoint file is atomically published first; OK
* means the file, generic row, checkpoint reference, and typed row are durable. */
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_raw_development_task(
Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbRawDevelopmentTask *parameters);
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_feature_extract_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_feature_extract_task(
Lardon3DAppState *state, const Lardon3DTask *task, Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbFeatureExtractTask *parameters); const Lardon3DProjectDbFeatureExtractTask *parameters);

View file

@ -11,7 +11,7 @@
#include <lardon3d/sparse_sfm_incremental.h> #include <lardon3d/sparse_sfm_incremental.h>
enum { enum {
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 21, LARDON3D_PROJECT_DB_SCHEMA_VERSION = 22,
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,
@ -126,6 +126,20 @@ typedef struct {
size_t request_size; size_t request_size;
} Lardon3DProjectDbPhotoQualityTask; } Lardon3DProjectDbPhotoQualityTask;
typedef enum {
LARDON3D_RAW_DEVELOPMENT_TASK_PENDING = 1,
LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED = 2,
} Lardon3DProjectDbRawDevelopmentTaskPhase;
typedef struct {
uint64_t task_id;
uint64_t capture_id;
uint64_t source_asset_id;
Lardon3DProjectDbRawDevelopmentTaskPhase phase;
bool has_image;
uint64_t image_id;
} Lardon3DProjectDbRawDevelopmentTask;
typedef struct { typedef struct {
uint64_t task_id; uint64_t task_id;
uint32_t group_id; uint32_t group_id;
@ -134,6 +148,44 @@ typedef struct {
Lardon3DPhotoQualityOverride override_value; Lardon3DPhotoQualityOverride override_value;
} Lardon3DProjectDbPhotoQualityResult; } Lardon3DProjectDbPhotoQualityResult;
typedef enum {
LARDON3D_SELECTED_EXECUTION_REPRESENTATIONS = 1,
LARDON3D_SELECTED_EXECUTION_CALIBRATION = 2,
LARDON3D_SELECTED_EXECUTION_READY = 3,
} Lardon3DProjectDbSelectedExecutionStage;
typedef struct {
uint64_t execution_id;
uint64_t quality_task_id;
uint64_t campaign_task_id;
bool has_calibration_scope;
uint64_t calibration_scope_id;
Lardon3DProjectDbSelectedExecutionStage stage;
uint32_t next_item_index;
uint32_t item_count;
int64_t created_at;
} Lardon3DProjectDbSelectedExecution;
typedef enum {
/* The selected representation is an existing source image; no RAW
* development identity is present or may be inferred later. */
LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE = 1,
/* source_asset_id explicitly identifies the Capture-owned SOURCE RAW whose
* deterministic S3-B1 representation is required. */
LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET = 2,
} Lardon3DProjectDbSelectedRepresentationSource;
typedef struct {
uint32_t item_index;
uint32_t quality_group_id;
uint32_t campaign_group_id;
uint64_t capture_id;
Lardon3DProjectDbSelectedRepresentationSource representation_source;
uint64_t source_asset_id;
bool has_image;
uint64_t image_id;
} Lardon3DProjectDbSelectedExecutionItem;
typedef struct { typedef struct {
uint64_t scanset_id; uint64_t scanset_id;
char name[LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY]; char name[LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY];
@ -180,6 +232,17 @@ typedef struct {
Lardon3DProjectDbCaptureAssetRole role; Lardon3DProjectDbCaptureAssetRole role;
} Lardon3DProjectDbCaptureAsset; } Lardon3DProjectDbCaptureAsset;
typedef enum {
LARDON3D_DB_CAPTURE_SOURCE_JPEG = 1,
LARDON3D_DB_CAPTURE_SOURCE_RAW = 2,
} Lardon3DProjectDbCaptureSourceKind;
typedef struct {
uint64_t capture_id;
uint64_t asset_id;
Lardon3DProjectDbCaptureSourceKind source_kind;
} Lardon3DProjectDbCaptureSourceAsset;
typedef enum { typedef enum {
LARDON3D_DB_ASSET_DERIVATION_GENERIC_VERSIONED = 1, LARDON3D_DB_ASSET_DERIVATION_GENERIC_VERSIONED = 1,
} Lardon3DProjectDbAssetDerivationKind; } Lardon3DProjectDbAssetDerivationKind;
@ -539,6 +602,11 @@ Lardon3DProjectDbResult lardon3d_project_db_register_image_asset(
Lardon3DProjectDb *database, Lardon3DProjectDb *database,
const unsigned char sha256[LARDON3D_PROJECT_DB_SHA256_SIZE], const char *asset_path, const unsigned char sha256[LARDON3D_PROJECT_DB_SHA256_SIZE], const char *asset_path,
uint64_t size_bytes, int64_t created_at, Lardon3DProjectDbImageAsset *asset); uint64_t size_bytes, int64_t created_at, Lardon3DProjectDbImageAsset *asset);
/* Load one immutable managed asset by its explicit durable ID. The caller owns
* output storage. The returned relative path is validated and NUL-terminated;
* this API performs no Capture/source identity discovery. */
Lardon3DProjectDbResult lardon3d_project_db_load_image_asset(
Lardon3DProjectDb *database, uint64_t asset_id, Lardon3DProjectDbImageAsset *asset);
Lardon3DProjectDbResult lardon3d_project_db_publish_derived_capture_image( Lardon3DProjectDbResult lardon3d_project_db_publish_derived_capture_image(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id, Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id,
const char *original_name, const char *source_path, uint64_t producer_task_id, const char *original_name, const char *source_path, uint64_t producer_task_id,
@ -574,6 +642,18 @@ Lardon3DProjectDbResult lardon3d_project_db_attach_capture_asset(
Lardon3DProjectDbCaptureAssetRole role); Lardon3DProjectDbCaptureAssetRole role);
Lardon3DProjectDbResult lardon3d_project_db_attach_capture_source_asset( Lardon3DProjectDbResult lardon3d_project_db_attach_capture_source_asset(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id); Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id);
/* Atomically attach an existing asset as SOURCE and retain the explicit source
* kind observed by S3-E. Exact retry is idempotent; a different kind conflicts.
* This provenance must never be synthesized from path, name, SHA-256,
* attachment order, or image identity. */
Lardon3DProjectDbResult lardon3d_project_db_record_capture_source_asset(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id,
Lardon3DProjectDbCaptureSourceKind source_kind);
/* Lists only explicitly retained source-kind relations. Captures migrated from
* v21 may truthfully return an empty page; absence is not permission to infer. */
Lardon3DProjectDbResult lardon3d_project_db_list_capture_source_assets(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t after_asset_id,
Lardon3DProjectDbCaptureSourceAsset *assets, size_t capacity, size_t *count);
Lardon3DProjectDbResult lardon3d_project_db_list_capture_assets( Lardon3DProjectDbResult lardon3d_project_db_list_capture_assets(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t after_asset_id, Lardon3DProjectDb *database, uint64_t capture_id, uint64_t after_asset_id,
Lardon3DProjectDbCaptureAsset *assets, size_t capacity, size_t *count); Lardon3DProjectDbCaptureAsset *assets, size_t capacity, size_t *count);
@ -633,6 +713,30 @@ Lardon3DProjectDbResult lardon3d_project_db_record_photo_quality_task(
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);
/* Atomically persist generic Task/checkpoint state with one immutable explicit
* Capture/SOURCE RAW identity and its monotonic publication phase. The caller
* must pass `task_kind` == `LARDON3D_RAW_DEVELOPMENT_TASK_KIND` and
* `task_kind_version` == `LARDON3D_RAW_DEVELOPMENT_TASK_KIND_VERSION`.
* Any mismatch is rejected with INVALID_ARGUMENT because generic Task dispatch and
* typed durable payload must remain coherent across restart. PUBLISHED requires
* the exact derived image retained by that Capture; exact retries are idempotent
* and another identity or image conflicts. */
Lardon3DProjectDbResult lardon3d_project_db_record_raw_development_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
const char *task_kind, uint32_t task_kind_version,
const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbRawDevelopmentTask *parameters, int64_t updated_at);
/* Load and validate one durable RAW Task. Persisted durable rows with
* `task_kind`/`task_kind_version` not equal to
* `LARDON3D_RAW_DEVELOPMENT_TASK_KIND`/`LARDON3D_RAW_DEVELOPMENT_TASK_KIND_VERSION`
* are corrupted durable state and return CORRUPT, preserving the contract that
* typed durable payload and generic Task identity never silently diverge.
* Invalid phases, missing explicit RAW provenance, or a published image outside
* the retained Capture are also reported as corrupt durable state; no source
* identity is inferred. */
Lardon3DProjectDbResult lardon3d_project_db_load_raw_development_task(
Lardon3DProjectDb *database, uint64_t task_id,
Lardon3DProjectDbRawDevelopmentTask *parameters);
/* Result publication and next_group_id advance are atomic. result->group_id is /* Result publication and next_group_id advance are atomic. result->group_id is
* the canonical plan ID in 1..N and must equal the task's current one-based * the canonical plan ID in 1..N and must equal the task's current one-based
* cursor; next_group_id must equal result->group_id+1 (N+1 after the last * cursor; next_group_id must equal result->group_id+1 (N+1 after the last
@ -652,6 +756,44 @@ Lardon3DProjectDbResult lardon3d_project_db_load_photo_quality_result(
Lardon3DProjectDbResult lardon3d_project_db_set_photo_quality_override( Lardon3DProjectDbResult lardon3d_project_db_set_photo_quality_override(
Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id, Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id,
Lardon3DPhotoQualityOverride override_value); Lardon3DPhotoQualityOverride override_value);
/* Create an immutable, ordered QUALITY_SELECTED snapshot. Each item explicitly
* bridges independent quality and campaign group namespaces to a retained
* Capture; equal numeric group values never imply identity. Each item also
* declares either an explicit Capture-owned SOURCE RAW asset or that its
* representation is a source image and therefore has no RAW identity. The DB
* validates RAW asset membership at creation and never recovers it from path,
* SHA-256, basename, source index, or group number. The caller-owned item array
* is borrowed only for the call. Every referenced quality result must be
* effectively included and both tasks must belong to the same ScanSet. An
* exact retry returns the existing execution; a changed mapping conflicts. */
Lardon3DProjectDbResult lardon3d_project_db_create_selected_execution(
Lardon3DProjectDb *database, uint64_t quality_task_id,
uint64_t campaign_task_id, const Lardon3DProjectDbSelectedExecutionItem *items,
size_t item_count, int64_t created_at,
Lardon3DProjectDbSelectedExecution *execution);
/* Load durable stage/cursor state. Output is caller-owned and contains no
* borrowed storage. Corrupt SQLite types, signs, bounds, or stage/cursor
* relations are rejected before narrowing to the public C17 fields. */
Lardon3DProjectDbResult lardon3d_project_db_load_selected_execution(
Lardon3DProjectDb *database, uint64_t execution_id,
Lardon3DProjectDbSelectedExecution *execution);
Lardon3DProjectDbResult lardon3d_project_db_load_selected_execution_item(
Lardon3DProjectDb *database, uint64_t execution_id, uint32_t item_index,
Lardon3DProjectDbSelectedExecutionItem *item);
/* Publish one representation mapping and advance the cursor atomically. The
* image must already be explicitly associated with this item's Capture.
* Retrying the exact mapping after cursor advance is idempotent; no SHA, path,
* Asset, basename, or group number is used to recover identity. */
Lardon3DProjectDbResult lardon3d_project_db_record_selected_representation(
Lardon3DProjectDb *database, uint64_t execution_id, uint32_t item_index,
uint64_t image_id, uint32_t next_item_index);
/* Attach an existing immutable calibration scope only after all selected
* representations are durable and every selected image is a member. The
* successful transition makes the snapshot scientifically executable; an
* exact scope retry is idempotent and a different scope conflicts. */
Lardon3DProjectDbResult lardon3d_project_db_assign_selected_calibration_scope(
Lardon3DProjectDb *database, uint64_t execution_id,
uint64_t calibration_scope_id);
Lardon3DProjectDbResult lardon3d_project_db_allocate_task_id(Lardon3DProjectDb *database, Lardon3DProjectDbResult lardon3d_project_db_allocate_task_id(Lardon3DProjectDb *database,
uint64_t *task_id); uint64_t *task_id);
Lardon3DProjectDbResult lardon3d_project_db_load_task(Lardon3DProjectDb *database, uint64_t task_id, Lardon3DProjectDbResult lardon3d_project_db_load_task(Lardon3DProjectDb *database, uint64_t task_id,

View file

@ -50,6 +50,16 @@ Lardon3DRawDevelopmentResult lardon3d_raw_develop_to_capture(
uint64_t producer_task_id, int64_t created_at, Lardon3DRawDevelopmentOutput *output uint64_t producer_task_id, int64_t created_at, Lardon3DRawDevelopmentOutput *output
); );
/* Develop the immutable managed RAW identified explicitly by source_asset_id.
* The asset must have a durable RAW source-kind relation to capture_id. This
* API validates the managed bytes against their stored SHA-256 and never finds
* source identity from path, name, hash, attachment order, or image identity.
* Output storage is caller-owned; published assets/images remain DB-owned. */
Lardon3DRawDevelopmentResult lardon3d_raw_develop_asset_to_capture(
Lardon3DAppState *state, uint64_t capture_id, uint64_t source_asset_id,
uint64_t producer_task_id, int64_t created_at, Lardon3DRawDevelopmentOutput *output
);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View file

@ -0,0 +1,44 @@
#ifndef LARDON3D_RAW_DEVELOPMENT_TASK_H
#define LARDON3D_RAW_DEVELOPMENT_TASK_H
#include <stdbool.h>
#include <stdint.h>
#include <lardon3d/app_state.h>
#include <lardon3d/task_kind_registry.h>
#ifdef __cplusplus
extern "C" {
#endif
#define LARDON3D_RAW_DEVELOPMENT_TASK_KIND "raw.develop"
enum { LARDON3D_RAW_DEVELOPMENT_TASK_KIND_VERSION = 1 };
/* Create one durable, initially unqueued S3-B1 Task for the exact existing
* Capture-owned SOURCE RAW asset. The two IDs are immutable and are never
* inferred from paths, hashes, names, images, or Task identity. task_id is
* required and receives the durable operational ID. The caller owns the
* returned Task and must enqueue or destroy it. */
Lardon3DTask *lardon3d_project_create_raw_development_task(
Lardon3DAppState *state, uint64_t capture_id, uint64_t source_asset_id,
uint64_t *task_id);
/* Create and transfer one durable RAW Task to state's existing Queue. Queue
* admission owns the Task's bounded CPU/RAM/I/O reservation until callback
* completion, failure, or cancellation; no worker or scheduler is created. */
bool lardon3d_project_enqueue_raw_development(
Lardon3DAppState *state, uint64_t capture_id, uint64_t source_asset_id,
uint64_t *task_id);
/* Reconstruct callback state only from the validated typed v22 row. snapshot,
* reconstruction context, and binding are required and caller-owned. The
* binding owns its returned context until the runtime invokes its destroyer. */
bool lardon3d_raw_development_task_reconstruct(
const Lardon3DTaskDurableSnapshot *snapshot, void *context,
Lardon3DTaskKindBinding *binding);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -149,6 +149,7 @@ executable(
'src/image_catalog.c', 'src/image_catalog.c',
'src/image_catalog_persistent.c', 'src/image_catalog_persistent.c',
'src/raw_development.cpp', 'src/raw_development.cpp',
'src/raw_development_task.cpp',
'src/acquisition_pairing.cpp', 'src/acquisition_pairing.cpp',
'src/acquisition_ingest.cpp', 'src/acquisition_ingest.cpp',
'src/acquisition_campaign.cpp', 'src/acquisition_campaign.cpp',
@ -168,6 +169,7 @@ 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/calibration_bootstrap.c',
'src/sparse_sfm_geometry.cpp', 'src/sparse_sfm_geometry.cpp',
'src/sparse_sfm_incremental.cpp', 'src/sparse_sfm_incremental.cpp',
'src/sparse_sfm_bundle_adjustment.cpp', 'src/sparse_sfm_bundle_adjustment.cpp',
@ -200,6 +202,7 @@ executable(
'-DLARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_AVAILABLE', '-DLARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_AVAILABLE',
'-DLARDON3D_ACQUISITION_CAMPAIGN_TASK_AVAILABLE', '-DLARDON3D_ACQUISITION_CAMPAIGN_TASK_AVAILABLE',
'-DLARDON3D_PHOTO_QUALITY_TASK_AVAILABLE', '-DLARDON3D_PHOTO_QUALITY_TASK_AVAILABLE',
'-DLARDON3D_RAW_DEVELOPMENT_TASK_AVAILABLE',
], ],
dependencies: [ dependencies: [
ncursesw, ncursesw,
@ -330,6 +333,27 @@ raw_development_test = executable(
test('raw-development', raw_development_test, timeout: 30) test('raw-development', raw_development_test, timeout: 30)
raw_development_task_test = executable(
'test-raw-development-task',
sources: [
'tests/test_raw_development_task.cpp',
'src/app_state.c',
'src/image_catalog.c', 'src/image_view.c',
'src/image_catalog_persistent.c',
'src/raw_development.cpp',
'src/raw_development_task.cpp',
'src/project.c',
'src/project_db.c', 'src/project_db_sparse_sfm.c',
'src/task.c', 'src/task_checkpoint.c', 'src/task_queue.c',
'src/task_kind_registry.c',
'src/resource_governor.c', 'src/resource_snapshot.c',
],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv, libraw, libpng, libdeflate],
)
test('raw-development-task', raw_development_task_test, timeout: 30)
acquisition_pairing_test = executable( acquisition_pairing_test = executable(
'test-acquisition-pairing', 'test-acquisition-pairing',
sources: ['tests/test_acquisition_pairing.cpp', 'src/acquisition_pairing.cpp'], sources: ['tests/test_acquisition_pairing.cpp', 'src/acquisition_pairing.cpp'],
@ -712,6 +736,35 @@ project_db_test = executable(
test('project-db', project_db_test, timeout: 30) test('project-db', project_db_test, timeout: 30)
selected_execution_test = executable(
'test-selected-execution',
sources: [
'tests/test_selected_execution.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('selected-execution', selected_execution_test, timeout: 30)
calibration_bootstrap_test = executable(
'test-calibration-bootstrap',
sources: [
'tests/test_calibration_bootstrap.c',
'src/calibration_bootstrap.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('calibration-bootstrap', calibration_bootstrap_test, timeout: 30)
sparse_sfm_model_test = executable( sparse_sfm_model_test = executable(
'test-sparse-sfm-model', 'test-sparse-sfm-model',
sources: [ sources: [

View file

@ -158,8 +158,18 @@ extern "C" Lardon3DAcquisitionIngestResult lardon3d_acquisition_ingest(
size_t jpeg = SIZE_MAX, raw = SIZE_MAX; size_t jpeg = SIZE_MAX, raw = SIZE_MAX;
for (size_t member = 0; member < group.source_count; ++member) { for (size_t member = 0; member < group.source_count; ++member) {
size_t index = group.source_indices[member]; size_t index = group.source_indices[member];
database_result = lardon3d_project_db_attach_capture_source_asset( Lardon3DProjectDbCaptureSourceKind source_kind{};
state->project_db, capture.capture_id, published[index].asset.asset_id); if (published[index].metadata.source_kind == LARDON3D_ACQUISITION_SOURCE_JPEG)
source_kind = LARDON3D_DB_CAPTURE_SOURCE_JPEG;
else if (published[index].metadata.source_kind == LARDON3D_ACQUISITION_SOURCE_RAW)
source_kind = LARDON3D_DB_CAPTURE_SOURCE_RAW;
else
return LARDON3D_ACQUISITION_INGEST_CONSTRAINT;
// S3-E commits SOURCE membership with the already validated source kind.
// Downstream RAW work may use only this explicit relation; path, hash,
// basename, attachment order, and image identity cannot replace it.
database_result = lardon3d_project_db_record_capture_source_asset(
state->project_db, capture.capture_id, published[index].asset.asset_id, source_kind);
if (database_result != LARDON3D_PROJECT_DB_OK) return db_result(database_result); if (database_result != LARDON3D_PROJECT_DB_OK) return db_result(database_result);
if (published[index].metadata.source_kind == LARDON3D_ACQUISITION_SOURCE_JPEG && jpeg == SIZE_MAX) if (published[index].metadata.source_kind == LARDON3D_ACQUISITION_SOURCE_JPEG && jpeg == SIZE_MAX)
jpeg = index; jpeg = index;

220
src/calibration_bootstrap.c Normal file
View file

@ -0,0 +1,220 @@
#include <lardon3d/calibration_bootstrap.h>
#include <math.h>
#include <openssl/evp.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
enum {
BOOTSTRAP_HEADER_SIZE = 152,
BOOTSTRAP_ENTRY_SIZE = 140,
BOOTSTRAP_VALIDATION_FLAGS = 15,
};
static const unsigned char bootstrap_magic[8] = {'L', '3', 'D', 'C', 'A', 'L', 'B', '1'};
typedef struct {
const unsigned char *bytes;
size_t remaining;
} BootstrapReader;
static bool take(BootstrapReader *reader, void *output, size_t count) {
if (count > reader->remaining) return false;
if (output) memcpy(output, reader->bytes, count);
reader->bytes += count;
reader->remaining -= count;
return true;
}
static bool read_u32(BootstrapReader *reader, uint32_t *output) {
unsigned char bytes[4];
if (!take(reader, bytes, sizeof(bytes))) return false;
*output = (uint32_t)bytes[0] | ((uint32_t)bytes[1] << 8) |
((uint32_t)bytes[2] << 16) | ((uint32_t)bytes[3] << 24);
return true;
}
static bool read_u64(BootstrapReader *reader, uint64_t *output) {
unsigned char bytes[8];
if (!take(reader, bytes, sizeof(bytes))) return false;
*output = 0;
for (size_t index = 0; index < sizeof(bytes); ++index)
*output |= (uint64_t)bytes[index] << (8u * index);
return true;
}
static bool read_f64(BootstrapReader *reader, double *output) {
uint64_t bits;
if (!read_u64(reader, &bits)) return false;
memcpy(output, &bits, sizeof(bits));
if (*output == 0.0) *output = 0.0;
return isfinite(*output);
}
static bool sha256(const unsigned char *bytes, size_t size, unsigned char output[32]) {
unsigned int output_size = 0;
return EVP_Digest(bytes, size, output, &output_size, EVP_sha256(), NULL) == 1 &&
output_size == 32;
}
static Lardon3DCalibrationBootstrapResult db_result(Lardon3DProjectDbResult result) {
if (result == LARDON3D_PROJECT_DB_CONSTRAINT || result == LARDON3D_PROJECT_DB_NOT_FOUND)
return LARDON3D_CALIBRATION_BOOTSTRAP_SELECTION_CONFLICT;
return LARDON3D_CALIBRATION_BOOTSTRAP_DB_ERROR;
}
Lardon3DCalibrationBootstrapResult lardon3d_calibration_bootstrap_import(
Lardon3DProjectDb *database, uint64_t execution_id,
const unsigned char *artifact, size_t artifact_size,
const unsigned char expected_artifact_sha256[32],
Lardon3DCalibrationBootstrapOutput *output) {
if (!database || execution_id == 0 || !artifact || !expected_artifact_sha256 || !output ||
artifact_size < BOOTSTRAP_HEADER_SIZE ||
artifact_size > LARDON3D_CALIBRATION_BOOTSTRAP_MAX_BYTES)
return LARDON3D_CALIBRATION_BOOTSTRAP_INVALID_ARGUMENT;
memset(output, 0, sizeof(*output));
unsigned char artifact_sha256[32];
if (!sha256(artifact, artifact_size, artifact_sha256))
return LARDON3D_CALIBRATION_BOOTSTRAP_DB_ERROR;
if (memcmp(artifact_sha256, expected_artifact_sha256, 32) != 0)
return LARDON3D_CALIBRATION_BOOTSTRAP_PROVENANCE_MISMATCH;
Lardon3DProjectDbSelectedExecution execution;
Lardon3DProjectDbResult loaded =
lardon3d_project_db_load_selected_execution(database, execution_id, &execution);
if (loaded != LARDON3D_PROJECT_DB_OK) return db_result(loaded);
if (execution.stage != LARDON3D_SELECTED_EXECUTION_CALIBRATION &&
execution.stage != LARDON3D_SELECTED_EXECUTION_READY)
return LARDON3D_CALIBRATION_BOOTSTRAP_SELECTION_CONFLICT;
BootstrapReader reader = {artifact, artifact_size};
unsigned char magic[8];
uint32_t version, model_kind, model_version, entry_count;
unsigned char solver_executable_sha256[32];
unsigned char solver_configuration_sha256[32];
unsigned char initialization_evidence_sha256[32];
unsigned char validation_evidence_sha256[32];
if (!take(&reader, magic, sizeof(magic)) ||
!read_u32(&reader, &version) || !read_u32(&reader, &model_kind) ||
!read_u32(&reader, &model_version) || !read_u32(&reader, &entry_count) ||
!take(&reader, solver_executable_sha256, 32) ||
!take(&reader, solver_configuration_sha256, 32) ||
!take(&reader, initialization_evidence_sha256, 32) ||
!take(&reader, validation_evidence_sha256, 32) ||
memcmp(magic, bootstrap_magic, sizeof(magic)) != 0 ||
version != LARDON3D_CALIBRATION_BOOTSTRAP_ARTIFACT_VERSION ||
model_kind != LARDON3D_SPARSE_SFM_CALIBRATION_KIND_PINHOLE ||
model_version != LARDON3D_SPARSE_SFM_CALIBRATION_VERSION || entry_count == 0 ||
entry_count != execution.item_count ||
artifact_size != BOOTSTRAP_HEADER_SIZE + (size_t)entry_count * BOOTSTRAP_ENTRY_SIZE)
return LARDON3D_CALIBRATION_BOOTSTRAP_MALFORMED_ARTIFACT;
/* These four digests bind executable identity, exact solver configuration,
* initialization evidence and validation diagnostics into artifact identity.
* An all-zero digest is not evidence and is therefore rejected. */
unsigned char executable_or = 0, configuration_or = 0, initialization_or = 0, validation_or = 0;
for (size_t index = 0; index < 32; ++index) {
executable_or |= solver_executable_sha256[index];
configuration_or |= solver_configuration_sha256[index];
initialization_or |= initialization_evidence_sha256[index];
validation_or |= validation_evidence_sha256[index];
}
if (executable_or == 0 || configuration_or == 0 || initialization_or == 0 ||
validation_or == 0)
return LARDON3D_CALIBRATION_BOOTSTRAP_MALFORMED_ARTIFACT;
Lardon3DSparseCalibration *calibrations = calloc(entry_count, sizeof(*calibrations));
Lardon3DSparseCalibrationMember *members = calloc(entry_count, sizeof(*members));
if (!calibrations || !members) {
free(calibrations);
free(members);
return LARDON3D_CALIBRATION_BOOTSTRAP_OUT_OF_MEMORY;
}
Lardon3DCalibrationBootstrapResult result = LARDON3D_CALIBRATION_BOOTSTRAP_OK;
for (uint32_t index = 0; index < entry_count && result == LARDON3D_CALIBRATION_BOOTSTRAP_OK;
++index) {
uint64_t image_id;
unsigned char representation_sha256[32];
uint32_t width, height, support_images, support_observations, validation_flags;
double reprojection_rmse, maximum_parameter_delta;
Lardon3DSparseCalibration *calibration = &calibrations[index];
if (!read_u64(&reader, &image_id) || !take(&reader, representation_sha256, 32) ||
!read_u32(&reader, &width) || !read_u32(&reader, &height) ||
!read_f64(&reader, &calibration->fx) || !read_f64(&reader, &calibration->fy) ||
!read_f64(&reader, &calibration->cx) || !read_f64(&reader, &calibration->cy) ||
!read_f64(&reader, &calibration->k1) || !read_f64(&reader, &calibration->k2) ||
!read_f64(&reader, &calibration->p1) || !read_f64(&reader, &calibration->p2) ||
!read_u32(&reader, &support_images) || !read_u32(&reader, &support_observations) ||
!read_f64(&reader, &reprojection_rmse) ||
!read_f64(&reader, &maximum_parameter_delta) ||
!read_u32(&reader, &validation_flags) || image_id == 0 || width == 0 || height == 0 ||
calibration->fx <= 0.0 || calibration->fy <= 0.0 || calibration->cx < 0.0 ||
calibration->cy < 0.0 || calibration->cx >= (double)width ||
calibration->cy >= (double)height || support_images == 0 || support_observations == 0 ||
reprojection_rmse < 0.0 || maximum_parameter_delta < 0.0 ||
validation_flags != BOOTSTRAP_VALIDATION_FLAGS) {
result = LARDON3D_CALIBRATION_BOOTSTRAP_MALFORMED_ARTIFACT;
break;
}
Lardon3DProjectDbSelectedExecutionItem selected;
Lardon3DProjectDbImage image;
Lardon3DProjectDbImageAsset asset;
loaded = lardon3d_project_db_load_selected_execution_item(database, execution_id, index,
&selected);
if (loaded != LARDON3D_PROJECT_DB_OK || !selected.has_image || selected.image_id != image_id) {
result = LARDON3D_CALIBRATION_BOOTSTRAP_SELECTION_CONFLICT;
break;
}
loaded = lardon3d_project_db_load_image(database, image_id, &image, &asset);
if (loaded != LARDON3D_PROJECT_DB_OK || memcmp(asset.sha256, representation_sha256, 32) != 0) {
result = LARDON3D_CALIBRATION_BOOTSTRAP_SELECTION_CONFLICT;
break;
}
calibration->model_kind = model_kind;
calibration->model_version = model_version;
calibration->width = width;
calibration->height = height;
calibration->provenance_kind = LARDON3D_SPARSE_SFM_PROVENANCE_IMPORTED_TRUSTED;
memcpy(calibration->provenance_fingerprint, artifact_sha256, 32);
members[index].image_id = image_id;
}
if (reader.remaining != 0 && result == LARDON3D_CALIBRATION_BOOTSTRAP_OK)
result = LARDON3D_CALIBRATION_BOOTSTRAP_MALFORMED_ARTIFACT;
/* Validation precedes publication. The established calibration APIs own
* short transactions; their immutable, content-addressed rows may survive a
* later failure, while READY remains guarded by the final scope attachment. */
for (uint32_t index = 0; index < entry_count && result == LARDON3D_CALIBRATION_BOOTSTRAP_OK;
++index) {
Lardon3DSparseCalibration stored;
loaded = lardon3d_sparse_calibration_create(database, &calibrations[index], &stored);
if (loaded != LARDON3D_PROJECT_DB_OK) {
result = db_result(loaded);
break;
}
members[index].calibration_id = stored.calibration_id;
memcpy(members[index].calibration_hash, stored.scientific_hash, 32);
}
Lardon3DSparseCalibrationScope scope;
if (result == LARDON3D_CALIBRATION_BOOTSTRAP_OK) {
loaded = lardon3d_sparse_calibration_scope_create(database, members, entry_count, &scope);
if (loaded != LARDON3D_PROJECT_DB_OK) result = db_result(loaded);
}
if (result == LARDON3D_CALIBRATION_BOOTSTRAP_OK) {
loaded = lardon3d_project_db_assign_selected_calibration_scope(database, execution_id,
scope.scope_id);
if (loaded != LARDON3D_PROJECT_DB_OK) result = db_result(loaded);
}
if (result == LARDON3D_CALIBRATION_BOOTSTRAP_OK) {
memcpy(output->artifact_sha256, artifact_sha256, 32);
output->scope = scope;
output->calibration_count = entry_count;
}
free(calibrations);
free(members);
return result;
}

View file

@ -676,7 +676,8 @@ checkpoint_task_internal(Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbMatcherTask *matcher_parameters, const Lardon3DProjectDbMatcherTask *matcher_parameters,
const Lardon3DProjectDbGeometricVerifierTask *geometric_parameters, const Lardon3DProjectDbGeometricVerifierTask *geometric_parameters,
const Lardon3DProjectDbAcquisitionCampaignTask *campaign_parameters, const Lardon3DProjectDbAcquisitionCampaignTask *campaign_parameters,
const Lardon3DProjectDbPhotoQualityTask *quality_parameters) { const Lardon3DProjectDbPhotoQualityTask *quality_parameters,
const Lardon3DProjectDbRawDevelopmentTask *raw_parameters) {
if (!state || !state->project_loaded || !state->project_db) { if (!state || !state->project_loaded || !state->project_db) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_NO_PROJECT; return LARDON3D_PROJECT_TASK_CHECKPOINT_NO_PROJECT;
} }
@ -751,6 +752,10 @@ checkpoint_task_internal(Lardon3DAppState *state, const Lardon3DTask *task,
? lardon3d_project_db_record_photo_quality_task( ? lardon3d_project_db_record_photo_quality_task(
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint, state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
quality_parameters, now.tv_sec) quality_parameters, now.tv_sec)
: raw_parameters
? lardon3d_project_db_record_raw_development_task(
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
raw_parameters, now.tv_sec)
: lardon3d_project_db_record_task(state->project_db, &snapshot, task_kind, : lardon3d_project_db_record_task(state->project_db, &snapshot, task_kind,
task_kind_version, &checkpoint, now.tv_sec); task_kind_version, &checkpoint, now.tv_sec);
if (recorded == LARDON3D_PROJECT_DB_BUSY) { if (recorded == LARDON3D_PROJECT_DB_BUSY) {
@ -767,7 +772,7 @@ checkpoint_task_internal(Lardon3DAppState *state, const Lardon3DTask *task,
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_task(Lardon3DAppState *state, Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_task(Lardon3DAppState *state,
const Lardon3DTask *task) { const Lardon3DTask *task) {
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL); NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult Lardon3DProjectTaskCheckpointResult
@ -777,7 +782,7 @@ lardon3d_project_checkpoint_image_import_task(Lardon3DAppState *state, const Lar
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
} }
return checkpoint_task_internal( return checkpoint_task_internal(
state, task, source_path, scanset_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); state, task, source_path, scanset_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_acquisition_campaign_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_acquisition_campaign_task(
@ -785,7 +790,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_acquisition_camp
const Lardon3DProjectDbAcquisitionCampaignTask *parameters) { const Lardon3DProjectDbAcquisitionCampaignTask *parameters) {
if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL,
parameters, NULL); parameters, NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_photo_quality_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_photo_quality_task(
@ -793,7 +798,15 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_photo_quality_ta
const Lardon3DProjectDbPhotoQualityTask *parameters) { const Lardon3DProjectDbPhotoQualityTask *parameters) {
if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
parameters); parameters, NULL);
}
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_raw_development_task(
Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbRawDevelopmentTask *parameters) {
if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, parameters);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_feature_extract_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_feature_extract_task(
@ -803,7 +816,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_feature_extract_
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
} }
return checkpoint_task_internal(state, task, NULL, 0, parameters, NULL, NULL, NULL, NULL, NULL, return checkpoint_task_internal(state, task, NULL, 0, parameters, NULL, NULL, NULL, NULL, NULL,
NULL, NULL); NULL, NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_sift_extract_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_sift_extract_task(
@ -811,7 +824,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_sift_extract_tas
const Lardon3DProjectDbSiftExtractTask *parameters) { const Lardon3DProjectDbSiftExtractTask *parameters) {
if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
return checkpoint_task_internal(state, task, NULL, 0, NULL, parameters, NULL, NULL, NULL, NULL, return checkpoint_task_internal(state, task, NULL, 0, NULL, parameters, NULL, NULL, NULL, NULL,
NULL, NULL); NULL, NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_visual_index_update_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_visual_index_update_task(
@ -821,7 +834,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_visual_index_upd
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
} }
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, parameters, NULL, NULL, NULL, return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, parameters, NULL, NULL, NULL,
NULL, NULL); NULL, NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_candidate_pair_generate_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_candidate_pair_generate_task(
@ -831,7 +844,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_candidate_pair_g
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
} }
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, parameters, NULL, NULL, return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, parameters, NULL, NULL,
NULL, NULL); NULL, NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_matcher_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_matcher_task(
@ -841,7 +854,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_matcher_task(
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
} }
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, parameters, NULL, return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, parameters, NULL,
NULL, NULL); NULL, NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_geometric_verifier_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_geometric_verifier_task(
@ -851,7 +864,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_geometric_verifi
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
} }
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL,
parameters, NULL, NULL); parameters, NULL, NULL, NULL);
} }
static bool coherent_recovery(const Lardon3DProjectDbTask *database_task, static bool coherent_recovery(const Lardon3DProjectDbTask *database_task,

View file

@ -12,6 +12,7 @@
#include <time.h> #include <time.h>
#include <lardon3d/project_db.h> #include <lardon3d/project_db.h>
#include <lardon3d/raw_development_task.h>
#include "project_db_internal.h" #include "project_db_internal.h"
static const char schema_v5[] = static const char schema_v5[] =
@ -649,6 +650,64 @@ static const char schema_photo_quality_v21[] =
"low_texture_fraction REAL NOT NULL,reasons TEXT NOT NULL CHECK(length(reasons)<256)," "low_texture_fraction REAL NOT NULL,reasons TEXT NOT NULL CHECK(length(reasons)<256),"
"PRIMARY KEY(task_id,group_id));"; "PRIMARY KEY(task_id,group_id));";
static const char schema_selected_execution_v22[] =
/* v22 makes the cross-request identity bridge explicit. Group IDs remain
operational identities scoped to their respective typed Task. */
/* Legacy v21 capture_assets rows are intentionally not backfilled: their
source kind is unavailable and must never be guessed. */
"CREATE TABLE IF NOT EXISTS capture_source_assets("
"capture_id INTEGER NOT NULL,asset_id INTEGER NOT NULL,"
"source_kind INTEGER NOT NULL CHECK(source_kind IN(1,2)),"
"PRIMARY KEY(capture_id,asset_id),"
"FOREIGN KEY(capture_id,asset_id) REFERENCES capture_assets(capture_id,asset_id) "
"ON DELETE CASCADE);"
"CREATE INDEX IF NOT EXISTS capture_source_assets_page_idx ON "
"capture_source_assets(capture_id,asset_id);"
"CREATE TABLE IF NOT EXISTS selected_executions("
"execution_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(execution_id>0),"
"quality_task_id INTEGER NOT NULL REFERENCES photo_quality_triage_tasks(task_id),"
"campaign_task_id INTEGER NOT NULL REFERENCES acquisition_campaign_tasks(task_id),"
"calibration_scope_id INTEGER REFERENCES sparse_calibration_scopes(scope_id),"
"stage INTEGER NOT NULL CHECK(stage BETWEEN 1 AND 3),"
"next_item_index INTEGER NOT NULL CHECK(next_item_index>=0 AND next_item_index<=4096),"
"item_count INTEGER NOT NULL CHECK(item_count>0 AND item_count<=4096),"
"created_at INTEGER NOT NULL CHECK(created_at>=0),"
"CHECK(next_item_index<=item_count),"
"CHECK((stage=1 AND next_item_index<item_count AND calibration_scope_id IS NULL) OR "
"(stage=2 AND next_item_index=item_count AND calibration_scope_id IS NULL) OR "
"(stage=3 AND next_item_index=item_count AND calibration_scope_id IS NOT NULL)),"
"UNIQUE(quality_task_id,campaign_task_id),"
"UNIQUE(execution_id,quality_task_id,campaign_task_id));"
"CREATE TABLE IF NOT EXISTS selected_execution_items("
"execution_id INTEGER NOT NULL REFERENCES selected_executions(execution_id) ON DELETE CASCADE,"
"item_index INTEGER NOT NULL CHECK(item_index>=0 AND item_index<4096),"
"quality_task_id INTEGER NOT NULL,campaign_task_id INTEGER NOT NULL,"
"quality_group_id INTEGER NOT NULL CHECK(quality_group_id>0 AND quality_group_id<=4096),"
"campaign_group_id INTEGER NOT NULL CHECK(campaign_group_id>0 AND campaign_group_id<=4096),"
"capture_id INTEGER NOT NULL,representation_source INTEGER NOT NULL "
"CHECK(representation_source IN(1,2)),source_asset_id INTEGER,image_id INTEGER,"
"CHECK((representation_source=1 AND source_asset_id IS NULL) OR "
"(representation_source=2 AND source_asset_id IS NOT NULL)),"
"PRIMARY KEY(execution_id,item_index),UNIQUE(execution_id,quality_group_id),"
"UNIQUE(execution_id,campaign_group_id),UNIQUE(execution_id,capture_id),"
"FOREIGN KEY(execution_id,quality_task_id,campaign_task_id) REFERENCES "
"selected_executions(execution_id,quality_task_id,campaign_task_id) ON DELETE CASCADE,"
"FOREIGN KEY(quality_task_id,quality_group_id) REFERENCES "
"photo_quality_triage_results(task_id,group_id),"
"FOREIGN KEY(campaign_task_id,campaign_group_id) REFERENCES "
"acquisition_campaign_captures(task_id,group_id),"
"FOREIGN KEY(capture_id,source_asset_id) REFERENCES "
"capture_source_assets(capture_id,asset_id),"
"FOREIGN KEY(capture_id,image_id) REFERENCES capture_images(capture_id,image_id));"
"CREATE TABLE IF NOT EXISTS raw_development_tasks("
"task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,"
"capture_id INTEGER NOT NULL,source_asset_id INTEGER NOT NULL,"
"phase INTEGER NOT NULL CHECK(phase IN(1,2)),image_id INTEGER,"
"CHECK((phase=1 AND image_id IS NULL) OR (phase=2 AND image_id IS NOT NULL)),"
"FOREIGN KEY(capture_id,source_asset_id) REFERENCES "
"capture_source_assets(capture_id,asset_id),"
"FOREIGN KEY(capture_id,image_id) REFERENCES capture_images(capture_id,image_id));";
static void copy_error(char destination[LARDON3D_PROJECT_DB_ERROR_CAPACITY], const char *text) { static void copy_error(char destination[LARDON3D_PROJECT_DB_ERROR_CAPACITY], const char *text) {
if (destination) { if (destination) {
(void)snprintf(destination, LARDON3D_PROJECT_DB_ERROR_CAPACITY, "%s", text ? text : ""); (void)snprintf(destination, LARDON3D_PROJECT_DB_ERROR_CAPACITY, "%s", text ? text : "");
@ -741,7 +800,7 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int
from_version != 8 && from_version != 9 && from_version != 10 && from_version != 11 && from_version != 8 && from_version != 9 && from_version != 10 && from_version != 11 &&
from_version != 12 && from_version != 13 && from_version != 14 && from_version != 12 && from_version != 13 && from_version != 14 &&
from_version != 15 && from_version != 16 && from_version != 17 && from_version != 18 && from_version != 15 && from_version != 16 && from_version != 17 && from_version != 18 &&
from_version != 19 && from_version != 20) { from_version != 19 && from_version != 20 && from_version != 21) {
return LARDON3D_PROJECT_DB_CORRUPT; return LARDON3D_PROJECT_DB_CORRUPT;
} }
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration"); Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration");
@ -1163,6 +1222,22 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int
"finish schema v21 migration"); "finish schema v21 migration");
} }
} }
if (result == LARDON3D_PROJECT_DB_OK && from_version < 22) {
result = execute(database, schema_selected_execution_v22,
"migrate selected execution v21 to v22");
#ifdef LARDON3D_PROJECT_DB_TESTING
if (result == LARDON3D_PROJECT_DB_OK &&
getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V22")) {
result = execute(database, "INSERT INTO missing_v22_test_table VALUES(1)",
"forced migration v22 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database,
"UPDATE metadata SET value=22 WHERE key='schema_version' AND value=21",
"finish schema v22 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK) { if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, "COMMIT", "commit migration"); result = execute(database, "COMMIT", "commit migration");
} }
@ -1297,12 +1372,16 @@ Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProje
"captures", "captures",
"capture_images", "capture_images",
"capture_assets", "capture_assets",
"capture_source_assets",
"raw_development_tasks",
"capture_selections", "capture_selections",
"asset_derivations", "asset_derivations",
"acquisition_campaign_tasks", "acquisition_campaign_tasks",
"acquisition_campaign_captures", "acquisition_campaign_captures",
"photo_quality_triage_tasks", "photo_quality_triage_tasks",
"photo_quality_triage_results"}; "photo_quality_triage_results",
"selected_executions",
"selected_execution_items"};
for (size_t index = 0; index < sizeof(required) / sizeof(required[0]) && for (size_t index = 0; index < sizeof(required) / sizeof(required[0]) &&
result == LARDON3D_PROJECT_DB_OK; result == LARDON3D_PROJECT_DB_OK;
++index) { ++index) {
@ -1528,6 +1607,7 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps
const Lardon3DProjectDbSparseSfmTask *sparse_sfm, const Lardon3DProjectDbSparseSfmTask *sparse_sfm,
const Lardon3DProjectDbAcquisitionCampaignTask *campaign, const Lardon3DProjectDbAcquisitionCampaignTask *campaign,
const Lardon3DProjectDbPhotoQualityTask *photo_quality, const Lardon3DProjectDbPhotoQualityTask *photo_quality,
const Lardon3DProjectDbRawDevelopmentTask *raw_development,
int64_t updated_at) { int64_t updated_at) {
bool typed = task_kind != NULL; bool typed = task_kind != NULL;
if (!database || !valid_durable_task(snapshot, updated_at) || if (!database || !valid_durable_task(snapshot, updated_at) ||
@ -1535,6 +1615,12 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps
(!typed && task_kind_version != 0) || (!typed && task_kind_version != 0) ||
(source_path && !bounded_text(source_path, LARDON3D_PROJECT_DB_PATH_CAPACITY, false)) || (source_path && !bounded_text(source_path, LARDON3D_PROJECT_DB_PATH_CAPACITY, false)) ||
(source_path && !valid_task_id(scanset_id)) || (source_path && !valid_task_id(scanset_id)) ||
/* Raw development durability must remain exactly paired with its public
* raw.develop durable signature, because this invariant is the only
* crash-safe boundary for reconstructing the typed state after restart. */
(raw_development &&
(!task_kind || strcmp(task_kind, LARDON3D_RAW_DEVELOPMENT_TASK_KIND) != 0 ||
task_kind_version != LARDON3D_RAW_DEVELOPMENT_TASK_KIND_VERSION)) ||
(feature && (feature &&
(!valid_task_id(feature->task_id) || feature->task_id != snapshot->id || (!valid_task_id(feature->task_id) || feature->task_id != snapshot->id ||
!valid_task_id(feature->image_id) || !valid_task_id(feature->image_id) ||
@ -1605,6 +1691,17 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps
photo_quality->group_count > 4096 || photo_quality->next_group_id == 0 || photo_quality->group_count > 4096 || photo_quality->next_group_id == 0 ||
photo_quality->next_group_id > photo_quality->group_count + 1u || !photo_quality->request || photo_quality->next_group_id > photo_quality->group_count + 1u || !photo_quality->request ||
photo_quality->request_size == 0 || photo_quality->request_size > INT_MAX)) || photo_quality->request_size == 0 || photo_quality->request_size > INT_MAX)) ||
(raw_development &&
(!valid_task_id(raw_development->task_id) ||
raw_development->task_id != snapshot->id ||
!valid_task_id(raw_development->capture_id) ||
!valid_task_id(raw_development->source_asset_id) ||
(raw_development->phase != LARDON3D_RAW_DEVELOPMENT_TASK_PENDING &&
raw_development->phase != LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED) ||
(raw_development->phase == LARDON3D_RAW_DEVELOPMENT_TASK_PENDING &&
(raw_development->has_image || raw_development->image_id != 0)) ||
(raw_development->phase == LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED &&
(!raw_development->has_image || !valid_task_id(raw_development->image_id))))) ||
(checkpoint && !valid_checkpoint(checkpoint))) { (checkpoint && !valid_checkpoint(checkpoint))) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
@ -1714,6 +1811,37 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps
} }
} }
} }
if (result == LARDON3D_PROJECT_DB_OK && raw_development) {
/* Capture and SOURCE RAW are immutable Task identity. Only the publication
phase advances, and it can advance only to an image already retained by
that Capture. This is the typed half of the atomic restart checkpoint. */
result = prepare(database,
"INSERT INTO raw_development_tasks(task_id,capture_id,source_asset_id,phase,image_id) "
"SELECT ?1,?2,?3,?4,?5 WHERE EXISTS(SELECT 1 FROM capture_source_assets "
"WHERE capture_id=?2 AND asset_id=?3 AND source_kind=2) AND "
"(?4=1 OR EXISTS(SELECT 1 FROM capture_images WHERE capture_id=?2 AND image_id=?5)) "
"ON CONFLICT(task_id) DO UPDATE SET phase=excluded.phase,image_id=excluded.image_id "
"WHERE raw_development_tasks.capture_id=excluded.capture_id AND "
"raw_development_tasks.source_asset_id=excluded.source_asset_id AND "
"excluded.phase>=raw_development_tasks.phase AND "
"(raw_development_tasks.phase<2 OR raw_development_tasks.image_id=excluded.image_id)",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)raw_development->task_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)raw_development->capture_id);
sqlite3_bind_int64(statement, 3, (sqlite3_int64)raw_development->source_asset_id);
sqlite3_bind_int(statement, 4, (int)raw_development->phase);
if (raw_development->has_image)
sqlite3_bind_int64(statement, 5, (sqlite3_int64)raw_development->image_id);
else
sqlite3_bind_null(statement, 5);
result = step_done(database, statement, "upsert RAW development task");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
copy_error(database->error, "Identité ou phase de développement RAW incohérente.");
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
}
if (result == LARDON3D_PROJECT_DB_OK) { if (result == LARDON3D_PROJECT_DB_OK) {
result = result =
prepare(database, prepare(database,
@ -2048,7 +2176,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot, const char *task_kind, Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot, const char *task_kind,
uint32_t task_kind_version, const Lardon3DProjectDbCheckpoint *checkpoint, int64_t updated_at) { uint32_t task_kind_version, const Lardon3DProjectDbCheckpoint *checkpoint, int64_t updated_at) {
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0, return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at); NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_record_image_import_task( Lardon3DProjectDbResult lardon3d_project_db_record_image_import_task(
@ -2060,7 +2188,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_image_import_task(
} }
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint,
source_path, scanset_id, NULL, NULL, NULL, NULL, NULL, NULL, source_path, scanset_id, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, updated_at); NULL, NULL, NULL, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_record_feature_extract_task( Lardon3DProjectDbResult lardon3d_project_db_record_feature_extract_task(
@ -2071,7 +2199,8 @@ Lardon3DProjectDbResult lardon3d_project_db_record_feature_extract_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0, return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
parameters, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at); parameters, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_record_sift_extract_task( Lardon3DProjectDbResult lardon3d_project_db_record_sift_extract_task(
@ -2081,7 +2210,8 @@ Lardon3DProjectDbResult lardon3d_project_db_record_sift_extract_task(
const Lardon3DProjectDbSiftExtractTask *parameters, int64_t updated_at) { const Lardon3DProjectDbSiftExtractTask *parameters, int64_t updated_at) {
if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0, return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
NULL, parameters, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at); NULL, parameters, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
updated_at);
} }
static bool read_task(sqlite3_stmt *statement, Lardon3DProjectDbTask *task) { static bool read_task(sqlite3_stmt *statement, Lardon3DProjectDbTask *task) {
@ -2326,7 +2456,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_acquisition_campaign_task(
never observes a valid campaign Task without its immutable request blob. */ never observes a valid campaign Task without its immutable request blob. */
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint,
NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, parameters, NULL, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, parameters, NULL,
updated_at); NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_load_acquisition_campaign_task( Lardon3DProjectDbResult lardon3d_project_db_load_acquisition_campaign_task(
@ -2480,7 +2610,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_photo_quality_task(
/* Generic runtime state and typed immutable request share the transaction; /* Generic runtime state and typed immutable request share the transaction;
recovery cannot observe one without the other. */ recovery cannot observe one without the other. */
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0, return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, parameters, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, parameters, NULL,
updated_at); updated_at);
} }
@ -2535,6 +2665,80 @@ Lardon3DProjectDbResult lardon3d_project_db_load_photo_quality_task(
return result; return result;
} }
Lardon3DProjectDbResult lardon3d_project_db_record_raw_development_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
const char *task_kind, uint32_t task_kind_version,
const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbRawDevelopmentTask *parameters, int64_t updated_at) {
if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, parameters,
updated_at);
}
Lardon3DProjectDbResult lardon3d_project_db_load_raw_development_task(
Lardon3DProjectDb *database, uint64_t task_id,
Lardon3DProjectDbRawDevelopmentTask *parameters) {
if (!database || !valid_task_id(task_id) || !parameters)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
memset(parameters, 0, sizeof(*parameters));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(database,
"SELECT r.capture_id,r.source_asset_id,r.phase,r.image_id,cs.source_kind,ci.image_id,"
"t.task_kind,t.task_kind_version "
"FROM raw_development_tasks r JOIN tasks t ON t.task_id=r.task_id "
"LEFT JOIN capture_source_assets cs ON "
"cs.capture_id=r.capture_id AND cs.asset_id=r.source_asset_id "
"LEFT JOIN capture_images ci ON ci.capture_id=r.capture_id AND ci.image_id=r.image_id "
"WHERE r.task_id=?1", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) result = LARDON3D_PROJECT_DB_NOT_FOUND;
else if (code != SQLITE_ROW) result = sqlite_result(database, code, "load RAW development task");
else {
sqlite3_int64 capture = sqlite3_column_int64(statement, 0);
sqlite3_int64 source = sqlite3_column_int64(statement, 1);
sqlite3_int64 phase = sqlite3_column_int64(statement, 2);
bool has_image = sqlite3_column_type(statement, 3) != SQLITE_NULL;
sqlite3_int64 image = sqlite3_column_int64(statement, 3);
const unsigned char *persisted_kind = sqlite3_column_text(statement, 6);
sqlite3_int64 persisted_kind_version = sqlite3_column_int64(statement, 7);
bool valid_types = sqlite3_column_type(statement, 0) == SQLITE_INTEGER &&
sqlite3_column_type(statement, 1) == SQLITE_INTEGER &&
sqlite3_column_type(statement, 2) == SQLITE_INTEGER &&
sqlite3_column_type(statement, 6) == SQLITE_TEXT &&
sqlite3_column_type(statement, 7) == SQLITE_INTEGER &&
sqlite3_column_type(statement, 4) == SQLITE_INTEGER;
bool published = phase == LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED;
if (!valid_types || capture <= 0 || source <= 0 ||
/* If generic task kind/version diverges from raw.develop/1,
* treat the persisted pair as corrupt rather than attempting recovery. */
!persisted_kind ||
strcmp((const char *)persisted_kind, LARDON3D_RAW_DEVELOPMENT_TASK_KIND) != 0 ||
persisted_kind_version != LARDON3D_RAW_DEVELOPMENT_TASK_KIND_VERSION ||
sqlite3_column_int64(statement, 4) != LARDON3D_DB_CAPTURE_SOURCE_RAW ||
(phase != LARDON3D_RAW_DEVELOPMENT_TASK_PENDING && !published) ||
(published != has_image) || (published &&
(image <= 0 || sqlite3_column_type(statement, 5) != SQLITE_INTEGER ||
sqlite3_column_int64(statement, 5) != image))) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
parameters->task_id = task_id;
parameters->capture_id = (uint64_t)capture;
parameters->source_asset_id = (uint64_t)source;
parameters->phase = (Lardon3DProjectDbRawDevelopmentTaskPhase)phase;
parameters->has_image = has_image;
parameters->image_id = has_image ? (uint64_t)image : 0;
}
}
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
static bool valid_quality_result(const Lardon3DProjectDbPhotoQualityResult *r) { static bool valid_quality_result(const Lardon3DProjectDbPhotoQualityResult *r) {
const Lardon3DPhotoQualityMetrics *m = r ? &r->metrics : NULL; const Lardon3DPhotoQualityMetrics *m = r ? &r->metrics : NULL;
bool decoded_pair = m && ((m->decoded_width == 0) == (m->decoded_height == 0)); bool decoded_pair = m && ((m->decoded_width == 0) == (m->decoded_height == 0));
@ -2780,6 +2984,353 @@ Lardon3DProjectDbResult lardon3d_project_db_set_photo_quality_override(
return result; return result;
} }
static bool read_selected_execution_row(sqlite3_stmt *statement,
Lardon3DProjectDbSelectedExecution *execution) {
for (int column = 0; column < 8; ++column) {
if (column != 3 && sqlite3_column_type(statement, column) != SQLITE_INTEGER)
return false;
}
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
sqlite3_int64 quality = sqlite3_column_int64(statement, 1);
sqlite3_int64 campaign = sqlite3_column_int64(statement, 2);
bool has_scope = sqlite3_column_type(statement, 3) != SQLITE_NULL;
sqlite3_int64 scope = has_scope ? sqlite3_column_int64(statement, 3) : 0;
sqlite3_int64 stage = sqlite3_column_int64(statement, 4);
sqlite3_int64 next = sqlite3_column_int64(statement, 5);
sqlite3_int64 count = sqlite3_column_int64(statement, 6);
sqlite3_int64 created = sqlite3_column_int64(statement, 7);
bool relation = (stage == LARDON3D_SELECTED_EXECUTION_REPRESENTATIONS && !has_scope &&
next < count) ||
(stage == LARDON3D_SELECTED_EXECUTION_CALIBRATION && !has_scope &&
next == count) ||
(stage == LARDON3D_SELECTED_EXECUTION_READY && has_scope && next == count);
if (id <= 0 || quality <= 0 || campaign <= 0 || (has_scope && scope <= 0) ||
stage < LARDON3D_SELECTED_EXECUTION_REPRESENTATIONS ||
stage > LARDON3D_SELECTED_EXECUTION_READY || next < 0 || count <= 0 || count > 4096 ||
next > count || created < 0 || !relation)
return false;
memset(execution, 0, sizeof(*execution));
execution->execution_id = (uint64_t)id;
execution->quality_task_id = (uint64_t)quality;
execution->campaign_task_id = (uint64_t)campaign;
execution->has_calibration_scope = has_scope;
execution->calibration_scope_id = (uint64_t)scope;
execution->stage = (Lardon3DProjectDbSelectedExecutionStage)stage;
execution->next_item_index = (uint32_t)next;
execution->item_count = (uint32_t)count;
execution->created_at = created;
return true;
}
static Lardon3DProjectDbResult load_selected_execution_locked(
Lardon3DProjectDb *database, uint64_t execution_id,
Lardon3DProjectDbSelectedExecution *execution) {
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT execution_id,quality_task_id,campaign_task_id,calibration_scope_id,"
"stage,next_item_index,item_count,created_at FROM selected_executions "
"WHERE execution_id=?1", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)execution_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) result = LARDON3D_PROJECT_DB_NOT_FOUND;
else if (code != SQLITE_ROW) result = sqlite_result(database, code, "load selected execution");
else if (!read_selected_execution_row(statement, execution))
result = LARDON3D_PROJECT_DB_CORRUPT;
sqlite3_finalize(statement);
}
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_load_selected_execution(
Lardon3DProjectDb *database, uint64_t execution_id,
Lardon3DProjectDbSelectedExecution *execution) {
if (!database || !valid_task_id(execution_id) || !execution)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
memset(execution, 0, sizeof(*execution));
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult result =
load_selected_execution_locked(database, execution_id, execution);
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_load_selected_execution_item(
Lardon3DProjectDb *database, uint64_t execution_id, uint32_t item_index,
Lardon3DProjectDbSelectedExecutionItem *item) {
if (!database || !valid_task_id(execution_id) || !item)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
memset(item, 0, sizeof(*item));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT i.item_index,i.quality_group_id,i.campaign_group_id,i.capture_id,"
"i.representation_source,i.source_asset_id,i.image_id,e.item_count "
"FROM selected_execution_items i JOIN selected_executions e "
"ON e.execution_id=i.execution_id WHERE i.execution_id=?1 AND i.item_index=?2",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)execution_id);
sqlite3_bind_int64(statement, 2, item_index);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) result = LARDON3D_PROJECT_DB_NOT_FOUND;
else if (code != SQLITE_ROW) result = sqlite_result(database, code, "load selected item");
else {
bool has_source_asset = sqlite3_column_type(statement, 5) != SQLITE_NULL;
bool has_image = sqlite3_column_type(statement, 6) != SQLITE_NULL;
sqlite3_int64 index = sqlite3_column_int64(statement, 0);
sqlite3_int64 quality = sqlite3_column_int64(statement, 1);
sqlite3_int64 campaign = sqlite3_column_int64(statement, 2);
sqlite3_int64 capture = sqlite3_column_int64(statement, 3);
sqlite3_int64 source_kind = sqlite3_column_int64(statement, 4);
sqlite3_int64 source_asset = has_source_asset ? sqlite3_column_int64(statement, 5) : 0;
sqlite3_int64 image = has_image ? sqlite3_column_int64(statement, 6) : 0;
sqlite3_int64 count = sqlite3_column_int64(statement, 7);
if (sqlite3_column_type(statement, 0) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 1) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 2) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 3) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 4) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 7) != SQLITE_INTEGER || index < 0 || index >= count ||
count <= 0 || count > 4096 || quality <= 0 || quality > 4096 || campaign <= 0 ||
campaign > 4096 || capture <= 0 ||
source_kind < LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE ||
source_kind > LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET ||
(source_kind == LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE && has_source_asset) ||
(source_kind == LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET &&
(!has_source_asset || source_asset <= 0)) || (has_image && image <= 0)) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
item->item_index = (uint32_t)index;
item->quality_group_id = (uint32_t)quality;
item->campaign_group_id = (uint32_t)campaign;
item->capture_id = (uint64_t)capture;
item->representation_source = (Lardon3DProjectDbSelectedRepresentationSource)source_kind;
item->source_asset_id = (uint64_t)source_asset;
item->has_image = has_image;
item->image_id = (uint64_t)image;
}
}
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
static Lardon3DProjectDbResult compare_selected_items_locked(
Lardon3DProjectDb *database, uint64_t execution_id,
const Lardon3DProjectDbSelectedExecutionItem *items, size_t item_count) {
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT item_index,quality_group_id,campaign_group_id,capture_id,"
"representation_source,source_asset_id,image_id "
"FROM selected_execution_items WHERE execution_id=?1 ORDER BY item_index",
&statement);
if (result != LARDON3D_PROJECT_DB_OK) return result;
sqlite3_bind_int64(statement, 1, (sqlite3_int64)execution_id);
size_t index = 0;
int code;
while ((code = sqlite3_step(statement)) == SQLITE_ROW && index < item_count) {
if (sqlite3_column_int64(statement, 0) != (sqlite3_int64)index ||
sqlite3_column_int64(statement, 1) != items[index].quality_group_id ||
sqlite3_column_int64(statement, 2) != items[index].campaign_group_id ||
sqlite3_column_int64(statement, 3) != (sqlite3_int64)items[index].capture_id ||
sqlite3_column_int64(statement, 4) != items[index].representation_source ||
((items[index].representation_source == LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE &&
sqlite3_column_type(statement, 5) != SQLITE_NULL) ||
(items[index].representation_source == LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET &&
(sqlite3_column_type(statement, 5) != SQLITE_INTEGER ||
sqlite3_column_int64(statement, 5) != (sqlite3_int64)items[index].source_asset_id)))) {
result = LARDON3D_PROJECT_DB_CONSTRAINT;
break;
}
++index;
}
if (result == LARDON3D_PROJECT_DB_OK && (code != SQLITE_DONE || index != item_count))
result = code == SQLITE_ROW || code == SQLITE_DONE
? LARDON3D_PROJECT_DB_CONSTRAINT
: sqlite_result(database, code, "compare selected items");
sqlite3_finalize(statement);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_create_selected_execution(
Lardon3DProjectDb *database, uint64_t quality_task_id,
uint64_t campaign_task_id, const Lardon3DProjectDbSelectedExecutionItem *items,
size_t item_count, int64_t created_at,
Lardon3DProjectDbSelectedExecution *execution) {
if (!database || !valid_task_id(quality_task_id) || !valid_task_id(campaign_task_id) ||
!items || item_count == 0 || item_count > 4096 || created_at < 0 || !execution)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
for (size_t index = 0; index < item_count; ++index) {
if (items[index].item_index != index || items[index].quality_group_id == 0 ||
items[index].campaign_group_id == 0 || !valid_task_id(items[index].capture_id) ||
(items[index].representation_source != LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE &&
items[index].representation_source != LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET) ||
(items[index].representation_source == LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE &&
items[index].source_asset_id != 0) ||
(items[index].representation_source == LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET &&
!valid_task_id(items[index].source_asset_id)) ||
items[index].has_image || items[index].image_id != 0)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(execution, 0, sizeof(*execution));
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin selected snapshot");
sqlite3_stmt *statement = NULL;
uint64_t existing_id = 0;
bool created_new = false;
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(database, "SELECT execution_id,item_count,created_at FROM selected_executions "
"WHERE quality_task_id=?1 AND campaign_task_id=?2", &statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)quality_task_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)campaign_task_id);
int code = sqlite3_step(statement);
if (code == SQLITE_ROW) {
existing_id = (uint64_t)sqlite3_column_int64(statement, 0);
if (sqlite3_column_int64(statement, 1) != (sqlite3_int64)item_count ||
sqlite3_column_int64(statement, 2) != created_at)
result = LARDON3D_PROJECT_DB_CONSTRAINT;
} else if (code != SQLITE_DONE) {
result = sqlite_result(database, code, "find selected snapshot");
}
sqlite3_finalize(statement);
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK && existing_id != 0)
result = compare_selected_items_locked(database, existing_id, items, item_count);
if (result == LARDON3D_PROJECT_DB_OK && existing_id == 0) {
result = prepare(database,
"INSERT INTO selected_executions(quality_task_id,campaign_task_id,stage,next_item_index,"
"item_count,created_at) SELECT ?1,?2,1,0,?3,?4 WHERE EXISTS(SELECT 1 FROM "
"photo_quality_triage_tasks q JOIN acquisition_campaign_tasks c ON q.scanset_id=c.scanset_id "
"WHERE q.task_id=?1 AND c.task_id=?2 AND q.next_group_id=q.group_count+1)", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)quality_task_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)campaign_task_id);
sqlite3_bind_int64(statement, 3, (sqlite3_int64)item_count);
sqlite3_bind_int64(statement, 4, created_at);
result = step_done(database, statement, "create selected snapshot");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1)
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_int64 id = sqlite3_last_insert_rowid(database->connection);
if (id <= 0) result = LARDON3D_PROJECT_DB_CORRUPT;
else {
existing_id = (uint64_t)id;
created_new = true;
}
}
}
for (size_t index = 0; result == LARDON3D_PROJECT_DB_OK && created_new &&
index < item_count; ++index) {
result = prepare(database,
"INSERT INTO selected_execution_items(execution_id,item_index,quality_task_id,"
"campaign_task_id,quality_group_id,campaign_group_id,capture_id,"
"representation_source,source_asset_id) "
"SELECT ?1,?2,?3,?4,?5,?6,?7,?8,?9 WHERE EXISTS(SELECT 1 FROM "
"photo_quality_triage_results q JOIN acquisition_campaign_captures c "
"ON c.task_id=?4 AND c.group_id=?6 AND c.capture_id=?7 "
"WHERE q.task_id=?3 AND q.group_id=?5 AND "
"(q.human_override=1 OR (q.human_override=0 AND q.recommendation=1))) "
"AND (?8=1 OR EXISTS(SELECT 1 FROM capture_source_assets ca WHERE ca.capture_id=?7 "
"AND ca.asset_id=?9 AND ca.source_kind=2))", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)existing_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)index);
sqlite3_bind_int64(statement, 3, (sqlite3_int64)quality_task_id);
sqlite3_bind_int64(statement, 4, (sqlite3_int64)campaign_task_id);
sqlite3_bind_int64(statement, 5, items[index].quality_group_id);
sqlite3_bind_int64(statement, 6, items[index].campaign_group_id);
sqlite3_bind_int64(statement, 7, (sqlite3_int64)items[index].capture_id);
sqlite3_bind_int(statement, 8, (int)items[index].representation_source);
if (items[index].representation_source == LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET)
sqlite3_bind_int64(statement, 9, (sqlite3_int64)items[index].source_asset_id);
else
sqlite3_bind_null(statement, 9);
result = step_done(database, statement, "insert selected item");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1)
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
if (result == LARDON3D_PROJECT_DB_OK)
result = execute(database, "COMMIT", "commit selected snapshot");
else
(void)execute(database, "ROLLBACK", "rollback selected snapshot");
if (result == LARDON3D_PROJECT_DB_OK)
result = load_selected_execution_locked(database, existing_id, execution);
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_record_selected_representation(
Lardon3DProjectDb *database, uint64_t execution_id, uint32_t item_index,
uint64_t image_id, uint32_t next_item_index) {
if (!database || !valid_task_id(execution_id) || !valid_task_id(image_id) ||
item_index == UINT32_MAX || next_item_index != item_index + 1u)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin representation");
sqlite3_stmt *statement = NULL;
if (result == LARDON3D_PROJECT_DB_OK)
result = prepare(database,
"UPDATE selected_execution_items SET image_id=?3 WHERE execution_id=?1 AND item_index=?2 "
"AND (image_id IS NULL OR image_id=?3) AND EXISTS(SELECT 1 FROM capture_images ci "
"WHERE ci.capture_id=selected_execution_items.capture_id AND ci.image_id=?3)", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)execution_id);
sqlite3_bind_int64(statement, 2, item_index);
sqlite3_bind_int64(statement, 3, (sqlite3_int64)image_id);
result = step_done(database, statement, "retain selected representation");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1)
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
if (result == LARDON3D_PROJECT_DB_OK)
result = prepare(database,
"UPDATE selected_executions SET next_item_index=?3,stage=CASE WHEN ?3=item_count THEN 2 "
"ELSE 1 END WHERE execution_id=?1 AND stage IN(1,2) AND "
"(next_item_index=?2 OR next_item_index=?3) AND item_count>=?3", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)execution_id);
sqlite3_bind_int64(statement, 2, item_index);
sqlite3_bind_int64(statement, 3, next_item_index);
result = step_done(database, statement, "advance representation cursor");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1)
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
if (result == LARDON3D_PROJECT_DB_OK) result = execute(database, "COMMIT", "commit representation");
else (void)execute(database, "ROLLBACK", "rollback representation");
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_assign_selected_calibration_scope(
Lardon3DProjectDb *database, uint64_t execution_id,
uint64_t calibration_scope_id) {
if (!database || !valid_task_id(execution_id) || !valid_task_id(calibration_scope_id))
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(database,
"UPDATE selected_executions SET calibration_scope_id=?2,stage=3 WHERE execution_id=?1 "
"AND next_item_index=item_count AND (stage=2 OR (stage=3 AND calibration_scope_id=?2)) "
"AND NOT EXISTS(SELECT 1 FROM selected_execution_items i WHERE i.execution_id=?1 AND "
"(i.image_id IS NULL OR NOT EXISTS(SELECT 1 FROM sparse_calibration_scope_images s "
"WHERE s.scope_id=?2 AND s.image_id=i.image_id)))", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)execution_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)calibration_scope_id);
result = step_done(database, statement, "assign selected calibration scope");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1)
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
static bool valid_catalog_id(uint64_t value) { return value > 0 && value <= INT64_MAX; } static bool valid_catalog_id(uint64_t value) { return value > 0 && value <= INT64_MAX; }
static bool valid_original_name(const char *name) { static bool valid_original_name(const char *name) {
@ -3305,6 +3856,50 @@ Lardon3DProjectDbResult lardon3d_project_db_register_image_asset(
return result; return result;
} }
Lardon3DProjectDbResult lardon3d_project_db_load_image_asset(
Lardon3DProjectDb *database, uint64_t asset_id, Lardon3DProjectDbImageAsset *asset) {
if (!database || !valid_catalog_id(asset_id) || !asset)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
memset(asset, 0, sizeof(*asset));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(database,
"SELECT asset_id,sha256,path,size_bytes,state,created_at FROM image_assets "
"WHERE asset_id=?1", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)asset_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) result = LARDON3D_PROJECT_DB_NOT_FOUND;
else if (code != SQLITE_ROW) result = sqlite_result(database, code, "load image asset");
else {
sqlite3_int64 stored_id = sqlite3_column_int64(statement, 0);
sqlite3_int64 stored_size = sqlite3_column_int64(statement, 3);
sqlite3_int64 stored_state = sqlite3_column_int64(statement, 4);
sqlite3_int64 stored_created = sqlite3_column_int64(statement, 5);
const void *stored_hash = sqlite3_column_blob(statement, 1);
if (sqlite3_column_type(statement, 0) != SQLITE_INTEGER || stored_id <= 0 ||
sqlite3_column_type(statement, 1) != SQLITE_BLOB || !stored_hash ||
sqlite3_column_bytes(statement, 1) != LARDON3D_PROJECT_DB_SHA256_SIZE ||
!copy_column(statement, 2, asset->path, sizeof(asset->path)) ||
!valid_relative_asset_path(asset->path) || stored_size < 0 ||
stored_state != LARDON3D_DB_IMAGE_ASSET_READY || stored_created < 0) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
asset->asset_id = (uint64_t)stored_id;
memcpy(asset->sha256, stored_hash, sizeof(asset->sha256));
asset->size_bytes = (uint64_t)stored_size;
asset->state = LARDON3D_DB_IMAGE_ASSET_READY;
asset->created_at = stored_created;
if (!canonical_asset_path(asset->sha256, asset->path))
result = LARDON3D_PROJECT_DB_CORRUPT;
}
}
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_register_image( Lardon3DProjectDbResult lardon3d_project_db_register_image(
Lardon3DProjectDb *database, uint64_t scanset_id, Lardon3DProjectDb *database, uint64_t scanset_id,
const unsigned char sha256[LARDON3D_PROJECT_DB_SHA256_SIZE], const char *asset_path, const unsigned char sha256[LARDON3D_PROJECT_DB_SHA256_SIZE], const char *asset_path,
@ -3785,6 +4380,10 @@ static bool valid_capture_asset_role(Lardon3DProjectDbCaptureAssetRole role) {
return role == LARDON3D_DB_CAPTURE_ASSET_SOURCE || role == LARDON3D_DB_CAPTURE_ASSET_DERIVED; return role == LARDON3D_DB_CAPTURE_ASSET_SOURCE || role == LARDON3D_DB_CAPTURE_ASSET_DERIVED;
} }
static bool valid_capture_source_kind(Lardon3DProjectDbCaptureSourceKind kind) {
return kind == LARDON3D_DB_CAPTURE_SOURCE_JPEG || kind == LARDON3D_DB_CAPTURE_SOURCE_RAW;
}
static bool valid_asset_derivation_kind(Lardon3DProjectDbAssetDerivationKind kind) { static bool valid_asset_derivation_kind(Lardon3DProjectDbAssetDerivationKind kind) {
return kind == LARDON3D_DB_ASSET_DERIVATION_GENERIC_VERSIONED; return kind == LARDON3D_DB_ASSET_DERIVATION_GENERIC_VERSIONED;
} }
@ -3936,6 +4535,109 @@ Lardon3DProjectDbResult lardon3d_project_db_attach_capture_source_asset(
return LARDON3D_PROJECT_DB_CONSTRAINT; return LARDON3D_PROJECT_DB_CONSTRAINT;
} }
Lardon3DProjectDbResult lardon3d_project_db_record_capture_source_asset(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id,
Lardon3DProjectDbCaptureSourceKind source_kind) {
if (!database || !valid_catalog_id(capture_id) || !valid_catalog_id(asset_id) ||
!valid_capture_source_kind(source_kind))
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin source provenance");
sqlite3_stmt *statement = NULL;
if (result == LARDON3D_PROJECT_DB_OK)
result = prepare(database,
"INSERT INTO capture_assets(capture_id,asset_id,role) VALUES(?1,?2,1) "
"ON CONFLICT(capture_id,asset_id) DO NOTHING", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)asset_id);
result = step_done(database, statement, "attach explicit source asset");
}
if (result == LARDON3D_PROJECT_DB_OK)
result = prepare(database,
"INSERT INTO capture_source_assets(capture_id,asset_id,source_kind) "
"SELECT ?1,?2,?3 WHERE EXISTS(SELECT 1 FROM capture_assets "
"WHERE capture_id=?1 AND asset_id=?2 AND role=1) "
"ON CONFLICT(capture_id,asset_id) DO NOTHING", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)asset_id);
sqlite3_bind_int(statement, 3, (int)source_kind);
result = step_done(database, statement, "retain explicit source kind");
}
if (result == LARDON3D_PROJECT_DB_OK)
result = prepare(database,
"SELECT ca.role,cs.source_kind FROM capture_assets ca LEFT JOIN capture_source_assets cs "
"ON cs.capture_id=ca.capture_id AND cs.asset_id=ca.asset_id "
"WHERE ca.capture_id=?1 AND ca.asset_id=?2", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)asset_id);
int code = sqlite3_step(statement);
if (code != SQLITE_ROW) {
result = code == SQLITE_DONE ? LARDON3D_PROJECT_DB_CONSTRAINT
: sqlite_result(database, code, "verify source provenance");
} else if (sqlite3_column_type(statement, 0) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 1) != SQLITE_INTEGER ||
sqlite3_column_int(statement, 0) != LARDON3D_DB_CAPTURE_ASSET_SOURCE ||
sqlite3_column_int(statement, 1) != (int)source_kind) {
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
sqlite3_finalize(statement);
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK)
result = execute(database, "COMMIT", "commit source provenance");
else
(void)execute(database, "ROLLBACK", "rollback source provenance");
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_list_capture_source_assets(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t after_asset_id,
Lardon3DProjectDbCaptureSourceAsset *assets, size_t capacity, size_t *count) {
if (count) *count = 0;
if (!database || !valid_catalog_id(capture_id) || after_asset_id > INT64_MAX || !assets ||
!count || capacity == 0 || capacity > LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(database,
"SELECT capture_id,asset_id,source_kind FROM capture_source_assets WHERE capture_id=?1 "
"AND asset_id>?2 ORDER BY asset_id LIMIT ?3", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)after_asset_id);
sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity);
int code = SQLITE_DONE;
while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) {
sqlite3_int64 stored_capture = sqlite3_column_int64(statement, 0);
sqlite3_int64 stored_asset = sqlite3_column_int64(statement, 1);
sqlite3_int64 stored_kind = sqlite3_column_int64(statement, 2);
if (sqlite3_column_type(statement, 0) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 1) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 2) != SQLITE_INTEGER || stored_capture <= 0 ||
stored_asset <= 0 ||
!valid_capture_source_kind((Lardon3DProjectDbCaptureSourceKind)stored_kind)) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
assets[*count] = (Lardon3DProjectDbCaptureSourceAsset){
.capture_id = (uint64_t)stored_capture, .asset_id = (uint64_t)stored_asset,
.source_kind = (Lardon3DProjectDbCaptureSourceKind)stored_kind};
++*count;
}
if (result == LARDON3D_PROJECT_DB_OK && *count < capacity && code != SQLITE_DONE)
result = sqlite_result(database, code, "list capture source provenance");
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_list_capture_assets( Lardon3DProjectDbResult lardon3d_project_db_list_capture_assets(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t after_asset_id, Lardon3DProjectDb *database, uint64_t capture_id, uint64_t after_asset_id,
Lardon3DProjectDbCaptureAsset *assets, size_t capacity, size_t *count) { Lardon3DProjectDbCaptureAsset *assets, size_t capacity, size_t *count) {
@ -5365,7 +6067,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_visual_index_update_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL, return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, parameters, NULL, NULL, NULL, NULL, NULL, NULL, updated_at); NULL, parameters, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_load_visual_index_update_task( Lardon3DProjectDbResult lardon3d_project_db_load_visual_index_update_task(
@ -5412,7 +6114,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_candidate_pair_generate_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL, return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, NULL, parameters, NULL, NULL, NULL, NULL, NULL, updated_at); NULL, NULL, parameters, NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_load_candidate_pair_generate_task( Lardon3DProjectDbResult lardon3d_project_db_load_candidate_pair_generate_task(
@ -5468,7 +6170,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_matcher_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL, return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, NULL, NULL, parameters, NULL, NULL, NULL, NULL, updated_at); NULL, NULL, NULL, parameters, NULL, NULL, NULL, NULL, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_load_matcher_task( Lardon3DProjectDbResult lardon3d_project_db_load_matcher_task(
@ -5526,7 +6228,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_geometric_verifier_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL, return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, NULL, NULL, NULL, parameters, NULL, NULL, NULL, updated_at); NULL, NULL, NULL, NULL, parameters, NULL, NULL, NULL, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_load_geometric_verifier_task( Lardon3DProjectDbResult lardon3d_project_db_load_geometric_verifier_task(
@ -5599,7 +6301,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_track_builder_task(
} }
Lardon3DProjectDbResult result = record_task_internal( Lardon3DProjectDbResult result = record_task_internal(
db, snapshot, kind, version, checkpoint, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, db, snapshot, kind, version, checkpoint, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, updated_at); NULL, NULL, NULL, updated_at);
if (result != LARDON3D_PROJECT_DB_OK) return result; if (result != LARDON3D_PROJECT_DB_OK) return result;
(void)pthread_mutex_lock(&db->mutex); (void)pthread_mutex_lock(&db->mutex);
result = execute(db, "BEGIN IMMEDIATE", "begin track builder task"); result = execute(db, "BEGIN IMMEDIATE", "begin track builder task");
@ -5710,7 +6412,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_sparse_sfm_task(
const Lardon3DProjectDbSparseSfmTask *parameters, int64_t updated_at) { const Lardon3DProjectDbSparseSfmTask *parameters, int64_t updated_at) {
if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
return record_task_internal(database, snapshot, kind, version, checkpoint, NULL, 0, NULL, NULL, return record_task_internal(database, snapshot, kind, version, checkpoint, NULL, 0, NULL, NULL,
NULL, NULL, NULL, NULL, parameters, NULL, NULL, updated_at); NULL, NULL, NULL, NULL, parameters, NULL, NULL, NULL, updated_at);
} }
static bool read_u64_blob(sqlite3_stmt *statement, int column, uint64_t *value) { static bool read_u64_blob(sqlite3_stmt *statement, int column, uint64_t *value) {

View file

@ -336,3 +336,47 @@ extern "C" Lardon3DRawDevelopmentResult lardon3d_raw_develop_to_capture(
} catch (const std::bad_alloc &) { return LARDON3D_RAW_DEVELOPMENT_OUT_OF_MEMORY; } } catch (const std::bad_alloc &) { return LARDON3D_RAW_DEVELOPMENT_OUT_OF_MEMORY; }
catch (...) { return LARDON3D_RAW_DEVELOPMENT_INTERNAL_ERROR; } catch (...) { return LARDON3D_RAW_DEVELOPMENT_INTERNAL_ERROR; }
} }
extern "C" Lardon3DRawDevelopmentResult lardon3d_raw_develop_asset_to_capture(
Lardon3DAppState *state, uint64_t capture_id, uint64_t source_asset_id,
uint64_t producer_task_id, int64_t created_at, Lardon3DRawDevelopmentOutput *output) {
if (!state || !state->project_loaded || !state->project_db || capture_id == 0 ||
source_asset_id == 0 || created_at < 0 || !output)
return LARDON3D_RAW_DEVELOPMENT_INVALID_ARGUMENT;
std::memset(output, 0, sizeof(*output));
try {
bool explicit_raw = false;
uint64_t after_asset_id = source_asset_id - 1u;
Lardon3DProjectDbCaptureSourceAsset relation{};
size_t count = 0;
Lardon3DProjectDbResult listed = lardon3d_project_db_list_capture_source_assets(
state->project_db, capture_id, after_asset_id, &relation, 1, &count);
if (listed != LARDON3D_PROJECT_DB_OK) return db_result(listed);
explicit_raw = count == 1 && relation.asset_id == source_asset_id &&
relation.source_kind == LARDON3D_DB_CAPTURE_SOURCE_RAW;
if (!explicit_raw) return LARDON3D_RAW_DEVELOPMENT_CONSTRAINT;
Lardon3DProjectDbImageAsset asset{};
Lardon3DProjectDbResult loaded = lardon3d_project_db_load_image_asset(
state->project_db, source_asset_id, &asset);
if (loaded != LARDON3D_PROJECT_DB_OK) return db_result(loaded);
char managed_source[LARDON3D_APP_STATE_PATH_CAPACITY];
if (!asset_file_path(state, asset, managed_source)) return LARDON3D_RAW_DEVELOPMENT_IO_ERROR;
if (!verify_managed_asset(managed_source, asset.sha256))
return LARDON3D_RAW_DEVELOPMENT_SOURCE_CHANGED;
// The pathname passed to the frozen developer is resolved only after the
// explicit durable ID and RAW relation are validated. Republishing these
// immutable managed bytes must converge to that same asset ID.
Lardon3DRawDevelopmentResult result = lardon3d_raw_develop_to_capture(
state, capture_id, managed_source, producer_task_id, created_at, output);
if (result != LARDON3D_RAW_DEVELOPMENT_OK) return result;
return output->source_asset.asset_id == source_asset_id
? LARDON3D_RAW_DEVELOPMENT_OK
: LARDON3D_RAW_DEVELOPMENT_CONSTRAINT;
} catch (const std::bad_alloc &) {
return LARDON3D_RAW_DEVELOPMENT_OUT_OF_MEMORY;
} catch (...) {
return LARDON3D_RAW_DEVELOPMENT_INTERNAL_ERROR;
}
}

View file

@ -0,0 +1,197 @@
extern "C" {
#include <lardon3d/project.h>
#include <lardon3d/raw_development.h>
#include <lardon3d/raw_development_task.h>
#include <lardon3d/task_queue.h>
}
#include <cstdio>
#include <new>
namespace {
/* One admitted RAW execution owns a conservative allowance for the bounded
* 40 MP decoder image, LibRaw storage, BGR/PNG buffers, validation decode, and
* immutable-asset publication. This is an operational hardware bound, not a
* scientific limit on the number of Captures. The Queue/Governor reservation
* exists only for this one Task callback and is released by Task runtime on
* every terminal path. */
constexpr uint64_t kRawWorkingBytes = UINT64_C(2) * 1024u * 1024u * 1024u;
struct Context {
char project_path[LARDON3D_APP_STATE_PATH_CAPACITY]{};
Lardon3DProjectDb *database{};
Lardon3DResourceGovernor *governor{};
uint64_t capture_id{};
uint64_t source_asset_id{};
};
void destroy(void *value) { delete static_cast<Context *>(value); }
void runtime(const Context *context, Lardon3DAppState *state) {
lardon3d_app_state_init(state);
state->project_loaded = true;
state->project_db = context->database;
state->resource_governor = context->governor;
std::snprintf(state->project_path, sizeof(state->project_path), "%s", context->project_path);
}
bool checkpoint(Context *context, Lardon3DTask *task,
Lardon3DProjectDbRawDevelopmentTaskPhase phase, uint64_t image_id) {
Lardon3DAppState state;
runtime(context, &state);
Lardon3DProjectDbRawDevelopmentTask parameters{
lardon3d_task_id(task), context->capture_id, context->source_asset_id, phase,
image_id != 0, image_id};
return lardon3d_project_checkpoint_raw_development_task(&state, task, &parameters) ==
LARDON3D_PROJECT_TASK_CHECKPOINT_OK;
}
bool run_impl(Lardon3DTask *task, Context *context) {
Lardon3DProjectDbRawDevelopmentTask persisted{};
if (lardon3d_project_db_load_raw_development_task(
context->database, lardon3d_task_id(task), &persisted) != LARDON3D_PROJECT_DB_OK)
return lardon3d_task_fail(task, "État durable du développement RAW invalide.");
if (!lardon3d_task_checkpoint(task)) return false;
if (persisted.phase == LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED) {
return lardon3d_task_set_progress(task, 100, "Développement RAW déjà publié.") &&
checkpoint(context, task, LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED,
persisted.image_id);
}
Lardon3DProjectDbCapture capture{};
if (lardon3d_project_db_load_capture(context->database, context->capture_id, &capture) !=
LARDON3D_PROJECT_DB_OK)
return lardon3d_task_fail(task, "Capture du développement RAW introuvable.");
Lardon3DAppState state;
runtime(context, &state);
Lardon3DRawDevelopmentOutput output{};
Lardon3DRawDevelopmentResult developed = lardon3d_raw_develop_asset_to_capture(
&state, context->capture_id, context->source_asset_id, lardon3d_task_id(task),
capture.created_at, &output);
if (developed != LARDON3D_RAW_DEVELOPMENT_OK)
return lardon3d_task_fail(task, "Développement RAW S3-B1 impossible.");
/* Immutable output publication may precede this checkpoint if the process
* dies. The frozen developer is content-addressed and exact retry converges;
* once this atomic typed/generic checkpoint succeeds, recovery never guesses
* which image was published. */
if (!lardon3d_task_set_progress(task, 100, "Représentation RAW publiée.") ||
!checkpoint(context, task, LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED,
output.image.image_id))
return lardon3d_task_fail(task, "Checkpoint du développement RAW impossible.");
return true;
}
bool run(Lardon3DTask *task, void *value) noexcept {
try {
return run_impl(task, static_cast<Context *>(value));
} catch (const std::bad_alloc &) {
return lardon3d_task_fail(task, "Mémoire insuffisante pour le développement RAW.");
} catch (...) {
return lardon3d_task_fail(task, "Erreur interne du développement RAW.");
}
}
void finished(const Lardon3DTask *task, void *value) noexcept {
try {
Lardon3DAppState state;
runtime(static_cast<Context *>(value), &state);
(void)lardon3d_project_checkpoint_task(&state, task);
} catch (...) {
}
}
Context *make_context(const Lardon3DTaskReconstructionContext *runtime_context,
uint64_t capture_id, uint64_t source_asset_id) {
if (!runtime_context || !runtime_context->project_path ||
!runtime_context->project_path[0] || !runtime_context->project_db ||
!runtime_context->resource_governor)
return nullptr;
Context *context = new (std::nothrow) Context;
if (!context) return nullptr;
int written = std::snprintf(context->project_path, sizeof(context->project_path), "%s",
runtime_context->project_path);
if (written <= 0 || written >= static_cast<int>(sizeof(context->project_path))) {
delete context;
return nullptr;
}
context->database = runtime_context->project_db;
context->governor = runtime_context->resource_governor;
context->capture_id = capture_id;
context->source_asset_id = source_asset_id;
return context;
}
} // namespace
extern "C" bool lardon3d_raw_development_task_reconstruct(
const Lardon3DTaskDurableSnapshot *snapshot, void *value,
Lardon3DTaskKindBinding *binding) {
try {
auto *runtime_context = static_cast<Lardon3DTaskReconstructionContext *>(value);
if (!snapshot || !runtime_context || !binding) return false;
Lardon3DProjectDbRawDevelopmentTask persisted{};
if (lardon3d_project_db_load_raw_development_task(
runtime_context->project_db, snapshot->id, &persisted) != LARDON3D_PROJECT_DB_OK)
return false;
Context *context = make_context(runtime_context, persisted.capture_id,
persisted.source_asset_id);
if (!context) return false;
*binding = {run, context, destroy, finished, context};
return true;
} catch (...) {
return false;
}
}
extern "C" Lardon3DTask *lardon3d_project_create_raw_development_task(
Lardon3DAppState *state, uint64_t capture_id, uint64_t source_asset_id,
uint64_t *task_id) {
if (task_id) *task_id = 0;
try {
if (!state || !state->project_loaded || !state->project_db ||
!state->resource_governor || !task_id || capture_id == 0 || source_asset_id == 0)
return nullptr;
Lardon3DTaskReconstructionContext runtime_context{
state->project_path, state->project_db, state->resource_governor, nullptr};
Context *context = make_context(&runtime_context, capture_id, source_asset_id);
if (!context) return nullptr;
uint64_t id = 0;
if (lardon3d_project_db_allocate_task_id(state->project_db, &id) != LARDON3D_PROJECT_DB_OK) {
delete context;
return nullptr;
}
Lardon3DResourceEstimate estimate{kRawWorkingBytes + sizeof(Context), 0, 0, 0,
1, 1, 1, 0, 1, LARDON3D_RESOURCE_TASK_MIXED};
Lardon3DTask *task = lardon3d_task_create_typed(
"Développement RAW S3-B1", &estimate, LARDON3D_RAW_DEVELOPMENT_TASK_KIND,
LARDON3D_RAW_DEVELOPMENT_TASK_KIND_VERSION, run, context, destroy);
if (!task || !lardon3d_task_assign_id(task, id) ||
!lardon3d_task_set_finished_callback(task, finished, context) ||
!checkpoint(context, task, LARDON3D_RAW_DEVELOPMENT_TASK_PENDING, 0)) {
lardon3d_task_destroy(task);
return nullptr;
}
*task_id = id;
return task;
} catch (...) {
return nullptr;
}
}
extern "C" bool lardon3d_project_enqueue_raw_development(
Lardon3DAppState *state, uint64_t capture_id, uint64_t source_asset_id,
uint64_t *task_id) {
try {
if (!state || !state->task_queue) return false;
Lardon3DTask *task = lardon3d_project_create_raw_development_task(
state, capture_id, source_asset_id, task_id);
if (!task) return false;
if (!lardon3d_task_queue_add(state->task_queue, task, nullptr)) {
lardon3d_task_destroy(task);
return false;
}
return true;
} catch (...) {
return false;
}
}

View file

@ -6,6 +6,9 @@
#ifdef LARDON3D_PHOTO_QUALITY_TASK_AVAILABLE #ifdef LARDON3D_PHOTO_QUALITY_TASK_AVAILABLE
#include <lardon3d/photo_quality_task.h> #include <lardon3d/photo_quality_task.h>
#endif #endif
#ifdef LARDON3D_RAW_DEVELOPMENT_TASK_AVAILABLE
#include <lardon3d/raw_development_task.h>
#endif
#include <lardon3d/geometric_verifier_task.h> #include <lardon3d/geometric_verifier_task.h>
#include <lardon3d/import_task.h> #include <lardon3d/import_task.h>
#include <lardon3d/matcher_task.h> #include <lardon3d/matcher_task.h>
@ -24,6 +27,13 @@
const Lardon3DTaskKindRegistry *lardon3d_task_kind_registry_production(void) { const Lardon3DTaskKindRegistry *lardon3d_task_kind_registry_production(void) {
static const Lardon3DTaskKindDescriptor descriptors[] = { static const Lardon3DTaskKindDescriptor descriptors[] = {
#ifdef LARDON3D_RAW_DEVELOPMENT_TASK_AVAILABLE
{
.kind = LARDON3D_RAW_DEVELOPMENT_TASK_KIND,
.kind_version = LARDON3D_RAW_DEVELOPMENT_TASK_KIND_VERSION,
.reconstruct = lardon3d_raw_development_task_reconstruct,
},
#endif
#ifdef LARDON3D_PHOTO_QUALITY_TASK_AVAILABLE #ifdef LARDON3D_PHOTO_QUALITY_TASK_AVAILABLE
{ {
.kind = LARDON3D_PHOTO_QUALITY_TASK_KIND, .kind = LARDON3D_PHOTO_QUALITY_TASK_KIND,

View file

@ -146,6 +146,24 @@ bool capture_assets(Lardon3DProjectDb *database, uint64_t capture_id, size_t exp
return true; return true;
} }
bool capture_source_kinds(Lardon3DProjectDb *database, uint64_t capture_id,
size_t expected_jpeg, size_t expected_raw) {
Lardon3DProjectDbCaptureSourceAsset assets[64]{};
size_t count = 0u;
if (lardon3d_project_db_list_capture_source_assets(database, capture_id, 0, assets, 64,
&count) != LARDON3D_PROJECT_DB_OK ||
count != expected_jpeg + expected_raw)
return false;
size_t jpeg = 0u, raw = 0u;
for (size_t index = 0; index < count; ++index) {
if (assets[index].capture_id != capture_id || assets[index].asset_id == 0) return false;
if (assets[index].source_kind == LARDON3D_DB_CAPTURE_SOURCE_JPEG) ++jpeg;
else if (assets[index].source_kind == LARDON3D_DB_CAPTURE_SOURCE_RAW) ++raw;
else return false;
}
return jpeg == expected_jpeg && raw == expected_raw;
}
bool test_ingest_grouping_and_resume() { bool test_ingest_grouping_and_resume() {
Fixture fixture; Fixture fixture;
CHECK(fixture.open()); CHECK(fixture.open());
@ -181,7 +199,8 @@ bool test_ingest_grouping_and_resume() {
&jpeg_options, &paired) == LARDON3D_ACQUISITION_INGEST_OK); &jpeg_options, &paired) == LARDON3D_ACQUISITION_INGEST_OK);
CHECK(paired.group_count == 1 && paired.groups[0].basis == LARDON3D_ACQUISITION_GROUP_STRONG && CHECK(paired.group_count == 1 && paired.groups[0].basis == LARDON3D_ACQUISITION_GROUP_STRONG &&
paired.groups[0].source_count == 2 && capture_count(strong.database, strong.scanset.scanset_id) == 1 && paired.groups[0].source_count == 2 && capture_count(strong.database, strong.scanset.scanset_id) == 1 &&
capture_assets(strong.database, paired.groups[0].capture_id, 2)); capture_assets(strong.database, paired.groups[0].capture_id, 2) &&
capture_source_kinds(strong.database, paired.groups[0].capture_id, 1, 1));
/* A competing equal strong candidate remains entirely ungrouped. */ /* A competing equal strong candidate remains entirely ungrouped. */
Fixture ambiguous; Fixture ambiguous;
@ -215,6 +234,7 @@ bool test_ingest_grouping_and_resume() {
CHECK(lardon3d_acquisition_ingest(&resumed.state, resumed.scanset.scanset_id, one, 1, &resume_options, &first) == LARDON3D_ACQUISITION_INGEST_OK && first.groups[0].capture_id == existing.capture_id); CHECK(lardon3d_acquisition_ingest(&resumed.state, resumed.scanset.scanset_id, one, 1, &resume_options, &first) == LARDON3D_ACQUISITION_INGEST_OK && first.groups[0].capture_id == existing.capture_id);
Lardon3DAcquisitionIngestOutput retry{}; Lardon3DAcquisitionIngestOutput retry{};
CHECK(lardon3d_acquisition_ingest(&resumed.state, resumed.scanset.scanset_id, one, 1, &resume_options, &retry) == LARDON3D_ACQUISITION_INGEST_OK && capture_count(resumed.database, resumed.scanset.scanset_id) == 1 && capture_assets(resumed.database, existing.capture_id, 1)); CHECK(lardon3d_acquisition_ingest(&resumed.state, resumed.scanset.scanset_id, one, 1, &resume_options, &retry) == LARDON3D_ACQUISITION_INGEST_OK && capture_count(resumed.database, resumed.scanset.scanset_id) == 1 && capture_assets(resumed.database, existing.capture_id, 1));
CHECK(capture_source_kinds(resumed.database, existing.capture_id, 1, 0));
uint64_t images = 0; CHECK(lardon3d_project_db_count_images(resumed.database, resumed.scanset.scanset_id, &images) == LARDON3D_PROJECT_DB_OK && images == 1); uint64_t images = 0; CHECK(lardon3d_project_db_count_images(resumed.database, resumed.scanset.scanset_id, &images) == LARDON3D_PROJECT_DB_OK && images == 1);
fixture.close(); strong.close(); ambiguous.close(); resumed.close(); fixture.close(); strong.close(); ambiguous.close(); resumed.close();
@ -245,6 +265,16 @@ bool test_explicit_and_db_helper() {
Lardon3DProjectDbImageRegisterStatus status{}; Lardon3DProjectDbImage image{}; Lardon3DProjectDbImageRegisterStatus status{}; Lardon3DProjectDbImage image{};
CHECK(lardon3d_project_db_publish_source_capture_image(fixture.database, capture.capture_id, asset.asset_id, CHECK(lardon3d_project_db_publish_source_capture_image(fixture.database, capture.capture_id, asset.asset_id,
"helper.JPG", helper_path, 0, 8, false, &status, &image) == LARDON3D_PROJECT_DB_OK && status == LARDON3D_PROJECT_DB_IMAGE_REGISTERED); "helper.JPG", helper_path, 0, 8, false, &status, &image) == LARDON3D_PROJECT_DB_OK && status == LARDON3D_PROJECT_DB_IMAGE_REGISTERED);
CHECK(lardon3d_project_db_record_capture_source_asset(
fixture.database, capture.capture_id, asset.asset_id,
LARDON3D_DB_CAPTURE_SOURCE_JPEG) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_record_capture_source_asset(
fixture.database, capture.capture_id, asset.asset_id,
LARDON3D_DB_CAPTURE_SOURCE_JPEG) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_record_capture_source_asset(
fixture.database, capture.capture_id, asset.asset_id,
LARDON3D_DB_CAPTURE_SOURCE_RAW) == LARDON3D_PROJECT_DB_CONSTRAINT);
CHECK(capture_source_kinds(fixture.database, capture.capture_id, 1, 0));
uint64_t selected = 0; uint64_t selected = 0;
CHECK(lardon3d_project_db_get_selected_capture_image(fixture.database, capture.capture_id, &selected) == LARDON3D_PROJECT_DB_NOT_FOUND); CHECK(lardon3d_project_db_get_selected_capture_image(fixture.database, capture.capture_id, &selected) == LARDON3D_PROJECT_DB_NOT_FOUND);
CHECK(lardon3d_project_db_publish_source_capture_image(fixture.database, capture.capture_id, asset.asset_id, CHECK(lardon3d_project_db_publish_source_capture_image(fixture.database, capture.capture_id, asset.asset_id,

View file

@ -0,0 +1,167 @@
#include <openssl/evp.h>
#include <sqlite3.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <lardon3d/calibration_bootstrap.h>
#define CHECK(condition) do { if (!(condition)) { \
fprintf(stderr, "calibration bootstrap failure at line %d: %s\n", __LINE__, #condition); \
return false; } } while (0)
enum { ARTIFACT_SIZE = 292 };
static bool sql(const char *path, const char *text) {
sqlite3 *connection = NULL;
if (sqlite3_open(path, &connection) != SQLITE_OK) return false;
int code = sqlite3_exec(connection, text, NULL, NULL, NULL);
return sqlite3_close(connection) == SQLITE_OK && code == SQLITE_OK;
}
static void put_u32(unsigned char *bytes, size_t *offset, uint32_t value) {
for (size_t index = 0; index < 4; ++index) bytes[(*offset)++] = (unsigned char)(value >> (8 * index));
}
static void put_u64(unsigned char *bytes, size_t *offset, uint64_t value) {
for (size_t index = 0; index < 8; ++index) bytes[(*offset)++] = (unsigned char)(value >> (8 * index));
}
static void put_f64(unsigned char *bytes, size_t *offset, double value) {
uint64_t bits;
memcpy(&bits, &value, sizeof(bits));
put_u64(bytes, offset, bits);
}
static bool digest(const unsigned char *bytes, size_t size, unsigned char output[32]) {
unsigned int output_size = 0;
return EVP_Digest(bytes, size, output, &output_size, EVP_sha256(), NULL) == 1 &&
output_size == 32;
}
static void artifact(unsigned char bytes[ARTIFACT_SIZE]) {
memset(bytes, 0, ARTIFACT_SIZE);
size_t offset = 0;
memcpy(bytes + offset, "L3DCALB1", 8); offset += 8;
put_u32(bytes, &offset, 1); put_u32(bytes, &offset, 1);
put_u32(bytes, &offset, 1); put_u32(bytes, &offset, 1);
/* Four distinct nonzero evidence digests stand for executable,
* configuration, initialization and validation evidence. */
for (unsigned int evidence = 1; evidence <= 4; ++evidence) {
memset(bytes + offset, (int)evidence, 32);
offset += 32;
}
put_u64(bytes, &offset, 1);
memset(bytes + offset, 0x11, 32); offset += 32;
put_u32(bytes, &offset, 4000); put_u32(bytes, &offset, 2250);
put_f64(bytes, &offset, 3072.6); put_f64(bytes, &offset, 3053.54);
put_f64(bytes, &offset, 1969.56); put_f64(bytes, &offset, 1118.52);
put_f64(bytes, &offset, 0.0452928); put_f64(bytes, &offset, -0.0377292);
put_f64(bytes, &offset, 0.00479862); put_f64(bytes, &offset, 0.00672575);
put_u32(bytes, &offset, 56); put_u32(bytes, &offset, 1000);
put_f64(bytes, &offset, 0.4); put_f64(bytes, &offset, 0.000001);
put_u32(bytes, &offset, 15);
}
static bool run(void) {
char directory[] = "/tmp/lardon3d-calibration-bootstrap-XXXXXX";
CHECK(mkdtemp(directory) != NULL);
char path[512];
CHECK(snprintf(path, sizeof(path), "%s/project.db", directory) > 0);
Lardon3DProjectDb *database = NULL;
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK);
lardon3d_project_db_close(database);
/* This fixture establishes the already-frozen operational mappings. The
* importer may validate them but must never infer them from equal IDs. */
CHECK(sql(path,
"INSERT INTO scansets(scanset_id,name,created_at,updated_at) VALUES(1,'scope',1,1);"
"INSERT INTO tasks VALUES(1,'quality','photo_quality.triage',1,5,5,100,1,0,0,0,0,1);"
"INSERT INTO tasks VALUES(2,'campaign','acquisition_campaign.run',1,5,5,100,1,0,0,0,0,1);"
"INSERT INTO photo_quality_triage_tasks VALUES(1,1,2,1,X'01');"
"INSERT INTO photo_quality_triage_results VALUES(1,1,0,1,0,1,0,100,100,100,100,"
"1.0,1.0,0.0,0.0,1.0,1.0,0.0,'GOOD');"
"INSERT INTO acquisition_campaign_tasks VALUES(2,1,2,2,X'02');"
"INSERT INTO captures(capture_id,scanset_id,created_at) VALUES(1,1,1);"
"INSERT INTO acquisition_campaign_captures VALUES(2,2,1);"
"INSERT INTO image_assets(asset_id,sha256,path,size_bytes,state,created_at) VALUES"
"(1,X'1111111111111111111111111111111111111111111111111111111111111111',"
"'assets/images/11/1111111111111111111111111111111111111111111111111111111111111111',"
"1,1,1);"
"INSERT INTO images(image_id,scanset_id,asset_id,original_name,source_path,imported_at) "
"VALUES(1,1,1,'selected.jpg','/source/selected.jpg',1);"
"INSERT INTO capture_images VALUES(1,1);"));
CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbSelectedExecutionItem item = {
.item_index = 0,
.quality_group_id = 1,
.campaign_group_id = 2,
.capture_id = 1,
.representation_source = LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE};
Lardon3DProjectDbSelectedExecution execution;
CHECK(lardon3d_project_db_create_selected_execution(database, 1, 2, &item, 1, 10,
&execution) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_record_selected_representation(database, execution.execution_id,
0, 1, 1) == LARDON3D_PROJECT_DB_OK);
unsigned char bytes[ARTIFACT_SIZE], hash[32], wrong_hash[32] = {0};
artifact(bytes);
CHECK(digest(bytes, sizeof(bytes), hash));
Lardon3DCalibrationBootstrapOutput output;
CHECK(lardon3d_calibration_bootstrap_import(database, execution.execution_id, bytes,
sizeof(bytes), wrong_hash, &output) ==
LARDON3D_CALIBRATION_BOOTSTRAP_PROVENANCE_MISMATCH);
bytes[160] ^= 1u;
CHECK(digest(bytes, sizeof(bytes), wrong_hash));
CHECK(lardon3d_calibration_bootstrap_import(database, execution.execution_id, bytes,
sizeof(bytes), wrong_hash, &output) ==
LARDON3D_CALIBRATION_BOOTSTRAP_SELECTION_CONFLICT);
artifact(bytes);
bytes[8] = 2;
CHECK(digest(bytes, sizeof(bytes), wrong_hash));
CHECK(lardon3d_calibration_bootstrap_import(database, execution.execution_id, bytes,
sizeof(bytes), wrong_hash, &output) ==
LARDON3D_CALIBRATION_BOOTSTRAP_MALFORMED_ARTIFACT);
artifact(bytes);
bytes[ARTIFACT_SIZE - 4] = 14;
CHECK(digest(bytes, sizeof(bytes), wrong_hash));
CHECK(lardon3d_calibration_bootstrap_import(database, execution.execution_id, bytes,
sizeof(bytes), wrong_hash, &output) ==
LARDON3D_CALIBRATION_BOOTSTRAP_MALFORMED_ARTIFACT);
artifact(bytes);
Lardon3DCalibrationBootstrapResult imported = lardon3d_calibration_bootstrap_import(
database, execution.execution_id, bytes, sizeof(bytes), hash, &output);
if (imported != LARDON3D_CALIBRATION_BOOTSTRAP_OK)
fprintf(stderr, "valid artifact import result=%d\n", (int)imported);
CHECK(imported == LARDON3D_CALIBRATION_BOOTSTRAP_OK);
CHECK(output.calibration_count == 1 && output.scope.scope_id > 0 &&
memcmp(output.artifact_sha256, hash, 32) == 0);
Lardon3DSparseCalibrationMember member;
size_t count = 0;
uint64_t next = 0;
CHECK(lardon3d_sparse_calibration_scope_list_members(database, output.scope.scope_id, 0,
&member, 1, &count, &next) == LARDON3D_PROJECT_DB_OK && count == 1 &&
member.image_id == 1);
Lardon3DSparseCalibration calibration;
CHECK(lardon3d_sparse_calibration_load(database, member.calibration_id, &calibration) ==
LARDON3D_PROJECT_DB_OK &&
calibration.provenance_kind == LARDON3D_SPARSE_SFM_PROVENANCE_IMPORTED_TRUSTED &&
memcmp(calibration.provenance_fingerprint, hash, 32) == 0);
CHECK(lardon3d_calibration_bootstrap_import(database, execution.execution_id, bytes,
sizeof(bytes), hash, &output) == LARDON3D_CALIBRATION_BOOTSTRAP_OK);
CHECK(lardon3d_project_db_load_selected_execution(database, execution.execution_id,
&execution) == LARDON3D_PROJECT_DB_OK &&
execution.stage == LARDON3D_SELECTED_EXECUTION_READY &&
execution.calibration_scope_id == output.scope.scope_id);
lardon3d_project_db_close(database);
CHECK(unlink(path) == 0);
CHECK(rmdir(directory) == 0);
return true;
}
int main(void) { return run() ? EXIT_SUCCESS : EXIT_FAILURE; }

View file

@ -116,7 +116,7 @@ static bool create_future_database(const char *path) {
sqlite3_exec( sqlite3_exec(
connection, connection,
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);" "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
"INSERT INTO metadata VALUES('schema_version',22);", "INSERT INTO metadata VALUES('schema_version',23);",
NULL, NULL, NULL) == SQLITE_OK; NULL, NULL, NULL) == SQLITE_OK;
return sqlite3_close(connection) == SQLITE_OK && ok; return sqlite3_close(connection) == SQLITE_OK && ok;
} }
@ -887,7 +887,7 @@ static bool run_test(void) {
char v10_path[512], failed_v11_path[512]; char v10_path[512], failed_v11_path[512];
char v13_path[512], true_v14_path[512], failed_v14_path[512], char v13_path[512], true_v14_path[512], failed_v14_path[512],
failed_v15_path[512], true_v15_path[512], v17_path[512], failed_v15_path[512], true_v15_path[512], v17_path[512],
failed_v18_path[512], failed_v19_path[512]; failed_v18_path[512], failed_v19_path[512], raw_missing_table_path[512];
CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db",
directory) > 0); directory) > 0);
CHECK(snprintf(artifact_path, sizeof(artifact_path), "%s/artifact.bin", CHECK(snprintf(artifact_path, sizeof(artifact_path), "%s/artifact.bin",
@ -935,12 +935,14 @@ static bool run_test(void) {
"%s/failed-v18.db", directory) > 0); "%s/failed-v18.db", directory) > 0);
CHECK(snprintf(failed_v19_path, sizeof(failed_v19_path), CHECK(snprintf(failed_v19_path, sizeof(failed_v19_path),
"%s/failed-v19.db", directory) > 0); "%s/failed-v19.db", directory) > 0);
CHECK(snprintf(raw_missing_table_path, sizeof(raw_missing_table_path),
"%s/raw-missing-table.db", directory) > 0);
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
Lardon3DProjectDb *database = NULL; Lardon3DProjectDb *database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) == CHECK(lardon3d_project_db_open(database_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(database && lardon3d_project_db_schema_version(database) == 21); CHECK(database && lardon3d_project_db_schema_version(database) == 22);
bool legacy_pending = true; bool legacy_pending = true;
CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) == CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
@ -1499,7 +1501,7 @@ static bool run_test(void) {
database = NULL; database = NULL;
CHECK(query_integer(database_path, CHECK(query_integer(database_path,
"SELECT value FROM metadata WHERE key='schema_version'", "SELECT value FROM metadata WHERE key='schema_version'",
21)); 22));
CHECK(query_integer(database_path, CHECK(query_integer(database_path,
"SELECT count(*) FROM tasks WHERE task_id=1", 1)); "SELECT count(*) FROM tasks WHERE task_id=1", 1));
@ -1521,13 +1523,15 @@ static bool run_test(void) {
"created_at) VALUES(1,1,1,1,1,zeroblob(32),1,2,1,0,0,1);" "created_at) VALUES(1,1,1,1,1,zeroblob(32),1,2,1,0,0,1);"
"DROP TABLE incremental_reconstruction_tasks;" "DROP TABLE incremental_reconstruction_tasks;"
"DROP TABLE incremental_reconstructions;" "DROP TABLE incremental_reconstructions;"
"DROP TABLE selected_execution_items;DROP TABLE selected_executions;"
"DROP TABLE photo_quality_triage_results;DROP TABLE photo_quality_triage_tasks;"
"DROP TABLE acquisition_campaign_captures;DROP TABLE acquisition_campaign_tasks;" "DROP TABLE acquisition_campaign_captures;DROP TABLE acquisition_campaign_tasks;"
"DROP TABLE asset_derivations;DROP TABLE capture_selections;DROP TABLE capture_assets;" "DROP TABLE asset_derivations;DROP TABLE capture_selections;DROP TABLE capture_assets;"
"DROP TABLE capture_images;DROP TABLE captures;" "DROP TABLE capture_images;DROP TABLE captures;"
"UPDATE metadata SET value=17 WHERE key='schema_version';")); "UPDATE metadata SET value=17 WHERE key='schema_version';"));
CHECK(lardon3d_project_db_open(v17_path, &database, error) == CHECK(lardon3d_project_db_open(v17_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 21); CHECK(lardon3d_project_db_schema_version(database) == 22);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer(v17_path, CHECK(query_integer(v17_path,
@ -1551,6 +1555,8 @@ static bool run_test(void) {
database = NULL; database = NULL;
CHECK(execute_test_sql( CHECK(execute_test_sql(
failed_v19_path, failed_v19_path,
"DROP TABLE selected_execution_items;DROP TABLE selected_executions;"
"DROP TABLE photo_quality_triage_results;DROP TABLE photo_quality_triage_tasks;"
"DROP TABLE acquisition_campaign_captures;DROP TABLE acquisition_campaign_tasks;" "DROP TABLE acquisition_campaign_captures;DROP TABLE acquisition_campaign_tasks;"
"DROP TABLE asset_derivations;DROP TABLE capture_selections;DROP TABLE capture_assets;" "DROP TABLE asset_derivations;DROP TABLE capture_selections;DROP TABLE capture_assets;"
"DROP TABLE capture_images;DROP TABLE captures;" "DROP TABLE capture_images;DROP TABLE captures;"
@ -1566,13 +1572,15 @@ static bool run_test(void) {
"name='captures'", 0)); "name='captures'", 0));
CHECK(lardon3d_project_db_open(failed_v19_path, &database, error) == CHECK(lardon3d_project_db_open(failed_v19_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 21); CHECK(lardon3d_project_db_schema_version(database) == 22);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(execute_test_sql( CHECK(execute_test_sql(
failed_v18_path, failed_v18_path,
"DROP TABLE incremental_reconstruction_tasks;" "DROP TABLE incremental_reconstruction_tasks;"
"DROP TABLE incremental_reconstructions;" "DROP TABLE incremental_reconstructions;"
"DROP TABLE selected_execution_items;DROP TABLE selected_executions;"
"DROP TABLE photo_quality_triage_results;DROP TABLE photo_quality_triage_tasks;"
"DROP TABLE acquisition_campaign_captures;DROP TABLE acquisition_campaign_tasks;" "DROP TABLE acquisition_campaign_captures;DROP TABLE acquisition_campaign_tasks;"
"DROP TABLE asset_derivations;DROP TABLE capture_selections;DROP TABLE capture_assets;" "DROP TABLE asset_derivations;DROP TABLE capture_selections;DROP TABLE capture_assets;"
"DROP TABLE capture_images;DROP TABLE captures;" "DROP TABLE capture_images;DROP TABLE captures;"
@ -1618,7 +1626,7 @@ static bool run_test(void) {
CHECK(create_v1_database(legacy_path)); CHECK(create_v1_database(legacy_path));
CHECK(lardon3d_project_db_open(legacy_path, &database, error) == CHECK(lardon3d_project_db_open(legacy_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 21); CHECK(lardon3d_project_db_schema_version(database) == 22);
CHECK(lardon3d_project_db_get_project(database, &loaded_project) == CHECK(lardon3d_project_db_get_project(database, &loaded_project) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
strcmp(loaded_project.stable_id, "legacy-project") == 0); strcmp(loaded_project.stable_id, "legacy-project") == 0);
@ -1633,7 +1641,17 @@ static bool run_test(void) {
database = NULL; database = NULL;
CHECK(query_integer(legacy_path, CHECK(query_integer(legacy_path,
"SELECT value FROM metadata WHERE key='schema_version'", "SELECT value FROM metadata WHERE key='schema_version'",
21)); 22));
CHECK(create_v1_database(raw_missing_table_path));
CHECK(lardon3d_project_db_open(raw_missing_table_path, &database, error) ==
LARDON3D_PROJECT_DB_OK);
lardon3d_project_db_close(database);
database = NULL;
CHECK(execute_test_sql(raw_missing_table_path,
"DROP TABLE raw_development_tasks;"));
CHECK(lardon3d_project_db_open(raw_missing_table_path, &database, error) ==
LARDON3D_PROJECT_DB_CORRUPT);
CHECK(database == NULL);
CHECK(create_v1_database(failed_migration_path)); CHECK(create_v1_database(failed_migration_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0);
@ -1664,7 +1682,7 @@ static bool run_test(void) {
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer( CHECK(query_integer(
v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 21)); v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 22));
CHECK(create_v2_database(failed_v3_migration_path)); CHECK(create_v2_database(failed_v3_migration_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0);
@ -1698,7 +1716,7 @@ static bool run_test(void) {
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer( CHECK(query_integer(
v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 21)); v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 22));
CHECK(create_v3_database(failed_v4_path)); CHECK(create_v3_database(failed_v4_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0);
@ -1720,7 +1738,7 @@ static bool run_test(void) {
fprintf(stderr, "Migration v4 (%d): %s\n", (int)v4_result, error); fprintf(stderr, "Migration v4 (%d): %s\n", (int)v4_result, error);
} }
CHECK(v4_result == LARDON3D_PROJECT_DB_OK); CHECK(v4_result == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 21); CHECK(lardon3d_project_db_schema_version(database) == 22);
CHECK(lardon3d_project_db_load_task(database, 9, &task) == CHECK(lardon3d_project_db_load_task(database, 9, &task) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact",
@ -1775,7 +1793,7 @@ static bool run_test(void) {
error); error);
} }
CHECK(retry_v7 == LARDON3D_PROJECT_DB_OK && CHECK(retry_v7 == LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 21); lardon3d_project_db_schema_version(database) == 22);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
@ -1785,7 +1803,7 @@ static bool run_test(void) {
5)); 5));
CHECK(lardon3d_project_db_open(direct_v5_path, &database, error) == CHECK(lardon3d_project_db_open(direct_v5_path, &database, error) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 21); lardon3d_project_db_schema_version(database) == 22);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
@ -1794,11 +1812,11 @@ static bool run_test(void) {
v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 7)); v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 7));
CHECK(lardon3d_project_db_open(v8_path, &database, error) == CHECK(lardon3d_project_db_open(v8_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 21); CHECK(lardon3d_project_db_schema_version(database) == 22);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer( CHECK(query_integer(
v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 21)); v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 22));
CHECK(create_v7_database(failed_v8_path)); CHECK(create_v7_database(failed_v8_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0);
@ -1815,7 +1833,7 @@ static bool run_test(void) {
0)); 0));
CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) == CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 21); lardon3d_project_db_schema_version(database) == 22);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
@ -1834,11 +1852,11 @@ static bool run_test(void) {
0)); 0));
CHECK(lardon3d_project_db_open(v10_path, &database, error) == CHECK(lardon3d_project_db_open(v10_path, &database, error) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 21); lardon3d_project_db_schema_version(database) == 22);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer( CHECK(query_integer(
v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 21)); v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 22));
CHECK( CHECK(
query_integer(v10_path, query_integer(v10_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND " "SELECT count(*) FROM sqlite_master WHERE type='table' AND "
@ -1865,7 +1883,7 @@ static bool run_test(void) {
1)); 1));
CHECK(lardon3d_project_db_open(failed_v11_path, &database, error) == CHECK(lardon3d_project_db_open(failed_v11_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 21); CHECK(lardon3d_project_db_schema_version(database) == 22);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
@ -1879,11 +1897,11 @@ static bool run_test(void) {
0)); 0));
CHECK(lardon3d_project_db_open(v13_path, &database, error) == CHECK(lardon3d_project_db_open(v13_path, &database, error) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 21); lardon3d_project_db_schema_version(database) == 22);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer( CHECK(query_integer(
v13_path, "SELECT value FROM metadata WHERE key='schema_version'", 21)); v13_path, "SELECT value FROM metadata WHERE key='schema_version'", 22));
CHECK( CHECK(
query_integer(v13_path, query_integer(v13_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND " "SELECT count(*) FROM sqlite_master WHERE type='table' AND "
@ -1906,7 +1924,7 @@ static bool run_test(void) {
true_v14_path); true_v14_path);
} }
CHECK(true_v14_result == LARDON3D_PROJECT_DB_OK && CHECK(true_v14_result == LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 21); lardon3d_project_db_schema_version(database) == 22);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK( CHECK(
@ -2021,7 +2039,7 @@ static bool run_test(void) {
if (true_v15_result != LARDON3D_PROJECT_DB_OK) if (true_v15_result != LARDON3D_PROJECT_DB_OK)
fprintf(stderr, "true v15 upgrade: %d %s\n", true_v15_result, error); fprintf(stderr, "true v15 upgrade: %d %s\n", true_v15_result, error);
CHECK(true_v15_result == LARDON3D_PROJECT_DB_OK); CHECK(true_v15_result == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 21); CHECK(lardon3d_project_db_schema_version(database) == 22);
Lardon3DProjectDbProject migrated_project; Lardon3DProjectDbProject migrated_project;
CHECK(lardon3d_project_db_get_project(database, &migrated_project) == CHECK(lardon3d_project_db_get_project(database, &migrated_project) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
@ -2067,7 +2085,7 @@ static bool run_test(void) {
database = NULL; database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) == CHECK(lardon3d_project_db_open(database_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 21); CHECK(lardon3d_project_db_schema_version(database) == 22);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(schema_compare(database_path, true_v15_path, true)); CHECK(schema_compare(database_path, true_v15_path, true));
@ -2102,7 +2120,7 @@ static bool run_test(void) {
0)); 0));
CHECK(lardon3d_project_db_open(true_v15_path, &database, error) == CHECK(lardon3d_project_db_open(true_v15_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 21); CHECK(lardon3d_project_db_schema_version(database) == 22);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
@ -2121,7 +2139,7 @@ static bool run_test(void) {
0)); 0));
CHECK(lardon3d_project_db_open(failed_v14_path, &database, error) == CHECK(lardon3d_project_db_open(failed_v14_path, &database, error) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 21); lardon3d_project_db_schema_version(database) == 22);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
@ -2140,7 +2158,7 @@ static bool run_test(void) {
0)); 0));
CHECK(lardon3d_project_db_open(failed_v15_path, &database, error) == CHECK(lardon3d_project_db_open(failed_v15_path, &database, error) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 21); lardon3d_project_db_schema_version(database) == 22);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
@ -2612,6 +2630,7 @@ static bool run_test(void) {
CHECK(unlink(v17_path) == 0); CHECK(unlink(v17_path) == 0);
CHECK(unlink(failed_v18_path) == 0); CHECK(unlink(failed_v18_path) == 0);
CHECK(unlink(failed_v19_path) == 0); CHECK(unlink(failed_v19_path) == 0);
CHECK(unlink(raw_missing_table_path) == 0);
CHECK(rmdir(directory) == 0); CHECK(rmdir(directory) == 0);
return true; return true;
} }

View file

@ -283,6 +283,18 @@ static bool run_test() {
&state, capture.capture_id, corrupt_path, 0, 1, &output); &state, capture.capture_id, corrupt_path, 0, 1, &output);
CHECK(corrupt == LARDON3D_RAW_DEVELOPMENT_UNSUPPORTED_RAW CHECK(corrupt == LARDON3D_RAW_DEVELOPMENT_UNSUPPORTED_RAW
|| corrupt == LARDON3D_RAW_DEVELOPMENT_CORRUPT_RAW); || corrupt == LARDON3D_RAW_DEVELOPMENT_CORRUPT_RAW);
CHECK(output.source_asset.asset_id != 0);
const uint64_t corrupt_asset_id = output.source_asset.asset_id;
CHECK(lardon3d_raw_develop_asset_to_capture(
&state, capture.capture_id, corrupt_asset_id, 0, 1, &output) ==
LARDON3D_RAW_DEVELOPMENT_CONSTRAINT);
CHECK(lardon3d_project_db_record_capture_source_asset(
db, capture.capture_id, corrupt_asset_id,
LARDON3D_DB_CAPTURE_SOURCE_RAW) == LARDON3D_PROJECT_DB_OK);
Lardon3DRawDevelopmentResult explicit_corrupt = lardon3d_raw_develop_asset_to_capture(
&state, capture.capture_id, corrupt_asset_id, 0, 1, &output);
CHECK(explicit_corrupt == LARDON3D_RAW_DEVELOPMENT_UNSUPPORTED_RAW ||
explicit_corrupt == LARDON3D_RAW_DEVELOPMENT_CORRUPT_RAW);
uint64_t selected = 0; uint64_t selected = 0;
CHECK(lardon3d_project_db_get_selected_capture_image(db, capture.capture_id, &selected) CHECK(lardon3d_project_db_get_selected_capture_image(db, capture.capture_id, &selected)
== LARDON3D_PROJECT_DB_NOT_FOUND); == LARDON3D_PROJECT_DB_NOT_FOUND);

View file

@ -0,0 +1,196 @@
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <string>
#include <fcntl.h>
#include <sqlite3.h>
#include <unistd.h>
extern "C" {
#include <lardon3d/image_catalog.h>
#include <lardon3d/project.h>
#include <lardon3d/raw_development_task.h>
#include <lardon3d/resource_governor.h>
}
extern "C" const Lardon3DTaskKindRegistry *lardon3d_task_kind_registry_production(void) {
static const Lardon3DTaskKindRegistry registry{};
return &registry;
}
#define CHECK(condition) \
do { \
if (!(condition)) { \
std::fprintf(stderr, "Failure line %d: %s\n", __LINE__, #condition); \
return 1; \
} \
} while (0)
static bool write_file(const std::string &path, const char *bytes) {
int descriptor = open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0600);
if (descriptor < 0) return false;
size_t size = std::strlen(bytes);
bool ok = write(descriptor, bytes, size) == static_cast<ssize_t>(size);
return close(descriptor) == 0 && ok;
}
int main() {
char directory_template[] = "/tmp/lardon3d-raw-task-XXXXXX";
char *directory = mkdtemp(directory_template);
CHECK(directory != nullptr);
std::string root(directory);
std::string database_path = root + "/project.lardon3d";
CHECK(std::filesystem::create_directories(root + "/.lardon3d/checkpoints"));
CHECK(std::filesystem::create_directories(root + "/assets/images"));
std::string raw_path = root + "/source.arw";
std::string png_path = root + "/derived.png";
CHECK(write_file(raw_path, "explicit raw bytes"));
CHECK(write_file(png_path, "derived png bytes"));
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{};
Lardon3DProjectDb *database = nullptr;
CHECK(lardon3d_project_db_open(database_path.c_str(), &database, error) ==
LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbScanSet scanset{};
Lardon3DProjectDbCapture capture{};
CHECK(lardon3d_project_db_create_scanset(database, "raw-task", &scanset) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_create_capture(database, scanset.scanset_id, 7, &capture) ==
LARDON3D_PROJECT_DB_OK);
Lardon3DAppState state{};
lardon3d_app_state_init(&state);
state.project_loaded = true;
state.project_db = database;
std::snprintf(state.project_path, sizeof(state.project_path), "%s", root.c_str());
state.hardware_profile.logical_cpu_count = 8;
state.hardware_profile.page_size_bytes = 4096;
state.hardware_profile.memory_total_bytes = UINT64_C(8) << 30;
std::snprintf(state.hardware_profile.cpu_architecture,
sizeof(state.hardware_profile.cpu_architecture), "test");
Lardon3DResourcePolicy policy{};
policy.maximum_cpu_load_ratio = 1.0;
policy.maximum_io_pressure_avg10 = 100.0;
policy.io_slot_capacity = 1;
state.resource_governor = lardon3d_resource_governor_create(&state.hardware_profile, &policy);
CHECK(state.resource_governor != nullptr);
Lardon3DProjectDbImageAsset raw_asset{};
Lardon3DProjectDbImageAsset derived_asset{};
CHECK(lardon3d_image_catalog_publish_asset_file(
&state, raw_path.c_str(), 7, UINT64_MAX, &raw_asset) ==
LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED);
CHECK(lardon3d_project_db_record_capture_source_asset(
database, capture.capture_id, raw_asset.asset_id,
LARDON3D_DB_CAPTURE_SOURCE_RAW) == LARDON3D_PROJECT_DB_OK);
uint64_t task_id = 0;
Lardon3DTask *task = lardon3d_project_create_raw_development_task(
&state, capture.capture_id, raw_asset.asset_id, &task_id);
CHECK(task != nullptr && task_id != 0);
Lardon3DProjectDbTask generic{};
Lardon3DProjectDbRawDevelopmentTask typed{};
CHECK(lardon3d_project_db_load_task(database, task_id, &generic) == LARDON3D_PROJECT_DB_OK);
CHECK(generic.has_checkpoint && generic.has_task_kind &&
std::strcmp(generic.task_kind, LARDON3D_RAW_DEVELOPMENT_TASK_KIND) == 0);
CHECK(lardon3d_project_db_load_raw_development_task(database, task_id, &typed) ==
LARDON3D_PROJECT_DB_OK);
CHECK(typed.capture_id == capture.capture_id && typed.source_asset_id == raw_asset.asset_id &&
typed.phase == LARDON3D_RAW_DEVELOPMENT_TASK_PENDING && !typed.has_image);
Lardon3DTaskDurableSnapshot snapshot{};
CHECK(lardon3d_task_durable_snapshot(task, &snapshot));
Lardon3DTaskReconstructionContext reconstruction{
root.c_str(), database, state.resource_governor, nullptr};
Lardon3DTaskKindBinding binding{};
CHECK(lardon3d_raw_development_task_reconstruct(&snapshot, &reconstruction, &binding));
CHECK(binding.callback != nullptr && binding.userdata != nullptr &&
binding.userdata_destroy != nullptr && binding.finished_callback != nullptr);
binding.userdata_destroy(binding.userdata);
CHECK(lardon3d_image_catalog_publish_asset_file(
&state, png_path.c_str(), 7, UINT64_MAX, &derived_asset) ==
LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED);
Lardon3DProjectDbAssetDerivation derivation{};
derivation.parent_asset_id = raw_asset.asset_id;
derivation.child_asset_id = derived_asset.asset_id;
derivation.kind = LARDON3D_DB_ASSET_DERIVATION_GENERIC_VERSIONED;
derivation.version = 1;
derivation.has_producer_task = true;
derivation.producer_task_id = task_id;
derivation.created_at = 7;
CHECK(lardon3d_project_db_record_asset_derivation(database, &derivation) ==
LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbImageRegisterStatus image_status{};
Lardon3DProjectDbImage image{};
CHECK(lardon3d_project_db_publish_derived_capture_image(
database, capture.capture_id, derived_asset.asset_id, "derived.png", png_path.c_str(),
task_id, 7, &image_status, &image) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_task_set_progress(task, 100, "published"));
typed.phase = LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED;
typed.has_image = true;
typed.image_id = image.image_id;
CHECK(lardon3d_project_checkpoint_raw_development_task(&state, task, &typed) ==
LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
Lardon3DProjectDbRawDevelopmentTask published{};
CHECK(lardon3d_project_db_load_raw_development_task(database, task_id, &published) ==
LARDON3D_PROJECT_DB_OK);
CHECK(published.phase == LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED &&
published.image_id == image.image_id);
Lardon3DProjectDbRawDevelopmentTask conflict = published;
conflict.source_asset_id = derived_asset.asset_id;
CHECK(lardon3d_project_checkpoint_raw_development_task(&state, task, &conflict) ==
LARDON3D_PROJECT_TASK_CHECKPOINT_DB_ERROR);
CHECK(lardon3d_project_db_load_raw_development_task(database, task_id, &published) ==
LARDON3D_PROJECT_DB_OK &&
published.source_asset_id == raw_asset.asset_id && published.image_id == image.image_id);
CHECK(lardon3d_project_db_record_raw_development_task(
database, &snapshot, "test.work", 1, nullptr, &typed,
7) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
CHECK(lardon3d_project_db_record_raw_development_task(
database, &snapshot, LARDON3D_RAW_DEVELOPMENT_TASK_KIND, 2, nullptr, &typed,
7) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
sqlite3 *raw_database = nullptr;
char update_mismatch_kind[128];
std::snprintf(update_mismatch_kind, sizeof(update_mismatch_kind),
"UPDATE tasks SET task_kind='test.work',task_kind_version=1 WHERE task_id=%llu",
(unsigned long long)task_id);
CHECK(sqlite3_open(database_path.c_str(), &raw_database) == SQLITE_OK);
CHECK(sqlite3_exec(raw_database, update_mismatch_kind, nullptr, nullptr,
nullptr) == SQLITE_OK);
CHECK(lardon3d_project_db_load_raw_development_task(database, task_id, &published) ==
LARDON3D_PROJECT_DB_CORRUPT);
char update_recoverable_kind[128];
std::snprintf(update_recoverable_kind, sizeof(update_recoverable_kind),
"UPDATE tasks SET task_kind='raw.develop',task_kind_version=1 "
"WHERE task_id=%llu",
(unsigned long long)task_id);
CHECK(sqlite3_exec(raw_database, update_recoverable_kind, nullptr, nullptr,
nullptr) == SQLITE_OK);
CHECK(sqlite3_close(raw_database) == SQLITE_OK);
lardon3d_task_destroy(task);
lardon3d_resource_governor_destroy(state.resource_governor);
lardon3d_project_db_close(database);
/* This fixture bypasses schema CHECK enforcement deliberately to prove that
* recovery treats malformed durable phase as corruption, not as pending. */
CHECK(sqlite3_open(database_path.c_str(), &raw_database) == SQLITE_OK);
CHECK(sqlite3_exec(raw_database, "PRAGMA ignore_check_constraints=ON", nullptr, nullptr,
nullptr) == SQLITE_OK);
CHECK(sqlite3_exec(raw_database, "UPDATE raw_development_tasks SET phase=9", nullptr, nullptr,
nullptr) == SQLITE_OK);
CHECK(sqlite3_close(raw_database) == SQLITE_OK);
database = nullptr;
CHECK(lardon3d_project_db_open(database_path.c_str(), &database, error) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_load_raw_development_task(database, task_id, &published) ==
LARDON3D_PROJECT_DB_CORRUPT);
lardon3d_project_db_close(database);
CHECK(std::filesystem::remove_all(root) > 0);
return 0;
}

View file

@ -0,0 +1,171 @@
#include <sqlite3.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <lardon3d/project_db.h>
#define CHECK(condition) do { if (!(condition)) { \
fprintf(stderr, "selected execution failure at line %d: %s\n", __LINE__, #condition); \
return false; } } while (0)
static bool sql(const char *path, const char *text) {
sqlite3 *connection = NULL;
if (sqlite3_open(path, &connection) != SQLITE_OK) return false;
int code = sqlite3_exec(connection, text, NULL, NULL, NULL);
return sqlite3_close(connection) == SQLITE_OK && code == SQLITE_OK;
}
static bool seed_v21(const char *path) {
Lardon3DProjectDb *database = NULL;
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK);
lardon3d_project_db_close(database);
/* The two group namespaces intentionally use different values. This fixture
proves that v22 retains the explicit bridge rather than equating IDs. */
return sql(path,
"INSERT INTO scansets(scanset_id,name,created_at,updated_at) VALUES(1,'scope',1,1);"
"INSERT INTO tasks VALUES(1,'quality','photo_quality.triage',1,5,5,100,1,0,0,0,0,1);"
"INSERT INTO tasks VALUES(2,'campaign','acquisition_campaign.run',1,5,5,100,1,0,0,0,0,1);"
"INSERT INTO photo_quality_triage_tasks VALUES(1,1,2,1,X'01');"
"INSERT INTO photo_quality_triage_results VALUES(1,1,0,1,0,1,0,100,100,100,100,"
"1.0,1.0,0.0,0.0,1.0,1.0,0.0,'GOOD');"
"INSERT INTO acquisition_campaign_tasks VALUES(2,1,2,2,X'02');"
"INSERT INTO captures(capture_id,scanset_id,created_at) VALUES(1,1,1);"
"INSERT INTO acquisition_campaign_captures VALUES(2,2,1);"
"INSERT INTO image_assets(asset_id,sha256,path,size_bytes,state,created_at) "
"VALUES(1,zeroblob(32),'assets/images/00/test',1,1,1);"
"INSERT INTO image_assets(asset_id,sha256,path,size_bytes,state,created_at) "
"VALUES(2,randomblob(32),'assets/images/00/raw',1,1,1);"
"INSERT INTO capture_assets(capture_id,asset_id,role) VALUES(1,2,1);"
"INSERT INTO capture_source_assets(capture_id,asset_id,source_kind) VALUES(1,2,2);"
"INSERT INTO images(image_id,scanset_id,asset_id,original_name,source_path,imported_at) "
"VALUES(1,1,1,'selected.jpg','/source/selected.jpg',1);"
"INSERT INTO capture_images VALUES(1,1);"
"INSERT INTO sparse_calibrations VALUES(1,randomblob(32),1,1,100,100,50,50,50,50,0,0,0,0,"
"2,randomblob(32));"
"INSERT INTO sparse_calibration_scopes VALUES(1,randomblob(32),1);"
"INSERT INTO sparse_calibration_scope_images VALUES(1,1,1);"
"DROP TABLE selected_execution_items;DROP TABLE selected_executions;"
"UPDATE metadata SET value=21 WHERE key='schema_version';");
}
static bool run(void) {
char directory[] = "/tmp/lardon3d-selected-XXXXXX";
CHECK(mkdtemp(directory) != NULL);
char path[512];
CHECK(snprintf(path, sizeof(path), "%s/project.db", directory) > 0);
CHECK(seed_v21(path));
Lardon3DProjectDb *database = NULL;
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V22", "1", 1) == 0);
Lardon3DProjectDbResult migration_result = lardon3d_project_db_open(path, &database, error);
CHECK(migration_result != LARDON3D_PROJECT_DB_OK);
CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V22") == 0);
CHECK(sql(path, "CREATE TEMP TABLE migration_check(value INTEGER);"));
sqlite3 *raw = NULL;
CHECK(sqlite3_open(path, &raw) == SQLITE_OK);
sqlite3_stmt *query = NULL;
CHECK(sqlite3_prepare_v2(raw, "SELECT value FROM metadata WHERE key='schema_version'", -1,
&query, NULL) == SQLITE_OK);
CHECK(sqlite3_step(query) == SQLITE_ROW && sqlite3_column_int(query, 0) == 21);
sqlite3_finalize(query);
CHECK(sqlite3_close(raw) == SQLITE_OK);
migration_result = lardon3d_project_db_open(path, &database, error);
if (migration_result != LARDON3D_PROJECT_DB_OK)
fprintf(stderr, "retry migration result=%d error=%s\n", (int)migration_result, error);
CHECK(migration_result == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 22);
Lardon3DProjectDbSelectedExecutionItem item = {
.item_index = 0,
.quality_group_id = 1,
.campaign_group_id = 2,
.capture_id = 1,
.representation_source = LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET,
.source_asset_id = 2};
Lardon3DProjectDbSelectedExecution execution;
Lardon3DProjectDbSelectedExecutionItem unattached = item;
unattached.source_asset_id = 1;
CHECK(lardon3d_project_db_create_selected_execution(database, 1, 2, &unattached, 1, 10,
&execution) ==
LARDON3D_PROJECT_DB_CONSTRAINT);
CHECK(lardon3d_project_db_create_selected_execution(database, 1, 2, &item, 1, 10,
&execution) == LARDON3D_PROJECT_DB_OK);
CHECK(execution.execution_id > 0 && execution.stage ==
LARDON3D_SELECTED_EXECUTION_REPRESENTATIONS && execution.next_item_index == 0);
uint64_t execution_id = execution.execution_id;
CHECK(lardon3d_project_db_create_selected_execution(database, 1, 2, &item, 1, 10,
&execution) == LARDON3D_PROJECT_DB_OK);
CHECK(execution.execution_id == execution_id);
Lardon3DProjectDbSelectedExecutionItem conflict = item;
conflict.representation_source = LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE;
conflict.source_asset_id = 0;
CHECK(lardon3d_project_db_create_selected_execution(database, 1, 2, &conflict, 1, 10,
&execution) ==
LARDON3D_PROJECT_DB_CONSTRAINT);
CHECK(lardon3d_project_db_record_selected_representation(database, execution_id, 0, 1, 1) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_record_selected_representation(database, execution_id, 0, 1, 1) ==
LARDON3D_PROJECT_DB_OK);
lardon3d_project_db_close(database);
database = NULL;
CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_load_selected_execution(database, execution_id, &execution) ==
LARDON3D_PROJECT_DB_OK);
CHECK(execution.stage == LARDON3D_SELECTED_EXECUTION_CALIBRATION &&
execution.next_item_index == 1 && !execution.has_calibration_scope);
Lardon3DProjectDbSelectedExecutionItem loaded;
CHECK(lardon3d_project_db_load_selected_execution_item(database, execution_id, 0, &loaded) ==
LARDON3D_PROJECT_DB_OK);
CHECK(loaded.quality_group_id == 1 && loaded.campaign_group_id == 2 &&
loaded.capture_id == 1 &&
loaded.representation_source == LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET &&
loaded.source_asset_id == 2 && loaded.has_image && loaded.image_id == 1);
CHECK(lardon3d_project_db_assign_selected_calibration_scope(database, execution_id, 1) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_assign_selected_calibration_scope(database, execution_id, 1) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_load_selected_execution(database, execution_id, &execution) ==
LARDON3D_PROJECT_DB_OK && execution.stage == LARDON3D_SELECTED_EXECUTION_READY &&
execution.has_calibration_scope && execution.calibration_scope_id == 1);
CHECK(lardon3d_project_db_create_selected_execution(database, 1, 2, &item, 1, 10,
&execution) == LARDON3D_PROJECT_DB_OK &&
execution.execution_id == execution_id &&
execution.stage == LARDON3D_SELECTED_EXECUTION_READY);
lardon3d_project_db_close(database);
database = NULL;
/* CHECK constraints are bypassed deliberately to verify wide-value and
stage/cursor corruption rejection at the public read boundary. */
CHECK(sqlite3_open(path, &raw) == SQLITE_OK);
CHECK(sqlite3_exec(raw, "PRAGMA ignore_check_constraints=ON;"
"UPDATE selected_execution_items SET representation_source=1 "
"WHERE execution_id=1;", NULL, NULL, NULL) == SQLITE_OK);
CHECK(sqlite3_close(raw) == SQLITE_OK);
CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_load_selected_execution_item(database, execution_id, 0, &loaded) ==
LARDON3D_PROJECT_DB_CORRUPT);
lardon3d_project_db_close(database);
CHECK(sql(path, "UPDATE selected_execution_items SET representation_source=2 "
"WHERE execution_id=1;"));
CHECK(sqlite3_open(path, &raw) == SQLITE_OK);
CHECK(sqlite3_exec(raw, "PRAGMA ignore_check_constraints=ON;"
"UPDATE selected_executions SET next_item_index=4294967296 "
"WHERE execution_id=1;", NULL, NULL, NULL) == SQLITE_OK);
CHECK(sqlite3_close(raw) == SQLITE_OK);
CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_load_selected_execution(database, execution_id, &execution) ==
LARDON3D_PROJECT_DB_CORRUPT);
lardon3d_project_db_close(database);
CHECK(unlink(path) == 0);
CHECK(rmdir(directory) == 0);
return true;
}
int main(void) { return run() ? EXIT_SUCCESS : EXIT_FAILURE; }