Author: Mason McParlane <[email protected]>
State: Withdrawn
Type: Project
Vote: Pending
Created: 2026-05-05
Tcl-Version: 9.0.5
Tk-branch: tip-754-ttk-composed-pixmaps-cache
Forum: https://core.tcl-lang.org/tips/forumpost/07974cca59
Abstract
Custom Ttk scrollbar elements created with ttk::style element create ... image
exhibit severe performance degradation on Windows due to per-tile GDI object
creation and destruction in the image rendering pipeline. This TIP adds a
per-element rendered-pixmap cache to ImageElementDraw so that repeated draws
at the same size and state blit a single pre-composed bitmap instead of tiling
via hundreds of individual Tk_RedrawImage calls. It also fixes an off-by-one
error in the Ttk_Fill tiling loop.
All changes are confined to generic/ttk/ttkImage.c.
Rationale
The Ttk_Fill function tiles a source image region into a destination region
by calling Tk_RedrawImage once per tile. For a scrollbar trough element
(e.g., 14px wide, 400px tall) using a small source image with -border 9-slice
settings, the center fill region may produce 200+ individual Tk_RedrawImage
calls for a single element draw.
On Windows, each Tk_RedrawImage call reaches TkPutImage (win/tkWinDraw.c)
which creates a memory DC and a DIB bitmap, performs a BitBlt, then destroys
both objects. For images with semi-transparent pixels (the COMPLEX_ALPHA path),
there is an additional XGetImage read-back and software alpha blend per tile.
During scrolling or window resizing with a custom image-based theme, this produces 6,000-12,000 GDI create/destroy cycles per second, causing visible UI lag and high CPU usage. The same elements render without issue on macOS, which uses hardware-accelerated Core Graphics compositing and avoids per-tile object allocation.
Built-in themes like clam are unaffected because they draw scrollbar elements
with direct GDI primitives (a single XFillRectangle for the trough), totaling
roughly 15-20 lightweight calls per frame versus hundreds of expensive ones.
Specification
Per-element pixmap cache
Each ImageData instance (one per registered image element) gains a
single-entry cache holding a pre-composed Pixmap of the fully-tiled element.
The cache is keyed by (width, height, x, y, state).
Position (x, y) is included in the key because the cache pixmap is seeded with the destination background content before tiling. This ensures correct alpha compositing for images with semi-transparent pixels — without the background seed, transparent areas would composite against an uninitialized (black) pixmap. If the element's position changes, the underlying background may differ, so the cache must be invalidated.
On each ImageElementDraw call:
If the requested dimensions, position, and state match the cache,
XCopyAreathe cached pixmap to the destination drawable in one call and return.Otherwise, allocate a new pixmap and copy the current destination background into it via
XCopyArea. Then render the tiled image over this background copy viaTtk_Tile. Store the result as the cache entry (freeing any prior cached pixmap), thenXCopyAreathe composited result to the destination.
The cache is invalidated (pixmap freed, dimensions zeroed) when:
- The underlying photo image data changes (via the
Tk_ImageChangedcallback mechanism). - The element is destroyed (
FreeImageData).
If the window is not yet realized (Tk_WindowId(tkwin) == None), caching is
skipped and the element is drawn directly via Ttk_Tile (preserving existing
behavior).
Off-by-one fix in Ttk_Fill
The tiling loop at line 231 uses y <= db where db = dst.y + dst.height (one
past the last row). This causes one extra iteration per column where the
computed tile height is zero, resulting in a wasted zero-height Tk_RedrawImage
call. The fix changes <= to <, consistent with the x loop on the
preceding line.
Properties
Single-entry cache: only the last (size, position, state) combination is stored. This is sufficient because during scrolling the size, position, and state are constant, and state changes (e.g., hover) are infrequent enough that a single re-render on transition is acceptable.
No cross-element sharing: each element instance owns its cache. Lifetime management is simple with no reference counting required.
Platform-neutral implementation: uses only Tk public API (
Tk_GetPixmap,Tk_FreePixmap,XCopyArea,Tk_GetGC,Tk_FreeGC). The performance benefit is most pronounced on Windows but is also a minor improvement on X11.Minimal memory overhead: a 14x400 scrollbar trough at 32bpp costs approximately 22 KB per cached pixmap. A full scrollbar (trough, thumb, two arrows) totals roughly 30-40 KB.
Reference Implementation
Implementation is in branch tip-754-ttk-composed-pixmaps-cache. See the performance test script for reproducing the issue.
Tested with Tcl/Tk 9.0.3 on Windows using custom image-based scrollbar themes. Performance improvement is on the order of 10-50x reduction in GDI object operations during scrolling and resize.
Copyright
This document has been placed in the public domain.
