Skip to content

PR-L1 — S-98 layer-stack plumbing (no inter-product rules)#118

Merged
philliphoff merged 7 commits into
mainfrom
philliphoff/pr-l1-s98-layer-stack-plumbing
May 20, 2026
Merged

PR-L1 — S-98 layer-stack plumbing (no inter-product rules)#118
philliphoff merged 7 commits into
mainfrom
philliphoff/pr-l1-s98-layer-stack-plumbing

Conversation

@philliphoff
Copy link
Copy Markdown
Owner

Summary

Implements PR-L1 of the S-98 interoperability rollout (per docs/design/s98-interoperability.md, PR-L0 #117): deterministic S-98-shaped layer stacking across all loaded datasets. No inter-product suppression rules. No IC-driven plane reassignment. No UI for layer controls.

⚠️ This PR stacks on top of PR #117 (PR-L0 design note). Merge that one first or rebase this once #117 lands.

What's in this PR

  1. Typed plane modelS98DisplayPlane enum with the 9 canonical planes from S-98 Annex A §A-6.9.1 / MSC.530(106)/Rev.1 §Appendix 2.
  2. Per-processor declarations — every dataset processor populates a new DatasetResult.StackEntries property with LayerStackEntry records describing which S-98 plane each emitted layer belongs in.
  3. Central authorityInteroperabilityAuthority.Default carries a hardcoded plane table (TBD-8 resolved) and a stable sort: (Plane, WithinPlanePriority, input order).
  4. LayerStackBuilder — collects entries from every loaded dataset top-of-UI-first and delegates to the authority. DatasetLoaderService.FlattenLayerOrder now defers entirely to it.
  5. Pick orderingPickService.ResolveHits sorts hits by descending stack index before dedup, so multi-hit picks return top-of-stack first. S-98 Main §10.12 / Annex A §A-6.12 designate interrogation as "under development" — this is a viewer-side decision.

Locked decisions (per spec brief)

TBD Decision Reflected in
TBD-2 S-104/S-111 surface under S-101 line work InteroperabilityAuthority table (OnDemandSurface (20) < BaseChartOver (30))
TBD-3 S-101 area-fill vs line-work split in the processor (double pipeline pass with type filter) S101DatasetProcessor.Render partitions AreaInstruction from the rest
TBD-4 S-129 → OnDemandSurface InteroperabilityAuthority. TODO comment notes PR-L2 may move it
TBD-8 MSC.530-derived planes for S-122/124/125/127/128/131/201/411/421 hardcoded InteroperabilityAuthority. No external rule pack

Test counts

Project Before After Δ
EncDotNet.S100.Pipelines.Tests 294 322 +28 (authority + sort + interleave)
EncDotNet.S100.Viewer.Tests 278 279 +1 (stack-ordered pick)
EncDotNet.S100.VisualRegression.Tests 26 26 unchanged

Full dotnet build EncDotNet.S100.slnx + targeted dotnet test --configuration Release green.

Visual regression rebaselining

None required. All 26 snapshots still match. Two reasons:

  1. Each non-S-101 spec emits a single layer that lands on the same plane as before, so cross-dataset paint order is unchanged when only one dataset is loaded.
  2. The S-101 split paints the exact same drawing instructions in the exact same paint order — they're just bucketed into two adjacent Mapsui layers instead of one. Mapsui walks them sequentially so per-pixel output is identical.

Performance — S-101 split cost

The split runs the pipeline once and partitions prepared (IReadOnlyList<DrawingInstruction>) into areas vs non-areas; only the Mapsui rendering step runs twice. For a typical single ENC cell this is a small constant overhead (no second pipeline pass). I haven't measured on a large multi-cell load — if profiling shows >100 ms regression, a v2 mitigation is a single-pass dual-sink renderer. Filed as a follow-up.

Explicitly NOT in this PR

  • IC parsing / rule tables / suppression / replacement / hybridization.
  • Per-feature plane filters (TBD-5 / Annex A §4.3).
  • Layer Controls UI (PR-L3).
  • Safety-contour MSC.232 exception (TBD-6).
  • PdC picker UX (TBD-7).
  • Any new bundled S-98 spec content.

Acceptance gates

  • Build + test green.
  • Loading S-101 ENC + S-102 bathy together: bathy draws under S-101 line work and above S-101 area fills, deterministically. (Validated by Build_interleaves_across_planes_independent_of_load_order.)
  • Loading S-124 alongside S-101: warnings draw above ENC.
  • Pick over a stacked cluster: hits ordered top-of-stack first. (Validated by HandlePick_OrdersHitsByLayerStackTopFirst.)
  • Visual regression suite passes (no rebaselining needed).

Notes for reviewers

  • IDatasetLoaderService.CurrentStackedLayers + LayerStackChanged are default-implemented on the interface (return empty / no-op event). This lets existing test doubles compile without changes. PR-L3 may promote them to required members once the Layer Controls UI consumes them.
  • DatasetResult.StackEntries is nullable but always populated by the in-tree processors. The fallback path in DatasetLoaderService.FlattenLayerOrder synthesises defaults via InteroperabilityAuthority.Default.GetDefaultPlane(spec) if a processor ever forgets — defensive.

No inter-product rule logic snuck in: InteroperabilityAuthority only consults the product name (and an optional layer-kind hint) when assigning a default plane. It never inspects another loaded dataset.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Phillip Hoff and others added 3 commits May 19, 2026 23:15
Adds docs/design/s98-interoperability.md documenting the proposed
plumbing for S-98 ECDIS interoperability across the loaded
dataset stack: display-plane model, an initial five inter-product
rules covering S-101/S-102, S-101/S-124, S-101/S-104, and
S-101/S-111, a codebase impact map (S98DisplayPlane enum,
IInteroperabilityAuthority, LayerStackBuilder, processor plane
defaults, S-101 fill/linework split, stack-ordered pick), the
catalogue-driven rule-table shape, a snapshot rebaseline plan,
and twelve open TBDs to be resolved before PR-L1.

Spec cited: S-98 Edition 2.0.0 (October 2025), in particular
Annex A clauses 4.4 (levels), 8 (interop rules), A-3.2 (Level 1
schema), B-3.1.2 (predefined combinations), A-6.9.1 (gridded
bathymetry rule), and Main 9.2.1 (priority layers).

No production code touched; only a new design doc and a one-line
docs/toc.yml entry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduces typed S-98 display planes and a central
InteroperabilityAuthority that sorts every loaded dataset's layers
into deterministic S-98 paint order. No IC parsing, no inter-product
suppression, no UI — those land in PR-L2/L3. The five v1 rules from
docs/design/s98-interoperability.md §3 are expressed purely as
default plane assignments.

Core
----
* S98DisplayPlane enum (9 canonical planes from S-98 Annex A
  §A-6.9.1 / MSC.530(106)/Rev.1 §Appendix 2) with spec citations.

Pipelines
---------
* LayerStackEntry / IInteroperabilityAuthority / InteroperabilityAuthority
  default impl with hardcoded plane table covering S-101 (split into
  area-fill vs line-work), S-102, S-104, S-111 (color-band / arrows /
  stations), S-122, S-124, S-125, S-127, S-128, S-129, S-131, S-201,
  S-411, S-421. Stable sort by (Plane, WithinPlanePriority, input order).
* LayerStackBuilder collects per-dataset entries top-of-UI first and
  delegates to the authority.
* DatasetResult.StackEntries: parallel to Layers; populated by every
  processor.
* S101DatasetProcessor: TBD-3 split — runs the pipeline once then
  partitions the drawing instructions into AreaInstruction (→
  BaseChartUnder) vs everything else (→ BaseChartOver). Emits two
  Mapsui layers + two StackEntries. LayerNames "s101.areas" /
  "s101.linework" for sub-layer toggles.
* S102/S104/S111/S122/S124/S125/S127/S128/S129/S131/S201/S411/S421/S57
  each emit StackEntries on their per-spec default plane.

Viewer
------
* DatasetLoaderService tracks _entryStackEntries parallel to
  _entryLayers; FlattenLayerOrder now feeds LayerStackBuilder. First-
  load no longer relies on "new layers go on top" — the S-98 sort
  always wins so a late-loaded S-102 still slots between S-101 area
  fills and line work.
* IDatasetLoaderService exposes CurrentStackedLayers + LayerStackChanged
  (default-implemented so existing test doubles needn't change).
* PickService.ResolveHits re-orders mapInfo.MapInfoRecords by
  descending stack index before the dedup walk so multi-hit picks
  return top-of-stack first. Cites S-98 Main §10.12 / Annex A §A-6.12
  ("interrogation under development").

Tests
-----
* InteroperabilityAuthorityTests (28 tests): plane assignment per
  product, sort stability, within-plane priority, tiebreaker, and
  cross-dataset interleave independent of load order.
* PickService.HandlePick_OrdersHitsByLayerStackTopFirst.
* All 322 Pipelines + 279 Viewer + 26 VisualRegression tests pass.
  No snapshots needed rebaselining — each per-spec single-layer
  dataset still paints at the same plane, and the S-101 split paints
  the same instructions in the same order across two layers Mapsui
  walks adjacently.

Conventions
-----------
* .NET 10, nullable enabled, ArgumentNullException.ThrowIfNull.
* XML doc citations to S-98 / MSC.530(106)/Rev.1 / S-100 Part 9 on
  every public API and every plane-assignment in the default table.
* No new bundled S98 spec content.

Out of scope (deferred to PR-L2 / L3 per design note §10)
---------------------------------------------------------
* IC parsing / rule tables / suppression / replacement / hybridization.
* Per-feature plane filters (Annex A §4.3).
* Layer Controls UI (PR-L3).
* Safety-contour MSC.232 exception.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 20, 2026

Performance Gate

PASSED — no regressions.

Threshold: 10.0%, MAD multiplier (k): 3.0, retry-zone mult: 2.0×

Scenario summary

Scenario Status Δ median (%) z (Δ/MAD) Base median (ms) Samples (b/c)
exchange-set-open ✅ pass -1.8 -0.12 0.61 20/20
s101-portray-cold ✅ pass -0.9 -0.54 329.56 20/20
s101-portray-warm ✅ pass -0.8 -0.16 182.43 20/20
s101-render-warm ✅ pass -3.5 -0.79 175.77 20/20
s102-coverage ✅ pass -15.0 -8.31 1.54 20/20
s102-coverage-open ✅ pass +4.8 +5.53 2.69 20/20
s102-coverage-render-large ✅ pass 0.0 -0.03 140.80 20/20
s124-vector ✅ pass -10.9 -0.76 0.44 20/20
s201-vector ✅ pass +0.8 +0.09 0.35 20/20

exchange-set-open

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 0.61 0.60
Baseline MAD (ms) 0.09
Δ median -1.8%
z (Δ/MAD) -0.12

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.asset.read 10.87 10.55 -2.9% ▫️
s100.exchangeset.parse 44.87 41.34 -7.9% ▫️

Metrics

Metric Baseline Candidate Delta Status
s100.asset.read.duration 18.06 18.04 -0.1% ▫️

s101-portray-cold

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 329.56 326.56
Baseline MAD (ms) 5.54
Δ median -0.9%
z (Δ/MAD) -0.54

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.lua.execute 4606.62 4631.33 +0.5% ▫️
s100.lua.rule.invoke 3880.41 3868.80 -0.3% ▫️
s100.pipeline.vector.process 4686.14 4705.92 +0.4% ▫️
s100.pipeline.vector.stage.assemble 0.27 0.25 -4.3% ▫️
s100.pipeline.vector.stage.feature_xml 48.46 49.82 +2.8% ▫️
s100.pipeline.vector.stage.lua 4608.13 4632.93 +0.5% ▫️
s100.pipeline.vector.stage.rule_select 4.96 5.77 +16.5%
s100.pipeline.vector.stage.sort 2.14 2.10 -1.6% ▫️
s100.pipeline.vector.stage.viewing_groups 11.57 3.74 -67.7%
s100.pipeline.vector.stage.xslt 0.25 0.26 +2.9% ▫️
s100.render.frame 4995.81 5132.46 +2.7% ▫️

Metrics

Metric Baseline Candidate Delta Status
s100.catalogue.match.count 7.00 7.00 +0.0% ▫️
s100.featurecatalogue.cache.hit.count 6.00 6.00 +0.0% ▫️
s100.featurecatalogue.cache.miss.count 1.00 1.00 +0.0% ▫️
s100.lua.execute.duration 988.63 1222.25 +23.6%
s100.lua.features.count 700.00 700.00 +0.0% ▫️
s100.lua.instructions.emitted.count 735.00 735.00 +0.0% ▫️
s100.lua.rule.invoke.count 7.00 7.00 +0.0% ▫️
s100.lua.rule.invoke.count 77.00 77.00 +0.0% ▫️
s100.lua.rule.invoke.duration 717.01 959.31 +33.8%
s100.lua.rule.invoke.duration 4.84 6.28 +29.9%
s100.lua.source.cache.hit.count 292.00 292.00 +0.0% ▫️
s100.lua.source.cache.miss.count 23.00 23.00 +0.0% ▫️
s100.pattern.cache.hit.count 63.00 63.00 +0.0% ▫️
s100.pattern.cache.miss.count 28.00 28.00 +0.0% ▫️
s100.pipeline.drawinginstructions.out 735.00 735.00 +0.0% ▫️
s100.pipeline.duration 1019.39 1252.06 +22.8%
s100.pipeline.features.in 84.00 84.00 +0.0% ▫️
s100.pipeline.stage.duration 0.25 0.26 +3.3% ▫️
s100.pipeline.stage.duration 19.41 18.37 -5.4% ▫️
s100.pipeline.stage.duration 989.89 1223.48 +23.6%
s100.pipeline.stage.duration 3.96 4.02 +1.6% ▫️
s100.pipeline.stage.duration 2.46 2.64 +7.3%
s100.pipeline.stage.duration 0.98 0.97 -0.7% ▫️
s100.pipeline.stage.duration 0.82 0.82 -0.3% ▫️
s100.pipeline.stage.instructions.count 0.00 0.00 N/A ▫️
s100.pipeline.stage.instructions.count 735.00 735.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 735.00 735.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 735.00 735.00 +0.0% ▫️
s100.portrayal.cache.hit.count 24.00 24.00 +0.0% ▫️
s100.portrayal.cache.hit.count 292.00 292.00 +0.0% ▫️
s100.portrayal.cache.hit.count 7.00 7.00 +0.0% ▫️
s100.portrayal.cache.hit.count 36.00 36.00 +0.0% ▫️
s100.portrayal.cache.miss.count 4.00 4.00 +0.0% ▫️
s100.portrayal.cache.miss.count 23.00 23.00 +0.0% ▫️
s100.portrayal.cache.miss.count 6.00 6.00 +0.0% ▫️
s100.render.frame.duration 1346.40 1338.56 -0.6% ▫️
s100.render.instructions.processed.count 735.00 735.00 +0.0% ▫️
s100.render.styles.applied.count 693.00 693.00 +0.0% ▫️
s100.symbol.cache.hit.count 7.00 7.00 +0.0% ▫️
s100.symbol.cache.miss.count 14.00 14.00 +0.0% ▫️
s100.symbol.resolve.duration 0.03 0.03 +8.5%
s100.symbol.resolve.duration 5.46 5.20 -4.8% ▫️

s101-portray-warm

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 182.43 180.94
Baseline MAD (ms) 9.24
Δ median -0.8%
z (Δ/MAD) -0.16

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.lua.execute 5103.48 4647.05 -8.9% ▫️
s100.lua.rule.invoke 4287.65 3863.44 -9.9% ▫️
s100.pipeline.vector.process 5198.37 4727.84 -9.1% ▫️
s100.pipeline.vector.stage.assemble 0.23 0.22 -5.2% ▫️
s100.pipeline.vector.stage.feature_xml 73.44 67.52 -8.1% ▫️
s100.pipeline.vector.stage.lua 5104.83 4648.26 -8.9% ▫️
s100.pipeline.vector.stage.rule_select 3.88 3.74 -3.8% ▫️
s100.pipeline.vector.stage.sort 2.98 2.96 -0.8% ▫️
s100.pipeline.vector.stage.viewing_groups 4.62 4.44 -4.0% ▫️
s100.pipeline.vector.stage.xslt 0.24 0.23 -6.3% ▫️
s100.render.frame 77.70 85.80 +10.4%

Metrics

Metric Baseline Candidate Delta Status
s100.catalogue.match.count 1.00 1.00 +0.0% ▫️
s100.featurecatalogue.cache.hit.count 7.00 7.00 +0.0% ▫️
s100.lua.execute.duration 724.58 1248.44 +72.3%
s100.lua.features.count 700.00 700.00 +0.0% ▫️
s100.lua.instructions.emitted.count 735.00 735.00 +0.0% ▫️
s100.lua.rule.invoke.count 7.00 7.00 +0.0% ▫️
s100.lua.rule.invoke.count 77.00 77.00 +0.0% ▫️
s100.lua.rule.invoke.duration 557.67 1060.19 +90.1%
s100.lua.rule.invoke.duration 3.18 6.02 +89.0%
s100.lua.source.cache.hit.count 315.00 315.00 +0.0% ▫️
s100.pattern.cache.hit.count 87.00 87.00 +0.0% ▫️
s100.pattern.cache.miss.count 4.00 4.00 +0.0% ▫️
s100.pipeline.drawinginstructions.out 735.00 735.00 +0.0% ▫️
s100.pipeline.duration 748.32 1264.12 +68.9%
s100.pipeline.features.in 84.00 84.00 +0.0% ▫️
s100.pipeline.stage.duration 0.02 0.02 +31.5%
s100.pipeline.stage.duration 20.40 12.63 -38.1%
s100.pipeline.stage.duration 724.86 1248.69 +72.3%
s100.pipeline.stage.duration 0.86 0.91 +4.9% ▫️
s100.pipeline.stage.duration 0.69 0.58 -15.3%
s100.pipeline.stage.duration 0.14 0.15 +11.1%
s100.pipeline.stage.duration 0.03 0.03 -8.5% ▫️
s100.pipeline.stage.instructions.count 0.00 0.00 N/A ▫️
s100.pipeline.stage.instructions.count 735.00 735.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 735.00 735.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 735.00 735.00 +0.0% ▫️
s100.portrayal.cache.hit.count 4.00 4.00 +0.0% ▫️
s100.portrayal.cache.hit.count 315.00 315.00 +0.0% ▫️
s100.portrayal.cache.hit.count 7.00 7.00 +0.0% ▫️
s100.portrayal.cache.hit.count 6.00 6.00 +0.0% ▫️
s100.render.frame.duration 175.33 178.72 +1.9% ▫️
s100.render.instructions.processed.count 735.00 735.00 +0.0% ▫️
s100.render.styles.applied.count 693.00 693.00 +0.0% ▫️
s100.symbol.cache.hit.count 19.00 19.00 +0.0% ▫️
s100.symbol.cache.miss.count 2.00 2.00 +0.0% ▫️
s100.symbol.resolve.duration 0.04 0.06 +48.8%
s100.symbol.resolve.duration 0.39 0.26 -33.9%

s101-render-warm

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 175.77 169.66
Baseline MAD (ms) 7.72
Δ median -3.5%
z (Δ/MAD) -0.79

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.lua.execute 4960.08 4124.33 -16.8%
s100.lua.rule.invoke 4089.76 3421.28 -16.3%
s100.pipeline.vector.process 5034.18 4199.17 -16.6%
s100.pipeline.vector.stage.assemble 0.22 0.21 -6.2% ▫️
s100.pipeline.vector.stage.feature_xml 52.39 61.52 +17.4%
s100.pipeline.vector.stage.lua 4961.44 4125.49 -16.8%
s100.pipeline.vector.stage.rule_select 3.78 3.56 -5.9% ▫️
s100.pipeline.vector.stage.sort 3.60 3.64 +1.1% ▫️
s100.pipeline.vector.stage.viewing_groups 5.21 5.08 -2.5% ▫️
s100.pipeline.vector.stage.xslt 0.25 0.22 -13.0%
s100.render.frame 73.28 73.20 -0.1% ▫️

Metrics

Metric Baseline Candidate Delta Status
s100.catalogue.match.count 1.00 1.00 +0.0% ▫️
s100.featurecatalogue.cache.hit.count 7.00 7.00 +0.0% ▫️
s100.lua.execute.duration 688.66 1228.66 +78.4%
s100.lua.features.count 700.00 700.00 +0.0% ▫️
s100.lua.instructions.emitted.count 735.00 735.00 +0.0% ▫️
s100.lua.rule.invoke.count 7.00 7.00 +0.0% ▫️
s100.lua.rule.invoke.count 77.00 77.00 +0.0% ▫️
s100.lua.rule.invoke.duration 517.87 1034.76 +99.8%
s100.lua.rule.invoke.duration 3.27 5.76 +76.2%
s100.lua.source.cache.hit.count 315.00 315.00 +0.0% ▫️
s100.pattern.cache.hit.count 87.00 87.00 +0.0% ▫️
s100.pattern.cache.miss.count 4.00 4.00 +0.0% ▫️
s100.pipeline.drawinginstructions.out 735.00 735.00 +0.0% ▫️
s100.pipeline.duration 708.61 1244.54 +75.6%
s100.pipeline.features.in 84.00 84.00 +0.0% ▫️
s100.pipeline.stage.duration 0.02 0.02 -12.6%
s100.pipeline.stage.duration 15.91 12.98 -18.4%
s100.pipeline.stage.duration 688.98 1228.85 +78.4%
s100.pipeline.stage.duration 1.00 0.89 -11.1%
s100.pipeline.stage.duration 0.96 0.79 -17.9%
s100.pipeline.stage.duration 0.17 0.14 -18.3%
s100.pipeline.stage.duration 0.04 0.03 -31.8%
s100.pipeline.stage.instructions.count 0.00 0.00 N/A ▫️
s100.pipeline.stage.instructions.count 735.00 735.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 735.00 735.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 735.00 735.00 +0.0% ▫️
s100.portrayal.cache.hit.count 4.00 4.00 +0.0% ▫️
s100.portrayal.cache.hit.count 315.00 315.00 +0.0% ▫️
s100.portrayal.cache.hit.count 7.00 7.00 +0.0% ▫️
s100.portrayal.cache.hit.count 6.00 6.00 +0.0% ▫️
s100.render.frame.duration 183.38 173.27 -5.5% ▫️
s100.render.instructions.processed.count 735.00 735.00 +0.0% ▫️
s100.render.styles.applied.count 693.00 693.00 +0.0% ▫️
s100.symbol.cache.hit.count 19.00 19.00 +0.0% ▫️
s100.symbol.cache.miss.count 2.00 2.00 +0.0% ▫️
s100.symbol.resolve.duration 0.04 0.05 +38.9%
s100.symbol.resolve.duration 0.40 0.24 -40.7%

s102-coverage

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 1.54 1.31
Baseline MAD (ms) 0.03
Δ median -15.0%
z (Δ/MAD) -8.31

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.pipeline.coverage.process 56.64 49.29 -13.0%
s100.pipeline.coverage.stage.read 4.90 4.71 -3.8% ▫️
s100.pipeline.coverage.stage.resolve 48.22 41.29 -14.4%
s100.render.coverage.build 80.08 77.83 -2.8% ▫️

Metrics

Metric Baseline Candidate Delta Status
s100.catalogue.match.count 1.00 1.00 +0.0% ▫️
s100.coverage.cells 4557.00 4557.00 +0.0% ▫️
s100.hdf5.read.bytes 5208.00 5208.00 +0.0% ▫️
s100.hdf5.read.duration 21.07 20.80 -1.3% ▫️
s100.hdf5.read.duration 30.03 28.99 -3.5% ▫️
s100.hdf5.read.duration 7.28 7.37 +1.3% ▫️
s100.pipeline.duration 11.70 11.46 -2.1% ▫️
s100.pipeline.stage.duration 1.00 0.86 -14.2%
s100.pipeline.stage.duration 9.87 10.07 +2.0% ▫️
s100.portrayal.cache.hit.count 7.00 7.00 +0.0% ▫️

s102-coverage-open

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 2.69 2.82
Baseline MAD (ms) 0.02
Δ median +4.8%
z (Δ/MAD) +5.53

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.dataset.open 76.67 78.48 +2.4% ▫️
s100.hdf5.dataset.read 11.34 11.75 +3.6% ▫️
s100.hdf5.file.open 18.55 17.57 -5.3% ▫️
s100.hdf5.open 18.76 18.80 +0.2% ▫️

Metrics

Metric Baseline Candidate Delta Status
s100.hdf5.read.bytes 36456.00 36456.00 +0.0% ▫️
s100.hdf5.read.duration 6.04 6.34 +4.8% ▫️
s100.hdf5.read.duration 2.31 2.28 -1.1% ▫️
s100.hdf5.read.duration 4.16 4.25 +2.1% ▫️

s102-coverage-render-large

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 140.80 140.73
Baseline MAD (ms) 2.09
Δ median 0.0%
z (Δ/MAD) -0.03

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.pipeline.coverage.process 200.25 198.07 -1.1% ▫️
s100.pipeline.coverage.stage.read 119.84 123.71 +3.2% ▫️
s100.pipeline.coverage.stage.resolve 77.08 71.24 -7.6% ▫️
s100.render.coverage.build 5140.42 5036.79 -2.0% ▫️

Metrics

Metric Baseline Candidate Delta Status
s100.catalogue.match.count 1.00 1.00 +0.0% ▫️
s100.coverage.cells 7000000.00 7000000.00 +0.0% ▫️
s100.hdf5.read.bytes 8000000.00 8000000.00 +0.0% ▫️
s100.hdf5.read.duration 0.13 0.09 -31.3%
s100.hdf5.read.duration 2.36 5.06 +114.7%
s100.hdf5.read.duration 1.25 1.21 -3.3% ▫️
s100.pipeline.duration 38.21 36.29 -5.0% ▫️
s100.pipeline.stage.duration 24.59 22.78 -7.3% ▫️
s100.pipeline.stage.duration 12.82 12.93 +0.9% ▫️
s100.portrayal.cache.hit.count 7.00 7.00 +0.0% ▫️

s124-vector

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 0.44 0.39
Baseline MAD (ms) 0.06
Δ median -10.9%
z (Δ/MAD) -0.76

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.pipeline.vector.process 9.08 8.89 -2.1% ▫️
s100.pipeline.vector.stage.assemble 0.32 0.33 +2.6% ▫️
s100.pipeline.vector.stage.feature_xml 1.39 1.33 -3.9% ▫️
s100.pipeline.vector.stage.rule_select 0.31 0.31 +0.2% ▫️
s100.pipeline.vector.stage.sort 0.25 0.24 -4.3% ▫️
s100.pipeline.vector.stage.viewing_groups 0.61 0.61 -0.1% ▫️
s100.pipeline.vector.stage.xslt 4.16 4.10 -1.3% ▫️
s100.render.frame 1.11 1.02 -8.4% ▫️
s100.xslt.transform 1.65 1.62 -1.6% ▫️

Metrics

Metric Baseline Candidate Delta Status
s100.catalogue.match.count 1.00 1.00 +0.0% ▫️
s100.featurecatalogue.cache.miss.count 1.00 1.00 +0.0% ▫️
s100.pipeline.drawinginstructions.out 14.00 14.00 +0.0% ▫️
s100.pipeline.duration 55.96 53.68 -4.1% ▫️
s100.pipeline.features.in 7.00 7.00 +0.0% ▫️
s100.pipeline.stage.duration 1.04 0.99 -5.6% ▫️
s100.pipeline.stage.duration 3.42 3.12 -8.9% ▫️
s100.pipeline.stage.duration 1.18 1.12 -4.8% ▫️
s100.pipeline.stage.duration 0.05 0.06 +1.2% ▫️
s100.pipeline.stage.duration 0.10 0.12 +14.9%
s100.pipeline.stage.duration 49.26 47.64 -3.3% ▫️
s100.pipeline.stage.instructions.count 14.00 14.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 14.00 14.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 14.00 14.00 +0.0% ▫️
s100.portrayal.cache.hit.count 13.00 13.00 +0.0% ▫️
s100.portrayal.cache.hit.count 7.00 7.00 +0.0% ▫️
s100.portrayal.cache.hit.count 6.00 6.00 +0.0% ▫️
s100.portrayal.cache.miss.count 1.00 1.00 +0.0% ▫️
s100.portrayal.cache.miss.count 1.00 1.00 +0.0% ▫️
s100.render.frame.duration 2.40 2.05 -14.9%
s100.render.instructions.processed.count 14.00 14.00 +0.0% ▫️
s100.render.styles.applied.count 14.00 14.00 +0.0% ▫️
s100.xslt.transform.duration 10.35 9.97 -3.7% ▫️

s201-vector

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 0.35 0.35
Baseline MAD (ms) 0.03
Δ median +0.8%
z (Δ/MAD) +0.09

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.pipeline.vector.process 7.23 7.45 +3.1% ▫️
s100.pipeline.vector.stage.assemble 0.12 0.12 -0.3% ▫️
s100.pipeline.vector.stage.feature_xml 0.89 0.91 +1.5% ▫️
s100.pipeline.vector.stage.rule_select 0.31 0.36 +15.0%
s100.pipeline.vector.stage.sort 0.16 0.13 -16.3%
s100.pipeline.vector.stage.viewing_groups 0.50 0.48 -3.6% ▫️
s100.pipeline.vector.stage.xslt 3.49 3.63 +4.0% ▫️
s100.render.frame 0.48 0.38 -20.4%
s100.xslt.transform 1.92 1.99 +3.7% ▫️

Metrics

Metric Baseline Candidate Delta Status
s100.catalogue.match.count 1.00 1.00 +0.0% ▫️
s100.featurecatalogue.cache.miss.count 1.00 1.00 +0.0% ▫️
s100.pipeline.drawinginstructions.out 0.00 0.00 N/A ▫️
s100.pipeline.duration 168.62 147.20 -12.7%
s100.pipeline.features.in 7.00 7.00 +0.0% ▫️
s100.pipeline.stage.duration 0.01 0.01 +6.3%
s100.pipeline.stage.duration 19.13 6.99 -63.4%
s100.pipeline.stage.duration 0.13 0.12 -7.3% ▫️
s100.pipeline.stage.duration 0.05 0.06 +21.2%
s100.pipeline.stage.duration 0.04 0.05 +19.7%
s100.pipeline.stage.duration 148.69 139.43 -6.2% ▫️
s100.pipeline.stage.instructions.count 0.00 0.00 N/A ▫️
s100.pipeline.stage.instructions.count 0.00 0.00 N/A ▫️
s100.pipeline.stage.instructions.count 0.00 0.00 N/A ▫️
s100.portrayal.cache.hit.count 7.00 7.00 +0.0% ▫️
s100.portrayal.cache.hit.count 6.00 6.00 +0.0% ▫️
s100.portrayal.cache.miss.count 1.00 1.00 +0.0% ▫️
s100.render.frame.duration 0.09 0.06 -30.5%
s100.render.instructions.processed.count 0.00 0.00 N/A ▫️
s100.render.styles.applied.count 0.00 0.00 N/A ▫️
s100.xslt.transform.duration 21.65 20.72 -4.3% ▫️

Generated by EncDotNet.S100.PerfReport gate command

Phillip Hoff and others added 4 commits May 20, 2026 08:38
The DatasetLoaderService was hardcoded to InteroperabilityAuthority.Default.
Promotes the authority to an optional constructor parameter so hosts (or
tests) can plug in alternative stacking policies via the existing
IInteroperabilityAuthority seam.

Adds LoadOrderInteroperabilityAuthority as a working alternative — a
strict dataset-by-dataset policy that paints the bottom-of-UI dataset's
layers first, then the next, etc., ignoring the S-98 plane entirely
(classic GIS 'load order is paint order' semantics). It still delegates
GetDefaultPlane to the S-98 oracle so layer-controls UIs can still
label layers with their conceptual plane.

Tests
-----
* LoadOrderAuthority_ignores_plane_and_uses_strict_dataset_ordering:
  same fixture as the S-98 interleave test produces a different
  ordering through the alternative authority.
* LoadOrderAuthority_still_exposes_S98_default_planes_for_labels.

Conventions
-----------
* Default of the optional ctor parameter is the S-98 authority — no
  behaviour change for existing callers.
* XML doc on the interface now advertises pluggability.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Consumers that participate in cross-dataset paint ordering now depend
on an authority *provider* rather than holding an
IInteroperabilityAuthority reference directly. The provider exposes
the currently active authority on each consult and raises
CurrentChanged when the host swaps it, so DatasetLoaderService
re-flattens the layer stack in response (e.g. when a viewer setting
toggles between S-98 and strict load-order policy).

- New IInteroperabilityAuthorityProvider interface with Current +
  CurrentChanged event; default mutable impl
  InteroperabilityAuthorityProvider whose Set(authority) raises the
  event (even when the new reference equals the previous one, so a
  host can force a re-sort after a catalogue tweak).
- DatasetLoaderService ctor now takes an
  IInteroperabilityAuthorityProvider? (defaulting to a fresh
  provider holding the S-98 oracle) and subscribes to CurrentChanged
  to call ReorderDatasetLayers(FlattenLayerOrder()).
- FlattenLayerOrder + the fallback plane-synthesis path consult
  _authorityProvider.Current on each call.
- GmlDatasetProcessorBase still stamps the canonical S-98 plane via
  InteroperabilityAuthority.Default at render time: that field
  records 'which conceptual plane does this layer's content belong
  to?' — a per-product fact — while the *sort policy* lives in the
  authority that DatasetLoaderService consults. Documented inline.
- 5 new InteroperabilityAuthorityProviderTests cover Current default,
  ctor initial-value, Set updates + raises event, force-resort on
  reference-equal Set, null-guard.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Removes every static 'Default' singleton on the interoperability
types and forces consumers to receive the authority provider through
their constructor (resolved from the DI container).

Production:
- Delete InteroperabilityAuthority.Default and
  LoadOrderInteroperabilityAuthority.Default + its parameterless ctor.
  Authorities are stateless types whose instances are produced by DI.
- InteroperabilityAuthorityProvider ctor now requires the initial
  authority; no implicit fallback.
- GmlDatasetProcessorBase requires IInteroperabilityAuthorityProvider
  in its ctor and consults provider.Current at render time when
  stamping each LayerStackEntry. All ten GML processor subclasses
  (S-122/S-124/S-125/S-127/S-128/S-129/S-201/S-411/S-421) take the
  provider as a required ctor parameter and forward it to the base.
- DatasetPipelineFactory takes the provider and forwards it to every
  GML processor it constructs.
- DatasetLoaderService ctor: provider is now required (was optional).
- App.axaml.cs registers IInteroperabilityAuthorityProvider as a
  singleton wrapping a fresh InteroperabilityAuthority, and injects
  it into DatasetPipelineFactory; DatasetLoaderService gets it via
  auto-resolved DI.

Tests:
- InteroperabilityAuthorityTests / InteroperabilityAuthorityProviderTests
  construct authorities/providers explicitly with .
- New TestAuthority helper in the viewer tests yields a fresh S-98
  provider for processor-instantiation tests.
- DatasetPipelineFactoryFeatureCatalogueReuseTests and the visual-
  regression RenderHarness pass a provider explicitly.
- PerfRunner reflection-based factory lookup now tries the PR-L1
  ctor (with provider) first and falls back to the base-SHA shapes
  for cross-SHA perf overlays.

Build + 329 pipeline + 279 viewer tests green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The perf-gate workflow overlays the PR's PerfRunner sources onto the
base SHA checkout and builds against the base-SHA library binaries
(see .github/workflows/perf.yml lines 31-45). Hard-typed references
to EncDotNet.S100.Datasets.Pipelines.Interoperability.* fail to
compile against the base SHA, which does not yet expose that
namespace.

Switch SharedInfrastructure.CreatePipelineFactory to probe the
provider interface and concrete types via Assembly.GetType and
construct instances via Activator.CreateInstance, so the tooling
compiles against both base and PR library surfaces. The legacy
4-param and resolver-based ctors remain as fallbacks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@philliphoff philliphoff merged commit abb4f20 into main May 20, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant