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
50 changes: 26 additions & 24 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -585,10 +585,10 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("src/lib_root.zig"),
.target = target,
.optimize = optimize,
// iOS: std.debug's stack-trace machinery references
// _dyld_get_image_header_containing_address, which iOS' libdyld doesn't
// export — strip so the panic path never pulls it in.
.strip = target.result.os.tag == .ios,
// iOS and visionOS: std.debug's stack-trace machinery references
// _dyld_get_image_header_containing_address, which those libdylds do
// not export. Strip so the panic path never pulls it in.
.strip = target.result.os.tag == .ios or target.result.os.tag == .visionos,
.pic = true, // links into a PIE C++ host
.link_libc = true, // Lua needs the C runtime
});
Expand Down Expand Up @@ -703,27 +703,25 @@ pub fn build(b: *std.Build) void {
else
target;

const bake = b.addExecutable(.{
.name = "tile57",
.root_module = b.createModule(.{
.root_source_file = b.path("tools/main.zig"),
.target = bake_target,
.optimize = optimize,
.link_libc = true,
.imports = &.{
.{ .name = "engine", .module = engine_full },
.{ .name = "style", .module = style_mod },
.{ .name = "sprite", .module = sprite_mod },
.{ .name = "catalog", .module = catalog_embed },
.{ .name = "bundle", .module = bundle_mod },
.{ .name = "compose", .module = compose_mod }, // compose-tile CLI
.{ .name = "geometry", .module = geometry_mod }, // compose-tile --scan reads the boolean diagnostics
.{ .name = "render", .module = render_mod }, // renderpng pixel path
.{ .name = "chart", .module = chart_mod }, // ENC_ROOT view renders
.{ .name = "raster", .module = raster_mod }, // `raster info`
},
}),
const bake_mod = b.createModule(.{
.root_source_file = b.path("tools/main.zig"),
.target = bake_target,
.optimize = optimize,
.link_libc = true,
.imports = &.{
.{ .name = "engine", .module = engine_full },
.{ .name = "style", .module = style_mod },
.{ .name = "sprite", .module = sprite_mod },
.{ .name = "catalog", .module = catalog_embed },
.{ .name = "bundle", .module = bundle_mod },
.{ .name = "compose", .module = compose_mod }, // compose-tile CLI
.{ .name = "geometry", .module = geometry_mod }, // compose-tile --scan reads the boolean diagnostics
.{ .name = "render", .module = render_mod }, // renderpng pixel path
.{ .name = "chart", .module = chart_mod }, // ENC_ROOT view renders
.{ .name = "raster", .module = raster_mod }, // `raster info`
},
});
const bake = b.addExecutable(.{ .name = "tile57", .root_module = bake_mod });
b.installArtifact(bake);

const run_bake = b.addRunArtifact(bake);
Expand Down Expand Up @@ -808,6 +806,10 @@ pub fn build(b: *std.Build) void {
// module import does NOT pull another module's `test {}` blocks in.
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = mod })).step);
// The CLI's own tests. They had never run: nothing but the executable
// referenced tools/*.zig, and an executable does not collect tests — so a
// test written beside the code it checks was silently dead.
test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = bake_mod })).step);
// The sprite module (SDF glyph atlas lives here): needs the C glue
// (stb_truetype/nanosvg) + libc + render.
const sprite_test = addPkgTest(b, test_step, "src/sprite/sprite.zig", target, optimize, &.{
Expand Down
59 changes: 59 additions & 0 deletions include/tile57.h
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,31 @@ tile57_status tile57_bake_tree(const char *in_dir, const char *out_dir, uint32_t
tile57_bake_progress progress, void *progress_ctx,
uint32_t *out_baked, tile57_error *err);

/* tile57_bake_tree for an exchange set STILL IN ITS .zip: find the cells, name
* every output, bake. Nothing is unpacked — each cell is inflated as its turn
* comes, so importing an 800 MB archive never costs the disk a second copy of
* the source.
*
* Outputs mirror each entry's path BELOW the directory the archive wraps
* everything in. NOAA's All_ENCs.zip puts every cell under `ENC_ROOT/`, and
* that name belongs to the archive rather than to the library being built: an
* `out_dir` of `.../ENC_ROOT` writes `.../ENC_ROOT/US5MD12M/US5MD12M.pmtiles`,
* NOT a second ENC_ROOT inside the first. The prefix is computed from what the
* cells actually share, so an archive holding two districts keeps them apart —
* two districts carrying the same boundary cell keep their own copies, and
* each cell's referenced text lands beside the right chart. A cell sharing its
* directory with no other gets a directory named for itself.
*
* Same contract as tile57_bake_tree otherwise: `workers` is a MEMORY bound,
* `progress` (NULL to skip) fires per chart and may CANCEL by returning false,
* *out_baked (NULL to ignore) counts what was written. An archive holding no
* .000 is TILE57_OK with *out_baked 0 — nothing to do is not a failure. A host
* that wants each finished chart NAMED owns the list itself and calls
* tile57_bake_zip_charts. */
tile57_status tile57_bake_zip(const char *zip_path, const char *out_dir, uint32_t workers,
tile57_bake_progress progress, void *progress_ctx,
uint32_t *out_baked, tile57_error *err);

/* Names the chart that just finished, by its INDEX into the caller's in_paths.
* Charts bake concurrently, so the count in tile57_bake_progress cannot say
* which chart a step belongs to. Called from worker threads, out of order, so
Expand Down Expand Up @@ -1442,6 +1467,29 @@ tile57_status tile57_compose_tile(tile57_compose *c, uint8_t z, uint32_t x, uint
uint8_t **out, size_t *out_len, bool *out_owned,
tile57_error *err);

/* How wide YOU draw a tile: 256 (the default, the native convention) or 512 (the
* MapLibre style spec's world tile).
*
* A vector tile from tile57_compose_tile has its complex (symbolised) linestyles
* WALKED into plain geometry — each dash "on" run a solid line, each embedded
* symbol a point rotated to the local tangent at its own offset in the period,
* phased from the run's arc origin. S-101 lays those figures out in
* 256-px-per-tile space, so this is what the rhythm is restated in. It moves
* spacing only: never stroke width, never symbol size. The wrong value is
* invisible in the geometry and plain on the chart, as the right rhythm at the
* wrong spacing.
*
* A composed tile is drawn from a style, and a style cannot say where in a
* period a symbol sits, which is why the walk is not optional: ACHARE51 places
* four symbols at 5, 13.1, 21.2 and 29.3 mm of its 32.3 mm period, and a style
* can only ask for "one per period". The archives are untouched — they keep the
* stored run, which is display-independent, and the engine's own replay walks it
* at whatever scale it draws at.
*
* 0 is ignored. A raster compositor ignores this entirely; pictures carry no
* linestyles. */
void tile57_compose_set_px_per_tile(tile57_compose *c, uint32_t px_per_tile);

/* The composed view outputs — tile57_chart_png / tile57_chart_pdf / tile57_chart_canvas /
* tile57_chart_surface across the WHOLE composed set: every covering tile is
* composed on demand (stitched through the ownership partition) and
Expand Down Expand Up @@ -1582,6 +1630,17 @@ tile57_status tile57_bake_assets(const char *catalog_dir, tile57_assets *out,
tile57_status tile57_bake_sprite_mln(const char *catalog_dir, double pixel_ratio,
tile57_scheme scheme,
tile57_assets *out, tile57_error *err);
/* Render ONE comma-joined symbol run (a sounding digit stack such as
* "SOUNDG11,SOUNDG53") to a pivot-centred RGBA image at pixel_ratio, coloured
* for `scheme`. The runtime path behind a map client's missing-image event: a
* chart library carries more distinct runs than any prebaked sheet can
* enumerate, so the host renders exactly the ones the display asks for.
* TILE57_OK with *out_rgba NULL when the run names no known glyph (absent,
* not an error). Free *out_rgba with tile57_free. */
tile57_status tile57_render_symbol_run(const char *catalog_dir, const char *run,
double pixel_ratio, int scheme,
uint8_t **out_rgba, uint32_t *out_w,
uint32_t *out_h, tile57_error *err);
/* SDF glyph atlas for GPU text: sprite_png is the RGBA signed-distance-field atlas
* of the label font; sprite_json is {"em_px","pad","glyphs":{codepoint:[u0,v0,u1,
* v1,off_x,off_y,w,h,advance]}} with the quad geometry in EM units (multiply by the
Expand Down
9 changes: 9 additions & 0 deletions src/bake_root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,12 @@ pub const zipsrc = root.zipsrc; // charts read straight out of a .zip
pub const geometry = @import("geometry"); // integer geometry: boolean, plane, partition

pub const portray = @import("portray");

// A test binary rooted on a consumer of this module links lua_shim.c through
// `portray` but analyses only what its tests reach. Reference the accessor
// files here so their tgp_* / tgc_* / tg_embedded_lua exports are emitted into
// every binary that carries the shim, as lib_root.zig does for the archive.
comptime {
_ = portray;
_ = catalogue;
}
20 changes: 20 additions & 0 deletions src/bundle.zig
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,19 @@ pub fn spriteMlnBytes(io: std.Io, a: std.mem.Allocator, catalog_dir: []const u8,
return sprite.spriteMln(a, symbols, fills, css, soundings, ratio);
}

/// Render one comma-joined symbol run (sounding digit stack) to a
/// pivot-centred RGBA image — the runtime answer to MapLibre's missing-image
/// event, for the runs no prebaked sheet can enumerate. Null when the run
/// names no known glyph. Caller owns the pixels (allocated in `a`).
pub fn symbolRunImage(io: std.Io, a: std.mem.Allocator, catalog_dir: []const u8, css_name: []const u8, run: []const u8, ratio: f64) !?sprite.RunImage {
var arena = std.heap.ArenaAllocator.init(a);
defer arena.deinit();
const ar = arena.allocator();
const symbols = try readSymbols(io, ar, catalog_dir);
const css = try readCss(io, ar, catalog_dir, css_name);
return sprite.renderSymbolRun(a, symbols, css, run, ratio);
}

// Emit sprite-mln.{json,png} + identical @2x copies (MapLibre requests @2x on
// HiDPI; a missing @2x sheet fails the whole sprite load).
pub fn emitSpriteMln(io: std.Io, a: std.mem.Allocator, catalog_dir: []const u8, css_name: []const u8, out_dir: []const u8, base: []const u8, soundings: []const []const u8) !sprite.Atlas {
Expand Down Expand Up @@ -527,3 +540,10 @@ fn emitFacesZoom(sw: *engine.pmtiles.StreamWriter, sa: std.mem.Allocator, faces:
try sw.add(z, kv.key_ptr.x, kv.key_ptr.y, enc);
}
}

test {
// A test binary rooted here links lua_shim.c through engine's portray
// module but analyses only what its tests reach. Referencing engine scans
// bake_root.zig, whose comptime block emits the shim's C-ABI accessors.
_ = engine;
}
124 changes: 119 additions & 5 deletions src/capi.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
const std = @import("std");
const chart = @import("chart.zig");
const auxfiles = @import("engine").auxfiles; // via the named module: engine owns the file
const scene = @import("engine").scene; // tile surface + the complex-linestyle walk
const mlt_dec = @import("tiles").mlt; // decoding a verbatim composed tile
const s57 = @import("s57");
const bundle = @import("bundle"); // portrayal-asset emitters + the partition debug bake
const compose = @import("compose"); // the runtime tile compositor (tile57_compose_*)
Expand All @@ -20,7 +22,9 @@ const raster = @import("raster"); // raster charts (tile57_raster_chart_*)
const zipsrc = @import("zipsrc"); // charts read straight out of a .zip
// The S-52 ColorProfiles/colorProfile.xml baked into the library (build.zig), so
// the style C ABI generates colortables + a base style template with no on-disk
// catalogue. Symbols/linestyles are NOT embedded here (only the bake exe needs them).
// catalogue. The full catalogue (symbols, linestyles, css) is embedded too, via
// the `catalog` module bundle.zig imports — tile57_style_template reads the
// analysed linestyles from it and tile57_bake_sprite_mln the symbol SVGs.
const colorprofile_registry = @import("colorprofile_registry");

// c_allocator, not smp_allocator: smp never returns freed slabs to the OS, so
Expand Down Expand Up @@ -386,6 +390,29 @@ export fn tile57_bake_tree(
return OK;
}

/// Bake a whole exchange set out of its .zip in one call — the archive twin of
/// tile57_bake_tree, naming every output itself. See tile57.h.
export fn tile57_bake_zip(
zip_path: ?[*:0]const u8,
out_dir: ?[*:0]const u8,
workers: u32,
progress: chart.BakeProgress,
progress_ctx: ?*anyopaque,
out_baked: ?*u32,
err: ?*CError,
) callconv(.c) c_int {
if (out_baked) |p| p.* = 0;
const zp = spanOpt(zip_path) orelse return failWith(err, .badarg, "zip_path must not be null");
const out_d = spanOpt(out_dir) orelse return failWith(err, .badarg, "out_dir must not be null");
// No label callback, for the same reason tile57_bake_tree has none: this
// ABI reports progress as a count, and naming a chart would put a string
// across the seam. A caller that wants names owns the list and calls
// tile57_bake_zip_charts instead.
const baked = chart.bakeZip(sharedIo(), zp, out_d, null, workers, progress, progress_ctx, null) catch |e| return failCtx(err, e, zp);
if (out_baked) |p| p.* = @intCast(baked);
return OK;
}

/// Bake the cells at `in_paths` to the archives at `out_paths`, in parallel.
/// The CALLER owns the list, so `label` names each finished chart by its index
/// into it and no string crosses the ABI. See tile57.h.
Expand Down Expand Up @@ -1706,16 +1733,63 @@ export fn tile57_compose_tile(
const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out);
if (out_owned) |p| p.* = false;
const src = handle orelse return failWith(err, .badarg, "compose handle must not be null");
// Pictures carry no linestyles, so a raster compositor takes the plain path.
if (src.kind == .vector) return composeWalked(src, z, x, y, o, n, out_owned, err);
const res = src.tile(gpa, z, x, y) catch |e| return fail(err, e);
if (out_owned) |p| p.* = res.owned;
if (res.tile) |t| return exportOut(err, o, n, t);
return OK;
}

/// Render a VIEW over the compositor to PNG — the composed twin of tile57_chart_png:
/// every covering tile is composed on demand (seams stitched through the
/// ownership partition) and replayed through the native S-52 pixel path. See
/// tile57.h.
/// tile57_compose_tile's body for a vector compositor: compose to FEATURES,
/// step every complex linestyle into plain geometry, re-encode.
///
/// Always, rather than on request. A composed tile is drawn from a STYLE, and a
/// style cannot say where in a linestyle period a symbol sits — so the
/// un-walked form draws every symbol of a style at one phase and loses the S-52
/// rhythm. The only thing that can re-walk a stored run is the engine's own
/// replay, which reads tileContent directly and never comes through here.
///
/// The compositor answers `.layers` where a tile seam-composes and `.bytes`
/// where one cell owns the whole tile and its stored blob passes through
/// verbatim. Both must be walked, or every tile away from a cell boundary
/// silently goes missing.
fn composeWalked(
src: *compose.ComposeSource,
z: u8,
x: u32,
y: u32,
o: *?[*]u8,
n: *usize,
out_owned: ?*bool,
err: ?*CError,
) c_int {
// The decoded features live only long enough to be walked: an arena for
// the compose and the walk's scratch, the encoded tile alone in gpa.
var arena = std.heap.ArenaAllocator.init(gpa);
defer arena.deinit();
const ta = arena.allocator();
const px: f64 = @floatFromInt(src.draw_px_per_tile);

const res = src.tileContent(ta, z, x, y) catch |e| return fail(err, e);
const layers = switch (res.content) {
.layers => |l| l,
.bytes => |b| mlt_dec.decode(ta, b) catch |e| return fail(err, e),
.none => return OK,
};
const bytes = scene.walkTile(ta, gpa, layers, .mlt, z, px) catch |e| return fail(err, e);
if (out_owned) |p| p.* = true;
return exportOut(err, o, n, bytes);
}

/// How wide the caller draws a tile: 256 for the native convention, 512 for the
/// MapLibre style spec's world tile. Sets what composed linestyle rhythms are
/// restated in. See tile57.h.
export fn tile57_compose_set_px_per_tile(handle: ?*compose.ComposeSource, px_per_tile: u32) void {
const src = handle orelse return;
if (px_per_tile != 0) src.draw_px_per_tile = px_per_tile;
}

export fn tile57_compose_png(
handle: ?*compose.ComposeSource,
lon: f64,
Expand Down Expand Up @@ -1999,6 +2073,46 @@ export fn tile57_bake_sprite_mln(catalog_dir: ?[*:0]const u8, pixel_ratio: f64,
return OK;
}

/// Render one comma-joined symbol run (a sounding digit stack like
/// "SOUNDG11,SOUNDG53") to a pivot-centred RGBA image at `pixel_ratio`, in
/// the palette of `scheme`. The runtime path behind MapLibre's missing-image
/// event: a chart library carries more distinct runs than a prebaked sheet
/// can enumerate, so the host renders exactly the ones the map asks for.
/// TILE57_OK with *out_rgba NULL when the run names no known glyph (absent,
/// not an error). Free *out_rgba with tile57_free.
export fn tile57_render_symbol_run(
catalog_dir: ?[*:0]const u8,
run: ?[*:0]const u8,
pixel_ratio: f64,
scheme: c_int,
out_rgba: ?*?[*]u8,
out_w: ?*u32,
out_h: ?*u32,
err: ?*CError,
) callconv(.c) c_int {
const o = out_rgba orelse return failWith(err, .badarg, "out_rgba must not be null");
const ow = out_w orelse return failWith(err, .badarg, "out_w must not be null");
const oh = out_h orelse return failWith(err, .badarg, "out_h must not be null");
o.* = null;
ow.* = 0;
oh.* = 0;
const r = run orelse return failWith(err, .badarg, "run must not be null");
const cd = spanOpt(catalog_dir) orelse "";
const ratio = if (pixel_ratio > 0) pixel_ratio else 1;
const img = bundle.symbolRunImage(sharedIo(), gpa, cd, svgCssFor(scheme), std.mem.span(r), ratio) catch |e| return fail(err, e);
if (img) |i| {
// Through the export-header convention (exportAlloc/tile57_free) like
// every other buffer this ABI hands out; the engine copy is freed here.
defer gpa.free(i.rgba);
const p = exportAlloc(i.rgba.len) orelse return failWith(err, .nomem, "out of memory");
@memcpy(p[0..i.rgba.len], i.rgba);
o.* = p;
ow.* = i.w;
oh.* = i.h;
}
return OK;
}

const glyph_sdf = @import("sprite").glyph;

// Glyph metrics as compact JSON: {"em_px","pad","glyphs":{cp:[u0,v0,u1,v1,ox,oy,w,h,adv]}}.
Expand Down
Loading
Loading