From 2b1f704759a0c8370b6ca46ac22e6d7bb94ed2f4 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Mon, 24 Aug 2026 16:23:28 -0400 Subject: [PATCH 1/5] Add CI and tagged releases, and build a shared library A vX.Y.Z tag builds macOS, Linux and Windows on arm64 and x86_64, then attaches an archive per target to a GitHub release. Linux also gets a Debian package. CI runs the same targets on every push and pull request. Each archive holds the C header, the static library and the shared library. `zig build shared` writes libcharttable.so, the .dylib, or charttable.dll with its import library. The soname comes from the version in build.zig.zon. The release builds the two linkages into separate prefixes, because Windows gives the static archive and the import library the same name. The Vulkan loader now sits on the test module, not the library module. Only the test binary has to name it. Before this, every artifact carried -lvulkan, and no cross target could find one. -Dcodec-source now works on any target, not just Windows and Android. The system headers of a build machine describe that machine, so a cross build cannot take the codecs from the platform. Every release build passes the option, so the libraries carry their own libwebp and libpng. Run zig fmt over map_object.zig and codecs.zig, so the format check passes. --- .github/workflows/ci.yml | 101 ++++++++++++ .github/workflows/release.yml | 94 +++++++++++ .gitignore | 4 + README.md | 27 ++- build.zig | 298 ++++++++++++++++++++-------------- build/codecs.zig | 6 +- scripts/package-release.sh | 95 +++++++++++ src/map_object.zig | 12 +- 8 files changed, 502 insertions(+), 135 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100755 scripts/package-release.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2629699 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,101 @@ +name: CI + +on: + # push only on main; PR branches are covered by pull_request. Scoping push this + # way avoids the double run (a push to a branch with an open PR otherwise fires + # both events). + push: + branches: [main] + pull_request: + +# charttable builds with Zig 0.16 into libcharttable.a, libcharttable.so and the +# C header. Every consumer takes one of those three, so CI builds all of them. +# +# Three jobs, because no single runner compiles the whole library: +# * build-test — the gate. Vulkan backend, system codecs, the full test suite. +# * macos — the only runner that compiles the Metal backend and the ObjC +# shim, and the only one that builds the demo app. +# * cross — every target a release ships, so a tag does not discover a +# broken cross build after the fact. +jobs: + build-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + # The test binary is a consumer of the renderer, so it links the Vulkan + # loader. The codecs come from the system here, which is the default path + # off Windows and the one an embedder on Linux takes. + - name: Install the Vulkan loader and the codecs + run: | + sudo apt-get update + sudo apt-get install -y libvulkan-dev libwebp-dev libpng-dev + + - name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + + - name: Format check + run: zig fmt --check src/ build/ build.zig + + # Debug keeps the tests' safety checks. The cross matrix covers ReleaseFast. + # The GPU tests open a device and skip themselves when the runner has none, + # so the suite passes headless. + - name: Build and test + run: zig build install test + + - name: Build the shared library + run: zig build shared + + macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v7 + + # The default codec path on macOS: Homebrew's static archives, which is + # what a developer on this platform links. + - name: Install the codecs + run: brew install webp libpng + + - name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + + # Compiles gpu_metal.zig and metal_shim.m, which no other runner reaches. + # Build only: the Metal tests need a real device, and this runner is not + # promised one. + - name: Build the libraries + run: zig build lib shared -Doptimize=ReleaseFast + + - name: Build the chart demo + run: zig build example + + cross: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - { target: x86_64-linux-gnu } + - { target: aarch64-linux-gnu } + - { target: x86_64-windows-gnu } + - { target: aarch64-windows-gnu } + name: ${{ matrix.target }} + steps: + - uses: actions/checkout@v7 + + - name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + + # -Dcodec-source, as the release builds use: the system headers of this + # runner describe this runner, so a cross build compiles its own libwebp + # and libpng instead. Builds both linkages, because the shared one is a + # real link and fails where the archive still builds. + - name: Cross-compile ${{ matrix.target }} + run: | + zig build lib shared -Doptimize=ReleaseFast -Dcodec-source \ + -Dtarget=${{ matrix.target }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d995da4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,94 @@ +name: Release + +# Push a `vX.Y.Z` tag to cut a release: every supported target is built and the +# archives are attached to a GitHub release. A tag carrying a suffix +# (`v0.2.0-rc1`) publishes as a prerelease — use one to prove the matrix before +# the real tag. +on: + push: + tags: ["v*"] + +permissions: + contents: write + +jobs: + build: + name: ${{ matrix.target }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + # macOS builds run natively per arch: the Metal backend compiles the + # ObjC shim against the platform SDK, which no other runner carries. + - { target: aarch64-macos, os: macos-latest } + - { target: x86_64-macos, os: macos-15-intel } + - { target: x86_64-linux-gnu, os: ubuntu-latest, deb: amd64 } + - { target: aarch64-linux-gnu, os: ubuntu-latest, deb: arm64 } + - { target: x86_64-windows-gnu, os: ubuntu-latest } + - { target: aarch64-windows-gnu, os: ubuntu-latest } + steps: + - uses: actions/checkout@v7 + + - name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + + # build.zig.zon is the one place the version is written, and the shared + # library takes its soname from it. The tag is the second place, and a + # mismatch ships a libcharttable.so.0 whose download says something else. + - name: Check the tag against the version in the source + run: | + v="${GITHUB_REF_NAME#v}"; v="${v%%-*}" + grep -q "\.version = \"$v\"" build.zig.zon + + # Two prefixes: on Windows the static archive and the DLL's import library + # are both charttable.lib, so one prefix loses the archive. + # + # -Dcodec-source everywhere, so every released library carries its own + # libwebp and libpng rather than hunting for them on the target machine. + - name: Build + run: | + for step in lib shared; do + case $step in lib) p=out/static ;; shared) p=out/shared ;; esac + zig build "$step" -p "$p" \ + -Dtarget=${{ matrix.target }} -Doptimize=ReleaseFast -Dcodec-source + done + + - name: Package + run: scripts/package-release.sh "${GITHUB_REF_NAME#v}" "${{ matrix.target }}" "${{ matrix.deb }}" + + - uses: actions/upload-artifact@v7 + with: + name: ${{ matrix.target }} + path: dist/* + + release: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v8 + with: + path: dist + merge-multiple: true + + - name: Checksums + run: cd dist && shasum -a 256 charttable-* libcharttable-* > SHA256SUMS + + - name: Publish + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + # A release may already exist: cutting one from the web UI is what + # created the tag that started this run, and a re-run after a failed + # leg lands here twice. Attach to it instead of failing on the 422, + # and leave hand-written notes alone. + if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then + gh release upload "$GITHUB_REF_NAME" dist/* --clobber + else + gh release create "$GITHUB_REF_NAME" --generate-notes --verify-tag \ + ${{ contains(github.ref_name, '-') && '--prerelease' || '' }} \ + dist/* + fi diff --git a/.gitignore b/.gitignore index d527deb..901e373 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ specs/ # Example app build output examples/macos/chartview + +# Where scripts/package-release.sh builds and stages a release +out/ +dist/ diff --git a/README.md b/README.md index 7b98f34..4cc8542 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,32 @@ - **Native everywhere the engine goes.** The same scene contract drives Metal, Vulkan, D3D12 and SDL backends. +## Install + +Every `vX.Y.Z` tag publishes an archive per target on +[Releases](https://github.com/beetlebugorg/charttable/releases). Each archive +holds the C header, the static library and the shared library: + +| Target | Static | Shared | +| --- | --- | --- | +| macOS, arm64 and x86_64 | `libcharttable.a` | `libcharttable.dylib` | +| Linux, arm64 and x86_64 | `libcharttable.a` | `libcharttable.so` | +| Windows, arm64 and x86_64 | `charttable.lib` | `charttable.dll` + `charttable.dll.lib` | + +Linux also gets a Debian package: +`apt install ./libcharttable-dev__.deb`. + +The released libraries carry their own libwebp and libpng. They leave the +Vulkan loader to the program that links them. + +## Build + +Zig 0.16.0. `zig build test` runs the suite. `zig build lib` writes +`libcharttable.a` and the header into `zig-out`; `zig build shared` writes the +shared library beside them. Add `-Dcodec-source` to compile libwebp and libpng +in instead of linking the platform copies — a cross build needs it. + ## Status Early extraction. See DESIGN.md for the architecture, the conformance tiers, -and the milestone plan. Building: `zig build test` (Zig 0.16.0). +and the milestone plan. diff --git a/build.zig b/build.zig index fbec2a4..0d6ae69 100644 --- a/build.zig +++ b/build.zig @@ -1,5 +1,8 @@ const std = @import("std"); const codecs = @import("build/codecs.zig"); +/// The manifest is the one place the version is written. The shared library +/// stamps it into its soname, so a consumer links against an ABI, not a file. +const zon = @import("build.zig.zon"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); @@ -30,84 +33,17 @@ pub fn build(b: *std.Build) void { .link_libc = true, }); - if (android) { - // bionic's nullability-on-array declarations ("const struct timeval - // _Nonnull [2]") break translate-c, and the @cImports here (png.h, - // vulkan.h) reach sys/ headers through the NDK sysroot. The - // annotations are hints only; defining them empty drops them for our - // parse and the C compiles alike — the same neutralisation - // lookout-marine applies to its own module. - mod.addCMacro("_Nonnull", ""); - mod.addCMacro("_Nullable", ""); - mod.addCMacro("_Null_unspecified", ""); - } - - // The Metal backend (src/gpu/gpu_metal.zig + metal_shim.m). The shader - // source rides an anonymous import (`@embedFile("metal_msl")`) and is - // compiled by the shim at runtime — no offline shader toolchain. The - // import itself is target-independent; only the ObjC shim and the - // frameworks are Apple-gated (non-mac targets select gpu_none.zig and - // never analyze the Metal backend). - mod.addAnonymousImport("metal_msl", .{ .root_source_file = b.path("shaders/metal.metal") }); - - // The Vulkan backend (src/gpu/gpu_vk.zig). Everything off Apple draws with - // it. Unlike Metal there is no runtime shader compiler, so the programs - // ride in precompiled as SPIR-V; shaders/vk/README.md holds the command - // that regenerates them. The headers are vendored with no VK_USE_PLATFORM_* - // (those drag in the X11, Wayland and Windows SDKs) and the loader is - // linked by the consumer, which is what lets one build serve every window - // system. - // The D3D12 backend (src/gpu/gpu_d3d12.zig). The HLSL rides in as source - // and is compiled by d3dcompiler_47.dll at open, so there is no offline - // shader toolchain and no import library: d3d12.dll, dxgi.dll and the - // compiler are all loaded by name at runtime (src/gpu/c_d3d12.zig). - if (use_d3d12) { - const hlsl = [_][2][]const u8{ - .{ "d3d12_fill_vert", "shaders/d3d12/fill.vert.hlsl" }, - .{ "d3d12_fill_frag", "shaders/d3d12/fill.frag.hlsl" }, - .{ "d3d12_pattern_vert", "shaders/d3d12/pattern.vert.hlsl" }, - .{ "d3d12_pattern_frag", "shaders/d3d12/pattern.frag.hlsl" }, - .{ "d3d12_sprite_vert", "shaders/d3d12/sprite.vert.hlsl" }, - .{ "d3d12_sprite_frag", "shaders/d3d12/sprite.frag.hlsl" }, - .{ "d3d12_sdf_frag", "shaders/d3d12/sdf.frag.hlsl" }, - .{ "d3d12_overlay_vert", "shaders/d3d12/overlay.vert.hlsl" }, - .{ "d3d12_overlay_frag", "shaders/d3d12/overlay.frag.hlsl" }, - }; - for (hlsl) |e| mod.addAnonymousImport(e[0], .{ .root_source_file = b.path(e[1]) }); - } - - if (use_vk) { - const spv = [_][2][]const u8{ - .{ "fill_vert_spv", "shaders/vk/fill.vert.spv" }, - .{ "fill_frag_spv", "shaders/vk/fill.frag.spv" }, - .{ "sprite_vert_spv", "shaders/vk/sprite.vert.spv" }, - .{ "sprite_frag_spv", "shaders/vk/sprite.frag.spv" }, - .{ "sdf_frag_spv", "shaders/vk/sdf.frag.spv" }, - .{ "pattern_vert_spv", "shaders/vk/pattern.vert.spv" }, - .{ "pattern_frag_spv", "shaders/vk/pattern.frag.spv" }, - .{ "overlay_vert_spv", "shaders/vk/overlay.vert.spv" }, - .{ "overlay_frag_spv", "shaders/vk/overlay.frag.spv" }, - }; - for (spv) |e| mod.addAnonymousImport(e[0], .{ .root_source_file = b.path(e[1]) }); - mod.addIncludePath(b.path("vendor/vulkan/include")); - } - - if (apple) { - mod.addIncludePath(b.path("src/gpu")); // metal_shim.h for the @cImport - // Manual retain/release on purpose — objects live in C structs - // (same pattern as lookout-marine's build.zig). - mod.addCSourceFile(.{ - .file = b.path("src/gpu/metal_shim.m"), - .flags = &.{ "-O2", "-fno-objc-arc", "-fno-sanitize=undefined" }, - }); - mod.linkFramework("Metal", .{}); - mod.linkFramework("QuartzCore", .{}); - mod.linkFramework("Foundation", .{}); - } + // The same module again, for `zig build test`. A test binary IS the + // consumer of the renderer, so it names the Vulkan loader itself; the + // library must not. Keeping the two apart is what leaves that one link + // input off every archive, shared library and cross build. + const test_mod = b.createModule(.{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }); - // Where the vendored spec conformance fixtures live (absolute, so the - // test binary finds them regardless of its own cwd). The harness skips - // itself when the directory is absent. // libwebp, when the host has it. Optional on purpose: charttable's own // PNG reader covers what tile servers usually send, and a hard // dependency would land on every embedder. Tile servers that serve WebP @@ -127,47 +63,133 @@ pub fn build(b: *std.Build) void { const d = b.option([]const u8, "codec-dir", "Directory of cross-built codec archives (include/ + lib/)") orelse break :blk null; break :blk if (d.len == 0) null else d; }; - // Under a sysroot the SDK's own headers are not on the search path for - // this module's C sources, and a framework header that includes a plain - // one (Security.h -> libDER/DERItem.h) stops resolving. The host build - // passes a sysroot on every Xcode cross build. - if (b.sysroot) |sr| { - mod.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ sr, "usr/include" }) }); - } - if (use_webp or use_libpng) { - // Windows and Android carry neither Homebrew nor a system copy of - // these, and both options default to on, so the sources are fetched - // by the package manager and compiled in (build/codecs.zig) — zlib - // included, so nothing here asks the linker to search for -lz. - // Naming a codec-dir still wins, for a build with its own archives. - if ((windows or android) and codec_dir == null) { - _ = codecs.addFromSource(b, mod, target, use_webp, use_libpng); - } else { - if (codec_dir) |dir| { - mod.addIncludePath(.{ .cwd_relative = b.pathJoin(&.{ dir, "include" }) }); - } else if (target.result.os.tag == .macos) { - mod.addIncludePath(.{ .cwd_relative = "/opt/homebrew/include" }); - } - if (use_webp) linkCodec(b, mod, target, codec_dir, "webp"); - if (use_libpng) { - linkCodec(b, mod, target, codec_dir, "png16"); - // libpng's objects arrive with their zlib symbols undefined, so - // zlib has to resolve here as well as in whatever links this. - linkZlib(b, mod); - } - } - } - + // Compile the codecs from source instead of taking them from the platform. + // Windows and Android have neither Homebrew nor a system copy, so they turn + // it on by themselves. A cross build asks for it too: the system headers of + // the host describe the host, and a release wants an archive that carries + // its own decoders rather than one that hunts for them on the target. + const codec_source = b.option(bool, "codec-source", "Compile libwebp and libpng from source") orelse (windows or android); const ct_opts = b.addOptions(); ct_opts.addOption(bool, "webp", use_webp); ct_opts.addOption(bool, "libpng", use_libpng); // Which renderer src/gpu/gpu.zig selects on Windows. ct_opts.addOption(bool, "gpu_d3d12", use_d3d12); + // Where the vendored spec conformance fixtures live (absolute, so the + // test binary finds them regardless of its own cwd). The harness skips + // itself when the directory is absent. ct_opts.addOption([]const u8, "spec_fixture_dir", b.pathFromRoot("test/spec/expression")); ct_opts.addOption([]const u8, "report_path", b.pathFromRoot("test/spec/conformance-failures.txt")); ct_opts.addOption([]const u8, "assets_dir", b.pathFromRoot("test/assets")); ct_opts.addOption([]const u8, "out_dir", b.pathFromRoot("zig-out")); - mod.addOptions("ct_build", ct_opts); + + // Both modules take the same configuration. Only the test module names the + // Vulkan loader (below), so no artifact a consumer takes away carries it. + for ([_]*std.Build.Module{ mod, test_mod }) |m| { + if (android) { + // bionic's nullability-on-array declarations ("const struct timeval + // _Nonnull [2]") break translate-c, and the @cImports here (png.h, + // vulkan.h) reach sys/ headers through the NDK sysroot. The + // annotations are hints only; defining them empty drops them for our + // parse and the C compiles alike — the same neutralisation + // lookout-marine applies to its own module. + m.addCMacro("_Nonnull", ""); + m.addCMacro("_Nullable", ""); + m.addCMacro("_Null_unspecified", ""); + } + + // The Metal backend (src/gpu/gpu_metal.zig + metal_shim.m). The shader + // source rides an anonymous import (`@embedFile("metal_msl")`) and is + // compiled by the shim at runtime — no offline shader toolchain. The + // import itself is target-independent; only the ObjC shim and the + // frameworks are Apple-gated (non-mac targets select gpu_none.zig and + // never analyze the Metal backend). + m.addAnonymousImport("metal_msl", .{ .root_source_file = b.path("shaders/metal.metal") }); + + // The Vulkan backend (src/gpu/gpu_vk.zig). Everything off Apple draws with + // it. Unlike Metal there is no runtime shader compiler, so the programs + // ride in precompiled as SPIR-V; shaders/vk/README.md holds the command + // that regenerates them. The headers are vendored with no VK_USE_PLATFORM_* + // (those drag in the X11, Wayland and Windows SDKs) and the loader is + // linked by the consumer, which is what lets one build serve every window + // system. + // The D3D12 backend (src/gpu/gpu_d3d12.zig). The HLSL rides in as source + // and is compiled by d3dcompiler_47.dll at open, so there is no offline + // shader toolchain and no import library: d3d12.dll, dxgi.dll and the + // compiler are all loaded by name at runtime (src/gpu/c_d3d12.zig). + if (use_d3d12) { + const hlsl = [_][2][]const u8{ + .{ "d3d12_fill_vert", "shaders/d3d12/fill.vert.hlsl" }, + .{ "d3d12_fill_frag", "shaders/d3d12/fill.frag.hlsl" }, + .{ "d3d12_pattern_vert", "shaders/d3d12/pattern.vert.hlsl" }, + .{ "d3d12_pattern_frag", "shaders/d3d12/pattern.frag.hlsl" }, + .{ "d3d12_sprite_vert", "shaders/d3d12/sprite.vert.hlsl" }, + .{ "d3d12_sprite_frag", "shaders/d3d12/sprite.frag.hlsl" }, + .{ "d3d12_sdf_frag", "shaders/d3d12/sdf.frag.hlsl" }, + .{ "d3d12_overlay_vert", "shaders/d3d12/overlay.vert.hlsl" }, + .{ "d3d12_overlay_frag", "shaders/d3d12/overlay.frag.hlsl" }, + }; + for (hlsl) |e| m.addAnonymousImport(e[0], .{ .root_source_file = b.path(e[1]) }); + } + + if (use_vk) { + const spv = [_][2][]const u8{ + .{ "fill_vert_spv", "shaders/vk/fill.vert.spv" }, + .{ "fill_frag_spv", "shaders/vk/fill.frag.spv" }, + .{ "sprite_vert_spv", "shaders/vk/sprite.vert.spv" }, + .{ "sprite_frag_spv", "shaders/vk/sprite.frag.spv" }, + .{ "sdf_frag_spv", "shaders/vk/sdf.frag.spv" }, + .{ "pattern_vert_spv", "shaders/vk/pattern.vert.spv" }, + .{ "pattern_frag_spv", "shaders/vk/pattern.frag.spv" }, + .{ "overlay_vert_spv", "shaders/vk/overlay.vert.spv" }, + .{ "overlay_frag_spv", "shaders/vk/overlay.frag.spv" }, + }; + for (spv) |e| m.addAnonymousImport(e[0], .{ .root_source_file = b.path(e[1]) }); + m.addIncludePath(b.path("vendor/vulkan/include")); + } + + if (apple) { + m.addIncludePath(b.path("src/gpu")); // metal_shim.h for the @cImport + // Manual retain/release on purpose — objects live in C structs + // (same pattern as lookout-marine's build.zig). + m.addCSourceFile(.{ + .file = b.path("src/gpu/metal_shim.m"), + .flags = &.{ "-O2", "-fno-objc-arc", "-fno-sanitize=undefined" }, + }); + m.linkFramework("Metal", .{}); + m.linkFramework("QuartzCore", .{}); + m.linkFramework("Foundation", .{}); + } + + // Under a sysroot the SDK's own headers are not on the search path for + // this module's C sources, and a framework header that includes a plain + // one (Security.h -> libDER/DERItem.h) stops resolving. The host build + // passes a sysroot on every Xcode cross build. + if (b.sysroot) |sr| { + m.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ sr, "usr/include" }) }); + } + if (use_webp or use_libpng) { + // The sources are fetched by the package manager (build.zig.zon) and + // built here — zlib included, so nothing asks the linker to search for + // -lz. Naming a codec-dir still wins, for a build with its own archives. + if (codec_source and codec_dir == null) { + _ = codecs.addFromSource(b, m, target, use_webp, use_libpng); + } else { + if (codec_dir) |dir| { + m.addIncludePath(.{ .cwd_relative = b.pathJoin(&.{ dir, "include" }) }); + } else if (target.result.os.tag == .macos) { + m.addIncludePath(.{ .cwd_relative = "/opt/homebrew/include" }); + } + if (use_webp) linkCodec(b, m, target, codec_dir, "webp"); + if (use_libpng) { + linkCodec(b, m, target, codec_dir, "png16"); + // libpng's objects arrive with their zlib symbols undefined, so + // zlib has to resolve here as well as in whatever links this. + linkZlib(b, m); + } + } + } + m.addOptions("ct_build", ct_opts); + } // The C ABI as a static library: `zig build lib` drops libcharttable.a // and include/charttable.h into zig-out for a host to link (src/capi.zig @@ -177,15 +199,38 @@ pub fn build(b: *std.Build) void { .root_module = mod, .linkage = .static, }); - const lib_step = b.step("lib", "Build the static library + C header"); - lib_step.dependOn(&b.addInstallArtifact(lib, .{}).step); - lib_step.dependOn(&b.addInstallFileWithDir( + const header = b.addInstallFileWithDir( b.path("include/charttable.h"), .header, "charttable.h", - ).step); + ); + const lib_step = b.step("lib", "Build the static library + C header"); + lib_step.dependOn(&b.addInstallArtifact(lib, .{}).step); + lib_step.dependOn(&header.step); b.getInstallStep().dependOn(lib_step); + // The same C ABI as a shared library: `zig build shared` drops + // libcharttable.so (.dylib, or charttable.dll plus its import library) next + // to the archive. A host that loads the renderer at run time takes this one; + // a host that links it into its own binary takes the archive. + // + // Its own step, not part of the default build: a shared library is a final + // link, so it fails where the archive still builds — a target with no + // dynamic linker, for one. + // + // A cross build leaves the Vulkan loader undefined, as the archive does: the + // library draws through whichever loader the process already holds, and that + // is what lets one build serve every window system. + const shared = b.addLibrary(.{ + .name = "charttable", + .root_module = mod, + .linkage = .dynamic, + .version = std.SemanticVersion.parse(zon.version) catch @panic("bad version in build.zig.zon"), + }); + const shared_step = b.step("shared", "Build the shared library + C header"); + shared_step.dependOn(&b.addInstallArtifact(shared, .{}).step); + shared_step.dependOn(&header.step); + // `zig build example -Dtile57=` — the macOS demo. // // Built HERE rather than by hand-linking libcharttable.a: ld rejects the @@ -211,10 +256,15 @@ pub fn build(b: *std.Build) void { }); exe_mod.addIncludePath(b.path("include")); exe_mod.linkLibrary(lib); - if (use_webp) linkCodec(b, exe_mod, target, codec_dir, "webp"); - if (use_libpng) { - linkCodec(b, exe_mod, target, codec_dir, "png16"); - linkZlib(b, exe_mod); + // The library it links already carries the codecs when they come from + // source. Otherwise the exe names the platform archives, as the module + // does: the archive holds the calls, not the decoders. + if (!codec_source or codec_dir != null) { + if (use_webp) linkCodec(b, exe_mod, target, codec_dir, "webp"); + if (use_libpng) { + linkCodec(b, exe_mod, target, codec_dir, "png16"); + linkZlib(b, exe_mod); + } } for ([_][]const u8{ "Cocoa", "Metal", "QuartzCore", "CoreGraphics", "ImageIO", "UniformTypeIdentifiers" }) |fw| { exe_mod.linkFramework(fw, .{}); @@ -225,15 +275,13 @@ pub fn build(b: *std.Build) void { // `zig build test` — the gate. Every source module is referenced from // src/root.zig so its tests ride this one build. - const tests = b.addTest(.{ .root_module = mod }); + const tests = b.addTest(.{ .root_module = test_mod }); // The library leaves the Vulkan loader to whoever links it (the shells do, - // through meson/gradle/MSBuild), but a test binary IS the consumer, so it - // has to name the loader itself. tests.root_module IS `mod`, so this flag - // rides on every consumer too — harmless where the shell links the loader - // anyway, but an android cross-build has no -lvulkan to find, and its - // tests never run; the gradle/CMake link names the loader there. The - // D3D12 backend links nothing. - if (use_vk and !android) tests.root_module.linkSystemLibrary("vulkan", .{}); + // through meson/gradle/MSBuild), and the test binary is that consumer, so + // it names the loader here. An android cross-build is the exception: it has + // no -lvulkan to find, and its tests never run. The D3D12 backend links + // nothing. + if (use_vk and !android) test_mod.linkSystemLibrary("vulkan", .{}); const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&b.addRunArtifact(tests).step); } diff --git a/build/codecs.zig b/build/codecs.zig index 81f152e..14d1299 100644 --- a/build/codecs.zig +++ b/build/codecs.zig @@ -84,9 +84,9 @@ const webp_dsp_neon = [_][]const u8{ }; const zlib_src = [_][]const u8{ - "adler32.c", "compress.c", "crc32.c", "deflate.c", "gzclose.c", - "gzlib.c", "gzread.c", "gzwrite.c", "infback.c", "inffast.c", - "inflate.c", "inftrees.c", "trees.c", "uncompr.c", "zutil.c", + "adler32.c", "compress.c", "crc32.c", "deflate.c", "gzclose.c", + "gzlib.c", "gzread.c", "gzwrite.c", "infback.c", "inffast.c", + "inflate.c", "inftrees.c", "trees.c", "uncompr.c", "zutil.c", }; const png_src = [_][]const u8{ diff --git a/scripts/package-release.sh b/scripts/package-release.sh new file mode 100755 index 0000000..e39ccab --- /dev/null +++ b/scripts/package-release.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# Stage the built trees into the release archive for one target, and — when a +# Debian architecture is given — the matching .deb. Run it after both builds: +# +# zig build lib -p out/static -Dtarget= -Doptimize=ReleaseFast -Dcodec-source +# zig build shared -p out/shared -Dtarget= -Doptimize=ReleaseFast -Dcodec-source +# +# Everything lands in dist/. Invoked per target by .github/workflows/release.yml. +# +# Two prefixes, not one: on Windows the static archive and the DLL's import +# library are both named charttable.lib, so a single prefix loses the first to +# the second. The archive keeps that name here and the import library becomes +# charttable.dll.lib. +# +# Usage: package-release.sh [deb-arch] +set -euo pipefail + +version="$1" +target="$2" +deb_arch="${3:-}" + +root="$(cd "$(dirname "$0")/.." && pwd)" +out="$root/dist" +name="charttable-$version-$target" +stage="$out/$name" + +rm -rf "$stage" +mkdir -p "$stage/lib" "$stage/include" + +cp -a "$root"/out/static/lib/. "$stage/lib/" +case "$target" in + *windows*) + mkdir -p "$stage/bin" + cp "$root/out/shared/bin/charttable.dll" "$stage/bin/" + cp "$root/out/shared/lib/charttable.lib" "$stage/lib/charttable.dll.lib" + ;; + *macos*) + # -a keeps the soname symlinks (libcharttable.dylib -> libcharttable.0.dylib + # -> libcharttable.0.1.0.dylib) as links instead of three copies. + cp -a "$root"/out/shared/lib/libcharttable*.dylib "$stage/lib/" + ;; + *) + cp -a "$root"/out/shared/lib/libcharttable.so* "$stage/lib/" + ;; +esac + +cp "$root/include/charttable.h" "$stage/include/" +cp "$root/LICENSE" "$root/THIRD-PARTY-NOTICES.md" "$root/README.md" "$stage/" + +cd "$out" +case "$target" in + *windows*) rm -f "$name.zip" && zip -qry "$name.zip" "$name" ;; + *) tar czf "$name.tar.gz" "$name" ;; +esac + +# The same payload under /usr, for `apt install ./libcharttable-dev_*.deb`. +# There is no apt repository — the .deb is a release asset. One package, not the +# usual runtime/dev pair: a release asset is installed to build against, and +# splitting it would ship two files that are only ever taken together. +if [ -n "$deb_arch" ]; then + # dpkg reads `0.2.0-rc1` as upstream 0.2.0 with revision rc1, which sorts + # AFTER plain 0.2.0 and makes the final release look like a downgrade. `~` + # is the separator that sorts before, so 0.2.0~rc1 upgrades to 0.2.0. + deb_version="$(printf '%s' "$version" | sed 's/-/~/')" + pkg="$out/deb" + rm -rf "$pkg" + mkdir -p "$pkg/DEBIAN" "$pkg/usr/lib" "$pkg/usr/include" \ + "$pkg/usr/share/doc/libcharttable-dev" + cp -a "$stage/lib/." "$pkg/usr/lib/" + chmod 644 "$pkg/usr/lib/libcharttable.a" + install -m644 "$stage/include/charttable.h" "$pkg/usr/include/" + install -m644 "$root/LICENSE" "$pkg/usr/share/doc/libcharttable-dev/copyright" + cat > "$pkg/DEBIAN/control" < +Section: libdevel +Priority: optional +Homepage: https://github.com/beetlebugorg/charttable +Description: Native map renderer for the MapLibre style spec + charttable takes a MapLibre style and vector tiles and draws them straight to + the GPU, holding 60 fps through pan, pinch-zoom and rotation. Tiles lay out + into resident GPU buckets once, so a pan, zoom or rotation is a matrix change + rather than a rebuild. + . + Ships libcharttable.a, libcharttable.so and the C header. The Vulkan loader + stays undefined in both libraries: the program that links charttable names it. +EOF + dpkg-deb --build --root-owner-group "$pkg" "$out/libcharttable-dev_${deb_version}_${deb_arch}.deb" + rm -rf "$pkg" +fi + +rm -rf "$stage" +ls -l "$out" diff --git a/src/map_object.zig b/src/map_object.zig index f08384b..e983910 100644 --- a/src/map_object.zig +++ b/src/map_object.zig @@ -648,10 +648,10 @@ pub const Map = struct { std.debug.print( "ct-trace: {s} dirty={} partial={} building={} covHolds={} bz={d:.2} cov={d:.2} cam={d:.2} tgt={d:.2} anim={} gest={} wanted={d} have={d} pend={d}\n", .{ - act, self.dirty, self.partial, + act, self.dirty, self.partial, self.building, self.coverageHolds(), self.buildZoom(), - self.cov_zoom, self.cam.zoom, self.cam.target_zoom, - self.cam.animating(), self.gesturing(), self.wanted.items.len, + self.cov_zoom, self.cam.zoom, self.cam.target_zoom, + self.cam.animating(), self.gesturing(), self.wanted.items.len, have, self.pendingWanted(), }, ); @@ -2942,9 +2942,9 @@ test "Map: the cost of a zoom sweep" { "zoom OUT z16->z14, 8 wheel notches of one quantum: {d} ms, {d} rebuilds, {d} tiles tessellated\n" ++ " buckets reused overall: {d}\n", .{ - eased_ms, eased_rebuilds, eased_built, - frames, step_ms, m.rebuilds - r1, - m.tiles_built - b1, out_ms, m.rebuilds - r2, + eased_ms, eased_rebuilds, eased_built, + frames, step_ms, m.rebuilds - r1, + m.tiles_built - b1, out_ms, m.rebuilds - r2, m.tiles_built - b2, m.tiles_reused - reused0, }, ); From 61b090ad4fe98886ba69c15b6af7263ff7bbcddf Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Mon, 24 Aug 2026 16:40:04 -0400 Subject: [PATCH 2/5] Publish a Homebrew formula on release `brew install beetlebugorg/tap/charttable` now installs the header, the static library and the shared library. A tag writes Formula/charttable.rb into beetlebugorg/homebrew-tap, with the sha256 of each archive the release just published. A prerelease tag leaves the tap alone. The formula names the installed path as the dylib id. Zig writes a bare file name there, and dyld does not search the Homebrew prefix, so a program that links -lcharttable would not find the library at run time. The tap update needs HOMEBREW_TAP_TOKEN. Without it the job warns and the release still stands. --- .github/workflows/release.yml | 34 +++++++++++++ README.md | 11 ++++- scripts/brew-formula.sh | 89 +++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100755 scripts/brew-formula.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d995da4..bf2e58f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -92,3 +92,37 @@ jobs: ${{ contains(github.ref_name, '-') && '--prerelease' || '' }} \ dist/* fi + + homebrew: + needs: release + if: ${{ !contains(github.ref_name, '-') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: actions/download-artifact@v8 + with: + path: dist + merge-multiple: true + + # A token with write access to the tap repo. Without it the release still + # stands; only the tap goes stale. + - name: Update the Homebrew tap + env: + TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + run: | + if [ -z "$TAP_TOKEN" ]; then + echo "::warning::HOMEBREW_TAP_TOKEN is not set — skipping the tap update" + exit 0 + fi + v="${GITHUB_REF_NAME#v}" + git clone --depth 1 \ + "https://x-access-token:$TAP_TOKEN@github.com/${{ github.repository_owner }}/homebrew-tap" tap + mkdir -p tap/Formula + scripts/brew-formula.sh "$v" dist > tap/Formula/charttable.rb + cd tap + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add Formula/charttable.rb + git diff --cached --quiet || git commit -m "charttable $v" + git push diff --git a/README.md b/README.md index 4cc8542..6380570 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,16 @@ ## Install -Every `vX.Y.Z` tag publishes an archive per target on +macOS and Linux, through Homebrew: + +``` +brew install beetlebugorg/tap/charttable +``` + +That puts `charttable.h` under `include/` and the static and shared libraries +under `lib/`. + +Every `vX.Y.Z` tag also publishes an archive per target on [Releases](https://github.com/beetlebugorg/charttable/releases). Each archive holds the C header, the static library and the shared library: diff --git a/scripts/brew-formula.sh b/scripts/brew-formula.sh new file mode 100755 index 0000000..9df6f61 --- /dev/null +++ b/scripts/brew-formula.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# Print the Homebrew formula for a released version, reading each archive's +# sha256 out of (the tarballs that release.yml just published). +# The tap job pipes this into Formula/charttable.rb. +# +# Usage: brew-formula.sh +set -euo pipefail + +version="$1" +dist="$2" +# The repo the release lives in, so a fork's test release yields a formula +# pointing at the fork's own downloads. +repo="${GITHUB_REPOSITORY:-beetlebugorg/charttable}" +base="https://github.com/$repo/releases/download/v$version" + +sha() { shasum -a 256 "$dist/charttable-$version-$1.tar.gz" | cut -d' ' -f1; } + +# Resolved before the heredoc: a command substitution that fails inside one is +# not caught by `set -e`, and a missing archive would emit an empty sha256. +mac_arm="$(sha aarch64-macos)" +mac_intel="$(sha x86_64-macos)" +linux_arm="$(sha aarch64-linux-gnu)" +linux_intel="$(sha x86_64-linux-gnu)" + +cat < + int main(void) { return charttable_abi_layout() == 0; } + C + + # charttable_abi_layout reports the struct-layout guard. It touches no GPU, + # so it runs anywhere. + if OS.mac? + system ENV.cc, "abi.c", "-I#{include}", "-L#{lib}", "-lcharttable", "-o", "abi" + system "./abi" + else + # The library leaves the Vulkan loader to the program that links it, and + # a test machine has none. Compile against the header instead. + system ENV.cc, "-I#{include}", "-c", "abi.c", "-o", "abi.o" + end + end +end +EOF From 45b524854a935a1a5d3601e4b85a6c4ea77ba536 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Mon, 24 Aug 2026 16:54:17 -0400 Subject: [PATCH 3/5] Take the version from the tag `-Dversion` sets what the library reports. The release passes the tag, so nobody edits a version in a file to match one. build.zig.zon holds the default for every other build. The build refuses a version that is not semantic. The version reaches two places. `charttable_version()` returns it, which is new: a host can now ask the library what it is. The shared library stamps it into its soname, as before. This drops the release step that checked the tag against build.zig.zon. The tag drives the build now, so there is nothing left to disagree with. --- .github/workflows/release.yml | 15 ++++++--------- README.md | 9 ++++++++- build.zig | 14 +++++++++++--- include/charttable.h | 4 ++++ src/capi.zig | 14 ++++++++++++++ 5 files changed, 43 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf2e58f..cd7f496 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,14 +35,10 @@ jobs: with: version: 0.16.0 - # build.zig.zon is the one place the version is written, and the shared - # library takes its soname from it. The tag is the second place, and a - # mismatch ships a libcharttable.so.0 whose download says something else. - - name: Check the tag against the version in the source - run: | - v="${GITHUB_REF_NAME#v}"; v="${v%%-*}" - grep -q "\.version = \"$v\"" build.zig.zon - + # -Dversion carries the tag into the library: charttable_version() reports + # it and the shared library stamps it into its soname. The build refuses a + # tag that is not a semantic version. + # # Two prefixes: on Windows the static archive and the DLL's import library # are both charttable.lib, so one prefix loses the archive. # @@ -50,9 +46,10 @@ jobs: # libwebp and libpng rather than hunting for them on the target machine. - name: Build run: | + v="${GITHUB_REF_NAME#v}" for step in lib shared; do case $step in lib) p=out/static ;; shared) p=out/shared ;; esac - zig build "$step" -p "$p" \ + zig build "$step" -p "$p" -Dversion="$v" \ -Dtarget=${{ matrix.target }} -Doptimize=ReleaseFast -Dcodec-source done diff --git a/README.md b/README.md index 6380570..1892165 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,8 @@ Linux also gets a Debian package: `apt install ./libcharttable-dev__.deb`. The released libraries carry their own libwebp and libpng. They leave the -Vulkan loader to the program that links them. +Vulkan loader to the program that links them. `charttable_version()` reports +the tag the library was built from. ## Build @@ -65,6 +66,12 @@ Zig 0.16.0. `zig build test` runs the suite. `zig build lib` writes shared library beside them. Add `-Dcodec-source` to compile libwebp and libpng in instead of linking the platform copies — a cross build needs it. +Two options carry into the artifact. `-Dversion` sets what +`charttable_version()` reports and what the shared library stamps into its +soname; a release passes the tag, and every other build takes the version in +`build.zig.zon`. `-Dgpu=vk` selects the Vulkan backend on Windows, where D3D12 +is the default. + ## Status Early extraction. See DESIGN.md for the architecture, the conformance tiers, diff --git a/build.zig b/build.zig index 0d6ae69..5c54cdf 100644 --- a/build.zig +++ b/build.zig @@ -1,7 +1,7 @@ const std = @import("std"); const codecs = @import("build/codecs.zig"); -/// The manifest is the one place the version is written. The shared library -/// stamps it into its soname, so a consumer links against an ABI, not a file. +/// The version a plain `zig build` reports. A release overrides it with +/// `-Dversion`, so the tag is what the published library carries. const zon = @import("build.zig.zon"); pub fn build(b: *std.Build) void { @@ -69,7 +69,15 @@ pub fn build(b: *std.Build) void { // the host describe the host, and a release wants an archive that carries // its own decoders rather than one that hunts for them on the target. const codec_source = b.option(bool, "codec-source", "Compile libwebp and libpng from source") orelse (windows or android); + // The version the library reports and the shared library stamps into its + // soname. A release passes the tag, so nobody hand-edits a version to match + // one. build.zig.zon carries the default, for every other build. + const version = b.option([]const u8, "version", "Version the library reports (default: build.zig.zon)") orelse zon.version; + const semver = std.SemanticVersion.parse(version) catch @panic("-Dversion is not a semantic version"); + const ct_opts = b.addOptions(); + // NUL-terminated, so charttable_version() hands the pointer straight to C. + ct_opts.addOption([:0]const u8, "version", b.allocator.dupeZ(u8, version) catch @panic("OOM")); ct_opts.addOption(bool, "webp", use_webp); ct_opts.addOption(bool, "libpng", use_libpng); // Which renderer src/gpu/gpu.zig selects on Windows. @@ -225,7 +233,7 @@ pub fn build(b: *std.Build) void { .name = "charttable", .root_module = mod, .linkage = .dynamic, - .version = std.SemanticVersion.parse(zon.version) catch @panic("bad version in build.zig.zon"), + .version = semver, }); const shared_step = b.step("shared", "Build the shared library + C header"); shared_step.dependOn(&b.addInstallArtifact(shared, .{}).step); diff --git a/include/charttable.h b/include/charttable.h index 60787f5..6df2a4d 100644 --- a/include/charttable.h +++ b/include/charttable.h @@ -109,6 +109,10 @@ void charttable_set_zoom_range(charttable *, double min_zoom, double max_zoom); * vertex/uniform layout, which shades wrong rather than failing. */ uint32_t charttable_abi_layout(void); +/* The version this library was built as ("0.4.1"), NUL-terminated. The string + * is static: do not free it, and it needs no handle. */ +const char *charttable_version(void); + /* ---- style and sources -------------------------------------------------- */ int charttable_set_style_json(charttable *, const char *json, size_t len); diff --git a/src/capi.zig b/src/capi.zig index fceba5d..767a26d 100644 --- a/src/capi.zig +++ b/src/capi.zig @@ -369,6 +369,13 @@ export fn charttable_abi_layout() callconv(.c) u32 { return @import("scene/types.zig").abiLayout(); } +/// The version this library was built as ("0.4.1"). A release build takes it +/// from the tag. The string is static: it needs no free and outlives the +/// handle, so there is no handle to pass. +export fn charttable_version() callconv(.c) [*:0]const u8 { + return @import("ct_build").version; +} + // ---- style + sources ------------------------------------------------------- export fn charttable_set_style_json(h: ?*anyopaque, json: [*]const u8, len: usize) callconv(.c) c_int { @@ -1116,6 +1123,13 @@ test "capi: a handle opens, takes a style, moves the camera, and closes" { try testing.expectEqual(@import("scene/types.zig").abiLayout(), charttable_abi_layout()); } +test "capi: the version comes from the build" { + const built = @import("ct_build").version; + try testing.expectEqualStrings(built, std.mem.span(charttable_version())); + // A tag drives this, so it has to parse as one. + _ = try std.SemanticVersion.parse(built); +} + test "capi: every entry point is null-safe" { try testing.expectEqual(ERR_HANDLE, charttable_set_style_json(null, smoke_style.ptr, smoke_style.len)); try testing.expectEqual(ERR_HANDLE, charttable_tick(null, 16)); From d4dd47270149c63d047e3e49658f8adf38909981 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Mon, 24 Aug 2026 16:59:33 -0400 Subject: [PATCH 4/5] Stop bumping the version in build.zig.zon The manifest now holds 0.0.0-dev and stays there. Nothing reads it except the default for -Dversion, and the Zig package manager resolves by URL and hash rather than by this string, so a stale number misled more than it told. Cutting a release is one step again: push the tag. A build from a source checkout reports 0.0.0-dev, which is what it is. --- .github/workflows/release.yml | 9 +++++---- README.md | 6 +++--- build.zig | 4 ++-- build.zig.zon | 5 ++++- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cd7f496..c25ed3d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,10 @@ name: Release -# Push a `vX.Y.Z` tag to cut a release: every supported target is built and the -# archives are attached to a GitHub release. A tag carrying a suffix -# (`v0.2.0-rc1`) publishes as a prerelease — use one to prove the matrix before -# the real tag. +# Push a `vX.Y.Z` tag to cut a release. That is the whole procedure: the tag is +# the version, so no file needs a bump first. Every supported target is built, +# the archives are attached to a GitHub release, and the Homebrew tap is +# updated. A tag carrying a suffix (`v0.2.0-rc1`) publishes as a prerelease and +# leaves the tap alone — use one to prove the matrix before the real tag. on: push: tags: ["v*"] diff --git a/README.md b/README.md index 1892165..5ff7679 100644 --- a/README.md +++ b/README.md @@ -68,9 +68,9 @@ in instead of linking the platform copies — a cross build needs it. Two options carry into the artifact. `-Dversion` sets what `charttable_version()` reports and what the shared library stamps into its -soname; a release passes the tag, and every other build takes the version in -`build.zig.zon`. `-Dgpu=vk` selects the Vulkan backend on Windows, where D3D12 -is the default. +soname; a release passes the tag. Without it a build reports `0.0.0-dev`, the +sentinel in `build.zig.zon`, because a source checkout is not a release. +`-Dgpu=vk` selects the Vulkan backend on Windows, where D3D12 is the default. ## Status diff --git a/build.zig b/build.zig index 5c54cdf..3973197 100644 --- a/build.zig +++ b/build.zig @@ -1,7 +1,7 @@ const std = @import("std"); const codecs = @import("build/codecs.zig"); -/// The version a plain `zig build` reports. A release overrides it with -/// `-Dversion`, so the tag is what the published library carries. +/// Read for `.version`, which is the dev sentinel a source build reports. A +/// release passes the tag as `-Dversion` instead. const zon = @import("build.zig.zon"); pub fn build(b: *std.Build) void { diff --git a/build.zig.zon b/build.zig.zon index 43e3671..27f457f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,9 @@ .{ .name = .charttable, - .version = "0.1.0", + // Not a release number, and it never changes. A release takes its version + // from the tag (-Dversion), so there is nothing here to keep in step. A + // build from a source checkout reports this instead, which is what it is. + .version = "0.0.0-dev", .minimum_zig_version = "0.16.0", .fingerprint = 0xf9efa2121e26fedc, // The image codecs, for a target that cannot take them from the platform From 664609bf8f08c8dfb19f31d79c51de676c143294 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Mon, 24 Aug 2026 17:11:52 -0400 Subject: [PATCH 5/5] Rewrite the README Leads with `brew install` and a short C example, so a reader sees how the library is taken and how it is called before anything else. States the goal plainly: what MapLibre draws from a style, charttable should draw from the same style. Names the layer and source types that work today, what comes next, and the conformance number behind the claim. The performance bullets said what the code does. "Tiles lay out into resident GPU buckets" and "zoom gates bake into per-vertex visibility windows" mean nothing without the source in front of you. Each bullet now leads with the effect and gives the mechanism as support. Drops SDL from the backend list. gpu_sdl.zig is in the DESIGN.md module map and is not written yet, so the README promised a fourth backend that does not exist. One line per paragraph and per bullet. The renderer decides where lines break. --- README.md | 106 +++++++++++++++++++++++++++++------------------------- 1 file changed, 57 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 5ff7679..1da973b 100644 --- a/README.md +++ b/README.md @@ -2,77 +2,85 @@

🗺️ A native map renderer for the MapLibre style spec.
- charttable takes a MapLibre style and vector tiles and draws them straight - to the GPU — Metal, Vulkan, Direct3D 12 or SDL — holding 60 fps through - pan, pinch-zoom and rotation. One Zig library with a C ABI. + charttable draws a MapLibre style and vector tiles on the GPU. It holds 60 fps through pan, pinch-zoom and rotation. +

+ +

+ + CI +

--- -> [!NOTE] -> Extracted from the [Lookout Marine](https://github.com/beetlebugorg/lookout-core) -> rendering engine and generalized: where Lookout renders one hard-wired -> nautical portrayal, charttable renders whatever the style says. It is a -> **clean-room** implementation of the published -> [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/) — -> see THIRD-PARTY-NOTICES.md for provenance. - -## Why it is different - -- **Tessellate once, transform per frame.** Tiles lay out into resident GPU - buckets; a pan, zoom or rotation is a matrix change, never a rebuild. An - idle map uses no CPU time. -- **Restyling is a buffer refill, not a re-layout.** Geometry and evaluated - paint live in separate vertex streams: a palette flip or a - `setPaintProperty` re-evaluates paint and re-uploads one stream. -- **Filters evaluate at fractional zoom.** Zoom gates bake into the resident - scene as per-vertex visibility windows, so features appear at their exact - zoom, not the next integer step. -- **Native everywhere the engine goes.** The same scene contract drives - Metal, Vulkan, D3D12 and SDL backends. +charttable is one Zig library behind a C ABI. It draws through Metal, Vulkan or Direct3D 12, so the same library serves macOS, iOS, Linux, Windows and Android. It does no networking. Tiles come from a local pmtiles archive, or from your code through a resource callback. ## Install -macOS and Linux, through Homebrew: - ``` brew install beetlebugorg/tap/charttable ``` -That puts `charttable.h` under `include/` and the static and shared libraries -under `lib/`. - -Every `vX.Y.Z` tag also publishes an archive per target on -[Releases](https://github.com/beetlebugorg/charttable/releases). Each archive -holds the C header, the static library and the shared library: +Every tag also publishes an archive per target on [Releases](https://github.com/beetlebugorg/charttable/releases). Each one holds the C header, a static library and a shared library. | Target | Static | Shared | | --- | --- | --- | | macOS, arm64 and x86_64 | `libcharttable.a` | `libcharttable.dylib` | | Linux, arm64 and x86_64 | `libcharttable.a` | `libcharttable.so` | -| Windows, arm64 and x86_64 | `charttable.lib` | `charttable.dll` + `charttable.dll.lib` | +| Windows, arm64 and x86_64 | `charttable.lib` | `charttable.dll` | -Linux also gets a Debian package: -`apt install ./libcharttable-dev__.deb`. +Linux also gets a Debian package. The released libraries carry their own libwebp and libpng, so nothing else has to be installed first. + +## Use + +```c +#include + +charttable *ct = charttable_open(NULL); +charttable_set_style_json(ct, style_json, strlen(style_json)); +charttable_add_source_pmtiles(ct, "basemap", "/maps/planet.pmtiles"); +charttable_attach_surface(ct, CHARTTABLE_NATIVE_METAL_LAYER, layer, w, h); + +charttable_view v = { .lon = -76.48, .lat = 38.98, .zoom = 12 }; +charttable_set_view(ct, &v); + +/* once per frame */ +charttable_tick(ct, dt_ms); +if (charttable_needs_redraw(ct)) charttable_render(ct); +``` -The released libraries carry their own libwebp and libpng. They leave the -Vulkan loader to the program that links them. `charttable_version()` reports -the tag the library was built from. +The API is one opaque handle and about forty functions. A mutex inside the handle serializes them, so every call is safe from any thread. The three functions that touch the surface must be called from the thread that owns it, because the platform requires that. + +## What it renders + +The target is the whole [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/). What MapLibre draws from a style, charttable should draw from the same style. There is no charttable dialect and no porting step. The official conformance fixtures are vendored and run as the oracle, so the coverage is measured. 575 of the 577 expression fixtures pass today. + +charttable now draws `background`, `fill`, `line`, `symbol`, `raster`, `hillshade` and `color-relief` layers, from `vector`, `raster` and `raster-dem` sources. `circle`, `geojson` and `fill-extrusion` are next. + +A layer or property charttable does not support yet is skipped, and the reason goes into `charttable_style_diagnostics()`. Read that after you load a style. + +## Why it is fast + +- **A tile is turned into triangles once.** After that, a pan, zoom or rotation draws the same geometry with a new matrix. A map sitting still uses no CPU. +- **Restyling touches the paint stream only.** Geometry and evaluated paint sit in separate vertex streams. Changing a color, an opacity or a whole palette re-uploads the paint and leaves the geometry alone, so a day-to-night switch lands in one frame. +- **Features appear at the zoom the style names.** Each vertex carries the zoom range it is visible in, so a feature fades in at 12.4 if the style says 12.4, not at 13. +- **One scene, three backends.** Metal, Vulkan and D3D12 draw from the same prepared scene, so a fix in the renderer reaches all three. ## Build -Zig 0.16.0. `zig build test` runs the suite. `zig build lib` writes -`libcharttable.a` and the header into `zig-out`; `zig build shared` writes the -shared library beside them. Add `-Dcodec-source` to compile libwebp and libpng -in instead of linking the platform copies — a cross build needs it. +Zig 0.16.0. -Two options carry into the artifact. `-Dversion` sets what -`charttable_version()` reports and what the shared library stamps into its -soname; a release passes the tag. Without it a build reports `0.0.0-dev`, the -sentinel in `build.zig.zon`, because a source checkout is not a release. -`-Dgpu=vk` selects the Vulkan backend on Windows, where D3D12 is the default. +``` +zig build test # run the suite +zig build lib # libcharttable.a and the header, into zig-out +zig build shared # the shared library, beside it +``` + +Add `-Dcodec-source` to compile libwebp and libpng in rather than link the platform copies. A cross build needs it. `-Dgpu=vk` picks the Vulkan backend on Windows, where D3D12 is the default. `-Dversion` sets what `charttable_version()` reports and what the shared library stamps into its soname. A release passes the tag, and a source build reports `0.0.0-dev`. ## Status -Early extraction. See DESIGN.md for the architecture, the conformance tiers, -and the milestone plan. +charttable is early. The engine draws, and the spec coverage above is the current work. See DESIGN.md for the architecture, the conformance tiers and the milestone plan. + +> [!NOTE] +> Extracted from the [Lookout Marine](https://github.com/beetlebugorg/lookout-core) rendering engine and generalized. Where Lookout renders one hard-wired nautical portrayal, charttable renders whatever the style says. It is a **clean-room** implementation of the published spec. See THIRD-PARTY-NOTICES.md for provenance.