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
101 changes: 101 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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 }}
126 changes: 126 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
name: Release

# 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*"]

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

# -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.
#
# -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: |
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" -Dversion="$v" \
-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

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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ specs/

# Example app build output
examples/macos/chartview

# Where scripts/package-release.sh builds and stages a release
out/
dist/
101 changes: 75 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,85 @@

<p align="center">
<b>🗺️ A native map renderer for the MapLibre style spec.</b><br>
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.
</p>

<p align="center">
<a href="https://github.com/beetlebugorg/charttable/actions/workflows/ci.yml">
<img src="https://github.com/beetlebugorg/charttable/actions/workflows/ci.yml/badge.svg" alt="CI">
</a>
</p>

---

> [!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

```
brew install beetlebugorg/tap/charttable
```

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` |

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.h>

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 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 # 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. Building: `zig build test` (Zig 0.16.0).
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.
Loading
Loading