Skip to content

perf: simplify render_text with one growable canvas per height - #19

Merged
AmitMY merged 1 commit into
mainfrom
perf/simplify-renderer
Oct 7, 2026
Merged

AmitMY merged 1 commit into
mainfrom
perf/simplify-renderer

Conversation

@AmitMY

@AmitMY AmitMY commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Simplifies pixel_renderer/renderer.py (−79/+40 lines) and makes short-text rendering ~12% faster, with pixel-identical output.

Changes

  • One growable canvas per block_size. This replaces the fixed 1024px reusable surface plus a fresh surface/context for every wider text. Long texts now reuse their surface too, and the two-branch logic is gone.
  • Fewer cairo calls per render. The black source is set once when a canvas is created. The white background is filled through the numpy view of the surface (mark_dirty() before drawing, flush() before reading).
  • @cache'd layout getter in place of the global _measurement_context tuple.
  • Dropped a redundant np.ascontiguousarray in render_signwriting and fixed the docstring's block_size default (16, not 32).

Benchmarks

Ubuntu 24.04 container matching CI (pango 1.52.1, cairo 1.18.0, Python 3.12), aarch64, best of several runs:

main this PR
benchmark.py word mix ~110k it/s ~123k it/s (+12%)
long_text_benchmark.py (512 words) ~1,125 it/s ~1,141 it/s (+1.5%)

Correctness

  • 4,420 renders compared against main's render_text: regression samples, benchmark words, the long lorem text, empty / newline / 3,000-char / SignWriting inputs, at sizes (16,12), (32,24), (16,12), (24,16) interleaved so canvases get both reused and grown. 0 mismatches, same exceptions.
  • Full suite passes (458), including the pixel-exact backward-compatibility tests.

Where the rest of the time goes (why this isn't faster)

Profiled per call:

  • Long text: Pango layout/shaping ~53%, drawing ~23%, BGRX→RGB ~18%.
  • Short words: after this PR, ~1.4µs of the ~7.9µs is conversion and ~4µs is Pango.

I tried and rejected:

  • Faster BGRX→RGB conversion. byteswap + V3 views, uint32 shifts and PIL's BGRX unpacker were all slower than the existing three strided assignments, which run close to memory bandwidth.
  • Rendering to an A8 surface. It breaks color emoji (Noto Color Emoji is in the font set).
  • Hand-driving Pango.itemize/shape. Fragile through GI, and unlikely to stay pixel-identical (bidi reordering, etc.).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fy1wuWdqcKuebgj3St8dME


Note

Low Risk
Performance refactor in rendering only; PR reports pixel-identical output across thousands of regression renders and full tests.

Overview
Refactors Pango/Cairo text rendering in renderer.py for speed and simpler reuse: one @cache’d shared layout replaces the old measurement-context globals, and one growable canvas per line height (with a cached numpy pixel view) replaces the fixed 1024px surface plus a separate path for wide text.

render_text now clears the needed width via pixels.fill(255) and mark_dirty(), draws once, then flush() before bgra_to_rgb; long strings reuse the same surface after it grows. render_signwriting drops redundant ascontiguousarray, and the block_size doc default is corrected to 16.

Reviewed by Cursor Bugbot for commit c582f77. Bugbot is set up for automated code reviews on this repo. Configure here.

- Replace the fixed 1024px reusable surface plus a per-call surface for
  wider text with one canvas per block_size that grows when needed, so
  long texts also reuse their surface.
- Set the black text source once per canvas and fill the white background
  through the numpy view (with mark_dirty/flush), dropping three cairo
  calls per render.
- Replace the global measurement-context bookkeeping with a @cache'd
  layout getter.
- Drop a redundant ascontiguousarray in render_signwriting and fix the
  block_size default in the docstring.

Output is pixel-identical (4,420 renders compared against main, mixed
sizes). Short words are ~12% faster; long text ~1% faster.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fy1wuWdqcKuebgj3St8dME
@AmitMY
AmitMY merged commit 807e3e8 into main Oct 7, 2026
4 checks passed
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