Skip to content
Open
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
113 changes: 113 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
name: Test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
zig-version: [master]
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Zig
uses: mlugg/setup-zig@v2
with:
version: ${{ matrix.zig-version }}

- name: Run tests
run: zig build test

benchmark:
name: Benchmark
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Zig
uses: mlugg/setup-zig@v2
with:
version: master

- name: Run benchmark
run: |
echo "Running benchmarks..."
zig build bench -Doptimize=ReleaseFast > benchmark_results.txt 2>&1 || true
echo "Benchmark completed"
continue-on-error: true

- name: Read benchmark results
id: benchmark
run: |
if [ -f benchmark_results.txt ]; then
echo "results<<EOF" >> $GITHUB_OUTPUT
cat benchmark_results.txt >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
echo "results=No benchmark results found" >> $GITHUB_OUTPUT
fi

- name: Comment PR with benchmark results
uses: actions/github-script@v7
with:
script: |
const results = `${{ steps.benchmark.outputs.results }}`;

const comment = `## 🚀 Benchmark Results

<details>
<summary>Click to view benchmark results</summary>

\`\`\`
${results}
\`\`\`

</details>

> Benchmarks run on: \`ubuntu-latest\` with \`ReleaseFast\` optimization
>
> To run benchmarks locally: \`zig build bench -Doptimize=ReleaseFast\`
`;

// Check if we already commented on this PR
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});

const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('🚀 Benchmark Results')
);

if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: comment
});
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.zig-cache/
zig-out/
zig-pkg/
.vscode/
ignore_*
100 changes: 98 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,101 @@
This library is a simple linear algebra library.
# Zig Math Library (ZML)

This library was written for my own use, and will be extended and optimized as I need it.
[![CI](https://github.com/flying-swallow/zig-linear-algebra/actions/workflows/ci.yml/badge.svg)](https://github.com/flying-swallow/zig-linear-algebra/actions/workflows/ci.yml)

A high-performance linear algebra library for Zig, providing vector, matrix, quaternion, and geometry operations.

## Features

- **Vectors** (`zml.vec`) — norm, dot, cross, normalize, reflect, distance, angle, swizzle, fused `sin_cos`, and more.
- **Matrices** (`zml.Mat`) — generic column-major `Mat(T, cols, rows)` with multiply, transforms, etc.
- **Quaternions** (`zml.quat`) — rotation, slerp/nlerp, axis-angle and Euler conversions.
- **Geometry** (`zml.geom`) — AABB, sphere, plane, capsule, OBB, ray, frustum, overlap/containment tests.
- Extras: `zml.scalar` (clamp/lerp/smoothstep), `zml.color`, `zml.packing`, `zml.random`.
- Built on Zig's native `@Vector` SIMD types — **but functions also accept plain arrays** (see below).

## Installation

```zig
zig fetch --save "git+https://github.com/flying-swallow/zig-linear-algebra.git"
```

Then in your `build.zig`:

```zig
const zml = b.dependency("zml", .{
.target = target,
.optimize = optimize,
});

exe.root_module.addImport("zml", zml.module("zml"));
```

## Usage

```zig
const zml = @import("zml");

const a = zml.Vec3f32{ 1, 2, 3 }; // @Vector(3, f32)
const b = zml.Vec3f32{ 4, 5, 6 };

const d = zml.vec.dot(a, b); // f32 = 32
const c = zml.vec.cross(a, b); // @Vector(3, f32)
const n = zml.vec.normalize(a); // @Vector(3, f32)
const r = zml.vec.sin_cos(a); // .{ .sin_out, .cos_out }
```

## `@Vector` and array inputs

Every length-generic vector function accepts **both** a native `@Vector(N, T)` and a plain
`[N]T` array, and the return **preserves the input's container kind** (array in → array out,
vector in → vector out):

```zig
const arr = [3]f32{ 1, 2, 3 };
const d = zml.vec.dot(arr, arr); // f32
const nz = zml.vec.normalize(arr); // [3]f32
```

Arrays are meant for arbitrarily long data. Rather than coercing a large array into one wide
`@Vector(N, T)` — which makes LLVM emit a single enormous SIMD instruction and bloats the binary
— array inputs are processed in **chunks sized to the CPU's native SIMD width**
(`std.simd.suggestVectorLengthForCpu`) via a compact loop. `@Vector` inputs keep the single-op
path (you chose that width explicitly).

For example, `norm` over a `[245]f32` compiles to **324 bytes** via the chunked path, versus
**2,529 bytes** for the equivalent `@Vector(245, f32)` op (`-OReleaseSmall`) — and it is faster
too (see below).

## Benchmarks

Run the benchmark suite (uses [zBench](https://github.com/hendriknielaender/zBench)):

```sh
zig build bench -Doptimize=ReleaseFast
```

Representative results (`ReleaseFast`; absolute numbers are machine-dependent — the point is the
array-vs-`@Vector` ratio at large `N`):

| Operation (N = 245) | array (chunked) | `@Vector(N)` (single op) | speedup |
| -------------------------- | --------------: | -----------------------: | ------: |
| `norm` | 37 ns | 235 ns | 6.3× |
| `dot` | 40 ns | 313 ns | 7.8× |
| `normalize` | 57 ns | 261 ns | 4.6× |

| Sin/Cos over 256 elements | time/run |
| -------------------------- | ---------: |
| scalar `std.math` loop | 695 ns |
| `sin_cos` (`@Vector`) | 106 ns |
| `sin_cos` (array, chunked) | 139 ns |

For the length-generic reductions/maps, the chunked array path is both **smaller and faster**
than one wide `@Vector` op at large `N`: the wide op forces LLVM into a slow, bloated instruction
sequence, while the native-width loop stays compact and vectorized. `sin_cos` is pure
element-wise, so the `@Vector` form already vectorizes cleanly and the two are comparable.

## Testing

```sh
zig build test
```
11 changes: 0 additions & 11 deletions TODO.md

This file was deleted.

Loading