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
6 changes: 6 additions & 0 deletions src/chart.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2450,6 +2450,12 @@ const BakeWork = struct {
// per cell (only for Text/centred-symbol features) instead of re-running the search
// for every tile a feature touches; the arena outlives the call via c.arenas.
cell.label_cache = scene.buildLabelCache(p.allocator(), &cell, geo, portrayal) catch null;
// Per-feature drawn-boundary cache (masked/coast-clipped area boundaries):
// assemble the drawableLineParts subset + precompute its world coords ONCE, so
// the per-tile stroke reprojects with a linear map instead of the transcendental
// projection on every tile the area spans — the last per-tile projection hotspot
// on Inland-ENC river cells (long shared coast boundaries).
cell.drawn_boundary = scene.buildDrawnBoundary(p.allocator(), &cell) catch null;
}
// M_COVR coverage + scale for per-cell quilting (allocate into the cell's own
// arena before the move, so it outlives with the backend).
Expand Down
23 changes: 23 additions & 0 deletions src/s57/s57.zig
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ pub const LonLat = struct {
return .{ .lon_e7 = degToE7(lon_deg), .lat_e7 = degToE7(lat_deg) };
}
};
/// One area feature's DRAWN boundary: the parts left after §8.6.2 masking drops the
/// masked / data-limit / coast-coincident edges (`Cell.drawableLineParts`), together
/// with each vertex's precomputed normalised web-mercator world coordinate parallel to
/// `parts` (`world[i].len == parts[i].len`). The baker fills `world` once per cell
/// (scene.buildDrawnBoundary via `tile.lonLatToWorld`) so the per-tile stroke path
/// reprojects with a linear `worldToTile` instead of the transcendental projection. The
/// coords are opaque f64 pairs to the Cell — only the renderer interprets them; the pair
/// is exactly what `tile.project` would recompute inline, so the cache stays byte-identical.
pub const DrawnBoundary = struct {
parts: [][]LonLat,
world: [][][2]f64,
};
pub const Sounding = struct {
lon_e7: i32,
lat_e7: i32,
Expand Down Expand Up @@ -618,6 +630,17 @@ pub const Cell = struct {
/// single-tile path falls back to an on-demand search). Allocated in the baker's
/// geometry arena, not the cell arena.
label_cache: ?[]const ?LonLat = null,
/// Per-feature DRAWN-boundary cache (area features whose stroked boundary differs
/// from the full fill ring — `needsDrawableBoundary`), built once per cell by the
/// baker (scene.buildDrawnBoundary). A null slot means "stroke the full ring" (no
/// masked/coast edges to drop). See `DrawnBoundary`: it carries the drawn parts plus
/// each vertex's precomputed web-mercator world coord, so the per-tile emit reprojects
/// the boundary with a cheap linear map instead of the transcendental projection on
/// every tile the area spans — the dominant cost on Inland-ENC cells whose fairway /
/// depth areas share long coast boundaries. Allocated in the baker's geometry arena,
/// not the cell arena; null = not built (the live single-tile path reconstructs +
/// projects on demand, byte-identical).
drawn_boundary: ?[]const ?DrawnBoundary = null,
/// True when this cell was assembled from a NATIVE S-101 dataset (s101.native)
/// rather than parsed from an S-57 source. It carries the S-57 geometry model
/// but its features draw their portrayal from S-101-native `adapter.Adapted`
Expand Down
83 changes: 66 additions & 17 deletions src/scene/scene.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1143,27 +1143,66 @@ pub fn buildLabelCache(a: Allocator, cell: *const s57.Cell, geo: ?GeoParts, stre
for (cell.features, 0..) |f, i| {
// Polylabel (areaRepresentativePoint) is AREA-only — lines anchor at their mid-vertex.
if (f.prim != 3) continue;
if (i >= ss.len) break;
const stream = ss[i] orelse continue; // portrayed at all (an unportrayed area never reaches labelPoint)
// Only an area that PLACES something at the representative point consults it: a centred
// label (TextInstruction), a centred symbol (PointInstruction), or the native INFORM01
// additional-info marker (hasAdditionalInfo). A plain fill/boundary area — the vast
// majority (DEPARE, LNDARE, SBDARE, DEPCNT, …) — anchors nothing there, and the pole-of-
// inaccessibility search is expensive, so skip it. QUESMRK1/SWPARE fallbacks carry a null
// stream (skipped above) and stay on the live path. The token test is a strict SUPERSET of
// what the per-tile path consults (an empty TextInstruction is dropped downstream), so a
// cached point is only ever spurious, never missing — labelPoint stays byte-identical and
// no per-tile recompute creeps back in.
if (!hasAdditionalInfo(f) and
std.mem.indexOf(u8, stream, "TextInstruction:") == null and
std.mem.indexOf(u8, stream, "PointInstruction:") == null) continue;
const stream: ?[]const u8 = if (i < ss.len) ss[i] else null;
// Cache exactly the areas whose per-tile emit anchors something at the representative
// point. A plain fill/boundary area — the vast majority (DEPARE, LNDARE, SBDARE, DEPCNT,
// …) — anchors nothing there, and the pole-of-inaccessibility search is expensive, so skip
// it. The set below is a strict SUPERSET of what the per-tile path consults, so a cached
// point is only ever spurious, never missing — labelPoint (which recomputes the SAME
// search from the SAME geometry on a miss) stays byte-identical either way:
// • hasAdditionalInfo -> the §10.6.1.1 INFORM01 marker (stream-independent),
// • an unmapped S-57 area -> the QUESMRK1 "?" fallback. It carries a NULL/errored
// portrayal stream yet still runs featureAnchor for EVERY tile the polygon spans —
// the per-tile pole-of-inaccessibility search that dominated Inland-ENC bakes (objl
// 17000+ area classes with no S-101 mapping, huge river/fairway polygons). Mirror the
// emit-side QUESMRK1 guard exactly so this leaves the live path only when that fires.
// • a portrayed area placing a centred label (TextInstruction) or symbol
// (PointInstruction).
const unmapped_qmark = !cell.native and f.objl != s57.OBJL_TOPMAR and adapter.resolveClass(f) == null;
const places_symbol = if (stream) |s|
std.mem.indexOf(u8, s, "TextInstruction:") != null or
std.mem.indexOf(u8, s, "PointInstruction:") != null
else
false;
if (!hasAdditionalInfo(f) and !unmapped_qmark and !places_symbol) continue;
const parts = featureParts(tmp.allocator(), cell.*, geo, i, f) catch continue;
out[i] = s57.areaRepresentativePoint(tmp.allocator(), parts);
_ = tmp.reset(.retain_capacity);
}
return out;
}

/// Per-feature DRAWN-boundary cache (cell.drawn_boundary), indexed by feature index.
/// An area feature whose stroked boundary differs from its full fill ring
/// (needsDrawableBoundary — MASK/USAG edge flags, or a coast-coincident edge on a
/// non-coast-definer area) restrokes only the drawableLineParts SUBSET. That subset is
/// tile-invariant, so assemble it ONCE here and precompute each vertex's web-mercator
/// world coordinate — the per-tile emit then reprojects with a linear worldToTile instead
/// of running the transcendental projection on the boundary for every tile the area spans
/// (the dominant cost on Inland-ENC river cells: fairway/depth areas with long shared coast
/// boundaries). A null slot leaves the feature on the live path (reconstruct + project),
/// byte-identical since `world` holds exactly `tile.lonLatToWorld` and `tile.project` is
/// `worldToTile ∘ lonLatToWorld`. Covers exactly the features the per-tile stroke path
/// routes through drawableLineParts — ANY prim whose `needsDrawableBoundary` holds, not
/// areas only: a LINE (prim 2) with MASK/USAG edge flags takes the same drawn-subset path
/// and was the bulk of the uncached per-tile projection on Inland-ENC cells.
pub fn buildDrawnBoundary(a: Allocator, cell: *const s57.Cell) ![]?s57.DrawnBoundary {
const out = try a.alloc(?s57.DrawnBoundary, cell.features.len);
@memset(out, null);
for (cell.features, 0..) |f, i| {
if (!cell.needsDrawableBoundary(f)) continue;
const parts = cell.drawableLineParts(a, f) catch continue;
const world = a.alloc([][2]f64, parts.len) catch continue;
for (parts, 0..) |part, pi| {
const wp = try a.alloc([2]f64, part.len);
for (part, 0..) |pt, j| wp[j] = tile.lonLatToWorld(pt.lon(), pt.lat());
world[pi] = wp;
}
out[i] = .{ .parts = parts, .world = world };
}
return out;
}

/// One assembled line/area part: its tile-independent web-mercator coords plus its
/// static lon/lat bbox [w,s,e,n] and the matching normalised world bbox
/// [min_wx,min_wy,max_wx,max_wy]. Both are computed ONCE here so the baker's
Expand Down Expand Up @@ -1596,13 +1635,23 @@ fn processFeatureParsed(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize,
var stroke_proj: []const []const mvt.Point = projected.items;
if (!opts.suppress_lines and p.lines.len > 0 and cell.needsDrawableBoundary(f)) {
var stroke_storage = std.ArrayList([]mvt.Point).empty;
const dparts = cell.drawableLineParts(a, f) catch &[_][]s57.LonLat{};
// The baker's per-cell drawn-boundary cache: the drawableLineParts subset plus each
// vertex's precomputed world coord. Reproject the cached world with a linear
// worldToTile instead of the transcendental projection on every tile — the drawn
// boundary is tile-invariant. Live path (no cache / single tile) reconstructs and
// projects directly; byte-identical, since project == worldToTile ∘ lonLatToWorld.
const cached: ?s57.DrawnBoundary = if (cell.drawn_boundary) |db| (if (fi < db.len) db[fi] else null) else null;
const dparts: []const []s57.LonLat = if (cached) |c| c.parts else (cell.drawableLineParts(a, f) catch &[_][]s57.LonLat{});
stroke_geo = dparts;
for (dparts) |dp| {
for (dparts, 0..) |dp, pi| {
if (dp.len < 2) continue;
if (!overlaps(geomBounds(dp), tb)) continue;
const proj = try a.alloc(mvt.Point, dp.len);
for (dp, 0..) |p2, i| proj[i] = tile.project(p2.lon(), p2.lat(), z, x, y, tile.EXTENT);
if (cached) |c| {
for (c.world[pi], 0..) |ww, i| proj[i] = tile.worldToTile(ww, z, x, y, tile.EXTENT);
} else {
for (dp, 0..) |p2, i| proj[i] = tile.project(p2.lon(), p2.lat(), z, x, y, tile.EXTENT);
}
try stroke_storage.append(a, proj);
}
stroke_proj = stroke_storage.items;
Expand Down
Loading