Skip to content

v0.3.0

Choose a tag to compare

@ZviBaratz ZviBaratz released this 17 Jul 13:12
5b14ab3

The "Refine & prove" release. It settles the pre-1.0 API — a fresco-owned
ColorProfile enum, up-front Palette.Validate, and a buffer-reusing
AppendRender — then proves the hot path with an allocation pass and validates
the whole surface against its real downstream consumer (atrium).

Added

  • AppendRender(dst []byte, w, h, frame int, opts Options) []byte — a
    buffer-friendly render path that appends the frame to a caller-owned slice, so
    a per-frame loop can reuse one buffer (buf = AppendRender(buf[:0], …))
    instead of allocating a fresh string every tick (#17). Render becomes a thin,
    byte-identical wrapper over it. Purely additive: measured at 120×40 truecolor,
    reusing the buffer removes the per-frame output allocation (≈172 KB → 82 KB).
  • Palette.Validate() error — an advisory, opt-in check that reports any
    anchor that is not a canonical hex colour ("#rgb" or "#rrggbb"), naming
    every offending field (#18). Render is unchanged: it still never errors or
    panics on a malformed palette — each bad anchor degrades to a documented
    fallback, so the exactly-h×w-cells contract always holds. Validate is
    deliberately stricter than the renderer's parser, so it flags typos (a missing
    #, a wrong length, trailing garbage) the renderer would otherwise paint.

Changed

  • Options.Profile is now a fresco-owned ColorProfile enum instead of
    *termenv.Profile, so pinning colour depth no longer requires importing
    termenv (#15). The zero value, Auto, auto-detects the terminal exactly as
    an unset (nil) profile did before; pin TrueColor, ANSI256, ANSI16, or
    NoColor for a fixed depth. This also settles the options-ergonomics review
    (#16): Options stays a plain struct — functional options were rejected as
    per-frame allocation churn for a 60fps render call — and LumRange stays a
    *float64 because 0 is a meaningful value, so no sentinel can mean "unset".
    Breaking: Profile: &p becomes Profile: p (a ColorProfile value);
    callers pinning depth swap termenv.AsciiNoColor, termenv.ANSIANSI16,
    and drop the termenv import.

Performance

  • Hot-path allocation pass (#21) — the render loop no longer materialises the
    field into two per-frame []float64 buffers (the "Pass 1"/"Pass 2" split is
    fused: each cell is evaluated inline as it is emitted), and the per-palette LUT
    cache is keyed by a comparable struct instead of a freshly joined string on every
    call. Output is byte-identical (verified by a 16,896-frame SHA-256 sweep across
    variants × profiles × frames × sizes × luminance ranges). On a reused
    AppendRender buffer this takes the warm hot path from 4 allocations/frame
    (≈82 KB at 120×40) to 1 (Rain/Ripple: to zero); via Render, per-frame
    B/op roughly halves. Wall-clock gains are modest and variant-dependent (≈8–12%
    for the lighter variants, flat for tunnel and the shaded path) — the pass targets
    garbage, not latency. Baselines and method are recorded in
    docs/perf.md.