Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@

A pure Swift 6.2 implementation of JPEG 2000 (ISO/IEC 15444) encoding and decoding with strict concurrency support.

**Current Version**: 10.15.0
**Status**: Apple Silicon-first JPEG 2000 / HTJ2K (Part-15) implementation. v10.15.0 adds **`JP3DDecoder.preWarm()` convenience API** — a thin wrapper around the existing `J2KDecoder.preWarm()` (v5.28.0) that's discoverable from the JP3D module surface. Closes a discoverability gap for consumers using `J2K3D` alone (no need to import `J2KCodec` separately). M2 release mr_3d_small 128×128×16: first-decode-in-process **28.26 ms → 14.60 ms** = **−13.93 ms cold-start savings** when called at app startup. MINOR / additive API — no perf change on warm paths, codestream bytes byte-identical to v10.14.0.
**Previous Release**: 10.14.0 (JP3D parallel per-slice encode)
**Current Version**: 10.16.0
**Status**: Apple Silicon-first JPEG 2000 / HTJ2K (Part-15) implementation. v10.16.0 adds **`JP3DDecoder` partial-decode convenience overloads** — `decode(_:resolutionLevel:)`, `decode(_:region:)`, and `decode(_:region:resolutionLevel:)` — surfacing the v10.18-research partial-resolution pipeline + v10.13.0 K+ROI composition behind the canonical `JP3DDecoder` type. Consumers no longer have to know to construct `JP3DDecoderConfiguration(resolutionLevel:)` or switch to `JP3DROIDecoder`: the same speedups are now reachable from `JP3DDecoder` itself — **2.2-3.1× faster** for level-1 thumbnails, **3.4-4.1× faster** for ~1/4-extent ROI decodes (v10.18-research bench). MINOR / additive API — no perf change on existing paths, codestream bytes byte-identical to v10.15.0.
**Previous Release**: 10.15.0 (JP3DDecoder.preWarm discoverable convenience API)
**Release process**: see [RELEASING.md](RELEASING.md). Every release MUST update this README (Current Version line + new Release Status paragraph) — see the Release artefacts checklist for the full requirements.

## 📦 Release Status

**v10.16.0** adds three **convenience overloads on `JP3DDecoder`** that surface the JP3D partial-decode capabilities behind the canonical type. The underlying pipelines have been production-default since v10.10.0 (true partial-resolution from v10.18-research, K+ROI composition from v10.13.0), but the only way to reach them from `import J2K3D` was to either construct `JP3DDecoderConfiguration(resolutionLevel: K)` (discoverable only by reading docs) or switch to `JP3DROIDecoder` (a separate actor type). v10.16.0 closes the gap with `decode(_:resolutionLevel:)`, `decode(_:region:)`, and `decode(_:region:resolutionLevel:)` overloads — all thin wrappers that construct a transient inner decoder/ROI-decoder with the requested options and delegate. The actor's `configuration` (`tolerateErrors`, `maxQualityLayers`) propagates to the transient inner instances; negative levels are clamped to 0; level 0 is bit-exact equivalent to `decode(_:)`. **Speedups surfaced (existing pipelines, v10.18-research bench)**: level-1 thumbnail decode **2.2-3.1× faster** than full decode on small-mid JP3D fixtures (mr_3d_small 2.22×, ct_3d_small 3.05×, mr_3d_mid 3.01×); ROI ~1/4-extent decode **3.4-4.1× faster** (mr_3d_small 3.37×, ct_3d_small 4.14×, mr_3d_mid 3.96×); K+ROI composition stacks both savings. Decoder-only, codestream bytes byte-identical to v10.15.0, encoder unchanged. **Validation**: `V10_28_JP3DDecoderPartialOverloadsParityTests` **6/6 PASS** (all three overloads byte-identical to existing JP3DDecoderConfiguration + JP3DROIDecoder usage; no-op level (0) bit-exact to `decode(_:)`; negative level clamped; outer-configuration propagation verified); full `swift test --filter JP3D` regression sweep **528/528 PASS** (519 pre-existing + 9 new = 2 V10_27 probe + 6 V10_28 parity + 1 pre-existing skip); mandatory commit gate 7/7 PASS. The natural symmetric ship to v10.15.0 would have been `JP3DEncoder.preWarm`; the V10_27 cold-start probe killed that candidate (leftover encoder-specific cold cost +0.10 / −1.25 ms on 128/256-cube fixtures — `JP3DDecoder.preWarm(includeWarmupDispatch: true)` already amortises encoder Metal init via `J2KMetalSession.processShared`). Also bumps the stale `getVersion()` constant `10.9.2 → 10.16.0` (drifted across 8 releases). See [RELEASE_NOTES_v10.16.0.md](RELEASE_NOTES_v10.16.0.md).

**v10.15.0** adds **`JP3DDecoder.preWarm()`** (and `JP3DROIDecoder.preWarm()`) as discoverable convenience wrappers around the existing `J2KDecoder.preWarm()` (v5.28.0). JP3D consumers inherit the one-shot Metal session init amortisation automatically (per-slice `J2KMetalSession.processShared`), but the canonical preWarm entry point lived only in `J2KCodec` — consumers using the JP3D module surface alone had to import `J2KCodec` separately. v10.15.0 closes this discoverability gap with 2-line static wrappers in `Sources/J2K3D/JP3DDecoder.swift` + `JP3DROIDecoder.swift` that delegate to `J2KDecoder.preWarm(includeWarmupDispatch:)`. Same idempotent semantics, same failure handling (silently caught on Linux where Metal is unavailable). **M2 release, mr_3d_small 128×128×16 LCG JP3D fixture**: first decode in process **28.26 ms** (cold; includes Metal init), first after `JP3DDecoder.preWarm()` **14.60 ms**, warm median **14.33 ms** — **−13.93 ms cold-start savings** that clears the 3 ms acceptance threshold. Savings are constant per process (one-shot init cost) so they're most visible on small-volume JP3D decodes; for consumers doing one-shot decodes (e.g., a DICOM viewer opening one study), this is a real-world latency win delivered through a discoverable API. No perf change on warm paths; codestream bytes byte-identical to v10.14.0; decoder-only. Validation: `V10_26_JP3DPreWarmProfile` 1/1 PASS confirms `JP3DDecoder.preWarm` wires through correctly; full `swift test --filter JP3D` regression sweep **520/520 PASS** (519 pre-existing + 1 new V10_26); mandatory commit gate 7/7 PASS. Two preceding research arcs (v10.24 + v10.25) confirmed M2 + Swift release lever-ceiling on the ROI iDWT optimization space — v10.15.0 pivots to additive discoverability and ships honestly. See [RELEASE_NOTES_v10.15.0.md](RELEASE_NOTES_v10.15.0.md).

**v10.14.0** is the first **JP3D encoder** speedup of the v10 arc. The per-slice 2D encode loop in `JP3DSliceStackCodec.encode` was sequential since shipping — each Z-slice's 2D J2K encode ran one after another even though encodes are independent across slices (Z-delta residual depends on the previous slice's INPUT bytes, not its codestream). v10.14.0 splits the loop: sequential slices 0-1 commit the M6/M7 Z-delta tile-mode decision (`tileSignedOnlyActive` / `tileTryBothActive`), and slices 2..N run in parallel via `withThrowingTaskGroup`. The per-slice body is extracted into `encodeOneSlice(...)` so both phases share the same logic (raw encode + optional signed encode + best-of-two pick). M2 release JP3D encode A/B (`J2KBenchMac --jp3d`, in-process, 7 runs / 2 warmups, median): mr_3d_small (262K vox) 11.84→7.58 ms (**1.56×**), ct_3d_small (1.05M) 33.92→26.17 ms (**1.30×**), us_3d_small (1.84M) 58.58→40.73 ms (**1.44×**), mr_3d_mid (2.10M) 71.59→65.68 ms (**1.09×**), ct_3d_mid (8.39M) 295.58→246.98 ms (**1.20×**), ct_3d_large (16.78M) 615.94→539.95 ms (**1.14×** = **−76 ms absolute on the radiologist-relevant 16M-voxel CT**). All 6/6 fixtures clear the 3 ms acceptance threshold; smallest fixture shows the largest relative speedup (1.56× — per-slice fixed cost amortises better). Sub-linear vs N=8 M2 cores because Z-delta probe is serialized + slices 0-1 sequential + memory contention on the 33 MB output. Codestream bytes byte-identical to v10.13.0 (round-trip 519/519 JP3D tests PASS). Combined with the v10.11–v10.13 decode wins: round-trip on 16M-voxel CT is ~3.5× faster vs v10.10.0. Opt-out via `J2K_JP3D_PARALLEL_ENCODE=0`. Mandatory commit gate 7/7 PASS. See [RELEASE_NOTES_v10.14.0.md](RELEASE_NOTES_v10.14.0.md) and the bench JSONs at [`Documentation/Benchmarks/data/jp3d-bench-arm64-v10_23-{parallel,serial}-encode-20260524.json`](Documentation/Benchmarks/data/).
Expand Down
216 changes: 216 additions & 0 deletions RELEASE_NOTES_v10.16.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# J2KSwift v10.16.0

**JP3DDecoder partial-decode discoverable overloads — `decode(_:resolutionLevel:)`,
`decode(_:region:)`, `decode(_:region:resolutionLevel:)`.** Closes a
discoverability gap: the v10.18-research partial-resolution pipeline (released as
production in v10.4 / v10.5) and the v10.13.0 K+ROI composition matrix have
been available since 2026-05-20, but the canonical `JP3DDecoder` exposed
only a single `decode(_:)` entry point. Consumers had to know to:

1. Construct a `JP3DDecoderConfiguration(resolutionLevel: K)` for partial
resolution; OR
2. Switch to `JP3DROIDecoder` for region-of-interest decoding; OR
3. Compose both for K+ROI sub-volume decode.

v10.16.0 surfaces the three options as discoverable convenience overloads
on `JP3DDecoder` itself. Same pipelines, same bit-exact output, same
measured wins; better ergonomics. MINOR per RELEASING.md — additive public
API only, no signature changes, no perf change on existing paths,
codestream bytes byte-identical to v10.15.0.

## Summary

After v10.15.0's `JP3DDecoder.preWarm` ship closed the preWarm
discoverability gap, the obvious next gap is partial-decode. The
underlying capability is mature: v10.4 + v10.5 added true partial-
resolution, v10.6 + v10.7 added ROI, v10.8 added the `decodePartial`
umbrella for the 2D codec, and v10.13.0 closed K+ROI composition for
JP3D. But the only way to reach these from `import J2K3D` was:

```swift
// Today — discoverable only by reading the docs
let cfg = JP3DDecoderConfiguration(resolutionLevel: 1)
let thumbnail = try await JP3DDecoder(configuration: cfg).decode(data)

let roi = try await JP3DROIDecoder().decode(data, region: someRegion)

let cfg2 = JP3DDecoderConfiguration(resolutionLevel: 1)
let combined = try await JP3DROIDecoder(configuration: cfg2).decode(data, region: someRegion)
```

v10.16.0:

```swift
let decoder = JP3DDecoder()
let thumbnail = try await decoder.decode(data, resolutionLevel: 1) // 2.2-3.1× faster
let roi = try await decoder.decode(data, region: someRegion) // 3.4-4.1× faster (1/4 extent)
let combined = try await decoder.decode(data, region: someRegion, resolutionLevel: 1)
```

The overloads are pure-additive thin wrappers — they construct a transient
inner decoder/ROI-decoder with the requested options and delegate. No
hot-path code changes; the same v10.18-research / v10.13.0 pipelines run
underneath.

## What's New — production-default

| Public API | v10.15.0 | v10.16.0 |
|---|---|---|
| `JP3DDecoder.decode(_:resolutionLevel:)` | _not present_ | **NEW** — partial-resolution decode (level K ⇒ output dims `ceil(D/2^K)`); 2.2-3.1× faster than full decode at K=1 (v10.18-research bench) |
| `JP3DDecoder.decode(_:region:)` | _not present_ | **NEW** — ROI decode, returns `JP3DROIDecoderResult`; 3.4-4.1× faster than full decode for 1/4-extent regions (v10.18-research) |
| `JP3DDecoder.decode(_:region:resolutionLevel:)` | _not present_ | **NEW** — K+ROI composition (downsampled sub-volume); closes the v10.13.0 matrix behind a single API |
| `JP3DDecoder.decode(_:)` | unchanged | unchanged |
| `JP3DDecoder.preWarm(includeWarmupDispatch:)` | unchanged | unchanged |
| `JP3DROIDecoder.decode(_:region:)` | unchanged | unchanged |

All three new overloads forward the actor's `configuration` (`tolerateErrors`,
`maxQualityLayers`) to the transient inner decoder, so existing
configuration semantics propagate cleanly. Negative `resolutionLevel`
values are clamped to `0`; level `0` is bit-exact equivalent to the
no-argument `decode(_:)`.

## Backward compatibility

- **Codestream bytes**: byte-identical to v10.15.0 on every input. Encoder
unchanged.
- **Existing decode paths**: unchanged. `JP3DDecoder.decode(_:)`,
`JP3DROIDecoder.decode(_:region:)`, the v10.4/v10.5 partial-resolution
pipeline, the v10.6/v10.7 ROI pipeline, and the v10.13.0 K+ROI composition
all run the exact same code as v10.15.0.
- **API surface**: additive only. No existing signatures changed. The new
overloads are method overloads on `JP3DDecoder` and do not collide with
the existing `decode(_:)`.
- **`getVersion()`**: bumped from the stale `10.9.2` to `10.16.0`
(the constant had drifted across 8 releases since v10.9.2; this catches
it up).

## Measured wins (surfaced through the new API)

The overloads expose existing pipelines — no new bench is needed. The
measured wins they surface are from the v10.18-research closure (which
shipped as the production pipeline in v10.4–v10.13):

| Workload | Full `decode(_:)` | New convenience | Pipeline source |
|---|---:|---:|---|
| Thumbnail (level 1) of small-mid JP3D | 1.0× | **2.2-3.1×** faster | v10.18-research P2 |
| ROI ~1/4 spatial extent on small JP3D | 1.0× | **3.4-4.1×** faster | v10.18-research P3 + P6 |
| K=1 + ROI 1/4 extent | 1.0× | combined (both savings stack) | v10.13.0 K+ROI matrix |

For batch full-volume workflows, no perf change versus v10.15.0 — these
overloads only fire when the user explicitly requests partial decode.

## Test Suite Results

| Suite | Tests | Result | Coverage |
|---|---:|---|---|
| `V10_28_JP3DDecoderPartialOverloadsParityTests` | 6/6 | PASS | All three overloads byte-identical to existing JP3DDecoderConfiguration + JP3DROIDecoder usage; no-op level (0) bit-exact to `decode(_:)`; negative level clamped; outer-configuration propagation verified |
| `swift test --filter JP3D` (full regression) | 528/528 | PASS | All 519 pre-existing JP3D tests still green + 9 new (V10_27 probe + V10_28 parity); 1 pre-existing skip |
| Mandatory commit gate (release mode) | 7/7 | PASS | `J2KMedicalCorpusEncodePerformanceTests` 2/2 + `J2KMedicalCorpusPerformanceTests` 2/2 + `J2KStrictCrossCodecValidationTests` 3/3 |

## API surface — additions only

```swift
extension JP3DDecoder {
/// v10.16.0 — partial-resolution convenience overload.
/// At level 0, identical to `decode(_:)`. At level K > 0, output dims
/// are `ceil(D / 2^K)` per spatial axis. Backed by the true-partial-
/// resolution pipeline (v10.18-research): 2.2-3.1× faster than full
/// decode for K=1 on small-mid JP3D fixtures.
public func decode(
_ data: Data,
resolutionLevel level: Int
) async throws -> JP3DDecoderResult

/// v10.16.0 — ROI convenience overload.
/// Equivalent to `JP3DROIDecoder(configuration: self.configuration).decode(data, region: region)`.
/// Tile-granular skip pipeline (v10.13.0): 3.4-4.1× faster than full
/// decode for ~1/4-extent regions.
public func decode(
_ data: Data,
region: JP3DRegion
) async throws -> JP3DROIDecoderResult

/// v10.16.0 — K+ROI convenience overload.
/// Returns the in-region sub-volume at `2^level`-down dimensions per
/// spatial axis. Closes the v10.13.0 K+ROI composition matrix behind
/// a single discoverable API.
public func decode(
_ data: Data,
region: JP3DRegion,
resolutionLevel level: Int
) async throws -> JP3DROIDecoderResult
}
```

No removals. No existing signatures changed.

## Recommended usage

```swift
import J2K3D

let decoder = JP3DDecoder()

// Full decode — unchanged
let full = try await decoder.decode(data)

// Thumbnail at 1/2 resolution (~3x faster on small-mid fixtures)
let thumbnail = try await decoder.decode(data, resolutionLevel: 1)

// Decode just a sub-region (~4x faster for ~1/4-extent regions)
let region = JP3DRegion(x: 64..<192, y: 64..<192, z: 4..<12)
let roi = try await decoder.decode(data, region: region)

// Decode the region at 1/2 resolution (both savings stack)
let combined = try await decoder.decode(data, region: region, resolutionLevel: 1)
```

## Known limitations

- The K+ROI overload returns a `JP3DROIDecoderResult`, which differs in
shape from `JP3DDecoderResult` (adds `decodedRegion`, `isFullVolume`,
`tilesSkipped`). This matches the existing `JP3DROIDecoder.decode`
return type — consumers wanting unified result handling can read
`.volume` from either type.
- `maxQualityLayers` honoured via configuration propagation, but the
JP3D decoder is single-layer (per v10.9.0's `decodeQuality` 2D ship
— the JP3D encoder cannot currently produce genuine multi-layer
per-slice codestreams). Multi-layer JP3D is a separate arc.
- On Linux (`J2KMetalSession.isAvailable == false`), the overloads
silently use the CPU pipelines and inherit the same Linux semantics
as their underlying decoders.

## Reproducing the parity numbers

```bash
swift test -c release --filter "V10_28_JP3DDecoderPartialOverloadsParityTests"
```

Six tests: three overload bit-exact parity tests, no-op level (0) parity,
negative-level clamp, configuration propagation.

## Backward upgrade

`swift package update` won't auto-pick this release if your `Package.swift`
pins an exact version; bump the requirement to `from: "10.16.0"`. No
source changes required for consumers — the new overloads are strictly
additive. Existing code calling `JP3DDecoder().decode(data)` continues
to work unchanged; switching to the new overloads is opt-in per call site.

## Companion work — encoder preWarm probe (research-only)

The natural symmetric ship to v10.15.0 would have been
`JP3DEncoder.preWarm`. A 2-fixture cold-start probe (V10_27, on the
`v10.16-research` branch, not merged) measured the leftover encoder-
specific cold cost AFTER `JP3DDecoder.preWarm(includeWarmupDispatch: true)`:

| Fixture | Path A (cold) | Path B (after decoder preWarm) | Path C (warm median) | (B - C) |
|---|---:|---:|---:|---:|
| 128×128×16 lossless HTJ2K | 57.56 ms | 8.10 ms | 8.01 ms | **+0.10 ms** |
| 256×256×16 lossless HTJ2K | 77.18 ms | 26.89 ms | 28.14 ms | **−1.25 ms** |

Both fixtures show < 3 ms leftover — the existing decoder preWarm's
warmup-dispatch (a real synthetic 256² encode+decode) already amortises
the encoder Metal init via `J2KMetalSession.processShared`. A
dedicated encoder preWarm would be a no-op API. Per
`feedback_no_half_releases.md`, NOT shipped.
Loading
Loading