Skip to content

feat(core): allocation-free *Into variants for hot paths - #201

Merged
pbroom merged 5 commits into
mainfrom
feat/out-variants
Oct 10, 2026
Merged

pbroom merged 5 commits into
mainfrom
feat/out-variants

Conversation

@pbroom

@pbroom pbroom commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Stack 2/4. Adds out-first, allocation-free *Into variants (conversions, low-level kernels, mixInto/interpolateInto, toSrgbGamutInto/toP3GamutInto). Allocating APIs become thin wrappers over the kernels — bit-identical (0 mismatches, 100k inputs × 30 functions).

  • bench:into: Into variants ~0 B/op, 0 GCs per 1e6 ops (vs up to 28); ns/op roughly equal.
  • Side benefit: existing gamut mapping 20–28% faster, toRgb/toP3 allocate 1 object instead of 4.
  • A React ColorPlane CPU-loop dogfood was measured and dropped: fewer GCs but clamp paths 8–10% slower (V8 boxes doubles in object fields).

Recommendation: adopt in addition — object API stays primary; Into reduces GC count, not time. Typed arrays (#202) are the better zero-alloc per-pixel target.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T22:24:25.552694Z eca3ad1 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

pbroom commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@greptile-apps

greptile-apps Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium risk] Adds allocation-free conversion functions for color math.

The PR should address the shared-scratch conversion error before merging; the documentation issues are non-blocking.

Fix All in CursorFindings

  1. P1 Shared scratch can corrupt conversions ▶
  2. P2 Allocation claim is too broad ▶
  3. P2 One-pixel example produces NaN ▶

Summary

The PR adds caller-supplied-output variants for conversions, interpolation, mixing, and gamut mapping, then routes allocating APIs through those implementations. It also adds differential and reference tests, a benchmark, and usage documentation.

Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Input[Caller Color] --> Lab[Shared OKLab scratch]
  Lab --> Linear[Shared linear RGB scratch]
  Linear --> Out[Caller-supplied output]
  Getter[Input accessor re-enters conversion] --> Lab
Loading

Reviews (1) · Last reviewed commit: "docs: record *Into / V8 boxing learnings"

Comment thread packages/core/src/conversion/into.ts
Comment thread README.md Outdated
Comment thread README.md Outdated
This was referenced Oct 6, 2026

pbroom commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner Author

Merge activity

  • Oct 10, 6:47 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Oct 10, 6:49 PM UTC: Graphite rebased this pull request as part of a merge.
  • Oct 10, 6:49 PM UTC: @pbroom merged this pull request with Graphite.

@pbroom
pbroom changed the base branch from feat/linear-light-mix to graphite-base/201 October 10, 2026 18:47
@pbroom
pbroom changed the base branch from graphite-base/201 to main October 10, 2026 18:47
pbroom and others added 5 commits October 10, 2026 18:48
Add out-first, allocation-free twins of the hot per-pixel / per-frame
functions, in the style of pmndrs/math. Each writes into `out`, returns
it, allocates nothing (intermediate stages use module-level scratch), and
is bit-identical to its allocating counterpart:

- Color <-> space: toOklabInto, fromOklabInto, toLinearSrgbInto,
  fromLinearSrgbInto, toRgbInto, fromRgbInto, toP3Into, fromP3Into
- Low-level: srgbToLinearInto, linearToSrgbInto, linearRgbToOklabInto,
  oklabToLinearRgbInto, oklabToOklchInto, oklchToOklabInto,
  linearSrgbToLinearP3Into, linearP3ToLinearSrgbInto, linearP3ToP3Into,
  p3ToLinearP3Into
- mixInto, interpolateInto (including the interpolation options)
- toSrgbGamutInto, toP3GamutInto

The allocating functions are now thin wrappers (`fn(x) = fnInto(new, x)`),
so there is no duplicated math. inSrgbGamut/inP3Gamut and the gamut-mapping
bisection no longer allocate per call, and fixupHues shares its rules with
an allocation-free hue lerp.

A differential check against the pre-refactor source (c681f71) over 100k
random inputs per function found 0 mismatches (Object.is on every field)
for every refactored allocating API, parse, contrast, mix/interpolate and
generateScale.

To keep the everyday import from growing, parse() now shares one
number/percentage helper and one alpha helper across its branches (same
results). Size-limit: parse+toHex+contrastRatio 2337 -> 2300 B, everyday
toolkit 2386 -> 2351 B.

Adds `pnpm --filter @color-kit/core bench:into` (ns/op, B/op, GC count over
1e6 iterations) and into.test.ts (identity, bit-identity and aliasing), plus
reference-accuracy coverage of the Into variants against colorjs.io.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…sors

The `*Into` pipelines share module-level scratch. If an input was an
object with accessors whose getter ran another conversion, the nested
call could overwrite scratch that the outer call had already written
(for example the OKLab stage, before the outer call read `alpha`), and
corrupt the result.

Snapshot inputs before any shared scratch is written:
- Every low-level kernel (srgb, oklab, oklch, p3) reads all of its input
  fields, alpha included, into locals before writing `out`. Only the
  first stage of a pipeline reads caller input, so a nested call can only
  run before this call's scratch is written.
- Gamut mapping works only from its l/c/h/alpha snapshot; the membership
  check now uses the trial color instead of re-reading the input.
- interpolateInSpaceInto reads the options and both endpoints into
  locals, copies them to START/END scratch, and mixes from those.

Results are unchanged: a differential check against the previous source
over 100k random inputs per function found 0 mismatches across 30
functions (Object.is on every field). The functions still allocate
nothing, and bench:into is unchanged within noise (all ops within
about ±3%).

New into.test.ts cases wrap Colors, Rgb, P3 and linear inputs with an
`alpha` getter (and a variant with getters on every field) that runs
toRgb, toP3, fromRgb, fromP3, gamut mapping and mix. All 6 cases fail on
the previous source and pass now.

Size-limit: parse+toHex+contrastRatio 2317 B, everyday toolkit 2367 B,
both still below the pre-*Into baselines (2337 / 2386 B).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Say that the *Into functions create no result or intermediate objects rather than that they allocate nothing: engines may still box doubles written into object fields (V8 does in code it does not fully inline), and V8 often scalar-replaces the allocating functions' temporaries. Recommend benchmarking at the call site.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
x / (width - 1) is NaN for a 1px-wide image. Use t = 0 (the start color) when width is 1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@pbroom
pbroom force-pushed the feat/out-variants branch from ee33734 to 93d87b8 Compare October 10, 2026 18:48
@vercel

vercel Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
color-kit Ready Ready Preview Oct 10, 2026 6:49pm UTC

@pbroom
pbroom merged commit 2a1e102 into main Oct 10, 2026
6 of 7 checks passed
@pbroom
pbroom deleted the feat/out-variants branch October 10, 2026 18:49

This branch was successfully deployed

1 active deployment
Preview — 93d87b86 Deployed Oct 10, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant