Tk Source Code

Artifact [846520d4]
Login

Artifact 846520d4fc1509f04208f5b4ac5745e8ecff3c1d01307f22d8d9284ec38046e3:

Attachment "ttk-layout-node-cache.diff" to ticket [7caf9e9e] added by mtmcp_ 2026-06-09 14:19:03.
Index: generic/tkPort.h
==================================================================
--- generic/tkPort.h
+++ generic/tkPort.h
@@ -12,20 +12,17 @@
  */
 
 #ifndef _TKPORT
 #define _TKPORT
 
-#if defined(_WIN32)
-#   include "tkWinPort.h"
-#endif
 #ifndef _TK
 #   include "tk.h"
 #endif
-#if !defined(_WIN32)
-#   if defined(MAC_OSX_TK)
-#	include "tkMacOSXPort.h"
-#   else
-#	include "tkUnixPort.h"
-#   endif
+#if defined(_WIN32)
+#   include "tkWinPort.h"
+#elif defined(MAC_OSX_TK)
+#   include "tkMacOSXPort.h"
+#else
+#   include "tkUnixPort.h"
 #endif
 
 #endif /* _TKPORT */

Index: generic/ttk/ttkImage.c
==================================================================
--- generic/ttk/ttkImage.c
+++ generic/ttk/ttkImage.c
@@ -9,23 +9,25 @@
  * list is a sequence of statespec/imagename options as per
  * [style map].
  */
 
 #include "tkInt.h"
-#include "ttkTheme.h"
+#include "ttkThemeInt.h"
 
 #define MIN(a,b) ((a) < (b) ? (a) : (b))
 
 /*------------------------------------------------------------------------
  * +++ ImageSpec management.
  */
 
 struct TtkImageSpec {
     Tk_Image		baseImage;	/* Base image to use */
+    Tcl_Obj		*baseName;	/* Name of base image */
     Tcl_Size		mapCount;	/* #state-specific overrides */
     Ttk_StateSpec	*states;	/* array[mapCount] of states ... */
     Tk_Image		*images;	/* ... per-state images to use */
+    Tcl_Obj		**names;	/* array[mapCount] of image names */
     Tk_ImageChangedProc *imageChanged;
     void		*imageChangedClientData;
 };
 
 /* NullImageChanged --
@@ -83,13 +85,15 @@
     Tcl_Size objc;
     Tcl_Obj **objv;
 
     imageSpec = (Ttk_ImageSpec *)Tcl_Alloc(sizeof(*imageSpec));
     imageSpec->baseImage = 0;
+    imageSpec->baseName = 0;
     imageSpec->mapCount = 0;
     imageSpec->states = 0;
     imageSpec->images = 0;
+    imageSpec->names = 0;
     imageSpec->imageChanged = imageChangedProc;
     imageSpec->imageChangedClientData = imageChangedClientData;
 
     if (Tcl_ListObjGetElements(interp, objPtr, &objc, &objv) != TCL_OK) {
 	goto error;
@@ -106,18 +110,21 @@
     }
 
     n = (objc - 1) / 2;
     imageSpec->states = (Ttk_StateSpec *)Tcl_Alloc(n * sizeof(Ttk_StateSpec));
     imageSpec->images = (Tk_Image *)Tcl_Alloc(n * sizeof(Tk_Image));
+    imageSpec->names = (Tcl_Obj **)Tcl_Alloc(n * sizeof(Tcl_Obj *));
 
     /* Get base image:
     */
     imageSpec->baseImage = Tk_GetImage(
 	    interp, tkwin, Tcl_GetString(objv[0]), ImageSpecImageChanged, imageSpec);
     if (!imageSpec->baseImage) {
 	goto error;
     }
+    imageSpec->baseName = objv[0];
+    Tcl_IncrRefCount(imageSpec->baseName);
 
     /* Extract state and image specifications:
      */
     for (i = 0; i < n; ++i) {
 	Tcl_Obj *stateSpec = objv[2*i + 1];
@@ -132,10 +139,12 @@
 	imageSpec->images[i] = Tk_GetImage(
 	    interp, tkwin, imageName, NullImageChanged, NULL);
 	if (imageSpec->images[i] == NULL) {
 	    goto error;
 	}
+	imageSpec->names[i] = objv[2*i + 2];
+	Tcl_IncrRefCount(imageSpec->names[i]);
 	imageSpec->mapCount = i+1;
     }
 
     return imageSpec;
 
@@ -151,15 +160,20 @@
 {
     int i;
 
     for (i=0; i < imageSpec->mapCount; ++i) {
 	Tk_FreeImage(imageSpec->images[i]);
+	if (imageSpec->names && imageSpec->names[i]) {
+	    Tcl_DecrRefCount(imageSpec->names[i]);
+	}
     }
 
     if (imageSpec->baseImage) { Tk_FreeImage(imageSpec->baseImage); }
+    if (imageSpec->baseName) { Tcl_DecrRefCount(imageSpec->baseName); }
     if (imageSpec->states) { Tcl_Free(imageSpec->states); }
     if (imageSpec->images) { Tcl_Free(imageSpec->images); }
+    if (imageSpec->names) { Tcl_Free(imageSpec->names); }
 
     Tcl_Free(imageSpec);
 }
 
 /* TtkSelectImage --
@@ -177,10 +191,28 @@
 	}
     }
     return imageSpec->baseImage;
 }
 
+#ifndef TK_NO_DOUBLE_BUFFERING
+/* TtkSelectImageName --
+ *	Name of the image TtkSelectImage would pick, for photo lookup.
+ */
+static Tcl_Obj *TtkSelectImageName(
+    Ttk_ImageSpec *imageSpec,
+    Ttk_State state)
+{
+    int i;
+    for (i = 0; i < imageSpec->mapCount; ++i) {
+	if (Ttk_StateMatches(state, imageSpec->states+i)) {
+	    return imageSpec->names[i];
+	}
+    }
+    return imageSpec->baseName;
+}
+#endif /* !TK_NO_DOUBLE_BUFFERING */
+
 /*------------------------------------------------------------------------
  * +++ Drawing utilities.
  */
 
 /* LPadding, CPadding, RPadding --
@@ -226,11 +258,11 @@
 	return;
     }
 
     for (x = dst.x; x < dr; x += src.width) {
 	int cw = MIN(src.width, dr - x);
-	for (y = dst.y; y <= db; y += src.height) {
+	for (y = dst.y; y < db; y += src.height) {
 	    int ch = MIN(src.height, db - y);
 	    Tk_RedrawImage(image, src.x, src.y, cw, ch, d, x, y);
 	}
     }
 }
@@ -259,25 +291,47 @@
     Ttk_Stripe(tkwin, d, image, BPadding(src,p), BPadding(dst,p), p);
 }
 
 /*------------------------------------------------------------------------
  * +++ Image element definition.
+ *
+ * The image element is a pure renderer (ImageElementDraw tiles into the given
+ * drawable); the per-node cache owns the composited result.  This element only
+ * advertises that it is cacheable and reports opacity + epoch via
+ * ImageElementCacheInfo.
  */
 
 typedef struct {		/* ClientData for image elements */
     Ttk_ImageSpec *imageSpec;	/* Image(s) to use */
     int minWidth;		/* Minimum width; overrides image width */
     int minHeight;		/* Minimum height; overrides image height */
     Ttk_Sticky sticky;		/* -stickiness specification */
     Ttk_Padding border;		/* Fixed border region */
     Ttk_Padding padding;	/* Internal padding */
+    unsigned epoch;		/* Bumps when a source image's pixels change */
 
 #ifdef TILE_07_COMPAT
     Ttk_ResourceCache cache;	/* Resource cache for images */
     Ttk_StateMap imageMap;	/* State-based lookup table for images */
 #endif
 } ImageData;
+
+/* ImageElementImageChanged --
+ *	A source image's pixels changed; bump the content epoch.
+ */
+static void ImageElementImageChanged(
+    void *clientData,
+    TCL_UNUSED(int),
+    TCL_UNUSED(int),
+    TCL_UNUSED(int),
+    TCL_UNUSED(int),
+    TCL_UNUSED(int),
+    TCL_UNUSED(int))
+{
+    ImageData *imageData = (ImageData *)clientData;
+    imageData->epoch++;
+}
 
 static void FreeImageData(void *clientData)
 {
     ImageData *imageData = (ImageData *)clientData;
     if (imageData->imageSpec)	{ TtkFreeImageSpec(imageData->imageSpec); }
@@ -285,10 +339,78 @@
     if (imageData->imageMap)	{ Tcl_DecrRefCount(imageData->imageMap); }
 #endif
     Tcl_Free(clientData);
 }
 
+#ifndef TK_NO_DOUBLE_BUFFERING
+/* ImageIsOpaque --
+ *	Return 1 if the named photo is fully opaque (every pixel alpha 255).
+ *	Anything not a readable photo is treated as translucent.
+ */
+static int ImageIsOpaque(Tcl_Interp *interp, Tcl_Obj *imageName)
+{
+    Tk_PhotoHandle photo;
+    Tk_PhotoImageBlock block;
+    int x, y, alphaOff;
+
+    if (interp == NULL || imageName == NULL) {
+	return 0;
+    }
+    photo = Tk_FindPhoto(interp, Tcl_GetString(imageName));
+    if (photo == NULL || !Tk_PhotoGetImage(photo, &block)) {
+	return 0;
+    }
+    if (block.pixelSize < 4) {
+	return 1;		/* No alpha channel: opaque. */
+    }
+    alphaOff = block.offset[3];
+    for (y = 0; y < block.height; y++) {
+	unsigned char *pixPtr = block.pixelPtr + y * block.pitch + alphaOff;
+	for (x = 0; x < block.width; x++) {
+	    if (*pixPtr != 255) {
+		return 0;
+	    }
+	    pixPtr += block.pixelSize;
+	}
+    }
+    return 1;
+}
+
+/* ImageElementCacheInfo --
+ *	Report the element's content epoch and whether it opaquely covers the
+ *	parcel -- the selected image is opaque AND -sticky fills the box.  A
+ *	non-filling -sticky leaves margins, so it counts as translucent.
+ */
+static void ImageElementCacheInfo(
+    void *clientData,
+    TCL_UNUSED(void *), /* elementRecord */
+    Tk_Window tkwin,
+    Ttk_Box b,
+    Ttk_State state,
+    Ttk_ElementCacheInfo *info)
+{
+    ImageData *imageData = (ImageData *)clientData;
+    Tk_Image image = TtkSelectImage(imageData->imageSpec, tkwin, state);
+    int imgWidth, imgHeight;
+    Ttk_Box dst;
+
+    info->epoch = imageData->epoch;
+    info->opaque = 0;
+
+    if (image == NULL) {
+	return;
+    }
+    Tk_SizeOfImage(image, &imgWidth, &imgHeight);
+    dst = Ttk_StickBox(b, imgWidth, imgHeight, imageData->sticky);
+    if (dst.x == b.x && dst.y == b.y
+	    && dst.width == b.width && dst.height == b.height) {
+	info->opaque = ImageIsOpaque(Tk_Interp(tkwin),
+		TtkSelectImageName(imageData->imageSpec, state));
+    }
+}
+#endif /* !TK_NO_DOUBLE_BUFFERING */
+
 static void ImageElementSize(
     void *clientData,
     TCL_UNUSED(void *), /* elementRecord */
     TCL_UNUSED(Tk_Window),
     TCL_UNUSED(Ttk_State), /* state */
@@ -374,10 +496,11 @@
 	 { "-border","-height","-padding","-sticky","-width",NULL };
     enum { O_BORDER, O_HEIGHT, O_PADDING, O_STICKY, O_WIDTH };
 
     Ttk_ImageSpec *imageSpec = 0;
     ImageData *imageData = 0;
+    Ttk_ElementClass *elementClass;
     int padding_specified = 0;
     Tcl_Size i;
 
     if (objc + 1 < 2) {
 	Tcl_SetObjResult(interp, Tcl_NewStringObj(
@@ -384,16 +507,20 @@
 		"Must supply a base image", -1));
 	Tcl_SetErrorCode(interp, "TTK", "IMAGE", "BASE", (char *)NULL);
 	return TCL_ERROR;
     }
 
-    imageSpec = TtkGetImageSpec(interp, Tk_MainWindow(interp), objv[0]);
+    imageData = (ImageData *)Tcl_Alloc(sizeof(*imageData));
+    memset(imageData, 0, sizeof(*imageData));
+
+    imageSpec = TtkGetImageSpecEx(interp, Tk_MainWindow(interp), objv[0],
+	    ImageElementImageChanged, imageData);
     if (!imageSpec) {
+	Tcl_Free(imageData);
 	return TCL_ERROR;
     }
 
-    imageData = (ImageData *)Tcl_Alloc(sizeof(*imageData));
     imageData->imageSpec = imageSpec;
     imageData->minWidth = imageData->minHeight = -1;
     imageData->sticky = TTK_FILL_BOTH;
     imageData->border = imageData->padding = Ttk_UniformPadding(0);
 #ifdef TILE_07_COMPAT
@@ -451,15 +578,19 @@
 		if (Ttk_GetStickyFromObj(interp, objv[i+1], &imageData->sticky)
 			!= TCL_OK) { goto error; }
 	}
     }
 
-    if (!Ttk_RegisterElement(interp, theme, elementName, &ImageElementSpec,
-		imageData))
-    {
+    elementClass = Ttk_RegisterElement(interp, theme, elementName,
+	    &ImageElementSpec, imageData);
+    if (!elementClass) {
 	goto error;
     }
+#ifndef TK_NO_DOUBLE_BUFFERING
+    TtkSetElementCachePolicy(elementClass, TTK_ELEMENT_CACHEABLE,
+	    ImageElementCacheInfo);
+#endif
 
     Ttk_RegisterCleanup(interp, imageData, FreeImageData);
     Tcl_SetObjResult(interp, Tcl_NewStringObj(elementName, -1));
     return TCL_OK;
 

Index: generic/ttk/ttkLayout.c
==================================================================
--- generic/ttk/ttkLayout.c
+++ generic/ttk/ttkLayout.c
@@ -526,10 +526,11 @@
     unsigned		flags;		/* Packing and sticky flags */
     Ttk_ElementClass	*eclass;	/* Class record */
     Ttk_State		state;		/* Current state */
     Ttk_Box		parcel;		/* allocated parcel */
     Ttk_LayoutNode	*next, *child;
+    NodeDrawCache	*drawCache;	/* Per-node render cache (ttkNodeCache.c) */
 };
 
 static Ttk_LayoutNode *Ttk_NewLayoutNode(
     unsigned flags, Ttk_ElementClass *elementClass)
 {
@@ -538,19 +539,21 @@
     node->flags = flags;
     node->eclass = elementClass;
     node->state = 0u;
     node->next = node->child = 0;
     node->parcel = Ttk_MakeBox(0,0,0,0);
+    node->drawCache = NULL;
 
     return node;
 }
 
 static void Ttk_FreeLayoutNode(Ttk_LayoutNode *node)
 {
     while (node) {
 	Ttk_LayoutNode *next = node->next;
 	Ttk_FreeLayoutNode(node->child);
+	TtkFreeNodeDrawCache(node->drawCache);
 	Tcl_Free(node);
 	node = next;
     }
 }
 
@@ -1118,13 +1121,17 @@
  */
 
 /*
  * Ttk_DrawLayout --
  *	Draw a layout tree.
+ *
+ * Each node's element is drawn through TtkDrawCachedElement (ttkNodeCache.c),
+ * which owns the per-node render cache.  This file only walks the tree.
  */
+
 static void Ttk_DrawNodeList(
-    Ttk_Layout layout, Ttk_State state, Ttk_LayoutNode *node, Drawable d)
+    NodeDrawContext *ctx, Ttk_State state, Ttk_LayoutNode *node)
 {
     for (; node; node = node->next)
     {
 	int border = node->flags & TTK_BORDER;
 	int substate = state;
@@ -1132,27 +1139,31 @@
 	if (node->flags & TTK_UNIT) {
 	    substate |= node->state;
 	}
 
 	if (node->child && border) {
-	    Ttk_DrawNodeList(layout, substate, node->child, d);
+	    Ttk_DrawNodeList(ctx, substate, node->child);
 	}
 
-	Ttk_DrawElement(
-	    node->eclass,
-	    layout->style,layout->recordPtr,layout->optionTable,layout->tkwin,
-	    d, node->parcel, state | node->state);
+	TtkDrawCachedElement(ctx, node->eclass, &node->drawCache,
+		node->parcel, state | node->state);
 
 	if (node->child && !border) {
-	    Ttk_DrawNodeList(layout, substate, node->child, d);
+	    Ttk_DrawNodeList(ctx, substate, node->child);
 	}
     }
 }
 
 void Ttk_DrawLayout(Ttk_Layout layout, Ttk_State state, Drawable d)
 {
-    Ttk_DrawNodeList(layout, state, layout->root, d);
+    NodeDrawContext *ctx = TtkNodeDrawBegin(
+	    layout->style, layout->recordPtr, layout->optionTable,
+	    layout->tkwin, d);
+
+    Ttk_DrawNodeList(ctx, state, layout->root);
+
+    TtkNodeDrawEnd(ctx);
 }
 
 /*------------------------------------------------------------------------
  * +++ Inquiry and modification.
  */

ADDED   generic/ttk/ttkNodeCache.c
Index: generic/ttk/ttkNodeCache.c
==================================================================
--- generic/ttk/ttkNodeCache.c
+++ generic/ttk/ttkNodeCache.c
@@ -0,0 +1,258 @@
+/*
+ * ttkNodeCache.c --
+ *
+ *	Per-node element *render* cache for the Ttk layout-draw traversal.
+ *	ttkLayout.c walks a widget's node tree in z-order and draws each element
+ *	through this module, which caches the composited result on the layout
+ *	node -- so it is per-widget-instance -- replacing the per-tile redraw
+ *	with a single copy on a hit.  A node is reused while its size, position,
+ *	state and content epoch hold; a translucent node also rebuilds when a
+ *	lower node drawn this pass overlaps it, so a changed background shows
+ *	through.  Invalidation needs no help from widgets or elements.
+ *
+ *	Compiled in only where XCopyArea accepts a Pixmap; without double
+ *	buffering every entry point degrades to a direct element draw.
+ *
+ * Copyright © 2026 Tk contributors.  Freely redistributable.
+ */
+
+#include "tkInt.h"
+#include "ttkThemeInt.h"
+
+/*
+ * NodeDrawContext --
+ *	Per-draw-pass state (TtkNodeDrawBegin/End): element-draw parameters, the
+ *	destination, a lazy GXcopy GC, and the parcels drawn this pass.
+ */
+struct NodeDrawContext {
+    Ttk_Style		style;
+    void		*recordPtr;
+    Tk_OptionTable	optionTable;
+    Tk_Window		tkwin;
+    Drawable		d;
+#ifndef TK_NO_DOUBLE_BUFFERING
+    Display		*display;
+    GC			copyGC;		/* Lazily allocated GXcopy GC, or NULL */
+    Ttk_Box		*dirty;		/* Parcels (re)drawn fresh this pass */
+    int			ndirty, dirtycap;
+#endif
+};
+
+#ifndef TK_NO_DOUBLE_BUFFERING
+
+/*
+ * NodeDrawCache --
+ *	One cached composited element, hung off a layout node.
+ */
+struct NodeDrawCache {
+    Pixmap	pixmap;		/* Cached composited element, or None */
+    Display	*display;	/* Display owning pixmap (for Tk_FreePixmap) */
+    int		x, y, w, h;	/* Parcel the pixmap was built for */
+    Ttk_State	state;		/* State the pixmap was built for */
+    int		opaque;		/* 1: background-independent; 0: bg baked in */
+    unsigned	epoch;		/* Element content epoch when built */
+    int		valid;		/* Nonzero when the cache holds the key */
+};
+
+static int BoxesIntersect(Ttk_Box a, Ttk_Box b)
+{
+    return a.x < b.x + b.width  && b.x < a.x + a.width
+	&& a.y < b.y + b.height && b.y < a.y + a.height;
+}
+
+/* DirtyOverlaps, DirtyAdd --
+ *	The parcels drawn this pass; a translucent node rebuilds when one
+ *	overlaps its parcel.
+ */
+static int DirtyOverlaps(NodeDrawContext *ctx, Ttk_Box b)
+{
+    int i;
+    for (i = 0; i < ctx->ndirty; ++i) {
+	if (BoxesIntersect(ctx->dirty[i], b)) {
+	    return 1;
+	}
+    }
+    return 0;
+}
+
+static void DirtyAdd(NodeDrawContext *ctx, Ttk_Box b)
+{
+    if (ctx->ndirty >= ctx->dirtycap) {
+	ctx->dirtycap = ctx->dirtycap ? 2 * ctx->dirtycap : 8;
+	ctx->dirty = (Ttk_Box *)Tcl_Realloc(ctx->dirty,
+		ctx->dirtycap * sizeof(Ttk_Box));
+    }
+    ctx->dirty[ctx->ndirty++] = b;
+}
+
+static GC NodeCopyGC(NodeDrawContext *ctx)
+{
+    if (ctx->copyGC == NULL) {
+	XGCValues gcv;
+	gcv.function = GXcopy;
+	gcv.graphics_exposures = False;
+	ctx->copyGC = Tk_GetGC(ctx->tkwin,
+		GCFunction | GCGraphicsExposures, &gcv);
+    }
+    return ctx->copyGC;
+}
+
+#endif /* !TK_NO_DOUBLE_BUFFERING */
+
+/* TtkNodeDrawBegin --
+ *	Open a draw pass; graphics state is allocated lazily.
+ */
+NodeDrawContext *TtkNodeDrawBegin(
+    Ttk_Style style,
+    void *recordPtr,
+    Tk_OptionTable optionTable,
+    Tk_Window tkwin,
+    Drawable d)
+{
+    NodeDrawContext *ctx = (NodeDrawContext *)Tcl_Alloc(sizeof(*ctx));
+
+    ctx->style = style;
+    ctx->recordPtr = recordPtr;
+    ctx->optionTable = optionTable;
+    ctx->tkwin = tkwin;
+    ctx->d = d;
+#ifndef TK_NO_DOUBLE_BUFFERING
+    ctx->display = Tk_Display(tkwin);
+    ctx->copyGC = NULL;
+    ctx->dirty = NULL;
+    ctx->ndirty = ctx->dirtycap = 0;
+#endif
+    return ctx;
+}
+
+/* TtkNodeDrawEnd --
+ *	Close a draw pass: free the shared GC and dirty set.  Node pixmaps
+ *	persist; they are freed by TtkFreeNodeDrawCache.
+ */
+void TtkNodeDrawEnd(NodeDrawContext *ctx)
+{
+#ifndef TK_NO_DOUBLE_BUFFERING
+    if (ctx->copyGC != NULL) {
+	Tk_FreeGC(ctx->display, ctx->copyGC);
+    }
+    if (ctx->dirty != NULL) {
+	Tcl_Free(ctx->dirty);
+    }
+#endif
+    Tcl_Free(ctx);
+}
+
+/* TtkFreeNodeDrawCache --
+ *	Free a node's cached pixmap and record; NULL-tolerant.
+ */
+void TtkFreeNodeDrawCache(NodeDrawCache *cache)
+{
+    if (cache == NULL) {
+	return;
+    }
+#ifndef TK_NO_DOUBLE_BUFFERING
+    if (cache->pixmap != None) {
+	Tk_FreePixmap(cache->display, cache->pixmap);
+    }
+    Tcl_Free(cache);
+#endif
+}
+
+/* TtkDrawCachedElement --
+ *	Draw one node's element through its per-node cache: copy the cached
+ *	pixmap on a hit, else (re)render and store it.  Falls back to a direct
+ *	draw for non-cacheable elements, an unrealized window, or alloc failure.
+ */
+void TtkDrawCachedElement(
+    NodeDrawContext *ctx,
+    Ttk_ElementClass *eclass,
+    NodeDrawCache **cacheSlot,
+    Ttk_Box b,
+    Ttk_State state)
+{
+    if (b.width <= 0 || b.height <= 0) {
+	return;
+    }
+
+#ifndef TK_NO_DOUBLE_BUFFERING
+    if (Ttk_ElementClassCacheable(eclass)
+	    && Tk_WindowId(ctx->tkwin) != None) {
+	Ttk_ElementCacheInfo info;
+	NodeDrawCache *cache = *cacheSlot;
+	GC gc = NULL;
+
+	Ttk_ElementGetCacheInfo(eclass, ctx->style, ctx->recordPtr,
+		ctx->optionTable, ctx->tkwin, b, state, &info);
+
+	/* Hit: size, position, state and epoch match, and -- if translucent
+	 * -- nothing beneath changed this pass. */
+	if (cache && cache->valid
+		&& cache->x == b.x && cache->y == b.y
+		&& cache->w == b.width && cache->h == b.height
+		&& cache->state == state
+		&& cache->opaque == info.opaque
+		&& cache->epoch == info.epoch
+		&& (info.opaque || !DirtyOverlaps(ctx, b))
+		&& (gc = NodeCopyGC(ctx)) != NULL) {
+	    XCopyArea(ctx->display, cache->pixmap, ctx->d, gc,
+		    0, 0, (unsigned)b.width, (unsigned)b.height, b.x, b.y);
+	    return;			/* clean: parcel not marked dirty */
+	}
+
+	/* Miss: (re)build the node pixmap. */
+	if (cache == NULL) {
+	    cache = *cacheSlot = (NodeDrawCache *)Tcl_Alloc(sizeof(*cache));
+	    memset(cache, 0, sizeof(*cache));
+	}
+	if (cache->pixmap != None
+		&& (cache->w != b.width || cache->h != b.height)) {
+	    Tk_FreePixmap(cache->display, cache->pixmap);
+	    cache->pixmap = None;
+	}
+	if (cache->pixmap == None) {
+	    cache->pixmap = Tk_GetPixmap(ctx->display, Tk_WindowId(ctx->tkwin),
+		    b.width, b.height, Tk_Depth(ctx->tkwin));
+	    cache->display = ctx->display;
+	}
+	gc = NodeCopyGC(ctx);
+	if (cache->pixmap != None && gc != NULL) {
+	    if (!info.opaque) {
+		/* Seed the live background for translucent pixels and
+		 * non-filling -sticky margins. */
+		XCopyArea(ctx->display, ctx->d, cache->pixmap, gc,
+			b.x, b.y, (unsigned)b.width, (unsigned)b.height, 0, 0);
+	    }
+	    Ttk_DrawElement(eclass, ctx->style, ctx->recordPtr,
+		    ctx->optionTable, ctx->tkwin, cache->pixmap,
+		    Ttk_MakeBox(0, 0, b.width, b.height), state);
+	    XCopyArea(ctx->display, cache->pixmap, ctx->d, gc,
+		    0, 0, (unsigned)b.width, (unsigned)b.height, b.x, b.y);
+
+	    cache->x = b.x;	cache->y = b.y;
+	    cache->w = b.width;	cache->h = b.height;
+	    cache->state = state;
+	    cache->opaque = info.opaque;
+	    cache->epoch = info.epoch;
+	    cache->valid = 1;
+	    DirtyAdd(ctx, b);
+	    return;
+	}
+
+	/* Pixmap or GC allocation failed: fall through to a direct draw. */
+	if (cache) {
+	    cache->valid = 0;
+	}
+    }
+
+    /* Direct draw: non-cacheable element, unrealized window, or alloc fail. */
+    Ttk_DrawElement(eclass, ctx->style, ctx->recordPtr, ctx->optionTable,
+	    ctx->tkwin, ctx->d, b, state);
+    DirtyAdd(ctx, b);
+#else
+    (void)cacheSlot;
+    Ttk_DrawElement(eclass, ctx->style, ctx->recordPtr, ctx->optionTable,
+	    ctx->tkwin, ctx->d, b, state);
+#endif /* !TK_NO_DOUBLE_BUFFERING */
+}
+
+/*EOF*/

Index: generic/ttk/ttkTheme.c
==================================================================
--- generic/ttk/ttkTheme.c
+++ generic/ttk/ttkTheme.c
@@ -122,10 +122,12 @@
     void *clientData;		/* Client data passed in at registration time */
     void *elementRecord;	/* Scratch buffer for element record storage */
     int nResources;		/* #Element options */
     Tcl_Obj **defaultValues;	/* Array of option default values */
     Tcl_HashTable optMapCache;	/* Map: Tk_OptionTable * -> OptionMap */
+    unsigned cacheFlags;	/* Render-cache policy (TTK_ELEMENT_* flags) */
+    Ttk_ElementCacheProc *cacheProc;	/* Cache-info query, or NULL */
 };
 
 /* TTKGetOptionSpec --
  *	Look up a Tk_OptionSpec by name from a Tk_OptionTable,
  *	and verify that it's compatible with the specified Tk_OptionType,
@@ -250,12 +252,37 @@
 
     /* Initialize option map cache:
      */
     Tcl_InitHashTable(&elementClass->optMapCache, TCL_ONE_WORD_KEYS);
 
+    /* Render-cache policy: off until the element opts in. */
+    elementClass->cacheFlags = 0;
+    elementClass->cacheProc = NULL;
+
     return elementClass;
 }
+
+/* TtkSetElementCachePolicy --
+ *	Record an element class's render-cache policy and optional cache-info
+ *	query.  Called once by an element after registration.
+ */
+void TtkSetElementCachePolicy(
+    Ttk_ElementClass *eclass,
+    unsigned cacheFlags,
+    Ttk_ElementCacheProc *cacheProc)
+{
+    eclass->cacheFlags = cacheFlags;
+    eclass->cacheProc = cacheProc;
+}
+
+/* Ttk_ElementClassCacheable --
+ *	Return nonzero if the element class opted into per-node render caching.
+ */
+int Ttk_ElementClassCacheable(Ttk_ElementClass *eclass)
+{
+    return (eclass->cacheFlags & TTK_ELEMENT_CACHEABLE) != 0;
+}
 
 /*
  * FreeElementClass --
  *	Release resources associated with an element class record.
  */
@@ -1134,10 +1161,38 @@
     }
     eclass->specPtr->draw(
 	eclass->clientData, eclass->elementRecord,
 	tkwin, d, b, state);
 }
+
+/* Ttk_ElementGetCacheInfo --
+ *	Query a cacheable element's opacity and content epoch for (tkwin, state).
+ *	Defaults to {not opaque, epoch 0} when no cache proc is registered.
+ */
+void
+Ttk_ElementGetCacheInfo(
+    Ttk_ElementClass *eclass,
+    Ttk_Style style,
+    void *recordPtr,
+    Tk_OptionTable optionTable,
+    Tk_Window tkwin,
+    Ttk_Box b,
+    Ttk_State state,
+    Ttk_ElementCacheInfo *info)
+{
+    info->opaque = 0;
+    info->epoch = 0;
+    if (eclass->cacheProc == NULL) {
+	return;
+    }
+    if (!InitializeElementRecord(
+	    eclass, style, recordPtr, optionTable, tkwin, state)) {
+	return;
+    }
+    eclass->cacheProc(
+	eclass->clientData, eclass->elementRecord, tkwin, b, state, info);
+}
 
 /*------------------------------------------------------------------------
  * +++ 'style' command ensemble procedures.
  */
 

Index: generic/ttk/ttkThemeInt.h
==================================================================
--- generic/ttk/ttkThemeInt.h
+++ generic/ttk/ttkThemeInt.h
@@ -20,10 +20,52 @@
 	int *widthPtr, int *heightPtr, Ttk_Padding*);
 MODULE_SCOPE void Ttk_DrawElement(
 	Ttk_ElementClass *, Ttk_Style, void *recordPtr, Tk_OptionTable,
 	Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state);
 
+/*
+ * Element render-cache policy.  An element class may opt into per-node caching
+ * of its composited output and supply a query reporting its opacity and content
+ * epoch.  Off by default.
+ */
+#define TTK_ELEMENT_CACHEABLE	0x1	/* opts into per-node render caching */
+
+/*
+ * Cache info an element reports for the current (tkwin, state): whether it is
+ * fully opaque, and a content epoch that bumps when its own pixels change.
+ */
+typedef struct Ttk_ElementCacheInfo {
+    int		opaque;		/* element fully opaque (bg-independent) */
+    unsigned	epoch;		/* bumps when the element's own content changes */
+} Ttk_ElementCacheInfo;
+
+typedef void Ttk_ElementCacheProc(void *clientData, void *elementRecord,
+	Tk_Window tkwin, Ttk_Box b, Ttk_State state, Ttk_ElementCacheInfo *info);
+
+MODULE_SCOPE void TtkSetElementCachePolicy(
+	Ttk_ElementClass *, unsigned cacheFlags, Ttk_ElementCacheProc *);
+MODULE_SCOPE int Ttk_ElementClassCacheable(Ttk_ElementClass *);
+MODULE_SCOPE void Ttk_ElementGetCacheInfo(
+	Ttk_ElementClass *, Ttk_Style, void *recordPtr, Tk_OptionTable,
+	Tk_Window tkwin, Ttk_Box b, Ttk_State state, Ttk_ElementCacheInfo *);
+
+/*
+ * Per-node element render cache (ttkNodeCache.c).  The layout-draw traversal
+ * opens a context, draws each node's element through it, and closes it.  Both
+ * types are opaque; a layout node stores a NodeDrawCache* slot.
+ */
+typedef struct NodeDrawCache NodeDrawCache;
+typedef struct NodeDrawContext NodeDrawContext;
+
+MODULE_SCOPE NodeDrawContext *TtkNodeDrawBegin(
+	Ttk_Style style, void *recordPtr, Tk_OptionTable optionTable,
+	Tk_Window tkwin, Drawable d);
+MODULE_SCOPE void TtkDrawCachedElement(NodeDrawContext *,
+	Ttk_ElementClass *, NodeDrawCache **cacheSlot, Ttk_Box b, Ttk_State);
+MODULE_SCOPE void TtkNodeDrawEnd(NodeDrawContext *);
+MODULE_SCOPE void TtkFreeNodeDrawCache(NodeDrawCache *);
+
 MODULE_SCOPE Tcl_Obj *Ttk_QueryStyle(
     Ttk_Style, void *, Tk_OptionTable, const char *, Ttk_State state);
 
 MODULE_SCOPE Ttk_LayoutTemplate Ttk_ParseLayoutTemplate(
 	Tcl_Interp *, Tcl_Obj *);

Index: unix/Makefile.in
==================================================================
--- unix/Makefile.in
+++ unix/Makefile.in
@@ -394,11 +394,12 @@
 	tkVisual.o tkWindow.o
 
 TTK_OBJS = \
 	ttkBlink.o ttkButton.o ttkCache.o ttkClamTheme.o ttkClassicTheme.o \
 	ttkDefaultTheme.o ttkElements.o ttkEntry.o ttkFrame.o ttkImage.o \
-	ttkInit.o ttkLabel.o ttkLayout.o ttkManager.o ttkNotebook.o \
+	ttkInit.o ttkLabel.o ttkLayout.o ttkManager.o ttkNodeCache.o \
+	ttkNotebook.o \
 	ttkPanedwindow.o ttkProgress.o ttkScale.o ttkScrollbar.o ttkScroll.o \
 	ttkSeparator.o ttkSquare.o ttkState.o \
 	ttkTagSet.o ttkTheme.o ttkToggleswitch.o ttkTrace.o ttkTrack.o \
 	ttkTreeview.o ttkWidget.o ttkStubInit.o
 
@@ -497,10 +498,11 @@
 	$(TTK_DIR)/ttkImage.c \
 	$(TTK_DIR)/ttkInit.c \
 	$(TTK_DIR)/ttkLabel.c \
 	$(TTK_DIR)/ttkLayout.c \
 	$(TTK_DIR)/ttkManager.c \
+	$(TTK_DIR)/ttkNodeCache.c \
 	$(TTK_DIR)/ttkNotebook.c \
 	$(TTK_DIR)/ttkPanedwindow.c \
 	$(TTK_DIR)/ttkProgress.c \
 	$(TTK_DIR)/ttkScale.c \
 	$(TTK_DIR)/ttkScrollbar.c \
@@ -1542,10 +1544,13 @@
 	$(CC) -c $(CC_SWITCHES) $(TTK_DIR)/ttkLabel.c
 
 ttkLayout.o: $(TTK_DIR)/ttkLayout.c
 	$(CC) -c $(CC_SWITCHES) $(TTK_DIR)/ttkLayout.c
 
+ttkNodeCache.o: $(TTK_DIR)/ttkNodeCache.c
+	$(CC) -c $(CC_SWITCHES) $(TTK_DIR)/ttkNodeCache.c
+
 ttkManager.o: $(TTK_DIR)/ttkManager.c
 	$(CC) -c $(CC_SWITCHES) $(TTK_DIR)/ttkManager.c
 
 ttkNotebook.o: $(TTK_DIR)/ttkNotebook.c
 	$(CC) -c $(CC_SWITCHES) $(TTK_DIR)/ttkNotebook.c

Index: win/Makefile.in
==================================================================
--- win/Makefile.in
+++ win/Makefile.in
@@ -421,10 +421,11 @@
 	ttkImage.$(OBJEXT) \
 	ttkInit.$(OBJEXT) \
 	ttkLabel.$(OBJEXT) \
 	ttkLayout.$(OBJEXT) \
 	ttkManager.$(OBJEXT) \
+	ttkNodeCache.$(OBJEXT) \
 	ttkNotebook.$(OBJEXT) \
 	ttkPanedwindow.$(OBJEXT) \
 	ttkProgress.$(OBJEXT) \
 	ttkScale.$(OBJEXT) \
 	ttkScrollbar.$(OBJEXT) \

Index: win/configure
==================================================================
--- win/configure
+++ win/configure
@@ -4742,11 +4742,11 @@
     if test "${GCC}" = "yes" ; then
 	SHLIB_LD=""
 	SHLIB_LD_LIBS='${LIBS}'
 	LIBS="-lnetapi32 -lkernel32 -luser32 -ladvapi32 -luserenv -lws2_32"
 	# mingw needs to link ole32 and oleaut32 for [send], but MSVC doesn't
-	LIBS_GUI="-lgdi32 -lcomdlg32 -limm32 -lcomctl32 -lshell32 -luuid -loleacc -lole32 -loleaut32 -lwinspool -luxtheme -lusp10 -ldwmapi"
+	LIBS_GUI="-lgdi32 -lcomdlg32 -limm32 -lcomctl32 -lshell32 -luuid -loleacc -lole32 -loleaut32 -lwinspool -luxtheme -lusp10 -ldwmapi -lmsimg32"
 	STLIB_LD='${AR} cr'
 	RC_OUT=-o
 	RC_TYPE=
 	RC_INCLUDE=--include
 	RC_DEFINE=--define
@@ -4956,11 +4956,11 @@
 	    CFLAGS_OPTIMIZE="-nologo -O2 ${runtime}"
 	    lflags="${lflags} -nologo"
 	    LINKBIN="link"
 	fi
 
-	LIBS_GUI="gdi32.lib comdlg32.lib imm32.lib comctl32.lib shell32.lib uuid.lib winspool.lib uxtheme.lib oleacc.lib ole32.lib usp10.lib ldwmapi.lib"
+	LIBS_GUI="gdi32.lib comdlg32.lib imm32.lib comctl32.lib shell32.lib uuid.lib winspool.lib uxtheme.lib oleacc.lib ole32.lib usp10.lib ldwmapi.lib msimg32.lib"
 
 	SHLIB_LD="${LINKBIN} -dll -incremental:no ${lflags}"
 	SHLIB_LD_LIBS='${LIBS}'
 	# link -lib only works when -lib is the first arg
 	STLIB_LD="${LINKBIN} -lib ${lflags}"

Index: win/makefile.vc
==================================================================
--- win/makefile.vc
+++ win/makefile.vc
@@ -287,10 +287,11 @@
 	$(TMP_DIR)\ttkImage.obj \
 	$(TMP_DIR)\ttkInit.obj \
 	$(TMP_DIR)\ttkLabel.obj \
 	$(TMP_DIR)\ttkLayout.obj \
 	$(TMP_DIR)\ttkManager.obj \
+	$(TMP_DIR)\ttkNodeCache.obj \
 	$(TMP_DIR)\ttkNotebook.obj \
 	$(TMP_DIR)\ttkPanedwindow.obj \
 	$(TMP_DIR)\ttkProgress.obj \
 	$(TMP_DIR)\ttkScale.obj \
 	$(TMP_DIR)\ttkScrollbar.obj \
@@ -338,11 +339,11 @@
 !endif
 
 PRJ_DEFINES	= /DBUILD_ttk $(CONFIG_DEFS) /Dinline=__inline /D_CRT_SECURE_NO_DEPRECATE /D_CRT_NONSTDC_NO_DEPRECATE
 
 # Additional Link libraries needed beyond those in rules.vc
-PRJ_LIBS   = netapi32.lib gdi32.lib user32.lib userenv.lib winspool.lib shell32.lib ole32.lib uuid.lib oleacc.lib usp10.lib dwmapi.lib
+PRJ_LIBS   = netapi32.lib gdi32.lib user32.lib userenv.lib winspool.lib shell32.lib ole32.lib uuid.lib oleacc.lib usp10.lib dwmapi.lib msimg32.lib
 
 #---------------------------------------------------------------------
 # TkTest flags
 #---------------------------------------------------------------------
 

Index: win/tcl.m4
==================================================================
--- win/tcl.m4
+++ win/tcl.m4
@@ -646,11 +646,11 @@
     if test "${GCC}" = "yes" ; then
 	SHLIB_LD=""
 	SHLIB_LD_LIBS='${LIBS}'
 	LIBS="-lnetapi32 -lkernel32 -luser32 -ladvapi32 -luserenv -lws2_32"
 	# mingw needs to link ole32 and oleaut32 for [send], but MSVC doesn't
-	LIBS_GUI="-lgdi32 -lcomdlg32 -limm32 -lcomctl32 -lshell32 -luuid -loleacc -lole32 -loleaut32 -lwinspool -luxtheme -lusp10 -ldwmapi"
+	LIBS_GUI="-lgdi32 -lcomdlg32 -limm32 -lcomctl32 -lshell32 -luuid -loleacc -lole32 -loleaut32 -lwinspool -luxtheme -lusp10 -ldwmapi -lmsimg32"
 	STLIB_LD='${AR} cr'
 	RC_OUT=-o
 	RC_TYPE=
 	RC_INCLUDE=--include
 	RC_DEFINE=--define
@@ -835,11 +835,11 @@
 	    CFLAGS_OPTIMIZE="-nologo -O2 ${runtime}"
 	    lflags="${lflags} -nologo"
 	    LINKBIN="link"
 	fi
 
-	LIBS_GUI="gdi32.lib comdlg32.lib imm32.lib comctl32.lib shell32.lib uuid.lib winspool.lib uxtheme.lib oleacc.lib ole32.lib usp10.lib ldwmapi.lib"
+	LIBS_GUI="gdi32.lib comdlg32.lib imm32.lib comctl32.lib shell32.lib uuid.lib winspool.lib uxtheme.lib oleacc.lib ole32.lib usp10.lib ldwmapi.lib msimg32.lib"
 
 	SHLIB_LD="${LINKBIN} -dll -incremental:no ${lflags}"
 	SHLIB_LD_LIBS='${LIBS}'
 	# link -lib only works when -lib is the first arg
 	STLIB_LD="${LINKBIN} -lib ${lflags}"

Index: win/tkWinImage.c
==================================================================
--- win/tkWinImage.c
+++ win/tkWinImage.c
@@ -686,13 +686,254 @@
 	ReleaseDC(NULL, dc);
     }
 
     return imagePtr;
 }
+
+/*
+ *----------------------------------------------------------------------
+ *
+ * RGBA compositing support (TkpPutRGBAImage) --
+ *
+ *	TkpPutRGBAImage composites an RGBA image onto a drawable with GDI
+ *	AlphaBlend.  It is called once per 9-slice tile, so creating and
+ *	destroying a DIB section and memory DC on each call dominated the cost.
+ *	A small per-thread source DIB + memory DC is therefore cached and
+ *	reused, grown on demand up to TK_RGBA_DIB_CACHE_MAX per side; larger
+ *	one-off draws bypass the cache and allocate transiently.
+ *
+ *----------------------------------------------------------------------
+ */
+
+#define TK_RGBA_DIB_CACHE_MAX 256	/* Cache DIBs up to this size per side. */
+
+typedef struct {
+    HDC		memDC;		/* Cached memory DC, or NULL. */
+    HBITMAP	dib;		/* DIB section selected into memDC, or NULL. */
+    HBITMAP	oldBmp;		/* memDC's original bitmap, restored on free. */
+    void	*bits;		/* Pixel pointer of dib. */
+    int		w, h;		/* Size dib was allocated at. */
+} RGBACache;
+
+static Tcl_ThreadDataKey rgbaCacheKey;
+
+/* RGBACacheExitHandler -- free this thread's cached DIB and memory DC. */
+static void
+RGBACacheExitHandler(
+    TCL_UNUSED(void *))
+{
+    RGBACache *cache = (RGBACache *)
+	    Tcl_GetThreadData(&rgbaCacheKey, sizeof(RGBACache));
+
+    if (cache->memDC != NULL) {
+	if (cache->oldBmp != NULL) {
+	    SelectObject(cache->memDC, cache->oldBmp);
+	}
+	DeleteDC(cache->memDC);
+    }
+    if (cache->dib != NULL) {
+	DeleteObject(cache->dib);
+    }
+    memset(cache, 0, sizeof(*cache));
+}
+
+/* CreateRGBADib -- a top-down 32-bit BGRA DIB section; returns it and bits. */
+static HBITMAP
+CreateRGBADib(
+    HDC refDC,
+    int w, int h,
+    void **bitsPtr)
+{
+    BITMAPINFO bmi;
+
+    memset(&bmi, 0, sizeof(bmi));
+    bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
+    bmi.bmiHeader.biWidth = w;
+    bmi.bmiHeader.biHeight = -h;		/* negative = top-down */
+    bmi.bmiHeader.biPlanes = 1;
+    bmi.bmiHeader.biBitCount = 32;
+    bmi.bmiHeader.biCompression = BI_RGB;
+    return CreateDIBSection(refDC, &bmi, DIB_RGB_COLORS, bitsPtr, NULL, 0);
+}
+
+/* GetRGBACache -- this thread's cached DIB + memory DC, grown to >= w x h. */
+static RGBACache *
+GetRGBACache(
+    HDC refDC,
+    int w, int h)
+{
+    RGBACache *cache = (RGBACache *)
+	    Tcl_GetThreadData(&rgbaCacheKey, sizeof(RGBACache));
+
+    if (cache->memDC == NULL) {
+	cache->memDC = CreateCompatibleDC(refDC);
+	if (cache->memDC == NULL) {
+	    return NULL;
+	}
+	Tcl_CreateThreadExitHandler(RGBACacheExitHandler, NULL);
+    }
+    if (cache->dib != NULL && (cache->w < w || cache->h < h)) {
+	SelectObject(cache->memDC, cache->oldBmp);
+	DeleteObject(cache->dib);
+	cache->dib = NULL;
+    }
+    if (cache->dib == NULL) {
+	int newW = (w > cache->w) ? w : cache->w;
+	int newH = (h > cache->h) ? h : cache->h;
+
+	cache->dib = CreateRGBADib(refDC, newW, newH, &cache->bits);
+	if (cache->dib == NULL) {
+	    return NULL;
+	}
+	cache->oldBmp = (HBITMAP) SelectObject(cache->memDC, cache->dib);
+	cache->w = newW;
+	cache->h = newH;
+    }
+    return cache;
+}
+
+/*
+ * PremultiplyRGBA --
+ *	Copy a w x h sub-rect of an RGBA image (alpha at byte 3) into a BGRA
+ *	buffer of the given byte stride, premultiplying by alpha.  AC_SRC_ALPHA
+ *	wants premultiplied data; pix32 is RGBA and GDI DIBs are BGRA, so R and
+ *	B are swapped.
+ */
+
+static void
+PremultiplyRGBA(
+    XImage *image,
+    int src_x, int src_y,
+    int w, int h,
+    unsigned char *dst,
+    int dstStride)
+{
+    int x, y;
+
+    for (y = 0; y < h; y++) {
+	unsigned char *s = (unsigned char *) image->data
+		+ (size_t) (src_y + y) * image->bytes_per_line
+		+ (size_t) src_x * 4;
+	unsigned char *o = dst + (size_t) y * dstStride;
+
+	for (x = 0; x < w; x++) {
+	    unsigned int a = s[3];
+
+	    o[0] = (unsigned char) ((s[2] * a + 127) / 255);	/* B */
+	    o[1] = (unsigned char) ((s[1] * a + 127) / 255);	/* G */
+	    o[2] = (unsigned char) ((s[0] * a + 127) / 255);	/* R */
+	    o[3] = (unsigned char) a;				/* A */
+	    s += 4;
+	    o += 4;
+	}
+    }
+}
+
+/*
+ *----------------------------------------------------------------------
+ *
+ * TkpPutRGBAImage --
+ *
+ *	Composite an RGBA image (4 bytes/pixel, alpha at byte 3, NOT
+ *	premultiplied) onto a drawable using GDI AlphaBlend (Porter-Duff
+ *	source-over).  Enabled by TK_CAN_RENDER_RGBA in tkWinPort.h; called by
+ *	TkImgPhotoDisplay in place of the software BlendComplexAlpha path, which
+ *	reads the destination back with XGetImage and blends per pixel on the
+ *	CPU.  Works for both window and pixmap drawables.
+ *
+ * Results:
+ *	Success, or BadDrawable if a device context or DIB could not be
+ *	obtained.
+ *
+ * Side effects:
+ *	Draws onto the drawable.
+ *
+ *----------------------------------------------------------------------
+ */
+
+int
+TkpPutRGBAImage(
+    Display *display,
+    Drawable drawable,
+    GC gc,
+    XImage *image,		/* Source image; alpha at byte 3, not
+				 * premultiplied. */
+    int src_x, int src_y,	/* Top-left of the sub-rect within image. */
+    int dest_x, int dest_y,	/* Top-left within drawable. */
+    unsigned int width, unsigned int height)
+{
+    TkWinDCState state;
+    HDC dstDC;
+    BLENDFUNCTION bf = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
+    int w = (int) width, h = (int) height;
+    (void) gc;
+
+    if (w <= 0 || h <= 0) {
+	return Success;
+    }
+
+    dstDC = TkWinGetDrawableDC(display, drawable, &state);
+    if (dstDC == NULL) {
+	return BadDrawable;
+    }
+
+    if (w <= TK_RGBA_DIB_CACHE_MAX && h <= TK_RGBA_DIB_CACHE_MAX) {
+	/*
+	 * Common case: composite through the per-thread cached DIB + memory
+	 * DC, so repeated small draws (e.g. 9-slice tiles) don't churn GDI
+	 * objects.  The DIB may be wider than w, so premultiply with its
+	 * stride and AlphaBlend just the (0,0,w,h) sub-rect.
+	 */
+
+	RGBACache *cache = GetRGBACache(dstDC, w, h);
+
+	if (cache != NULL) {
+	    PremultiplyRGBA(image, src_x, src_y, w, h,
+		    (unsigned char *) cache->bits, cache->w * 4);
+	    if (!AlphaBlend(dstDC, dest_x, dest_y, w, h,
+		    cache->memDC, 0, 0, w, h, bf)) {
+		BitBlt(dstDC, dest_x, dest_y, w, h, cache->memDC, 0, 0, SRCCOPY);
+	    }
+	    TkWinReleaseDrawableDC(drawable, dstDC, &state);
+	    return Success;
+	}
+
+	/* Cache setup failed; fall through to a one-off allocation. */
+    }
+
+    /*
+     * Oversized draw (or cache allocation failed): one-off DIB + memory DC.
+     */
+
+    {
+	HDC memDC;
+	HBITMAP dib, oldBmp;
+	void *bits = NULL;
+
+	dib = CreateRGBADib(dstDC, w, h, &bits);
+	if (dib == NULL) {
+	    TkWinReleaseDrawableDC(drawable, dstDC, &state);
+	    return BadDrawable;
+	}
+	PremultiplyRGBA(image, src_x, src_y, w, h, (unsigned char *) bits, w * 4);
+
+	memDC = CreateCompatibleDC(dstDC);
+	oldBmp = (HBITMAP) SelectObject(memDC, dib);
+	if (!AlphaBlend(dstDC, dest_x, dest_y, w, h, memDC, 0, 0, w, h, bf)) {
+	    BitBlt(dstDC, dest_x, dest_y, w, h, memDC, 0, 0, SRCCOPY);
+	}
+	SelectObject(memDC, oldBmp);
+	DeleteDC(memDC);
+	DeleteObject(dib);
+    }
+
+    TkWinReleaseDrawableDC(drawable, dstDC, &state);
+    return Success;
+}
 
 /*
  * Local Variables:
  * mode: c
  * c-basic-offset: 4
  * fill-column: 78
  * End:
  */

Index: win/tkWinPort.h
==================================================================
--- win/tkWinPort.h
+++ win/tkWinPort.h
@@ -118,6 +118,19 @@
  */
 
 #define TkpGetPixel(p) (((((p)->red >> 8) & 0xff) \
 	| ((p)->green & 0xff00) | (((p)->blue << 8) & 0xff0000)) | 0x20000000)
 
+/*
+ * Inform tkImgPhInstance.c that we implement TkpPutRGBAImage to composite RGBA
+ * images directly (via GDI AlphaBlend), so it uses that instead of the software
+ * BlendComplexAlpha path and its XGetImage read-back.
+ */
+
+#define TK_CAN_RENDER_RGBA
+
+MODULE_SCOPE int TkpPutRGBAImage(
+	Display *display, Drawable drawable, GC gc, XImage *image,
+	int src_x, int src_y, int dest_x, int dest_y,
+	unsigned int width, unsigned int height);
+
 #endif /* _WINPORT */