pragma_core 1.0.0

Solid, high-performance multithreaded core in pure Rust for game engines and frameworks
Documentation

pragma.core

CI crates.io docs.rs MSRV License

pragma.core is a pure-Rust, solid and high-performance multithreaded core for razed.production's game engines and frameworks.

It is the layer under an engine: it owns the threads, schedules the work of a frame and runs background I/O, so that ECS, rendering, physics and audio can be written on top of it without each inventing their own thread pool.

Status

1.0.0 — stable. All milestones M0–M6 are done: the work-stealing job system, the per-frame task graph, async tasks, observability, tuning, the hardening pass (determinism suite, benchmark gate and semver checks in CI), the 1.0 API review, complete rustdoc with an embedding checklist, and the MSRV and versioning policy. The public API follows semver: breaking changes happen only in a major release, and every change is listed in the changelog. See the roadmap.

Features

Area Crate Facade
Typed errors, generational handles (Handle, HandlePool), simulation ticks pragma_base base
Sync primitives: latches, counters, task labels (plus the scheduler's internal building blocks) pragma_sync sync
Platform layer: thread names, priorities, core affinity (Windows/Linux) pragma_sys sys
Work-stealing pool: join, scope, spawn, parallel_for, parallel_reduce, priorities pragma_jobs jobs
Per-frame task graph with declared resource access, validated before the first frame pragma_graph graph
Async tasks on the pool, bounded blocking pool for synchronous I/O pragma_async tasks
Vertical slice: 100k entities, 9-system graph, background load, per-tick state hash examples/frame_sim —

Highlights:

  • Help-while-wait. Every wait (join, scope, JobHandle::join) runs other jobs, so nested parallelism never deadlocks, even on a single worker.
  • Panics are errors. A panicking job, graph task or future returns JobError::Panicked / AsyncError::Panicked; the worker survives and nothing hangs.
  • Conflicts are caught at build time. A graph whose tasks touch the same resource without an explicit order fails in build() with GraphError::AmbiguousOrder. It cannot become a data race in frame 10 000.
  • Allocation-free frames. After the warm-up frame, a 50-task graph runs with zero heap allocations, with or without a tracing subscriber.
  • Deterministic. Results do not depend on which thread ran what: frame_sim produces the same per-tick hash on 1, 2 and N workers, and CI checks this on every PR.
  • Observable. PoolStats snapshots, tracing spans, optional puffin scopes, a debug-only watchdog for stuck jobs, and an allocation-free "which task is running here" query for panic hooks and crash reports.
  • Bounded memory. Input-sized allocations go through try_reserve or a hard limit: you get GraphError::TooLarge / BaseError::OutOfMemory instead of an abort.

Quick look

use pragma_core::prelude::*;

struct Velocity(f32);
struct Positions(Vec<f32>);

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let pool = ThreadPool::new(PoolConfig::default())?;

    // Fork-join and data-parallel loops; jobs may borrow from the stack.
    let (a, b) = pool.join(|| 1 + 1, || 2 + 2)?;
    assert_eq!((a, b), (2, 4));

    let mut data = vec![1_u32; 10_000];
    pool.parallel_for(&mut data, Grain::Auto, |x| *x *= 2)?;

    // Frame graph: declare what each task reads and writes; ordering is validated once.
    let mut g = FrameGraph::builder();
    g.task("input", |t| t.writes::<Velocity>(), |ctx| -> Result<(), GraphError> {
        ctx.write::<Velocity>()?.0 = 0.5;
        Ok(())
    });
    g.task("move", |t| t.reads::<Velocity>().writes::<Positions>().after("input"), |ctx| {
        let v = ctx.read::<Velocity>()?.0;
        let mut pos = ctx.write::<Positions>()?;
        ctx.pool().parallel_for(&mut pos.0, Grain::Auto, |p| *p += v)?;
        Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
    });
    let mut graph = g.build()?;

    let mut world = Resources::new();
    world.insert(Velocity(0.0));
    world.insert(Positions(vec![0.0; 100_000]));
    graph.run(&pool, &mut world)?; // one frame

    // Background work: async on the pool, blocking I/O in a bounded side pool.
    let exec = Executor::new(&pool, AsyncConfig::default())?;
    let io = exec.clone();
    let mut load = exec.spawn_async(async move {
        let bytes = io.unblock(|| vec![0_u8; 64]).await?; // std::fs::read(...) in a real game
        Ok::<usize, AsyncError>(bytes.len())
    });
    let size = loop {
        if let Some(result) = load.poll_ready() {
            break result??; // check once per frame, never block the frame
        }
        std::thread::yield_now();
    };
    assert_eq!(size, 64);
    Ok(())
}

More examples are in the crate docs (cargo doc --open -p pragma_core) and in examples/frame_sim.

Using it

[dependencies]

pragma_core = "1.0"

Depend on the pragma_core facade rather than on the individual pragma_* crates (see Versioning).

Feature Default Enables
jobs ✅ ThreadPool and the platform layer (sys)
graph ✅ FrameGraph, Resources (implies jobs)
async ✅ Executor, AsyncHandle (implies jobs)
puffin — Profiler scopes for pool operations and graph tasks; re-exports puffin

base and sync are always available. Each feature builds and passes clippy on its own (cargo hack --each-feature in CI).

MSRV policy

The minimum supported Rust version is 1.88 (rust-version in [workspace.package], DECISIONS #10), the minimum required by proptest.

  • Checked in CI. The MSRV (1.88) job runs cargo +1.88 check --workspace --all-features on every PR and every push to main. Development itself uses the toolchain pinned in rust-toolchain.toml (1.99.0), so lints are identical locally and in CI.
  • Raised only in a minor release. A higher MSRV is not treated as a breaking change, but it never ships in a patch release: it goes into the next minor version (1.x.0; 0.x.0 before 1.0), with its own line in the changelog and a decision record explaining why (DECISIONS #44).
  • One change, three places. rust-version, the CI msrv job and the badge above move together in the same PR.

Versioning

pragma.core follows SemVer; the rules are in docs/reliability.md §8.

  • Lockstep. All crates share one version (version in [workspace.package], currently 1.0.0): the pragma_core facade exposes all of them, so separate versions would give users nothing (DECISIONS #33). A breaking change in any crate is a breaking release of all of them.
  • Exact internal dependencies. The crates depend on each other with =X.Y.Z, so pragma_jobs 1.0.0 never builds against a pragma_sync 1.0.1 whose internals have moved (DECISIONS #42). Depend on pragma_core, or keep all pragma_* crates at the same version.
  • Outside semver. No compatibility guarantees cover:
    • pragma_sync::__private (hidden from the docs): the scheduler's building blocks, which change with every optimization of join/scope;
    • the puffin feature and the pragma_core::puffin re-export: puffin is not 1.0 yet, and moving to a new puffin version is a minor release.
  • Breaking changes. Before 1.0, a minor version (0.x.0) may break the API, and every break is listed in the changelog. After 1.0, breaks happen only in a major release; an item is first marked #[deprecated(since, note)] for at least one minor release.
  • Checked in CI. cargo semver-checks --workspace runs on every PR against the PR's base on main (not against crates.io or a tag): a breaking change that comes without a version bump fails the PR that introduces it.

Performance

Windows 10, AMD FX-8350 (8 logical cores), cargo bench. Ranges are min…max across A/B rounds. Full method and numbers are in docs/perf.md.

Benchmark pragma.core rayon
single join 284…288 ns 1.47…1.51 µs
fib(20), ~11k nested join 197…200 µs 99…108 µs
parallel_for, 10⁶ elements 454…478 µs 477…508 µs
parallel_reduce, 10⁶ elements 198…214 µs 230…282 µs
frame graph, 50 near-empty tasks 8.3…8.4 µs per frame —
frame_sim, 100k entities ~5 ms/tick on 1 worker, ~2 ms on 11 —

Nested fork-join is the one place where rayon is still ahead (about 2×). A CI gate fails any PR that regresses a benchmark by more than 10% beyond runner noise.

Reliability

Reliability here is a set of properties that CI checks on every PR. Details are in docs/reliability.md.

  • Pure Rust. No C/C++ compiled into the dependency tree; enforced by cargo deny and tools/audit_workspace.py. System FFI (windows-sys, libc) only.
  • No panics in the runtime. unwrap/expect/panic!/unchecked indexing are denied by lints; failures are #[non_exhaustive] typed errors with context.
  • Contained unsafe. Only in pragma_sync (lifetime erasure, on-stack jobs, task labels) and pragma_sys (OS calls). Every occurrence has a budget in layers.toml; any growth needs a new decision record. All of it is reviewed in docs/unsafe.md.
  • Verified concurrency. Every atomic protocol has a loom model. pragma_sync runs under Miri, and a one-worker pool runs under Miri with Tree Borrows.
  • Tested in depth. Unit, property (proptest) and stress tests, plus fuzzing of the graph builder against an independent oracle. Nightly CI runs 30-minute fuzzing and soak tests.
  • Layered. Each crate depends only on lower layers (layers.toml):
L4  pragma_core     facade: re-exports, prelude, features
L3  pragma_graph    frame graph           pragma_async   async tasks
L2  pragma_jobs     work-stealing pool
L1  pragma_sync     sync primitives       pragma_sys     platform layer
L0  pragma_base     errors, handles, ticks

Platforms

Windows and Linux (x86_64) are first-class and tested in CI. Other targets are not supported yet; the API does not rule out a single-threaded mode for wasm later. See DECISIONS.md.

Building

cargo test --workspace --all-features

cargo run -p frame_sim --release -- --ticks 300 --verify 1,4

The toolchain is pinned in rust-toolchain.toml (1.99.0); the minimum supported Rust version is 1.88 (MSRV policy). See CONTRIBUTING.md for the full check list (Miri, loom, fuzz, benches).

Documentation

License

MIT © 2026 razed