diff --git a/docs/COVERAGE.md b/docs/COVERAGE.md index a5b1c09..dee62e9 100644 --- a/docs/COVERAGE.md +++ b/docs/COVERAGE.md @@ -18,7 +18,7 @@ The status column uses three buckets: |--------------------------|------------|-------| | Display lifecycle | Functional | `XOpenDisplay`, default screen and visual, `ConnectionNumber` for `select()` integration. | | Windows | Functional | Creation, mapping, hierarchy, attribute queries, destruction, debug introspection. | -| Drawables and GCs | Partial | Lines, points, rectangles, single-arc `XDrawArc`/`XFillArc`, text, line attributes, foreground/background, clip rectangles. `XFillPolygon`, `XDrawArcs`, and `XFillArcs` are stubs. | +| Drawables and GCs | Functional | Lines, points, rectangles, polygons, single and batched arcs, text, line attributes, foreground/background, clip rectangles, and GC raster functions for solid point/line/rectangle/polygon draws. | | Pixmaps | Functional | `XCreatePixmap`, `XCopyArea`, double-buffering patterns. | | Images | Functional | `XCreateImage` / `XPutImage` / `XGetImage` for ZPixmap and bitmap formats. | | Events | Functional | Expose, key, button, motion, configure, enter/leave, focus, client message, mapping notify. | @@ -50,10 +50,23 @@ The status column uses three buckets: - Mouse wheel input surfaces as `Button4` / `Button5` (vertical) and `Button6` / `Button7` (horizontal) `ButtonPress` events with the current modifier state, matching Xorg server convention. -- `GXinvert` is supported on filled rectangles through a read-back, invert, - and blit path. Other raster ops (`GXxor`, `GXand`, ...) silently fall back - to `GXcopy`; `XSetFunction` stores the requested value but the drawing - paths only act on `GXinvert`. +- GC raster functions (`GXclear` through `GXset`) are supported for solid + `XDrawPoint` / `XDrawPoints`, `XDrawLine` / `XDrawLines` / `XDrawSegments`, + `XFillRectangle` / `XFillRectangles`, and `XFillPolygon` through a software + read-back, mutate, and blit path when SDL has no direct renderer equivalent. +- Large solid `XDrawArc` / `XFillArc` calls route through the in-tree + arc-to-cubic path accelerator and scanline raster path. Small `LineSolid` + one-pixel arcs stay on the legacy point renderer to avoid regressing tiny + primitive overhead, but `ArcChord` fills, dashed line styles, and any + `lineWidth > 1` are always routed through the path accelerator so the + legacy pie-only / dotty fallbacks never run. Wide solid strokes and + `LineOnOffDash` / `LineDoubleDash` use the path stroke expansion for line, + segment, rectangle, polyline, and large arc drawing. The stroke outline + builder honors `JoinMiter` (with the X11 default miter-limit of 10), + `JoinBevel`, `JoinRound`, and `CapButt` / `CapRound` / `CapProjecting`. + Polygon region creation shares the same path edge/span builder and uses + pixman region union for final storage. Tile, stipple, non-solid fill, + and non-copy raster ops still use the pre-accelerator drawing paths. ## Compatibility limits diff --git a/docs/PORTING.md b/docs/PORTING.md index bb713d8..0276db2 100644 --- a/docs/PORTING.md +++ b/docs/PORTING.md @@ -135,8 +135,7 @@ because they have no in-process analogue: - GLX or any OpenGL-via-X11 surface creation. - A real, conformant XIM input method server. - ICC color-managed output through Xcms conversions. -- Wide polygon and multi-arc rendering (`XFillPolygon`, `XDrawArcs`, - `XFillArcs`) until those entry points are implemented. +- Advanced path rendering beyond the current polygon and arc primitives. For those cases the library is best used as a stepping stone: it keeps the application running on the new platform while the affected subsystem is diff --git a/mk/sources.mk b/mk/sources.mk index bdd320d..a3a6ea0 100644 --- a/mk/sources.mk +++ b/mk/sources.mk @@ -1,4 +1,4 @@ -SRCS := $(wildcard src/*.c) +SRCS := $(wildcard src/*.c) $(wildcard src/path/*.c) # Upstream libX11 translation units staged by mk/upstream-headers.mk via # scripts/sync-upstream-headers.py. The Makefile compiles them in place so diff --git a/mk/tests.mk b/mk/tests.mk index d2104d1..e7eb77e 100644 --- a/mk/tests.mk +++ b/mk/tests.mk @@ -1,6 +1,7 @@ CHECK_BINS := $(OUT)/tests/check $(OUT)/tests/symbol-coverage +BENCH_BINS := $(OUT)/tests/bench-paths -.PHONY: check symbol-coverage api-symbol-coverage +.PHONY: check symbol-coverage api-symbol-coverage bench-paths ## Build and run the regression test suite check: $(CHECK_BINS) @@ -18,6 +19,9 @@ symbol-coverage: $(OUT)/tests/symbol-coverage api-symbol-coverage api-symbol-coverage: $(TARGET) tests/api-symbols.txt tests/check-api-symbols.py $(PYTHON) tests/check-api-symbols.py $(TARGET) tests/api-symbols.txt +bench-paths: $(BENCH_BINS) + SDL_VIDEODRIVER=dummy $(OUT)/tests/bench-paths + $(OUT)/tests/%: tests/%.c $(TARGET) @mkdir -p $(dir $@) @echo " CC $<" diff --git a/src/drawing.c b/src/drawing.c index 88bd044..2e61815 100644 --- a/src/drawing.c +++ b/src/drawing.c @@ -10,6 +10,7 @@ #include "gc.h" #include "colors.h" #include "events.h" +#include "path/raster.h" #include #include @@ -20,42 +21,44 @@ void drawWindowDataToScreen() { SDL_Renderer *screen = GET_WINDOW_STRUCT(SCREEN_WINDOW)->sdlRenderer; - if (!screen) { + if (!screen) return; - } + Window *children = GET_CHILDREN(SCREEN_WINDOW); SDL_Texture *prevTarget = SDL_GetRenderTarget(screen); Bool screenTargetMutated = False; size_t i; for (i = 0; i < GET_WINDOW_STRUCT(SCREEN_WINDOW)->children.length; i++) { WindowStruct *child = GET_WINDOW_STRUCT(children[i]); - if (!child->sdlWindow) { + if (!child->sdlWindow) continue; - } - /* A window can enter Mapped state without ever drawing, so its - * backing texture is still NULL. Triggering getWindowRenderer - * here lazily allocates and background-fills the texture so - * the present pipeline never silently skips a mapped window. */ + + /* A window can enter Mapped state without ever drawing, so its backing + * texture is still NULL. Triggering getWindowRenderer here lazily + * allocates and background-fills the texture so the present pipeline + * never silently skips a mapped window. + */ if (!child->sdlTexture) { (void) getWindowRenderer(children[i]); screenTargetMutated = True; - if (!child->sdlTexture) { + if (!child->sdlTexture) continue; - } } + SDL_Surface *winSurface = SDL_GetWindowSurface(child->sdlWindow); if (!winSurface) { LOG("SDL_GetWindowSurface failed in %s: %s\n", __func__, SDL_GetError()); continue; } + int w, h; GET_WINDOW_DIMS(children[i], w, h); - /* Clamp the readback rect to all three of the X11 logical size, - * the backing texture size, and the SDL window surface size. - * Window surface and X11 logical w/h can diverge during resize - * and on high-DPI setups; an unclamped read would overflow - * winSurface->pixels. */ + /* Clamp the readback rect to all three of the X11 logical size, the + * backing texture size, and the SDL window surface size. Window surface + * and X11 logical w/h can diverge during resize and on high-DPI setups; + * an unclamped read would overflow winSurface->pixels. + */ int texW = 0, texH = 0; SDL_QueryTexture(child->sdlTexture, NULL, NULL, &texW, &texH); int rw = w < texW ? w : texW; @@ -64,22 +67,28 @@ void drawWindowDataToScreen() rw = winSurface->w; if (rh > winSurface->h) rh = winSurface->h; - if (rw <= 0 || rh <= 0) { + if (rw <= 0 || rh <= 0) continue; - } if (SDL_SetRenderTarget(screen, child->sdlTexture) != 0) { LOG("SDL_SetRenderTarget(backing) failed in %s: %s\n", __func__, SDL_GetError()); continue; } + screenTargetMutated = True; Uint32 winFmt = winSurface->format->format; Uint32 readFmt = SDL_PIXELFORMAT_RGBA8888; - SDL_Rect r = {0, 0, rw, rh}; + SDL_Rect r = { + .x = 0, + .y = 0, + .w = rw, + .h = rh, + }; int readRc = -1; if (winFmt == readFmt) { /* Direct readback into the window's framebuffer. Saves one - * full-image memcpy on every present. */ + * full-image memcpy on every present. + */ if (SDL_LockSurface(winSurface) == 0) { readRc = SDL_RenderReadPixels( screen, &r, readFmt, winSurface->pixels, winSurface->pitch); @@ -110,8 +119,6 @@ void drawWindowDataToScreen() } #ifdef DEBUG_WINDOWS printWindowsHierarchy(); -// drawDebugWindowSurfacePlanes(); -// drawWindowDebugView(); #endif } @@ -123,6 +130,7 @@ SDL_Renderer *getWindowRenderer(Window window) int x = 0, y = 0; viewPort.x = 0; viewPort.y = 0; + GET_WINDOW_DIMS(window, viewPort.w, viewPort.h); while (GET_PARENT(window) != None && !GET_WINDOW_STRUCT(window)->sdlWindow && @@ -133,17 +141,18 @@ SDL_Renderer *getWindowRenderer(Window window) drawWindow = GET_PARENT(drawWindow); window = drawWindow; } + renderer = GET_WINDOW_STRUCT(drawWindow)->sdlRenderer; Bool justCreatedRenderer = False; if (!renderer) { - /* All windows draw on the SCREEN renderer with a per-window - * backing texture as the render target. Mapped top-level - * windows used to own a separate SDL_Renderer attached to - * their SDL_Window, which forced every XCopyArea between two - * top-level windows (and every Pixmap->Window copy) into a - * cross-renderer readback. Unifying on SCREEN renderer keeps - * those copies in renderer space; presentation to the actual - * SDL_Window happens in drawWindowDataToScreen. */ + /* All windows draw on the SCREEN renderer with a per-window backing + * texture as the render target. Mapped top-level windows used to own + * a separate SDL_Renderer attached to their SDL_Window, which forced + * every XCopyArea between two top-level windows (and every + * Pixmap->Window copy) into a cross-renderer readback. Unifying on + * SCREEN renderer keeps those copies in renderer space; presentation + * to the actual SDL_Window happens in drawWindowDataToScreen. + */ renderer = GET_WINDOW_STRUCT(SCREEN_WINDOW)->sdlRenderer; SDL_Texture *texture = GET_WINDOW_STRUCT(drawWindow)->sdlTexture; if (!texture) { @@ -164,9 +173,11 @@ SDL_Renderer *getWindowRenderer(Window window) } } } + /* Real X11 paints a freshly mapped window with its background pixel. - * We allocate the SDL surface/texture lazily, so do the equivalent - * one-shot fill here to avoid presenting uninitialized memory. */ + * We allocate the SDL surface/texture lazily, so do the equivalent one-shot + * fill here to avoid presenting uninitialized memory. + */ if (justCreatedRenderer) { SDL_Texture *prevTarget = SDL_GetRenderTarget(renderer); SDL_Texture *initTarget = GET_WINDOW_STRUCT(drawWindow)->sdlTexture; @@ -177,11 +188,14 @@ SDL_Renderer *getWindowRenderer(Window window) GET_BLUE_FROM_COLOR(bg), GET_ALPHA_FROM_COLOR(bg)); SDL_RenderClear(renderer); SDL_SetRenderTarget(renderer, prevTarget); - /* Direct color push above bypassed the (gc, generation) cache; - * drop the cache so the next applySdlDrawState re-pushes state - * instead of trusting a stale entry. */ + + /* Direct color push above bypassed the (gc, generation) cache; drop the + * cache so the next applySdlDrawState re-pushes state instead of + * trusting a stale entry. + */ invalidateSdlDrawStateCache(); } + if (GET_WINDOW_STRUCT(drawWindow)->sdlTexture) { if (SDL_SetRenderTarget( renderer, GET_WINDOW_STRUCT(drawWindow)->sdlTexture) != 0) { @@ -210,6 +224,7 @@ SDL_Surface *getRenderSurface(SDL_Renderer *renderer) LOG("Got NULL renderer in %s\n", __func__); return NULL; } + SDL_Rect rect; SDL_RenderGetViewport(renderer, &rect); SDL_Surface *surface = SDL_CreateRGBSurface( @@ -233,9 +248,9 @@ SDL_Surface *getRenderSurface(SDL_Renderer *renderer) SDL_Surface *getRenderSurfaceRect(SDL_Renderer *renderer, const SDL_Rect *source) { - if (!renderer || !source || source->w <= 0 || source->h <= 0) { + if (!renderer || !source || source->w <= 0 || source->h <= 0) return NULL; - } + SDL_Surface *surface = SDL_CreateRGBSurface( 0, source->w, source->h, SDL_SURFACE_DEPTH, DEFAULT_RED_MASK, DEFAULT_GREEN_MASK, DEFAULT_BLUE_MASK, DEFAULT_ALPHA_MASK); @@ -244,19 +259,25 @@ SDL_Surface *getRenderSurfaceRect(SDL_Renderer *renderer, SDL_GetError()); return NULL; } + SDL_FillRect(surface, NULL, 0); - /* Clip the requested rect to the viewport. SDL_RenderReadPixels - * fails outright if the rect leaves the render target, so an X11 - * caller asking for a drawable-relative area that runs off the edge - * would otherwise lose all pixels instead of getting the in-bounds - * portion zero-padded. */ + /* Clip the requested rect to the viewport. SDL_RenderReadPixels fails + * outright if the rect leaves the render target, so an X11 caller asking + * for a drawable-relative area that runs off the edge would otherwise + * lose all pixels instead of getting the in-bounds portion zero-padded. + */ SDL_Rect viewport; SDL_RenderGetViewport(renderer, &viewport); - SDL_Rect viewBounds = {0, 0, viewport.w, viewport.h}; + SDL_Rect viewBounds = { + .x = 0, + .y = 0, + .w = viewport.w, + .h = viewport.h, + }; SDL_Rect clipped; - if (!SDL_IntersectRect(source, &viewBounds, &clipped)) { + if (!SDL_IntersectRect(source, &viewBounds, &clipped)) return surface; - } + SDL_Rect readRect = { .x = viewport.x + clipped.x, .y = viewport.y + clipped.y, @@ -341,6 +362,7 @@ void applySdlDrawState(SDL_Renderer *renderer, { if (!renderer) return; + GraphicContext *gContext = gc ? GET_GC(gc) : NULL; unsigned long generation = gContext ? gContext->generation : 0; if (lastDrawState.valid && lastDrawState.renderer == renderer && @@ -348,6 +370,7 @@ void applySdlDrawState(SDL_Renderer *renderer, lastDrawState.generation == generation && lastDrawState.blendMode == blendMode && lastDrawState.color == color) return; + SDL_SetRenderDrawBlendMode(renderer, blendMode); SDL_SetRenderDrawColor( renderer, GET_RED_FROM_COLOR(color), GET_GREEN_FROM_COLOR(color), @@ -426,57 +449,6 @@ Uint32 getPixel(SDL_Surface *surface, unsigned int x, unsigned int y) return 0; } -#define APPLY_OPERATION_TO_SURFACE(surface, operation, startX, startY, w, h) \ - { \ - LOCK_SURFACE(surface); \ - int bytesPerPixel = SDL_SURFACE_DEPTH / 8; \ - if (surface->format->BytesPerPixel != bytesPerPixel) { \ - LOG("Got invalid depth in %s\n", __func__); \ - } \ - Uint32 *pixelPointer; \ - int y, i; \ - for (y = startY; y < h; y++) { \ - pixelPointer = (Uint32 *) surface->pixels + \ - y * (surface->pitch / bytesPerPixel) + startX; \ - for (i = 0; i < w; i++) \ - operation \ - } \ - UNLOCK_SURFACE(surface); \ - } - -void clipSurface(SDL_Surface *surface, - SDL_Surface *clipSurface, - int originX, - int originY) -{ - LOCK_SURFACE(clipSurface); - Uint32 *clipPixel = clipSurface->pixels; - APPLY_OPERATION_TO_SURFACE( - surface, { pixelPointer[i] &= 0xFFFFFF00 | *clipPixel++; }, originX, - originY, clipSurface->w, clipSurface->h); - UNLOCK_SURFACE(clipSurface); -} - -void colorClipped(SDL_Surface *surface, - SDL_Surface *clipSurface, - int originX, - int originY, - long color) -{ - LOCK_SURFACE(clipSurface); - Uint32 *clipPixel = clipSurface->pixels; - APPLY_OPERATION_TO_SURFACE( - surface, - { - if (*clipPixel++ & 0x000000FF && 0) { - pixelPointer[i] = color; - LOG("clipping with fg color\n"); - } - }, - originX, originY, clipSurface->w, clipSurface->h); - UNLOCK_SURFACE(clipSurface); -} - typedef struct { double x; int winding; @@ -489,14 +461,185 @@ static int compareCrossings(const void *left, const void *right) return (a->x > b->x) - (a->x < b->x); } -static void invertRendererRect(SDL_Renderer *renderer, const SDL_Rect *rect) +static Bool isConvexPolygon(const SDL_Point *points, int npoints) +{ + long long sign = 0; + for (int i = 0; i < npoints; i++) { + SDL_Point a = points[i]; + SDL_Point b = points[(i + 1) % npoints]; + SDL_Point c = points[(i + 2) % npoints]; + long long abx = (long long) b.x - a.x; + long long aby = (long long) b.y - a.y; + long long bcx = (long long) c.x - b.x; + long long bcy = (long long) c.y - b.y; + long long cross = abx * bcy - aby * bcx; + if (cross == 0) + continue; + if (sign == 0) { + sign = cross; + } else if ((sign > 0 && cross < 0) || (sign < 0 && cross > 0)) { + return False; + } + } + return True; +} + +static Bool shouldUsePathArc(GC gc, + unsigned int width, + unsigned int height, + Bool fill) +{ + GraphicContext *gContext = GET_GC(gc); + if (gContext->function != GXcopy) + return False; + if (fill) { + if (gContext->fillStyle != FillSolid) + return False; + + /* ArcChord rendering requires honoring the chord/pie distinction, + * which only the path accelerator does. Force the path even for small + * arcs to avoid the legacy pie-only fallback. + */ + if (gContext->arcMode == ArcChord) + return True; + return width >= 16 || height >= 16; + } + + /* Stroke path handles wide lines and dashing; the legacy point-spray loop + * draws disconnected dots for wide arcs and ignores LineOnOffDash / + * LineDoubleDash entirely. + */ + if (gContext->lineWidth > 1) + return True; + if (gContext->lineStyle != LineSolid) + return True; + return width >= 16 || height >= 16; +} + +static Bool strokeLineOnRenderer(SDL_Renderer *renderer, + GC gc, + int x1, + int y1, + int x2, + int y2) +{ + Path path; + if (!pathInit(&path)) + return False; + Bool ok = pathMoveTo(&path, x1, y1) && pathLineTo(&path, x2, y2) && + rasterStrokePathOnRenderer(renderer, gc, &path); + pathFree(&path); + return ok; +} + +static Bool appendXArcPath(Path *path, + int x, + int y, + unsigned int width, + unsigned int height, + int angle1, + int angle2, + int arcMode) +{ + double rx = width / 2.0; + double ry = height / 2.0; + double cx = x + rx; + double cy = y + ry; + double startRad = (angle1 / 64.0) * M_PI / 180.0; + double sweepRad = (angle2 / 64.0) * M_PI / 180.0; + return pathAddArc(path, cx, cy, rx, ry, startRad, sweepRad, arcMode); +} + +static Uint32 applyRasterFunction(int function, + Uint32 planeMask, + Uint32 src, + Uint32 dst) +{ + Uint32 alpha = dst & 0xFF000000u; + Uint32 colorMask = planeMask & 0x00FFFFFFu; + Uint32 rgbResult = 0; + switch (function) { + case GXclear: + rgbResult = 0; + break; + case GXand: + rgbResult = src & dst; + break; + case GXandReverse: + rgbResult = src & ~dst; + break; + case GXcopy: + rgbResult = src; + break; + case GXandInverted: + rgbResult = ~src & dst; + break; + case GXnoop: + return dst; + case GXxor: + rgbResult = src ^ dst; + break; + case GXor: + rgbResult = src | dst; + break; + case GXnor: + rgbResult = ~(src | dst); + break; + case GXequiv: + rgbResult = ~(src ^ dst); + break; + case GXinvert: + rgbResult = ~dst; + break; + case GXorReverse: + rgbResult = src | ~dst; + break; + case GXcopyInverted: + rgbResult = ~src; + break; + case GXorInverted: + rgbResult = ~src | dst; + break; + case GXnand: + rgbResult = ~(src & dst); + break; + case GXset: + rgbResult = 0x00FFFFFFu; + break; + default: + rgbResult = src; + break; + } + return alpha | (dst & ~colorMask & 0x00FFFFFFu) | (rgbResult & colorMask); +} + +static Uint32 colorToArgb8888(unsigned long color) +{ + return ((Uint32) GET_ALPHA_FROM_COLOR(color) << 24) | + ((Uint32) GET_RED_FROM_COLOR(color) << 16) | + ((Uint32) GET_GREEN_FROM_COLOR(color) << 8) | + (Uint32) GET_BLUE_FROM_COLOR(color); +} + +static void rasterOpRendererRect(SDL_Renderer *renderer, + const SDL_Rect *rect, + int function, + unsigned long planeMask, + unsigned long sourceColor) { if (!renderer || !rect || rect->w <= 0 || rect->h <= 0) return; + if (function == GXnoop) + return; SDL_Rect viewport; SDL_RenderGetViewport(renderer, &viewport); - SDL_Rect bounds = {0, 0, viewport.w, viewport.h}; + SDL_Rect bounds = { + .x = 0, + .y = 0, + .w = viewport.w, + .h = viewport.h, + }; SDL_Rect clipped; if (!SDL_IntersectRect(rect, &bounds, &clipped)) return; @@ -508,13 +651,12 @@ static void invertRendererRect(SDL_Renderer *renderer, const SDL_Rect *rect) if (SDL_RenderReadPixels(renderer, &clipped, SDL_PIXELFORMAT_ARGB8888, pixels, (int) pitch) == 0) { - /* Invert R/G/B while preserving alpha. Operate on the packed Uint32 - * so byte order doesn't matter: 0x00FFFFFF masks the RGB channels for - * ARGB8888 regardless of host endianness. */ + Uint32 src = colorToArgb8888(sourceColor); for (int py = 0; py < clipped.h; py++) { Uint32 *row = (Uint32 *) (pixels + (size_t) py * pitch); for (int px = 0; px < clipped.w; px++) { - row[px] = (row[px] & 0xFF000000u) | ((~row[px]) & 0x00FFFFFFu); + row[px] = + applyRasterFunction(function, planeMask, src, row[px]); } } SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormatFrom( @@ -524,6 +666,7 @@ static void invertRendererRect(SDL_Renderer *renderer, const SDL_Rect *rect) SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface); if (texture) { + SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_NONE); if (SDL_RenderCopy(renderer, texture, NULL, &clipped) != 0) { LOG("SDL_RenderCopy failed in %s: %s\n", __func__, SDL_GetError()); @@ -537,6 +680,195 @@ static void invertRendererRect(SDL_Renderer *renderer, const SDL_Rect *rect) free(pixels); } +static void rasterOpRendererPoint(SDL_Renderer *renderer, + int x, + int y, + int function, + unsigned long planeMask, + unsigned long sourceColor) +{ + SDL_Rect point = { + .x = x, + .y = y, + .w = 1, + .h = 1, + }; + rasterOpRendererRect(renderer, &point, function, planeMask, sourceColor); +} + +/* int64 math: abs(INT_MIN) and 2*err would overflow at 32-bit width. + * Fast path reads the line's bounding box once, walks Bresenham mutating the + * in-memory buffer, and blits back once instead of doing a full + * SDL_RenderReadPixels round trip per pixel. The per-pixel fallback kicks in + * for huge bboxes so we don't try to allocate gigabytes for a pathological + * diagonal. + */ +static void rasterOpRendererLine(SDL_Renderer *renderer, + int x1, + int y1, + int x2, + int y2, + int function, + unsigned long planeMask, + unsigned long sourceColor, + Bool includeFirst) +{ + enum { BATCH_AREA_LIMIT = 1024 * 1024 }; + int xLo = x1 < x2 ? x1 : x2; + int xHi = x1 < x2 ? x2 : x1; + int yLo = y1 < y2 ? y1 : y2; + int yHi = y1 < y2 ? y2 : y1; + int64_t bboxArea = ((int64_t) xHi - xLo + 1) * ((int64_t) yHi - yLo + 1); + + int64_t dx = llabs((int64_t) x2 - (int64_t) x1); + int64_t sx = x1 < x2 ? 1 : -1; + int64_t dy = -llabs((int64_t) y2 - (int64_t) y1); + int64_t sy = y1 < y2 ? 1 : -1; + + if (bboxArea > BATCH_AREA_LIMIT) { + int64_t err = dx + dy; + int64_t px = x1, py = y1; + Bool first = True; + for (;;) { + if (includeFirst || !first) { + rasterOpRendererPoint(renderer, (int) px, (int) py, function, + planeMask, sourceColor); + } + if (px == x2 && py == y2) + break; + first = False; + int64_t e2 = 2 * err; + if (e2 >= dy) { + err += dy; + px += sx; + } + if (e2 <= dx) { + err += dx; + py += sy; + } + } + return; + } + + SDL_Rect viewport; + SDL_RenderGetViewport(renderer, &viewport); + SDL_Rect bbox = { + .x = xLo, + .y = yLo, + .w = xHi - xLo + 1, + .h = yHi - yLo + 1, + }; + SDL_Rect bounds = { + .x = 0, + .y = 0, + .w = viewport.w, + .h = viewport.h, + }; + SDL_Rect clipped; + if (!SDL_IntersectRect(&bbox, &bounds, &clipped)) + return; + + size_t pitch = (size_t) clipped.w * 4u; + unsigned char *pixels = malloc(pitch * (size_t) clipped.h); + if (!pixels) + return; + + if (SDL_RenderReadPixels(renderer, &clipped, SDL_PIXELFORMAT_ARGB8888, + pixels, (int) pitch) == 0) { + Uint32 src = colorToArgb8888(sourceColor); + int64_t err = dx + dy; + int64_t px = x1, py = y1; + Bool first = True; + for (;;) { + if (includeFirst || !first) { + int relX = (int) px - clipped.x; + int relY = (int) py - clipped.y; + if (relX >= 0 && relX < clipped.w && relY >= 0 && + relY < clipped.h) { + Uint32 *p = (Uint32 *) (pixels + (size_t) relY * pitch + + (size_t) relX * 4u); + *p = applyRasterFunction(function, planeMask, src, *p); + } + } + if (px == x2 && py == y2) + break; + first = False; + int64_t e2 = 2 * err; + if (e2 >= dy) { + err += dy; + px += sx; + } + if (e2 <= dx) { + err += dx; + py += sy; + } + } + SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormatFrom( + pixels, clipped.w, clipped.h, 32, (int) pitch, + SDL_PIXELFORMAT_ARGB8888); + if (surface) { + SDL_Texture *texture = + SDL_CreateTextureFromSurface(renderer, surface); + if (texture) { + SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_NONE); + if (SDL_RenderCopy(renderer, texture, NULL, &clipped) != 0) { + LOG("SDL_RenderCopy failed in %s: %s\n", __func__, + SDL_GetError()); + } + SDL_DestroyTexture(texture); + } + SDL_FreeSurface(surface); + } + } + free(pixels); +} + +/* Trace a rectangle outline of (w+1) by (h+1) pixels (the X11 spec for + * XDrawRectangle) under a non-GXcopy GC function. Each pixel must be touched + * exactly once or raster ops like XOR cancel themselves at the corners; we + * sequence the four edges so corner pixels appear in exactly one segment. + */ +static void rasterOpRendererRectOutline(SDL_Renderer *renderer, + int x, + int y, + unsigned int width, + unsigned int height, + int function, + unsigned long planeMask, + unsigned long sourceColor) +{ + int xRight = x + (int) width; + int yBot = y + (int) height; + if (width == 0 && height == 0) { + rasterOpRendererPoint(renderer, x, y, function, planeMask, sourceColor); + return; + } + if (width == 0) { + rasterOpRendererLine(renderer, x, y, x, yBot, function, planeMask, + sourceColor, True); + return; + } + if (height == 0) { + rasterOpRendererLine(renderer, x, y, xRight, y, function, planeMask, + sourceColor, True); + return; + } + /* Top: full, including both top corners. */ + rasterOpRendererLine(renderer, x, y, xRight, y, function, planeMask, + sourceColor, True); + /* Right: skip top-right corner (just drawn). */ + rasterOpRendererLine(renderer, xRight, y, xRight, yBot, function, planeMask, + sourceColor, False); + /* Bottom: skip bottom-right corner (just drawn). */ + rasterOpRendererLine(renderer, xRight, yBot, x, yBot, function, planeMask, + sourceColor, False); + /* Left: skip bottom-left corner (just drawn) AND stop one short of the + * top-left corner (first edge already drew it). + */ + rasterOpRendererLine(renderer, x, yBot, x, y + 1, function, planeMask, + sourceColor, False); +} + static void drawFilledSpan(SDL_Renderer *renderer, const GraphicContext *gContext, int x1, @@ -546,9 +878,15 @@ static void drawFilledSpan(SDL_Renderer *renderer, if (x1 > x2) return; - if (gContext->fillStyle == FillSolid && gContext->function == GXinvert) { - SDL_Rect span = {x1, y, x2 - x1 + 1, 1}; - invertRendererRect(renderer, &span); + if (gContext->fillStyle == FillSolid && gContext->function != GXcopy) { + SDL_Rect span = { + .x = x1, + .y = y, + .w = x2 - x1 + 1, + .h = 1, + }; + rasterOpRendererRect(renderer, &span, gContext->function, + gContext->planeMask, gContext->foreground); return; } @@ -630,9 +968,10 @@ int XFillPolygon(Display *display, } GraphicContext *gContext = GET_GC(gc); + Bool useConvexFastPath = shape == Convex && isConvexPolygon(poly, npoints); if (gContext->fillStyle == FillSolid) { LOG("Fill_style is %s\n", "FillSolid"); - if (gContext->function != GXinvert) { + if (gContext->function == GXcopy) { applySdlDrawState(renderer, gc, SDL_BLENDMODE_NONE, gContext->foreground); } @@ -672,6 +1011,20 @@ int XFillPolygon(Display *display, } if (crossingCount < 2) continue; + if (useConvexFastPath) { + double minX = crossings[0].x; + double maxX = crossings[0].x; + for (int i = 1; i < crossingCount; i++) { + if (crossings[i].x < minX) + minX = crossings[i].x; + if (crossings[i].x > maxX) + maxX = crossings[i].x; + } + int x1 = (int) ceil(minX); + int x2 = (int) ceil(maxX) - 1; + drawFilledSpan(renderer, gContext, x1, y, x2); + continue; + } qsort(crossings, (size_t) crossingCount, sizeof(PolygonCrossing), compareCrossings); if (gContext->fillRule == WindingRule) { @@ -702,27 +1055,30 @@ int XFillPolygon(Display *display, return 1; } -int XFillArc(Display *display, - Drawable d, - GC gc, - int x, - int y, - unsigned int width, - unsigned int height, - int angle1, - int angle2) +static int fillArcOnRenderer(SDL_Renderer *renderer, + GC gc, + int x, + int y, + unsigned int width, + unsigned int height, + int angle1, + int angle2) { - // https://tronche.com/gui/x/xlib/graphics/filling-areas/XFillArc.html - SET_X_SERVER_REQUEST(display, X_PolyFillArc); - TYPE_CHECK(d, DRAWABLE, display, 0); - SDL_Renderer *renderer; - GET_RENDERER(d, renderer); - if (!renderer) { - handleError(0, display, d, 0, BadDrawable, 0); - return 0; - } GraphicContext *gContext = GET_GC(gc); - applySdlDrawState(renderer, gc, SDL_BLENDMODE_BLEND, gContext->foreground); + if (shouldUsePathArc(gc, width, height, True)) { + Path path; + if (pathInit(&path)) { + Bool ok = appendXArcPath(&path, x, y, width, height, angle1, angle2, + gContext->arcMode) && + rasterFillPathOnRenderer(renderer, gc, &path); + pathFree(&path); + if (ok) + return 1; + } + } + SDL_BlendMode blendMode = + gContext->function == GXcopy ? SDL_BLENDMODE_NONE : SDL_BLENDMODE_BLEND; + applySdlDrawState(renderer, gc, blendMode, gContext->foreground); double rx = width / 2.0; double ry = height / 2.0; if (rx <= 0.0 || ry <= 0.0) @@ -731,7 +1087,6 @@ int XFillArc(Display *display, double cy = y + ry; double start = angle1 / 64.0; double extent = angle2 / 64.0; - double end = start + extent; int clipCount = getGcClipIterationCount(gc); for (int clip = 0; clip < clipCount; clip++) { if (!setGcClipForIteration(renderer, gc, clip)) @@ -756,11 +1111,10 @@ int XFillArc(Display *display, } } clearRendererClip(renderer); - (void) end; return 1; } -int XDrawArc(Display *display, +int XFillArc(Display *display, Drawable d, GC gc, int x, @@ -770,16 +1124,43 @@ int XDrawArc(Display *display, int angle1, int angle2) { - // https://tronche.com/gui/x/xlib/graphics/drawing/XDrawArc.html - SET_X_SERVER_REQUEST(display, X_PolyArc); + // https://tronche.com/gui/x/xlib/graphics/filling-areas/XFillArc.html + SET_X_SERVER_REQUEST(display, X_PolyFillArc); TYPE_CHECK(d, DRAWABLE, display, 0); + if (!gc) { + handleError(0, display, None, 0, BadGC, 0); + return 0; + } SDL_Renderer *renderer; GET_RENDERER(d, renderer); if (!renderer) { handleError(0, display, d, 0, BadDrawable, 0); return 0; } + return fillArcOnRenderer(renderer, gc, x, y, width, height, angle1, angle2); +} + +static int drawArcOnRenderer(SDL_Renderer *renderer, + GC gc, + int x, + int y, + unsigned int width, + unsigned int height, + int angle1, + int angle2) +{ GraphicContext *gContext = GET_GC(gc); + if (shouldUsePathArc(gc, width, height, False)) { + Path path; + if (pathInit(&path)) { + Bool ok = appendXArcPath(&path, x, y, width, height, angle1, angle2, + -1) && + rasterStrokePathOnRenderer(renderer, gc, &path); + pathFree(&path); + if (ok) + return 1; + } + } applySdlDrawState(renderer, gc, SDL_BLENDMODE_BLEND, gContext->foreground); double rx = width / 2.0; double ry = height / 2.0; @@ -798,6 +1179,7 @@ int XDrawArc(Display *display, for (int clip = 0; clip < clipCount; clip++) { if (!setGcClipForIteration(renderer, gc, clip)) continue; + for (int i = 0; i <= steps; i++) { double degrees = start + extent * (double) i / (double) steps; double radians = degrees * M_PI / 180.0; @@ -814,6 +1196,109 @@ int XDrawArc(Display *display, return 1; } +int XDrawArc(Display *display, + Drawable d, + GC gc, + int x, + int y, + unsigned int width, + unsigned int height, + int angle1, + int angle2) +{ + // https://tronche.com/gui/x/xlib/graphics/drawing/XDrawArc.html + SET_X_SERVER_REQUEST(display, X_PolyArc); + TYPE_CHECK(d, DRAWABLE, display, 0); + if (!gc) { + handleError(0, display, None, 0, BadGC, 0); + return 0; + } + SDL_Renderer *renderer; + GET_RENDERER(d, renderer); + if (!renderer) { + handleError(0, display, d, 0, BadDrawable, 0); + return 0; + } + return drawArcOnRenderer(renderer, gc, x, y, width, height, angle1, angle2); +} + +int XDrawArcs(Display *display, Drawable d, GC gc, XArc *arcs, int n_arcs) +{ + // https://tronche.com/gui/x/xlib/graphics/drawing/XDrawArcs.html + SET_X_SERVER_REQUEST(display, X_PolyArc); + TYPE_CHECK(d, DRAWABLE, display, 0); + if (!gc) { + handleError(0, display, None, 0, BadGC, 0); + return 0; + } + if (n_arcs <= 0) + return 1; + SDL_Renderer *renderer; + GET_RENDERER(d, renderer); + if (!renderer) { + handleError(0, display, d, 0, BadDrawable, 0); + return 0; + } + Bool canBatchPath = True; + for (int i = 0; i < n_arcs; i++) { + if (!shouldUsePathArc(gc, arcs[i].width, arcs[i].height, False)) { + canBatchPath = False; + break; + } + } + if (canBatchPath) { + Path path; + if (pathInit(&path)) { + Bool ok = True; + for (int i = 0; ok && i < n_arcs; i++) { + ok = appendXArcPath(&path, arcs[i].x, arcs[i].y, arcs[i].width, + arcs[i].height, arcs[i].angle1, + arcs[i].angle2, -1); + } + if (ok) + ok = rasterStrokePathOnRenderer(renderer, gc, &path); + pathFree(&path); + if (ok) + return 1; + } + } + for (int i = 0; i < n_arcs; i++) { + if (!drawArcOnRenderer(renderer, gc, arcs[i].x, arcs[i].y, + arcs[i].width, arcs[i].height, arcs[i].angle1, + arcs[i].angle2)) { + return 0; + } + } + return 1; +} + +int XFillArcs(Display *display, Drawable d, GC gc, XArc *arcs, int n_arcs) +{ + // https://tronche.com/gui/x/xlib/graphics/filling-areas/XFillArcs.html + SET_X_SERVER_REQUEST(display, X_PolyFillArc); + TYPE_CHECK(d, DRAWABLE, display, 0); + if (!gc) { + handleError(0, display, None, 0, BadGC, 0); + return 0; + } + if (n_arcs <= 0) + return 1; + SDL_Renderer *renderer; + GET_RENDERER(d, renderer); + if (!renderer) { + handleError(0, display, d, 0, BadDrawable, 0); + return 0; + } + for (int i = 0; i < n_arcs; i++) { + if (!fillArcOnRenderer(renderer, gc, arcs[i].x, arcs[i].y, + arcs[i].width, arcs[i].height, arcs[i].angle1, + arcs[i].angle2)) { + return 0; + } + } + return 1; +} + int XCopyPlane(Display *display, Drawable src, Drawable dest, @@ -843,10 +1328,10 @@ int XCopyPlane(Display *display, return 0; } - /* SDL_Rect and SDL_CreateTexture take signed ints, and the pixel - * scratch buffer is width * height * sizeof(Uint32) bytes. Reject - * dimensions that would overflow either signed int or size_t before - * touching the allocator. */ + /* SDL_Rect and SDL_CreateTexture take signed ints, and the pixel scratch + * buffer is width * height * sizeof(Uint32) bytes. Reject dimensions that + * would overflow either signed int or size_t before touching the allocator. + */ if (width > (unsigned int) INT_MAX || height > (unsigned int) INT_MAX || (height != 0 && width > SIZE_MAX / sizeof(Uint32) / (size_t) height)) { handleError(0, display, dest, 0, BadValue, 0); @@ -860,7 +1345,12 @@ int XCopyPlane(Display *display, return 0; } - SDL_Rect srcRect = {src_x, src_y, (int) width, (int) height}; + SDL_Rect srcRect = { + .x = src_x, + .y = src_y, + .w = (int) width, + .h = (int) height, + }; SDL_Surface *srcSurface = getRenderSurfaceRect(srcRenderer, &srcRect); if (!srcSurface) { handleError(0, display, src, 0, BadMatch, 0); @@ -929,7 +1419,12 @@ int XCopyPlane(Display *display, } free(pixels); SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_NONE); - SDL_Rect destRect = {dest_x, dest_y, (int) width, (int) height}; + SDL_Rect destRect = { + .x = dest_x, + .y = dest_y, + .w = (int) width, + .h = (int) height, + }; int clipCount = getGcClipIterationCount(gc); int rcCopy = 0; for (int clip = 0; clip < clipCount; clip++) { @@ -946,9 +1441,8 @@ int XCopyPlane(Display *display, handleError(0, display, dest, 0, BadMatch, 0); return 0; } - if (gContext->graphicsExposures) { + if (gContext->graphicsExposures) postEvent(display, dest, NoExpose, X_CopyPlane, 0); - } return 1; } @@ -969,12 +1463,18 @@ int XDrawPoint(Display *display, Drawable d, GC gc, int x, int y) return 0; } GraphicContext *gContext = GET_GC(gc); - applySdlDrawState(renderer, gc, SDL_BLENDMODE_NONE, gContext->foreground); + if (gContext->function == GXcopy) { + applySdlDrawState(renderer, gc, SDL_BLENDMODE_NONE, + gContext->foreground); + } int clipCount = getGcClipIterationCount(gc); for (int clip = 0; clip < clipCount; clip++) { if (!setGcClipForIteration(renderer, gc, clip)) continue; - if (SDL_RenderDrawPoint(renderer, x, y) != 0) { + if (gContext->function != GXcopy) { + rasterOpRendererPoint(renderer, x, y, gContext->function, + gContext->planeMask, gContext->foreground); + } else if (SDL_RenderDrawPoint(renderer, x, y) != 0) { clearRendererClip(renderer); LOG("SDL_RenderDrawPoint failed in %s: %s\n", __func__, SDL_GetError()); @@ -986,6 +1486,67 @@ int XDrawPoint(Display *display, Drawable d, GC gc, int x, int y) return 1; } +int XDrawPoints(Display *display, + Drawable d, + GC gc, + XPoint *points, + int n_points, + int mode) +{ + // https://tronche.com/gui/x/xlib/graphics/drawing/XDrawPoints.html + SET_X_SERVER_REQUEST(display, X_PolyPoint); + TYPE_CHECK(d, DRAWABLE, display, 0); + if (!gc) { + handleError(0, display, None, 0, BadGC, 0); + return 0; + } + if (mode != CoordModeOrigin && mode != CoordModePrevious) { + handleError(0, display, None, 0, BadValue, 0); + return 0; + } + if (n_points <= 0) + return 1; + SDL_Renderer *renderer = NULL; + GET_RENDERER(d, renderer); + if (!renderer) { + handleError(0, display, d, 0, BadDrawable, 0); + return 0; + } + GraphicContext *gContext = GET_GC(gc); + if (gContext->function == GXcopy) { + applySdlDrawState(renderer, gc, SDL_BLENDMODE_NONE, + gContext->foreground); + } + int clipCount = getGcClipIterationCount(gc); + for (int clip = 0; clip < clipCount; clip++) { + if (!setGcClipForIteration(renderer, gc, clip)) + continue; + /* int64 accumulator dodges signed overflow on hostile point lists. */ + int64_t px = 0, py = 0; + for (int i = 0; i < n_points; i++) { + if (mode == CoordModePrevious && i > 0) { + px += points[i].x; + py += points[i].y; + } else { + px = points[i].x; + py = points[i].y; + } + if (px < INT_MIN || px > INT_MAX || py < INT_MIN || py > INT_MAX) + continue; + if (gContext->function != GXcopy) { + rasterOpRendererPoint(renderer, (int) px, (int) py, + gContext->function, gContext->planeMask, + gContext->foreground); + } else if (SDL_RenderDrawPoint(renderer, (int) px, (int) py) != 0) { + LOG("SDL_RenderDrawPoint failed in %s: %s\n", __func__, + SDL_GetError()); + } + } + } + clearRendererClip(renderer); + return 1; +} + int XDrawSegments(Display *display, Drawable d, GC gc, @@ -1008,10 +1569,49 @@ int XDrawSegments(Display *display, handleError(0, display, d, 0, BadDrawable, 0); return 0; } - applySdlDrawState(renderer, gc, SDL_BLENDMODE_BLEND, - GET_GC(gc)->foreground); - /* XSegment specifies disjoint segments, so we cannot use - * SDL_RenderDrawLines (which connects them). One call per segment. */ + GraphicContext *gContext = GET_GC(gc); + if (gContext->function == GXcopy && gContext->lineStyle != LineSolid) { + Bool ok = True; + for (int i = 0; ok && i < nsegments; i++) + ok = strokeLineOnRenderer(renderer, gc, segments[i].x1, + segments[i].y1, segments[i].x2, + segments[i].y2); + if (ok) + return 1; + return 0; + } + if (gContext->function == GXcopy && gContext->lineWidth > 1) { + Path path; + if (pathInit(&path)) { + Bool ok = True; + for (int i = 0; ok && i < nsegments; i++) { + ok = pathMoveTo(&path, segments[i].x1, segments[i].y1) && + pathLineTo(&path, segments[i].x2, segments[i].y2); + } + if (ok) + ok = rasterStrokePathOnRenderer(renderer, gc, &path); + pathFree(&path); + if (ok) + return 1; + } + } + if (gContext->function != GXcopy) { + int clipCount = getGcClipIterationCount(gc); + for (int clip = 0; clip < clipCount; clip++) { + if (!setGcClipForIteration(renderer, gc, clip)) + continue; + for (int i = 0; i < nsegments; i++) { + rasterOpRendererLine(renderer, segments[i].x1, segments[i].y1, + segments[i].x2, segments[i].y2, + gContext->function, gContext->planeMask, + gContext->foreground, True); + } + } + clearRendererClip(renderer); + return 1; + } + applySdlDrawState(renderer, gc, SDL_BLENDMODE_BLEND, gContext->foreground); + /* SDL_RenderDrawLines would connect the segments; XSegment is disjoint. */ int clipCount = getGcClipIterationCount(gc); for (int clip = 0; clip < clipCount; clip++) { if (!setGcClipForIteration(renderer, gc, clip)) @@ -1046,12 +1646,24 @@ int XDrawLine(Display *display, return 0; } GraphicContext *gContext = GET_GC(gc); - applySdlDrawState(renderer, gc, SDL_BLENDMODE_BLEND, gContext->foreground); + if (gContext->function == GXcopy && + (gContext->lineWidth > 1 || gContext->lineStyle != LineSolid) && + strokeLineOnRenderer(renderer, gc, x1, y1, x2, y2)) { + return 1; + } + if (gContext->function == GXcopy) { + applySdlDrawState(renderer, gc, SDL_BLENDMODE_BLEND, + gContext->foreground); + } int clipCount = getGcClipIterationCount(gc); for (int clip = 0; clip < clipCount; clip++) { if (!setGcClipForIteration(renderer, gc, clip)) continue; - if (SDL_RenderDrawLine(renderer, x1, y1, x2, y2) != 0) { + if (gContext->function != GXcopy) { + rasterOpRendererLine(renderer, x1, y1, x2, y2, gContext->function, + gContext->planeMask, gContext->foreground, + True); + } else if (SDL_RenderDrawLine(renderer, x1, y1, x2, y2) != 0) { LOG("SDL_RenderDrawLine failed in %s: %s\n", __func__, SDL_GetError()); } @@ -1087,10 +1699,11 @@ int XDrawLines(Display *display, handleError(0, display, None, 0, BadValue, 0); return 0; } - /* The X protocol allows arbitrarily large point counts. Stack-allocating - * an SDL_Point[npoints] for untrusted input is a stack-overflow path, - * so fall back to the heap once the request exceeds the small inline - * budget. */ + + /* The X protocol allows arbitrarily large point counts. Stack-allocating an + * SDL_Point[npoints] for untrusted input is a stack-overflow path, so fall + * back to the heap once the request exceeds the small inline budget. + */ enum { POINT_INLINE_BUDGET = 128 }; SDL_Point inlinePoints[POINT_INLINE_BUDGET]; SDL_Point *heapPoints = NULL; @@ -1114,6 +1727,38 @@ int XDrawLines(Display *display, } } GraphicContext *gContext = GET_GC(gc); + if (gContext->function == GXcopy && + (gContext->lineWidth > 1 || gContext->lineStyle != LineSolid)) { + Path path; + if (pathInit(&path)) { + Bool ok = pathMoveTo(&path, sdlPoints[0].x, sdlPoints[0].y); + for (int i = 1; ok && i < npoints; i++) + ok = pathLineTo(&path, sdlPoints[i].x, sdlPoints[i].y); + if (ok) + ok = rasterStrokePathOnRenderer(renderer, gc, &path); + pathFree(&path); + if (ok) { + free(heapPoints); + return 1; + } + } + } + if (gContext->function != GXcopy) { + int clipCount = getGcClipIterationCount(gc); + for (int clip = 0; clip < clipCount; clip++) { + if (!setGcClipForIteration(renderer, gc, clip)) + continue; + for (int i = 0; i + 1 < npoints; i++) { + rasterOpRendererLine(renderer, sdlPoints[i].x, sdlPoints[i].y, + sdlPoints[i + 1].x, sdlPoints[i + 1].y, + gContext->function, gContext->planeMask, + gContext->foreground, i == 0); + } + } + clearRendererClip(renderer); + free(heapPoints); + return 1; + } applySdlDrawState(renderer, gc, SDL_BLENDMODE_BLEND, gContext->foreground); int clipCount = getGcClipIterationCount(gc); for (int clip = 0; clip < clipCount; clip++) { @@ -1179,7 +1824,12 @@ int XClearArea(register Display *dpy, return 0; } - SDL_Rect clearRect = {clearX, clearY, clearWidth, clearHeight}; + SDL_Rect clearRect = { + .x = clearX, + .y = clearY, + .w = clearWidth, + .h = clearHeight, + }; Pixmap backgroundPixmap = windowStruct->background; unsigned long backgroundColor = windowStruct->backgroundColor; if (backgroundPixmap == (Pixmap) ParentRelative && GET_PARENT(w) != None) { @@ -1200,9 +1850,19 @@ int XClearArea(register Display *dpy, tileY += textureHeight) { for (int tileX = clearRect.x; tileX < clearRect.x + clearRect.w; tileX += textureWidth) { - SDL_Rect dest = {tileX, tileY, textureWidth, textureHeight}; + SDL_Rect dest = { + .x = tileX, + .y = tileY, + .w = textureWidth, + .h = textureHeight, + }; SDL_IntersectRect(&dest, &clearRect, &dest); - SDL_Rect src = {0, 0, dest.w, dest.h}; + SDL_Rect src = { + .x = 0, + .y = 0, + .w = dest.w, + .h = dest.h, + }; SDL_RenderCopy(renderer, background, &src, &dest); } } @@ -1213,7 +1873,12 @@ int XClearArea(register Display *dpy, } if (exposures) { - SDL_Rect exposeRect = {clearX, clearY, clearWidth, clearHeight}; + SDL_Rect exposeRect = { + .x = clearX, + .y = clearY, + .w = clearWidth, + .h = clearHeight, + }; postExposeEvent(dpy, w, &exposeRect, 1); } @@ -1259,19 +1924,32 @@ int XCopyArea(Display *display, handleError(0, display, dest, 0, BadDrawable, 0); return 0; } - /* Fast path: source is a Pixmap (already an SDL_Texture) and - * the destination is drawing on the same renderer that owns the - * pixmap texture. Skip the readback-and-upload dance and let - * SDL_RenderCopy stay in renderer space. */ + + /* Fast path: source is a Pixmap (already an SDL_Texture) and the + * destination is drawing on the same renderer that owns the pixmap + * texture. Skip the readback-and-upload dance and let SDL_RenderCopy + * stay in renderer space. + */ if (IS_TYPE(src, PIXMAP)) { SDL_Texture *pixmapTexture = GET_PIXMAP_TEXTURE(src); SDL_Renderer *pixmapRenderer = GET_WINDOW_STRUCT(SCREEN_WINDOW)->sdlRenderer; if (pixmapTexture && destRenderer == pixmapRenderer) { - SDL_Rect fastSrc = {src_x, src_y, (int) width, (int) height}; - SDL_Rect fastDest = {dest_x, dest_y, (int) width, (int) height}; + SDL_Rect fastSrc = { + .x = src_x, + .y = src_y, + .w = (int) width, + .h = (int) height, + }; + SDL_Rect fastDest = { + .x = dest_x, + .y = dest_y, + .w = (int) width, + .h = (int) height, + }; SDL_SetTextureBlendMode(pixmapTexture, SDL_BLENDMODE_NONE); int fastClipCount = getGcClipIterationCount(gc); + int fastRcCopy = 0; for (int clip = 0; clip < fastClipCount; clip++) { if (!setGcClipForIteration(destRenderer, gc, clip)) continue; @@ -1279,10 +1957,15 @@ int XCopyArea(Display *display, &fastDest) != 0) { LOG("SDL_RenderCopy failed in %s fast path: %s\n", __func__, SDL_GetError()); + fastRcCopy = -1; break; } } clearRendererClip(destRenderer); + if (fastRcCopy != 0) { + handleError(0, display, src, 0, BadMatch, 0); + return 0; + } if (GET_GC(gc)->graphicsExposures) { postEvent(display, dest, NoExpose, X_CopyArea, 0); } @@ -1295,13 +1978,18 @@ int XCopyArea(Display *display, handleError(0, display, src, 0, BadDrawable, 0); return 0; } - SDL_Rect srcRect, destRect; - srcRect.x = src_x; - srcRect.y = src_y; - destRect.x = dest_x; - destRect.y = dest_y; - srcRect.w = destRect.w = width; - srcRect.h = destRect.h = height; + SDL_Rect srcRect = { + .x = src_x, + .y = src_y, + .w = (int) width, + .h = (int) height, + }; + SDL_Rect destRect = { + .x = dest_x, + .y = dest_y, + .w = (int) width, + .h = (int) height, + }; SDL_Surface *srcSurface = getRenderSurfaceRect(srcRenderer, &srcRect); if (!srcSurface) { handleError(0, display, src, 0, BadMatch, 0); @@ -1371,14 +2059,43 @@ int XDrawRectangle(Display *display, handleError(0, display, d, 0, BadDrawable, 0); return 0; } - SDL_Rect sdlRect; - sdlRect.x = x; - sdlRect.y = y; - sdlRect.w = (int) width; - sdlRect.h = (int) height; - LOG("{x = %d, y = %d, w = %d, h = %d}\n", sdlRect.x, sdlRect.y, sdlRect.w, - sdlRect.h); GraphicContext *gContext = GET_GC(gc); + if (gContext->function == GXcopy && + (gContext->lineWidth > 1 || gContext->lineStyle != LineSolid)) { + Path path; + if (pathInit(&path)) { + Bool ok = pathMoveTo(&path, x, y) && + pathLineTo(&path, x + (int) width, y) && + pathLineTo(&path, x + (int) width, y + (int) height) && + pathLineTo(&path, x, y + (int) height) && + pathLineTo(&path, x, y); + if (ok) + ok = rasterStrokePathOnRenderer(renderer, gc, &path); + pathFree(&path); + if (ok) + return 1; + } + } + if (gContext->function != GXcopy) { + int clipCount = getGcClipIterationCount(gc); + for (int clip = 0; clip < clipCount; clip++) { + if (!setGcClipForIteration(renderer, gc, clip)) + continue; + rasterOpRendererRectOutline(renderer, x, y, width, height, + gContext->function, gContext->planeMask, + gContext->foreground); + } + clearRendererClip(renderer); + return 1; + } + /* SDL_RenderDrawRect outlines a w-by-h pixel rect; the X11 spec is + * (w+1) by (h+1) (matches what the wide-stroke path emits). */ + SDL_Rect sdlRect = { + .x = x, + .y = y, + .w = (int) width + 1, + .h = (int) height + 1, + }; applySdlDrawState(renderer, gc, SDL_BLENDMODE_BLEND, gContext->foreground); int clipCount = getGcClipIterationCount(gc); for (int clip = 0; clip < clipCount; clip++) { @@ -1393,6 +2110,29 @@ int XDrawRectangle(Display *display, return 1; } +int XDrawRectangles(Display *display, + Drawable d, + GC gc, + XRectangle *rectangles, + int n_rectangles) +{ + // https://tronche.com/gui/x/xlib/graphics/drawing/XDrawRectangles.html + SET_X_SERVER_REQUEST(display, X_PolyRectangle); + TYPE_CHECK(d, DRAWABLE, display, 0); + if (!gc) { + handleError(0, display, None, 0, BadGC, 0); + return 0; + } + if (n_rectangles <= 0) + return 1; + for (int i = 0; i < n_rectangles; i++) { + if (!XDrawRectangle(display, d, gc, rectangles[i].x, rectangles[i].y, + rectangles[i].width, rectangles[i].height)) + return 0; + } + return 1; +} + int XFillRectangle(Display *dpy, Drawable d, GC gc, @@ -1401,7 +2141,12 @@ int XFillRectangle(Display *dpy, unsigned int width, unsigned int height) { - XRectangle rect = {x, y, width, height}; + XRectangle rect = { + .x = x, + .y = y, + .width = width, + .height = height, + }; return XFillRectangles(dpy, d, gc, &rect, 1); } @@ -1456,17 +2201,20 @@ int XFillRectangles(Display *display, gContext->foreground); if (gContext->fillStyle == FillSolid) { LOG("Fill_style is %s\n", "FillSolid"); - if (gContext->function == GXinvert) { - /* Software-side invert. SDL_Renderer has no raster-op - * equivalent, so read the affected pixels back, invert each - * RGB byte, and blit them in place. */ + if (gContext->function != GXcopy) { + /* SDL_Renderer has no raster-op equivalent, so read the affected + * pixels back, apply the GC function in software, and blit them + * in place. + */ int clipCount = getGcClipIterationCount(gc); for (int clip = 0; clip < clipCount; clip++) { if (!setGcClipForIteration(renderer, gc, clip)) continue; for (int r = 0; r < nrectangles; r++) { SDL_Rect rr = sdlRectangles[r]; - invertRendererRect(renderer, &rr); + rasterOpRendererRect(renderer, &rr, gContext->function, + gContext->planeMask, + gContext->foreground); } } clearRendererClip(renderer); diff --git a/src/missing.c b/src/missing.c index 14ca245..4b17491 100644 --- a/src/missing.c +++ b/src/missing.c @@ -347,17 +347,6 @@ int XkbTranslateKeySym(Display *dpy, return 0; } -int XDrawPoints(register Display *dpy, - Drawable d, - GC gc, - XPoint *points, - int n_points, - int mode) /* CoordMode */ -{ - WARN_UNIMPLEMENTED; - return 0; -} - int XStoreColor(register Display *dpy, Colormap cmap, XColor *def) { WARN_UNIMPLEMENTED; @@ -1009,12 +998,6 @@ int XChangePointerControl(register Display *dpy, return 1; } -int XDrawArcs(register Display *dpy, Drawable d, GC gc, XArc *arcs, int n_arcs) -{ - WARN_UNIMPLEMENTED; - return 0; -} - int XQueryTextExtents(register Display *dpy, Font fid, register _Xconst char *string, @@ -1296,16 +1279,6 @@ int XQueryKeymap(register Display *dpy, char keys[32]) return 0; } -int XDrawRectangles(register Display *dpy, - Drawable d, - GC gc, - XRectangle *rects, - int n_rects) -{ - WARN_UNIMPLEMENTED; - return 0; -} - unsigned long XDisplayMotionBufferSize(Display *dpy) { WARN_UNIMPLEMENTED; @@ -1320,12 +1293,6 @@ int XSetPointerMapping(register Display *dpy, return 0; } -int XFillArcs(register Display *dpy, Drawable d, GC gc, XArc *arcs, int n_arcs) -{ - WARN_UNIMPLEMENTED; - return 0; -} - int XAddHost(register Display *dpy, XHostAddress *host) { WARN_UNIMPLEMENTED; diff --git a/src/path/arc.c b/src/path/arc.c new file mode 100644 index 0000000..021771e --- /dev/null +++ b/src/path/arc.c @@ -0,0 +1,88 @@ +/* Arc-to-cubic path conversion for libx11-compat + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ +#include "path.h" + +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +static Bool appendArcSegment(Path *path, + double cx, + double cy, + double rx, + double ry, + double a0, + double a1) +{ + double delta = a1 - a0; + double k = (4.0 / 3.0) * tan(delta / 4.0); + double cos0 = cos(a0); + double sin0 = sin(a0); + double cos1 = cos(a1); + double sin1 = sin(a1); + + double x1 = cx + rx * (cos0 - k * sin0); + double y1 = cy - ry * (sin0 + k * cos0); + double x2 = cx + rx * (cos1 + k * sin1); + double y2 = cy - ry * (sin1 - k * cos1); + double x3 = cx + rx * cos1; + double y3 = cy - ry * sin1; + return pathCubicTo(path, x1, y1, x2, y2, x3, y3); +} + +Bool pathAddArc(Path *path, + double cx, + double cy, + double rx, + double ry, + double startRad, + double sweepRad, + int arcMode) +{ + if (!path) + return False; + if (rx == 0.0 || ry == 0.0 || sweepRad == 0.0) + return True; + if (rx < 0.0) + rx = -rx; + if (ry < 0.0) + ry = -ry; + + double full = 2.0 * M_PI; + if (fabs(sweepRad) >= full) + sweepRad = sweepRad < 0.0 ? -full : full; + + double startX = cx + rx * cos(startRad); + double startY = cy - ry * sin(startRad); + Bool fillArc = arcMode == ArcPieSlice || arcMode == ArcChord; + if (fillArc && arcMode == ArcPieSlice) { + if (!pathMoveTo(path, cx, cy)) + return False; + if (!pathLineTo(path, startX, startY)) + return False; + } else { + if (!pathMoveTo(path, startX, startY)) + return False; + } + + int segments = (int) ceil(fabs(sweepRad) / (M_PI / 2.0)); + if (segments < 1) + segments = 1; + double step = sweepRad / (double) segments; + double a0 = startRad; + for (int i = 0; i < segments; i++) { + double a1 = i + 1 == segments ? startRad + sweepRad : a0 + step; + if (!appendArcSegment(path, cx, cy, rx, ry, a0, a1)) + return False; + a0 = a1; + } + if (fillArc) + return pathClose(path); + return True; +} diff --git a/src/path/compose.c b/src/path/compose.c new file mode 100644 index 0000000..d2ccc40 --- /dev/null +++ b/src/path/compose.c @@ -0,0 +1,158 @@ +/* Span composition helpers for libx11-compat + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ +#include "compose.h" + +#include +#include +#include +#include +#include + +#include "../colors.h" + +static Uint32 colorToRgba8888(unsigned long color) +{ + return ((Uint32) color << 8) | ((Uint32) color >> 24); +} + +static Bool spanFitsPixmanRect(PathSpan span, int width, int height) +{ + if (span.coverage != 255) + return False; + if (span.y < 0 || span.y >= height) + return False; + int xStart = span.xStart < 0 ? 0 : span.xStart; + int xEnd = span.xEnd >= width ? width - 1 : span.xEnd; + if (xEnd < xStart) + return False; + if (xStart > INT16_MAX || span.y > INT16_MAX) + return False; + if (xEnd - xStart + 1 > UINT16_MAX) + return False; + return True; +} + +static Bool fillFullCoverageSpansWithPixman(Uint32 *buffer, + int width, + int height, + const PathSpanList *spans, + unsigned long color, + Bool *usedPixman) +{ + *usedPixman = False; + if (width > INT16_MAX || height > INT16_MAX) + return True; + + size_t rectCount = 0; + for (size_t i = 0; i < spans->count; i++) { + if (spanFitsPixmanRect(spans->spans[i], width, height)) + rectCount++; + } + if (rectCount == 0) + return True; + if (rectCount > SIZE_MAX / sizeof(pixman_rectangle16_t)) + return False; + pixman_rectangle16_t *rects = + malloc(rectCount * sizeof(pixman_rectangle16_t)); + if (!rects) + return False; + + size_t out = 0; + for (size_t i = 0; i < spans->count; i++) { + PathSpan span = spans->spans[i]; + if (!spanFitsPixmanRect(span, width, height)) + continue; + int xStart = span.xStart < 0 ? 0 : span.xStart; + int xEnd = span.xEnd >= width ? width - 1 : span.xEnd; + rects[out++] = (pixman_rectangle16_t) { + .x = (int16_t) xStart, + .y = (int16_t) span.y, + .width = (uint16_t) (xEnd - xStart + 1), + .height = 1, + }; + } + + pixman_image_t *image = pixman_image_create_bits( + PIXMAN_r8g8b8a8, width, height, buffer, width * (int) sizeof(Uint32)); + if (!image) { + free(rects); + return False; + } + pixman_color_t pixmanColor = { + .red = (uint16_t) GET_RED_FROM_COLOR(color) * 257u, + .green = (uint16_t) GET_GREEN_FROM_COLOR(color) * 257u, + .blue = (uint16_t) GET_BLUE_FROM_COLOR(color) * 257u, + .alpha = (uint16_t) GET_ALPHA_FROM_COLOR(color) * 257u, + }; + Bool ok = pixman_image_fill_rectangles(PIXMAN_OP_SRC, image, &pixmanColor, + (int) rectCount, rects) + ? True + : False; + pixman_image_unref(image); + free(rects); + *usedPixman = ok; + return ok; +} + +Bool pathComposeSpansToBuffer(Uint32 *buffer, + int width, + int height, + const PathSpanList *spans, + unsigned long color) +{ + if (!buffer || !spans || width < 0 || height < 0) + return False; + if (width > INT_MAX || height > INT_MAX) + return False; + if (height != 0 && + (size_t) width > SIZE_MAX / sizeof(Uint32) / (size_t) height) { + return False; + } + + Uint32 src = colorToRgba8888(color); + Bool usedPixman = False; + if (!fillFullCoverageSpansWithPixman(buffer, width, height, spans, color, + &usedPixman)) { + return False; + } + for (size_t i = 0; i < spans->count; i++) { + PathSpan span = spans->spans[i]; + if (span.y < 0 || span.y >= height) + continue; + int xStart = span.xStart < 0 ? 0 : span.xStart; + int xEnd = span.xEnd >= width ? width - 1 : span.xEnd; + if (xEnd < xStart) + continue; + Uint32 *row = buffer + (size_t) span.y * (size_t) width; + if (span.coverage == 255) { + if (usedPixman && spanFitsPixmanRect(span, width, height)) + continue; + for (int x = xStart; x <= xEnd; x++) + row[x] = src; + } else { + Uint8 alpha = span.coverage; + Uint8 invAlpha = 255 - alpha; + Uint8 sr = GET_RED_FROM_COLOR(color); + Uint8 sg = GET_GREEN_FROM_COLOR(color); + Uint8 sb = GET_BLUE_FROM_COLOR(color); + Uint8 sa = GET_ALPHA_FROM_COLOR(color); + for (int x = xStart; x <= xEnd; x++) { + Uint32 dst = row[x]; + Uint8 dr = (Uint8) ((dst >> 24) & 0xff); + Uint8 dg = (Uint8) ((dst >> 16) & 0xff); + Uint8 db = (Uint8) ((dst >> 8) & 0xff); + Uint8 da = (Uint8) (dst & 0xff); + Uint8 outR = (Uint8) ((sr * alpha + dr * invAlpha) / 255); + Uint8 outG = (Uint8) ((sg * alpha + dg * invAlpha) / 255); + Uint8 outB = (Uint8) ((sb * alpha + db * invAlpha) / 255); + Uint8 outA = (Uint8) ((sa * alpha + da * invAlpha) / 255); + row[x] = ((Uint32) outR << 24) | ((Uint32) outG << 16) | + ((Uint32) outB << 8) | outA; + } + } + } + return True; +} diff --git a/src/path/compose.h b/src/path/compose.h new file mode 100644 index 0000000..0ee606d --- /dev/null +++ b/src/path/compose.h @@ -0,0 +1,20 @@ +/* Span composition helpers for libx11-compat + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ +#ifndef PATH_COMPOSE_H +#define PATH_COMPOSE_H + +#include +#include + +#include "rasterize.h" + +Bool pathComposeSpansToBuffer(Uint32 *buffer, + int width, + int height, + const PathSpanList *spans, + unsigned long color); + +#endif /* PATH_COMPOSE_H */ diff --git a/src/path/edges.c b/src/path/edges.c new file mode 100644 index 0000000..4a00c43 --- /dev/null +++ b/src/path/edges.c @@ -0,0 +1,83 @@ +/* Fixed-point edge builder for libx11-compat path fills + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ +#include "edges.h" + +#include +#include +#include +#include + +static Bool doubleToFixed(double value, int64_t *out) +{ + double scaled = value * (double) PATH_EDGE_FIXED_ONE; + if (scaled < (double) INT32_MIN || scaled > (double) INT32_MAX) + return False; + *out = (int64_t) llround(scaled); + return True; +} + +static Bool reserveEdge(PathEdgeList *list) +{ + if (list->count < list->capacity) + return True; + size_t capacity = list->capacity ? list->capacity * 2 : 32; + if (capacity <= list->capacity || capacity > SIZE_MAX / sizeof(PathEdge)) + return False; + PathEdge *grown = realloc(list->edges, capacity * sizeof(PathEdge)); + if (!grown) + return False; + list->edges = grown; + list->capacity = capacity; + return True; +} + +static Bool appendEdge(PathEdgeList *list, PathPoint a, PathPoint b) +{ + if (a.y == b.y) + return True; + if (!reserveEdge(list)) + return False; + PathEdge edge; + if (!doubleToFixed(a.x, &edge.x0) || !doubleToFixed(a.y, &edge.y0) || + !doubleToFixed(b.x, &edge.x1) || !doubleToFixed(b.y, &edge.y1)) { + return False; + } + edge.winding = b.y > a.y ? 1 : -1; + list->edges[list->count++] = edge; + int64_t edgeMinY = edge.y0 < edge.y1 ? edge.y0 : edge.y1; + int64_t edgeMaxY = edge.y0 > edge.y1 ? edge.y0 : edge.y1; + if (list->count == 1 || edgeMinY < list->minY) + list->minY = edgeMinY; + if (list->count == 1 || edgeMaxY > list->maxY) + list->maxY = edgeMaxY; + return True; +} + +Bool pathBuildEdges(const PathPoint *points, size_t count, PathEdgeList *out) +{ + if (!out) + return False; + memset(out, 0, sizeof(*out)); + if (!points) + return True; + for (size_t i = 0; i + 1 < count; i++) { + if (pathPointIsBreak(points[i]) || pathPointIsBreak(points[i + 1])) + continue; + if (!appendEdge(out, points[i], points[i + 1])) { + pathFreeEdges(out); + return False; + } + } + return True; +} + +void pathFreeEdges(PathEdgeList *edges) +{ + if (!edges) + return; + free(edges->edges); + memset(edges, 0, sizeof(*edges)); +} diff --git a/src/path/edges.h b/src/path/edges.h new file mode 100644 index 0000000..899dca6 --- /dev/null +++ b/src/path/edges.h @@ -0,0 +1,37 @@ +/* Fixed-point edge storage for libx11-compat path fills + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ +#ifndef PATH_EDGES_H +#define PATH_EDGES_H + +#include +#include +#include + +#include "path.h" + +#define PATH_EDGE_FIXED_SHIFT 8 +#define PATH_EDGE_FIXED_ONE (1 << PATH_EDGE_FIXED_SHIFT) + +typedef struct { + int64_t x0; + int64_t y0; + int64_t x1; + int64_t y1; + int winding; +} PathEdge; + +typedef struct { + PathEdge *edges; + size_t count; + size_t capacity; + int64_t minY; + int64_t maxY; +} PathEdgeList; + +Bool pathBuildEdges(const PathPoint *points, size_t count, PathEdgeList *out); +void pathFreeEdges(PathEdgeList *edges); + +#endif /* PATH_EDGES_H */ diff --git a/src/path/flatten.c b/src/path/flatten.c new file mode 100644 index 0000000..13d3a9f --- /dev/null +++ b/src/path/flatten.c @@ -0,0 +1,272 @@ +/* Adaptive path flattening for libx11-compat + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ +#include "path.h" + +#include +#include +#include +#include + +#define PATH_FLATTEN_MAX_DEPTH 32 +#define PATH_FLATTEN_SEGMENT_POINT_CAP 65536 + +typedef struct { + PathPoint *points; + size_t count; + size_t capacity; +} PointList; + +typedef struct { + PathPoint p0; + PathPoint p1; + PathPoint p2; + PathPoint p3; + int depth; +} CubicWork; + +typedef struct { + PathPoint p0; + PathPoint p1; + PathPoint p2; + int depth; +} QuadWork; + +static double pointDistanceToLine(PathPoint p, PathPoint a, PathPoint b) +{ + double dx = b.x - a.x; + double dy = b.y - a.y; + double len = hypot(dx, dy); + if (len == 0.0) + return hypot(p.x - a.x, p.y - a.y); + return fabs(dy * p.x - dx * p.y + b.x * a.y - b.y * a.x) / len; +} + +static PathPoint midpoint(PathPoint a, PathPoint b) +{ + PathPoint out = {(a.x + b.x) * 0.5, (a.y + b.y) * 0.5}; + return out; +} + +static Bool appendPointInternal(PointList *list, PathPoint point, Bool dedup) +{ + if (dedup && list->count > 0) { + PathPoint last = list->points[list->count - 1]; + if (last.x == point.x && last.y == point.y) + return True; + } + if (list->count == list->capacity) { + size_t capacity = list->capacity ? list->capacity * 2 : 32; + if (capacity <= list->capacity || + capacity > SIZE_MAX / sizeof(PathPoint)) { + return False; + } + PathPoint *grown = realloc(list->points, capacity * sizeof(PathPoint)); + if (!grown) + return False; + list->points = grown; + list->capacity = capacity; + } + list->points[list->count++] = point; + return True; +} + +static Bool appendPoint(PointList *list, PathPoint point) +{ + return appendPointInternal(list, point, True); +} + +static Bool appendLine(PointList *list, PathPoint from, PathPoint to) +{ + if (!appendPoint(list, from)) + return False; + if (from.x == to.x && from.y == to.y) + return appendPointInternal(list, to, False); + return appendPoint(list, to); +} + +static Bool appendContourBreak(PointList *list) +{ + PathPoint point = {NAN, NAN}; + return appendPoint(list, point); +} + +Bool pathPointIsBreak(PathPoint point) +{ + return isnan(point.x) || isnan(point.y); +} + +static Bool flattenQuad(PointList *list, + PathPoint p0, + PathPoint p1, + PathPoint p2, + double tolerance) +{ + QuadWork stack[PATH_FLATTEN_MAX_DEPTH + 1]; + int stackCount = 1; + size_t emitted = 0; + stack[0] = (QuadWork) {p0, p1, p2, 0}; + while (stackCount > 0) { + QuadWork work = stack[--stackCount]; + double flatness = pointDistanceToLine(work.p1, work.p0, work.p2); + if (flatness <= tolerance) { + if (++emitted > PATH_FLATTEN_SEGMENT_POINT_CAP) + return False; + if (!appendLine(list, work.p0, work.p2)) + return False; + continue; + } + if (work.depth >= PATH_FLATTEN_MAX_DEPTH) + return False; + PathPoint p01 = midpoint(work.p0, work.p1); + PathPoint p12 = midpoint(work.p1, work.p2); + PathPoint p012 = midpoint(p01, p12); + if (stackCount + 2 > (int) (sizeof(stack) / sizeof(stack[0]))) { + return False; + } + stack[stackCount++] = (QuadWork) {p012, p12, work.p2, work.depth + 1}; + stack[stackCount++] = (QuadWork) {work.p0, p01, p012, work.depth + 1}; + } + return True; +} + +static Bool flattenCubic(PointList *list, + PathPoint p0, + PathPoint p1, + PathPoint p2, + PathPoint p3, + double tolerance) +{ + CubicWork stack[PATH_FLATTEN_MAX_DEPTH + 1]; + int stackCount = 1; + size_t emitted = 0; + stack[0] = (CubicWork) {p0, p1, p2, p3, 0}; + while (stackCount > 0) { + CubicWork work = stack[--stackCount]; + double flatness1 = pointDistanceToLine(work.p1, work.p0, work.p3); + double flatness2 = pointDistanceToLine(work.p2, work.p0, work.p3); + if (flatness1 <= tolerance && flatness2 <= tolerance) { + if (++emitted > PATH_FLATTEN_SEGMENT_POINT_CAP) + return False; + if (!appendLine(list, work.p0, work.p3)) + return False; + continue; + } + if (work.depth >= PATH_FLATTEN_MAX_DEPTH) + return False; + PathPoint p01 = midpoint(work.p0, work.p1); + PathPoint p12 = midpoint(work.p1, work.p2); + PathPoint p23 = midpoint(work.p2, work.p3); + PathPoint p012 = midpoint(p01, p12); + PathPoint p123 = midpoint(p12, p23); + PathPoint p0123 = midpoint(p012, p123); + if (stackCount + 2 > (int) (sizeof(stack) / sizeof(stack[0]))) { + return False; + } + stack[stackCount++] = + (CubicWork) {p0123, p123, p23, work.p3, work.depth + 1}; + stack[stackCount++] = + (CubicWork) {work.p0, p01, p012, p0123, work.depth + 1}; + } + return True; +} + +Bool pathFlatten(const Path *path, + double tolerance, + PathPoint **pointsReturn, + size_t *countReturn) +{ + if (!path || !pointsReturn || !countReturn) + return False; + *pointsReturn = NULL; + *countReturn = 0; + if (tolerance <= 0.0) + tolerance = 0.25; + + PointList list; + memset(&list, 0, sizeof(list)); + size_t vertex = 0; + PathPoint current = {0.0, 0.0}; + PathPoint subpathStart = {0.0, 0.0}; + Bool hasCurrent = False; + Bool ok = True; + + for (size_t i = 0; ok && i < path->commandCount; i++) { + switch ((PathCommand) path->commands[i]) { + case PATH_CMD_MOVE: + if (vertex + 2 > path->vertexCount) { + ok = False; + break; + } + if (hasCurrent) + ok = appendContourBreak(&list); + if (!ok) + break; + current.x = path->vertices[vertex++]; + current.y = path->vertices[vertex++]; + subpathStart = current; + hasCurrent = True; + ok = appendPoint(&list, current); + break; + case PATH_CMD_LINE: { + if (!hasCurrent || vertex + 2 > path->vertexCount) { + ok = False; + break; + } + PathPoint to = {path->vertices[vertex], path->vertices[vertex + 1]}; + vertex += 2; + ok = appendLine(&list, current, to); + current = to; + break; + } + case PATH_CMD_QUAD: { + if (!hasCurrent || vertex + 4 > path->vertexCount) { + ok = False; + break; + } + PathPoint p1 = {path->vertices[vertex], path->vertices[vertex + 1]}; + PathPoint p2 = {path->vertices[vertex + 2], + path->vertices[vertex + 3]}; + vertex += 4; + ok = flattenQuad(&list, current, p1, p2, tolerance); + current = p2; + break; + } + case PATH_CMD_CUBIC: { + if (!hasCurrent || vertex + 6 > path->vertexCount) { + ok = False; + break; + } + PathPoint p1 = {path->vertices[vertex], path->vertices[vertex + 1]}; + PathPoint p2 = {path->vertices[vertex + 2], + path->vertices[vertex + 3]}; + PathPoint p3 = {path->vertices[vertex + 4], + path->vertices[vertex + 5]}; + vertex += 6; + ok = flattenCubic(&list, current, p1, p2, p3, tolerance); + current = p3; + break; + } + case PATH_CMD_CLOSE: + if (!hasCurrent) { + ok = False; + break; + } + ok = appendLine(&list, current, subpathStart); + current = subpathStart; + break; + default: + ok = False; + break; + } + } + if (!ok) { + free(list.points); + return False; + } + *pointsReturn = list.points; + *countReturn = list.count; + return True; +} diff --git a/src/path/path.c b/src/path/path.c new file mode 100644 index 0000000..084282e --- /dev/null +++ b/src/path/path.c @@ -0,0 +1,148 @@ +/* Path builder storage for libx11-compat + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ +#include "path.h" + +#include +#include +#include + +static Bool reserveCommands(Path *path, size_t extra) +{ + if (extra > SIZE_MAX - path->commandCount) + return False; + size_t needed = path->commandCount + extra; + if (needed <= path->commandCapacity) + return True; + size_t capacity = path->commandCapacity ? path->commandCapacity : 8; + while (capacity < needed) { + if (capacity > SIZE_MAX / 2) + return False; + capacity *= 2; + } + unsigned char *grown = realloc(path->commands, capacity); + if (!grown) + return False; + path->commands = grown; + path->commandCapacity = capacity; + return True; +} + +static Bool reserveVertices(Path *path, size_t extraDoubles) +{ + if (extraDoubles > SIZE_MAX - path->vertexCount) + return False; + size_t needed = path->vertexCount + extraDoubles; + if (needed <= path->vertexCapacity) + return True; + size_t capacity = path->vertexCapacity ? path->vertexCapacity : 16; + while (capacity < needed) { + if (capacity > SIZE_MAX / 2) + return False; + capacity *= 2; + } + if (capacity > SIZE_MAX / sizeof(double)) + return False; + double *grown = realloc(path->vertices, capacity * sizeof(double)); + if (!grown) + return False; + path->vertices = grown; + path->vertexCapacity = capacity; + return True; +} + +static Bool appendCommand(Path *path, PathCommand command) +{ + if (!reserveCommands(path, 1)) + return False; + path->commands[path->commandCount++] = (unsigned char) command; + return True; +} + +static Bool appendVertex(Path *path, double x, double y) +{ + if (!reserveVertices(path, 2)) + return False; + path->vertices[path->vertexCount++] = x; + path->vertices[path->vertexCount++] = y; + return True; +} + +Bool pathInit(Path *path) +{ + if (!path) + return False; + memset(path, 0, sizeof(*path)); + return True; +} + +void pathFree(Path *path) +{ + if (!path) + return; + free(path->commands); + free(path->vertices); + memset(path, 0, sizeof(*path)); +} + +Bool pathMoveTo(Path *path, double x, double y) +{ + if (!appendCommand(path, PATH_CMD_MOVE)) + return False; + if (!appendVertex(path, x, y)) { + path->commandCount--; + return False; + } + return True; +} + +Bool pathLineTo(Path *path, double x, double y) +{ + if (!appendCommand(path, PATH_CMD_LINE)) + return False; + if (!appendVertex(path, x, y)) { + path->commandCount--; + return False; + } + return True; +} + +Bool pathQuadTo(Path *path, double x1, double y1, double x2, double y2) +{ + if (!appendCommand(path, PATH_CMD_QUAD)) + return False; + if (!reserveVertices(path, 4)) { + path->commandCount--; + return False; + } + appendVertex(path, x1, y1); + appendVertex(path, x2, y2); + return True; +} + +Bool pathCubicTo(Path *path, + double x1, + double y1, + double x2, + double y2, + double x3, + double y3) +{ + if (!appendCommand(path, PATH_CMD_CUBIC)) + return False; + if (!reserveVertices(path, 6)) { + path->commandCount--; + return False; + } + appendVertex(path, x1, y1); + appendVertex(path, x2, y2); + appendVertex(path, x3, y3); + return True; +} + +Bool pathClose(Path *path) +{ + return appendCommand(path, PATH_CMD_CLOSE); +} diff --git a/src/path/path.h b/src/path/path.h new file mode 100644 index 0000000..8dc5741 --- /dev/null +++ b/src/path/path.h @@ -0,0 +1,68 @@ +/* Path builder for libx11-compat + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ +#ifndef PATH_PATH_H +#define PATH_PATH_H + +#include +#include + +typedef enum { + PATH_CMD_MOVE = 1, + PATH_CMD_LINE, + PATH_CMD_QUAD, + PATH_CMD_CUBIC, + PATH_CMD_CLOSE +} PathCommand; + +typedef struct { + unsigned char *commands; + size_t commandCount; + size_t commandCapacity; + double *vertices; + size_t vertexCount; + size_t vertexCapacity; +} Path; + +typedef struct { + double x; + double y; +} PathPoint; + +Bool pathInit(Path *path); +void pathFree(Path *path); +Bool pathMoveTo(Path *path, double x, double y); +Bool pathLineTo(Path *path, double x, double y); +Bool pathQuadTo(Path *path, double x1, double y1, double x2, double y2); +Bool pathCubicTo(Path *path, + double x1, + double y1, + double x2, + double y2, + double x3, + double y3); +Bool pathClose(Path *path); + +Bool pathFlatten(const Path *path, + double tolerance, + PathPoint **pointsReturn, + size_t *countReturn); +Bool pathAddArc(Path *path, + double cx, + double cy, + double rx, + double ry, + double startRad, + double sweepRad, + int arcMode); +Bool pathStrokePolyline(Path *out, + const PathPoint *points, + size_t count, + double width, + int capStyle, + int joinStyle); +Bool pathPointIsBreak(PathPoint point); + +#endif /* PATH_PATH_H */ diff --git a/src/path/raster.c b/src/path/raster.c new file mode 100644 index 0000000..6008137 --- /dev/null +++ b/src/path/raster.c @@ -0,0 +1,340 @@ +/* Scanline path rasterization for libx11-compat + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ +#include "raster.h" + +#include +#include +#include + +#include "../drawing.h" +#include "../gc.h" +#include "edges.h" +#include "rasterize.h" + +static Bool buildPixmanRegionFromSpans(const PathSpanList *spans, + pixman_region16_t *region) +{ + pixman_region_init(region); + for (size_t i = 0; i < spans->count; i++) { + PathSpan span = spans->spans[i]; + if (span.coverage == 0 || span.xEnd < span.xStart) + continue; + if (!pixman_region_union_rect( + region, region, span.xStart, span.y, + (unsigned int) (span.xEnd - span.xStart + 1), 1)) { + pixman_region_fini(region); + return False; + } + } + return True; +} + +/* Color and fillRule are explicit so the wide-stroke path can force + * WindingRule (its outline self-overlaps at joins) and dashed strokes can + * fill each sub-path with foreground or background without mutating the + * live GC. */ +static Bool rasterFillPathInternal(SDL_Renderer *renderer, + GC gc, + const Path *path, + int fillRule, + unsigned long color, + Bool coalesceWithPixman) +{ + if (!renderer || !gc || !path) + return False; + GraphicContext *gContext = GET_GC(gc); + if (gContext->fillStyle != FillSolid || gContext->function != GXcopy || + (gContext->planeMask != AllPlanes && + gContext->planeMask != 0xFFFFFFFFu)) { + return False; + } + + PathPoint *points = NULL; + size_t pointCount = 0; + PathEdgeList edges; + PathSpanList spans; + pixman_region16_t spanRegion; + Bool spanRegionInitialized = False; + Bool ok = False; + edges.edges = NULL; + spans.spans = NULL; + if (!pathFlatten(path, 0.25, &points, &pointCount)) + goto cleanup; + if (pointCount < 3) { + ok = True; + goto cleanup; + } + if (!pathBuildEdges(points, pointCount, &edges)) + goto cleanup; + if (edges.count == 0) { + ok = True; + goto cleanup; + } + if (!pathRasterizeEdges(&edges, fillRule, &spans)) + goto cleanup; + if (coalesceWithPixman) { + if (!buildPixmanRegionFromSpans(&spans, &spanRegion)) + goto cleanup; + spanRegionInitialized = True; + } + + applySdlDrawState(renderer, gc, SDL_BLENDMODE_NONE, color); + int clipCount = getGcClipIterationCount(gc); + if (spanRegionInitialized) { + int rectCount = 0; + pixman_box16_t *rects = + pixman_region_rectangles(&spanRegion, &rectCount); + for (int clip = 0; clip < clipCount; clip++) { + if (!setGcClipForIteration(renderer, gc, clip)) + continue; + for (int i = 0; i < rectCount; i++) { + SDL_Rect rect = { + .x = rects[i].x1, + .y = rects[i].y1, + .w = rects[i].x2 - rects[i].x1, + .h = rects[i].y2 - rects[i].y1, + }; + SDL_RenderFillRect(renderer, &rect); + } + } + } else { + for (int clip = 0; clip < clipCount; clip++) { + if (!setGcClipForIteration(renderer, gc, clip)) + continue; + for (size_t i = 0; i < spans.count; i++) { + PathSpan span = spans.spans[i]; + if (span.xEnd < span.xStart) + continue; + SDL_RenderDrawLine(renderer, span.xStart, span.y, span.xEnd, + span.y); + } + } + } + clearRendererClip(renderer); + ok = True; + +cleanup: + if (spanRegionInitialized) + pixman_region_fini(&spanRegion); + pathFreeSpans(&spans); + pathFreeEdges(&edges); + free(points); + return ok; +} + +Bool rasterFillPathOnRendererWithOptions(SDL_Renderer *renderer, + GC gc, + const Path *path, + Bool coalesceWithPixman) +{ + if (!renderer || !gc || !path) + return False; + GraphicContext *gContext = GET_GC(gc); + return rasterFillPathInternal(renderer, gc, path, gContext->fillRule, + gContext->foreground, coalesceWithPixman); +} + +Bool rasterFillPathOnRenderer(SDL_Renderer *renderer, GC gc, const Path *path) +{ + return rasterFillPathOnRendererWithOptions(renderer, gc, path, True); +} + +static Bool rasterStrokeSolidPath(SDL_Renderer *renderer, + GC gc, + const Path *path, + unsigned long color, + int lineWidth, + int capStyle, + int joinStyle) +{ + if (!renderer || !gc || !path) + return False; + + PathPoint *points = NULL; + size_t pointCount = 0; + Bool ok = False; + if (!pathFlatten(path, 0.25, &points, &pointCount)) + goto cleanup; + if (pointCount < 2) { + ok = True; + goto cleanup; + } + + if (lineWidth > 1) { + Path outline; + if (!pathInit(&outline)) + goto cleanup; + size_t start = 0; + ok = True; + for (size_t i = 0; i <= pointCount; i++) { + if (i < pointCount && !pathPointIsBreak(points[i])) + continue; + if (i > start + 1) { + ok = + pathStrokePolyline(&outline, &points[start], i - start, + (double) lineWidth, capStyle, joinStyle); + if (!ok) + break; + } + start = i + 1; + } + if (ok) { + /* Force WindingRule: see rasterFillPathInternal. */ + ok = rasterFillPathInternal(renderer, gc, &outline, WindingRule, + color, False); + } + pathFree(&outline); + goto cleanup; + } + + applySdlDrawState(renderer, gc, SDL_BLENDMODE_BLEND, color); + int clipCount = getGcClipIterationCount(gc); + for (int clip = 0; clip < clipCount; clip++) { + if (!setGcClipForIteration(renderer, gc, clip)) + continue; + for (size_t i = 0; i + 1 < pointCount; i++) { + if (pathPointIsBreak(points[i]) || pathPointIsBreak(points[i + 1])) + continue; + SDL_RenderDrawLine( + renderer, (int) lround(points[i].x), (int) lround(points[i].y), + (int) lround(points[i + 1].x), (int) lround(points[i + 1].y)); + } + } + clearRendererClip(renderer); + ok = True; + +cleanup: + free(points); + return ok; +} + +Bool rasterStrokePathOnRenderer(SDL_Renderer *renderer, GC gc, const Path *path) +{ + if (!renderer || !gc || !path) + return False; + GraphicContext *gContext = GET_GC(gc); + if (gContext->function != GXcopy) + return False; + + /* Non-dashed, OR dashed-with-empty-dash-list (treat as solid since + * there is no pattern to apply). The dash loop would spin forever + * if numDashes is 0 because dashLeft can never refill. */ + Bool dashed = gContext->lineStyle == LineOnOffDash || + gContext->lineStyle == LineDoubleDash; + if (!dashed || gContext->numDashes == 0) { + if (!dashed && gContext->lineStyle != LineSolid) + return False; + return rasterStrokeSolidPath(renderer, gc, path, gContext->foreground, + gContext->lineWidth, gContext->capStyle, + gContext->joinStyle); + } + + /* Split the flattened polyline into on/off sub-paths by dash phase, + * then stroke each with the appropriate color via the helper above. */ + PathPoint *points = NULL; + size_t pointCount = 0; + Bool ok = False; + if (!pathFlatten(path, 0.25, &points, &pointCount)) + goto cleanup; + if (pointCount < 2) { + ok = True; + goto cleanup; + } + + Path onPath; + Path offPath; + if (!pathInit(&onPath)) + goto cleanup; + if (!pathInit(&offPath)) { + pathFree(&onPath); + goto cleanup; + } + size_t dashIndex = 0; + double dashLeft = + gContext->numDashes > 0 ? (unsigned char) gContext->dashes[0] : 1.0; + Bool dashOn = True; + /* Reduce dashOffset mod the pattern period before consuming it. + * Unsigned negation dodges -INT_MIN UB; modulo caps the consume loop + * iteration count. X11 doubles the period for odd-length lists. */ + unsigned int patternTotal = 0; + for (size_t k = 0; k < gContext->numDashes; k++) + patternTotal += (unsigned char) gContext->dashes[k]; + if (gContext->numDashes > 0 && (gContext->numDashes & 1u)) + patternTotal *= 2u; + unsigned int dashOffset = 0; + if (patternTotal > 0) { + int rawOffset = gContext->dashOffset; + unsigned int absOffset = rawOffset >= 0 ? (unsigned int) rawOffset + : -(unsigned int) rawOffset; + absOffset %= patternTotal; + if (rawOffset < 0 && absOffset > 0) + absOffset = patternTotal - absOffset; + dashOffset = absOffset; + } + while (gContext->numDashes > 0 && dashOffset > 0) { + double consume = fmin((double) dashOffset, dashLeft); + dashLeft -= consume; + dashOffset -= (unsigned int) consume; + if (dashLeft <= 0.0) { + dashIndex = (dashIndex + 1) % gContext->numDashes; + dashLeft = (unsigned char) gContext->dashes[dashIndex]; + dashOn = !dashOn; + } + } + ok = True; + /* Dash state lives outside the contour loop: X11 carries phase + * across MoveTo boundaries instead of restarting per subpath. */ + int dashedLineStyle = gContext->lineStyle; + for (size_t i = 0; ok && i + 1 < pointCount; i++) { + if (pathPointIsBreak(points[i]) || pathPointIsBreak(points[i + 1])) + continue; + PathPoint a = points[i]; + PathPoint b = points[i + 1]; + double dx = b.x - a.x; + double dy = b.y - a.y; + double len = hypot(dx, dy); + if (len == 0.0) { + Path *target = dashOn ? &onPath : &offPath; + if (dashOn || dashedLineStyle == LineDoubleDash) + ok = pathMoveTo(target, a.x, a.y) && + pathLineTo(target, b.x, b.y); + continue; + } + double pos = 0.0; + while (ok && pos < len) { + double run = fmin(dashLeft, len - pos); + double t0 = pos / len; + double t1 = (pos + run) / len; + PathPoint p0 = {a.x + dx * t0, a.y + dy * t0}; + PathPoint p1 = {a.x + dx * t1, a.y + dy * t1}; + Path *target = dashOn ? &onPath : &offPath; + if (dashOn || dashedLineStyle == LineDoubleDash) + ok = pathMoveTo(target, p0.x, p0.y) && + pathLineTo(target, p1.x, p1.y); + pos += run; + dashLeft -= run; + if (dashLeft <= 0.0 && gContext->numDashes > 0) { + dashIndex = (dashIndex + 1) % gContext->numDashes; + dashLeft = (unsigned char) gContext->dashes[dashIndex]; + dashOn = !dashOn; + } + } + } + if (ok) + ok = rasterStrokeSolidPath(renderer, gc, &onPath, gContext->foreground, + gContext->lineWidth, gContext->capStyle, + gContext->joinStyle); + if (ok && dashedLineStyle == LineDoubleDash) + ok = rasterStrokeSolidPath(renderer, gc, &offPath, gContext->background, + gContext->lineWidth, gContext->capStyle, + gContext->joinStyle); + pathFree(&offPath); + pathFree(&onPath); + +cleanup: + free(points); + return ok; +} diff --git a/src/path/raster.h b/src/path/raster.h new file mode 100644 index 0000000..81871d8 --- /dev/null +++ b/src/path/raster.h @@ -0,0 +1,23 @@ +/* Path raster entry points for libx11-compat + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ +#ifndef PATH_RASTER_H +#define PATH_RASTER_H + +#include +#include + +#include "path.h" + +Bool rasterFillPathOnRenderer(SDL_Renderer *renderer, GC gc, const Path *path); +Bool rasterFillPathOnRendererWithOptions(SDL_Renderer *renderer, + GC gc, + const Path *path, + Bool coalesceWithPixman); +Bool rasterStrokePathOnRenderer(SDL_Renderer *renderer, + GC gc, + const Path *path); + +#endif /* PATH_RASTER_H */ diff --git a/src/path/rasterize.c b/src/path/rasterize.c new file mode 100644 index 0000000..6922561 --- /dev/null +++ b/src/path/rasterize.c @@ -0,0 +1,138 @@ +/* Active-edge span rasterization for libx11-compat + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ +#include "rasterize.h" + +#include +#include +#include + +typedef struct { + double x; + int winding; +} Crossing; + +static int compareCrossing(const void *left, const void *right) +{ + const Crossing *a = left; + const Crossing *b = right; + return (a->x > b->x) - (a->x < b->x); +} + +static Bool appendSpan(PathSpanList *list, int y, int xStart, int xEnd) +{ + if (xEnd < xStart) + return True; + if (list->count == list->capacity) { + size_t capacity = list->capacity ? list->capacity * 2 : 64; + if (capacity <= list->capacity || + capacity > SIZE_MAX / sizeof(PathSpan)) { + return False; + } + PathSpan *grown = realloc(list->spans, capacity * sizeof(PathSpan)); + if (!grown) + return False; + list->spans = grown; + list->capacity = capacity; + } + list->spans[list->count++] = (PathSpan) { + .y = y, + .xStart = xStart, + .xEnd = xEnd, + .coverage = 255, + }; + return True; +} + +static Bool emitWindingSpans(PathSpanList *out, + Crossing *crossings, + size_t count, + int y) +{ + int winding = 0; + int xStart = 0; + for (size_t i = 0; i < count; i++) { + int oldWinding = winding; + winding += crossings[i].winding; + if (oldWinding == 0 && winding != 0) { + xStart = (int) ceil(crossings[i].x); + } else if (oldWinding != 0 && winding == 0) { + if (!appendSpan(out, y, xStart, (int) ceil(crossings[i].x) - 1)) + return False; + } + } + return True; +} + +static Bool emitEvenOddSpans(PathSpanList *out, + Crossing *crossings, + size_t count, + int y) +{ + for (size_t i = 0; i + 1 < count; i += 2) { + if (!appendSpan(out, y, (int) ceil(crossings[i].x), + (int) ceil(crossings[i + 1].x) - 1)) { + return False; + } + } + return True; +} + +Bool pathRasterizeEdges(const PathEdgeList *edges, + int fillRule, + PathSpanList *out) +{ + if (!edges || !out) + return False; + memset(out, 0, sizeof(*out)); + if (edges->count == 0) + return True; + + Crossing *crossings = malloc(edges->count * sizeof(Crossing)); + if (!crossings) + return False; + + Bool ok = True; + int minY = (int) floor((double) edges->minY / (double) PATH_EDGE_FIXED_ONE); + int maxY = (int) ceil((double) edges->maxY / (double) PATH_EDGE_FIXED_ONE); + for (int y = minY; ok && y <= maxY; y++) { + double scanY = y + 0.5; + size_t crossingCount = 0; + for (size_t i = 0; i < edges->count; i++) { + PathEdge edge = edges->edges[i]; + double ax = (double) edge.x0 / (double) PATH_EDGE_FIXED_ONE; + double ay = (double) edge.y0 / (double) PATH_EDGE_FIXED_ONE; + double bx = (double) edge.x1 / (double) PATH_EDGE_FIXED_ONE; + double by = (double) edge.y1 / (double) PATH_EDGE_FIXED_ONE; + double edgeMinY = ay < by ? ay : by; + double edgeMaxY = ay > by ? ay : by; + if (scanY < edgeMinY || scanY >= edgeMaxY) + continue; + crossings[crossingCount].x = + ax + (scanY - ay) * (bx - ax) / (by - ay); + crossings[crossingCount].winding = edge.winding; + crossingCount++; + } + if (crossingCount < 2) + continue; + qsort(crossings, crossingCount, sizeof(Crossing), compareCrossing); + ok = fillRule == WindingRule + ? emitWindingSpans(out, crossings, crossingCount, y) + : emitEvenOddSpans(out, crossings, crossingCount, y); + } + + free(crossings); + if (!ok) + pathFreeSpans(out); + return ok; +} + +void pathFreeSpans(PathSpanList *spans) +{ + if (!spans) + return; + free(spans->spans); + memset(spans, 0, sizeof(*spans)); +} diff --git a/src/path/rasterize.h b/src/path/rasterize.h new file mode 100644 index 0000000..fca1148 --- /dev/null +++ b/src/path/rasterize.h @@ -0,0 +1,33 @@ +/* Span rasterization for libx11-compat path fills + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ +#ifndef PATH_RASTERIZE_H +#define PATH_RASTERIZE_H + +#include +#include +#include + +#include "edges.h" + +typedef struct { + int y; + int xStart; + int xEnd; + uint8_t coverage; +} PathSpan; + +typedef struct { + PathSpan *spans; + size_t count; + size_t capacity; +} PathSpanList; + +Bool pathRasterizeEdges(const PathEdgeList *edges, + int fillRule, + PathSpanList *out); +void pathFreeSpans(PathSpanList *spans); + +#endif /* PATH_RASTERIZE_H */ diff --git a/src/path/stroke.c b/src/path/stroke.c new file mode 100644 index 0000000..f14b297 --- /dev/null +++ b/src/path/stroke.c @@ -0,0 +1,372 @@ +/* Stroke outline construction for libx11-compat + * + * Builds a single continuous outline polygon per polyline contour. The + * outline traverses the left offset edges forward, around the end cap, + * the right offset edges backward, and around the start cap. Joins on + * the OUTER side of each bend get proper miter / bevel / round geometry; + * the inner side is connected with a plain line and WindingRule fill + * cleans up the resulting self-overlap. + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ +#include "path.h" + +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +/* X11 default per the protocol spec. Clients can't override it through + * Xlib so a fixed value matches every conforming server. */ +#define STROKE_MITER_LIMIT 10.0 + +/* Round joins/caps tessellation step in radians. Roughly 11.25 degrees + * per segment gives smooth strokes for the widths libx11-compat + * clients actually use without ballooning the outline vertex count. */ +#define STROKE_ARC_STEP (M_PI / 16.0) + +static Bool appendArc(Path *out, + double cx, + double cy, + double radius, + double startAngle, + double sweep) +{ + int segments = (int) ceil(fabs(sweep) / STROKE_ARC_STEP); + if (segments < 1) + segments = 1; + for (int k = 1; k <= segments; k++) { + double t = (double) k / (double) segments; + double a = startAngle + sweep * t; + if (!pathLineTo(out, cx + cos(a) * radius, cy + sin(a) * radius)) + return False; + } + return True; +} + +/* End of contour cap. We are positioned at p + halfWidth*(nx,ny) (left + * edge end) and need to traverse around to p - halfWidth*(nx,ny) (right + * edge end). (ux,uy) is the segment's unit direction so the cap can bulge + * outward. */ +static Bool appendCap(Path *out, + double px, + double py, + double ux, + double uy, + double nx, + double ny, + double halfWidth, + int capStyle) +{ + if (capStyle == CapRound) { + /* Semicircle from +n through +u to -n. The cap_left point is at + * angle atan2(ny,nx); we sweep by -PI (clockwise) so the curve + * passes through the +u direction. */ + double a0 = atan2(ny, nx); + return appendArc(out, px, py, halfWidth, a0, -M_PI); + } + if (capStyle == CapProjecting) { + double ex = ux * halfWidth; + double ey = uy * halfWidth; + if (!pathLineTo(out, px + nx * halfWidth + ex, + py + ny * halfWidth + ey)) + return False; + if (!pathLineTo(out, px - nx * halfWidth + ex, + py - ny * halfWidth + ey)) + return False; + return pathLineTo(out, px - nx * halfWidth, py - ny * halfWidth); + } + /* CapButt and CapNotLast both render as a straight crossing. */ + return pathLineTo(out, px - nx * halfWidth, py - ny * halfWidth); +} + +static Bool appendDegenerateStroke(Path *out, + double px, + double py, + double halfWidth, + int capStyle) +{ + if (capStyle == CapRound) { + if (!pathMoveTo(out, px + halfWidth, py)) + return False; + if (!appendArc(out, px, py, halfWidth, 0.0, 2.0 * M_PI)) + return False; + return pathClose(out); + } + + if (capStyle == CapProjecting) { + if (!pathMoveTo(out, px - halfWidth, py - halfWidth)) + return False; + if (!pathLineTo(out, px + halfWidth, py - halfWidth)) + return False; + if (!pathLineTo(out, px + halfWidth, py + halfWidth)) + return False; + if (!pathLineTo(out, px - halfWidth, py + halfWidth)) + return False; + return pathClose(out); + } + + return True; +} + +/* Emit join geometry from edgeFrom to edgeTo. The vertex is at (vx, vy); + * (uFromX,uFromY) is the incoming edge's tangent into the vertex, + * (uToX,uToY) is the outgoing edge's tangent leaving the vertex. + * outerSide is True when this side of the bend is the outer one (i.e. + * the side with a visible gap that needs filling). For the inner side + * we simply line to edgeTo. */ +static Bool appendJoin(Path *out, + double vx, + double vy, + double edgeFromX, + double edgeFromY, + double edgeToX, + double edgeToY, + double uFromX, + double uFromY, + double uToX, + double uToY, + double halfWidth, + int joinStyle, + Bool outerSide) +{ + if (!outerSide) + return pathLineTo(out, edgeToX, edgeToY); + + if (joinStyle == JoinMiter) { + /* Solve edgeFrom + t*uFrom = edgeTo - s*uTo for the miter point. */ + double det = uFromX * uToY - uFromY * uToX; + if (fabs(det) >= 1e-12) { + double dx = edgeToX - edgeFromX; + double dy = edgeToY - edgeFromY; + double t = (dx * uToY - dy * uToX) / det; + double mx = edgeFromX + t * uFromX; + double my = edgeFromY + t * uFromY; + double miterDx = mx - vx; + double miterDy = my - vy; + double miterLen = hypot(miterDx, miterDy); + if (miterLen <= halfWidth * STROKE_MITER_LIMIT) { + if (!pathLineTo(out, mx, my)) + return False; + return pathLineTo(out, edgeToX, edgeToY); + } + } + /* Parallel edges or miter exceeds the limit: bevel fallback. */ + return pathLineTo(out, edgeToX, edgeToY); + } + + if (joinStyle == JoinRound) { + double a0 = atan2(edgeFromY - vy, edgeFromX - vx); + double a1 = atan2(edgeToY - vy, edgeToX - vx); + double sweep = a1 - a0; + /* Sweep on the outer side: pick the direction that matches the + * cross product of edge-from-to-edge-to (computed from V). */ + double fx = edgeFromX - vx; + double fy = edgeFromY - vy; + double tx = edgeToX - vx; + double ty = edgeToY - vy; + double cross = fx * ty - fy * tx; + if (cross > 0) { + while (sweep < 0) + sweep += 2.0 * M_PI; + } else { + while (sweep > 0) + sweep -= 2.0 * M_PI; + } + return appendArc(out, vx, vy, halfWidth, a0, sweep); + } + + /* JoinBevel and any unknown style: straight line. */ + return pathLineTo(out, edgeToX, edgeToY); +} + +Bool pathStrokePolyline(Path *out, + const PathPoint *points, + size_t count, + double width, + int capStyle, + int joinStyle) +{ + if (!out || !points || count < 2 || width <= 1.0) + return False; + double halfWidth = width / 2.0; + + /* Collapse zero-length segments by dedup-copy. A zero-length segment + * has no defined direction, which would corrupt the join math; the + * easiest cure is to drop those points entirely. */ + PathPoint *pts = malloc(sizeof(PathPoint) * count); + if (!pts) + return False; + size_t n = 0; + for (size_t i = 0; i < count; i++) { + if (n > 0 && pts[n - 1].x == points[i].x && pts[n - 1].y == points[i].y) + continue; + pts[n++] = points[i]; + } + if (n < 2) { + Bool ok = appendDegenerateStroke(out, pts[0].x, pts[0].y, halfWidth, + capStyle); + free(pts); + return ok; + } + + size_t numSegments = n - 1; + /* Per-segment unit tangent (ux, uy) and left normal (nx, ny). */ + typedef struct { + double ux, uy; + double nx, ny; + } SegmentFrame; + SegmentFrame *seg = malloc(sizeof(SegmentFrame) * numSegments); + if (!seg) { + free(pts); + return False; + } + for (size_t i = 0; i < numSegments; i++) { + double dx = pts[i + 1].x - pts[i].x; + double dy = pts[i + 1].y - pts[i].y; + double len = hypot(dx, dy); + /* len > 0 here because dedup ran above. */ + seg[i].ux = dx / len; + seg[i].uy = dy / len; + seg[i].nx = -seg[i].uy; + seg[i].ny = seg[i].ux; + } + + Bool ok = True; + /* A contour with coincident endpoints is closed: emit joins at every + * vertex including the closure, no caps. Two bridge segments + * (LSTART(0)<->RSTART(0)) traversed in opposite directions cancel + * under the WindingRule fill, so a single self-intersecting outline + * is enough. Requires n>=4 to have at least 3 unique vertices. */ + Bool closed = + n >= 4 && pts[0].x == pts[n - 1].x && pts[0].y == pts[n - 1].y; + + double lstartX = pts[0].x + halfWidth * seg[0].nx; + double lstartY = pts[0].y + halfWidth * seg[0].ny; + if (!pathMoveTo(out, lstartX, lstartY)) { + ok = False; + goto done; + } + + /* Forward walk along the left offset edges. For a closed contour the + * last iteration also emits a join (the closure) instead of falling + * through to an end cap. */ + for (size_t i = 0; i < numSegments && ok; i++) { + double lendX = pts[i + 1].x + halfWidth * seg[i].nx; + double lendY = pts[i + 1].y + halfWidth * seg[i].ny; + if (!pathLineTo(out, lendX, lendY)) { + ok = False; + break; + } + if (i + 1 < numSegments || closed) { + size_t nextSeg = (i + 1) % numSegments; + size_t vIdx = (i + 1 < numSegments) ? (i + 1) : 0; + double nextLstartX = pts[vIdx].x + halfWidth * seg[nextSeg].nx; + double nextLstartY = pts[vIdx].y + halfWidth * seg[nextSeg].ny; + double cross = + seg[i].ux * seg[nextSeg].uy - seg[i].uy * seg[nextSeg].ux; + /* Left side is outer when the polyline turns right (cross < 0). */ + Bool outer = cross < 0; + ok = appendJoin(out, pts[vIdx].x, pts[vIdx].y, lendX, lendY, + nextLstartX, nextLstartY, seg[i].ux, seg[i].uy, + seg[nextSeg].ux, seg[nextSeg].uy, halfWidth, + joinStyle, outer); + } + } + + if (!closed && ok) { + size_t last = numSegments - 1; + ok = appendCap(out, pts[n - 1].x, pts[n - 1].y, seg[last].ux, + seg[last].uy, seg[last].nx, seg[last].ny, halfWidth, + capStyle); + } else if (closed && ok) { + /* Bridge from the forward walk's final position (LSTART(0)) to + * the backward walk's start (RSTART(0)). The pathClose at the + * end traces the same bridge in reverse so winding cancels. */ + ok = pathLineTo(out, pts[0].x - halfWidth * seg[0].nx, + pts[0].y - halfWidth * seg[0].ny); + /* Emit the reverse closure join: from RSTART(0) to REND(num-1) + * at vertex pts[0]. Walking backward the incoming tangent is + * -seg[0].u and the outgoing tangent is -seg[last].u. */ + if (ok) { + size_t last = numSegments - 1; + double prevRendX = pts[0].x - halfWidth * seg[last].nx; + double prevRendY = pts[0].y - halfWidth * seg[last].ny; + double cross = seg[last].ux * seg[0].uy - seg[last].uy * seg[0].ux; + Bool outer = cross > 0; + ok = appendJoin(out, pts[0].x, pts[0].y, + pts[0].x - halfWidth * seg[0].nx, + pts[0].y - halfWidth * seg[0].ny, prevRendX, + prevRendY, -seg[0].ux, -seg[0].uy, -seg[last].ux, + -seg[last].uy, halfWidth, joinStyle, outer); + } + } + + /* Backward walk along the right offset edges. Going backward, the + * incoming tangent is -u[i] and the outgoing tangent is -u[i-1]. */ + if (closed) { + /* Closure join was emitted above and left us at REND(num-1). + * Walk i = num-1 down to 1: lineTo RSTART(i) then join at pts[i] + * to REND(i-1). The final lineTo RSTART(0) closes the bridge so + * pathClose retraces the forward bridge in reverse. */ + for (size_t i = numSegments - 1; i > 0 && ok; i--) { + double rstartX = pts[i].x - halfWidth * seg[i].nx; + double rstartY = pts[i].y - halfWidth * seg[i].ny; + if (!pathLineTo(out, rstartX, rstartY)) { + ok = False; + break; + } + double prevRendX = pts[i].x - halfWidth * seg[i - 1].nx; + double prevRendY = pts[i].y - halfWidth * seg[i - 1].ny; + double cross = + seg[i - 1].ux * seg[i].uy - seg[i - 1].uy * seg[i].ux; + Bool outer = cross > 0; + ok = + appendJoin(out, pts[i].x, pts[i].y, rstartX, rstartY, prevRendX, + prevRendY, -seg[i].ux, -seg[i].uy, -seg[i - 1].ux, + -seg[i - 1].uy, halfWidth, joinStyle, outer); + } + if (ok) + ok = pathLineTo(out, pts[0].x - halfWidth * seg[0].nx, + pts[0].y - halfWidth * seg[0].ny); + } else { + for (size_t i = numSegments; i-- > 0 && ok;) { + double rstartX = pts[i].x - halfWidth * seg[i].nx; + double rstartY = pts[i].y - halfWidth * seg[i].ny; + if (!pathLineTo(out, rstartX, rstartY)) { + ok = False; + break; + } + if (i > 0) { + double prevRendX = pts[i].x - halfWidth * seg[i - 1].nx; + double prevRendY = pts[i].y - halfWidth * seg[i - 1].ny; + double cross = + seg[i - 1].ux * seg[i].uy - seg[i - 1].uy * seg[i].ux; + Bool outer = cross > 0; + ok = appendJoin(out, pts[i].x, pts[i].y, rstartX, rstartY, + prevRendX, prevRendY, -seg[i].ux, -seg[i].uy, + -seg[i - 1].ux, -seg[i - 1].uy, halfWidth, + joinStyle, outer); + } + } + if (ok) { + /* Start cap walks from RSTART(0) to LSTART(0) along the + * reversed first-segment direction. */ + ok = appendCap(out, pts[0].x, pts[0].y, -seg[0].ux, -seg[0].uy, + -seg[0].nx, -seg[0].ny, halfWidth, capStyle); + } + } + + if (ok) + ok = pathClose(out); + +done: + free(seg); + free(pts); + return ok; +} diff --git a/src/region.c b/src/region.c index edb9d00..d5534d1 100644 --- a/src/region.c +++ b/src/region.c @@ -1,33 +1,18 @@ #include "X11/Xlib.h" #include "X11/Xutil.h" -#include -#include +#include #include #include #include -#include "drawing.h" #include "resource-types.h" +#include "util.h" +#include "path/edges.h" +#include "path/rasterize.h" typedef struct pixman_region16 *pRegion; #define GET_REGION(pixmanRegion) ((Region) (void *) pixmanRegion) #define GET_P_REGION(region) ((pRegion) (void *) region) -typedef struct { - double x; - int winding; -} PolygonCrossing; - -static int compareCrossing(const void *left, const void *right) -{ - const PolygonCrossing *a = left; - const PolygonCrossing *b = right; - if (a->x < b->x) - return -1; - if (a->x > b->x) - return 1; - return 0; -} - int XDestroyRegion(Region region) { // https://tronche.com/gui/x/xlib/utilities/regions/XDestroyRegion.html @@ -81,11 +66,12 @@ int XRectInRegion(Region region, int XClipBox(Region region, XRectangle *rect_return) { // https://tronche.com/gui/x/xlib/utilities/regions/XClipBox.html - pixman_box16_t *extends = pixman_region_extents(GET_P_REGION(region)); - rect_return->width = (unsigned short) abs(extends->x2 - extends->x1); - rect_return->y = extends->y1; - rect_return->height = (unsigned short) abs(extends->y2 - extends->y1); - rect_return->x = extends->x1; + /* pixman_region_extents always returns x1<=x2 and y1<=y2. */ + pixman_box16_t *extents = pixman_region_extents(GET_P_REGION(region)); + rect_return->x = extents->x1; + rect_return->y = extents->y1; + rect_return->width = (unsigned short) (extents->x2 - extents->x1); + rect_return->height = (unsigned short) (extents->y2 - extents->y1); return 1; } @@ -116,7 +102,6 @@ int XSubtractRegion(Region sra, Region srb, Region dr_return) : 0; } - int XXorRegion(Region sra, Region srb, Region dr_return) { // https://tronche.com/gui/x/xlib/utilities/regions/XXorRegion.html @@ -190,84 +175,41 @@ Region XPolygonRegion(XPoint *points, int count, int fill_rule) if (!region || !points || count < 3) { return region; } - - int minY = points[0].y; - int maxY = points[0].y; - for (int i = 1; i < count; i++) { - if (points[i].y < minY) - minY = points[i].y; - if (points[i].y > maxY) - maxY = points[i].y; + /* +1 trailing closing point. Compute in size_t to avoid signed overflow + * when count is near INT_MAX, then bound by allocator-safe size. */ + if ((size_t) count > SIZE_MAX / sizeof(PathPoint) - 1) { + XDestroyRegion(region); + return NULL; } - - PolygonCrossing *crossings = - malloc(sizeof(PolygonCrossing) * (size_t) count); - if (!crossings) { + PathPoint *pathPoints = malloc(sizeof(PathPoint) * ((size_t) count + 1)); + if (!pathPoints) { XDestroyRegion(region); return NULL; } - - for (int y = minY; y < maxY; y++) { - double scanY = (double) y + 0.5; - int crossingCount = 0; - for (int i = 0; i < count; i++) { - XPoint p1 = points[i]; - XPoint p2 = points[(i + 1) % count]; - if (p1.y == p2.y) { - continue; - } - int ymin = p1.y < p2.y ? p1.y : p2.y; - int ymax = p1.y > p2.y ? p1.y : p2.y; - if (scanY < ymin || scanY >= ymax) { - continue; - } - double t = (scanY - p1.y) / (double) (p2.y - p1.y); - crossings[crossingCount].x = p1.x + t * (double) (p2.x - p1.x); - crossings[crossingCount].winding = p2.y > p1.y ? 1 : -1; - crossingCount++; - } - qsort(crossings, (size_t) crossingCount, sizeof(PolygonCrossing), - compareCrossing); - - if (fill_rule == WindingRule) { - int winding = 0; - int startX = 0; - for (int i = 0; i < crossingCount; i++) { - int oldWinding = winding; - winding += crossings[i].winding; - if (oldWinding == 0 && winding != 0) { - startX = (int) ceil(crossings[i].x); - } else if (oldWinding != 0 && winding == 0) { - int endX = (int) ceil(crossings[i].x); - if (endX > startX) { - if (!pixman_region_union_rect( - GET_P_REGION(region), GET_P_REGION(region), - startX, y, (unsigned int) (endX - startX), 1)) { - free(crossings); - XDestroyRegion(region); - return NULL; - } - } - } - } - } else { - for (int i = 0; i + 1 < crossingCount; i += 2) { - int startX = (int) ceil(crossings[i].x); - int endX = (int) ceil(crossings[i + 1].x); - if (endX > startX) { - if (!pixman_region_union_rect( - GET_P_REGION(region), GET_P_REGION(region), startX, - y, (unsigned int) (endX - startX), 1)) { - free(crossings); - XDestroyRegion(region); - return NULL; - } - } - } - } + for (int i = 0; i < count; i++) { + pathPoints[i].x = points[i].x; + pathPoints[i].y = points[i].y; + } + pathPoints[count] = pathPoints[0]; + + PathEdgeList edges = {.edges = NULL}; + PathSpanList spans = {.spans = NULL}; + Bool ok = pathBuildEdges(pathPoints, (size_t) count + 1, &edges) && + pathRasterizeEdges(&edges, fill_rule, &spans); + for (size_t i = 0; ok && i < spans.count; i++) { + PathSpan span = spans.spans[i]; + ok = pixman_region_union_rect( + GET_P_REGION(region), GET_P_REGION(region), span.xStart, span.y, + (unsigned int) (span.xEnd - span.xStart + 1), 1); } - free(crossings); + pathFreeSpans(&spans); + pathFreeEdges(&edges); + free(pathPoints); + if (!ok) { + XDestroyRegion(region); + return NULL; + } return region; } diff --git a/tests/bench-paths.c b/tests/bench-paths.c new file mode 100644 index 0000000..57c34c6 --- /dev/null +++ b/tests/bench-paths.c @@ -0,0 +1,91 @@ +#include +#include +#include + +static double now_seconds(void) +{ + struct timeval tv; + gettimeofday(&tv, NULL); + return (double) tv.tv_sec + (double) tv.tv_usec / 1000000.0; +} + +static void bench_many_small_draw_arcs(Display *display, Pixmap pixmap, GC gc) +{ + double start = now_seconds(); + for (int i = 0; i < 2000; i++) { + int x = i % 96; + int y = (i / 96) % 96; + XDrawArc(display, pixmap, gc, x, y, 8, 8, 0, 180 * 64); + } + double elapsed = now_seconds() - start; + printf("many-small-XDrawArc %.6f sec %.3f usec/op\n", elapsed, + elapsed * 1000000.0 / 2000.0); +} + +static void bench_large_fill_arc(Display *display, Pixmap pixmap, GC gc) +{ + double start = now_seconds(); + for (int i = 0; i < 50; i++) + XFillArc(display, pixmap, gc, 8, 8, 112, 112, 0, 360 * 64); + double elapsed = now_seconds() - start; + printf("large-XFillArc %.6f sec %.3f usec/op\n", elapsed, + elapsed * 1000000.0 / 50.0); +} + +static void bench_convex_polygon(Display *display, Pixmap pixmap, GC gc) +{ + XPoint points[] = {{8, 8}, {120, 12}, {80, 120}, {16, 96}}; + double start = now_seconds(); + for (int i = 0; i < 1000; i++) + XFillPolygon(display, pixmap, gc, points, 4, Convex, CoordModeOrigin); + double elapsed = now_seconds() - start; + printf("convex-XFillPolygon %.6f sec %.3f usec/op\n", elapsed, + elapsed * 1000000.0 / 1000.0); +} + +static void bench_self_intersecting_polygon(Display *display, + Pixmap pixmap, + GC gc) +{ + XPoint points[] = {{16, 16}, {112, 112}, {112, 16}, {16, 112}}; + double start = now_seconds(); + for (int i = 0; i < 1000; i++) + XFillPolygon(display, pixmap, gc, points, 4, Complex, CoordModeOrigin); + double elapsed = now_seconds() - start; + printf("self-intersecting-XFillPolygon %.6f sec %.3f usec/op\n", elapsed, + elapsed * 1000000.0 / 1000.0); +} + +static void bench_wide_line(Display *display, Pixmap pixmap, GC gc) +{ + XSetLineAttributes(display, gc, 5, LineSolid, CapButt, JoinMiter); + double start = now_seconds(); + for (int i = 0; i < 1000; i++) + XDrawLine(display, pixmap, gc, 4, 4, 124, 124); + double elapsed = now_seconds() - start; + printf("wide-XDrawLine %.6f sec %.3f usec/op\n", elapsed, + elapsed * 1000000.0 / 1000.0); +} + +int main(void) +{ + Display *display = XOpenDisplay(NULL); + if (!display) { + fprintf(stderr, "XOpenDisplay failed\n"); + return 1; + } + Window root = RootWindow(display, DefaultScreen(display)); + Pixmap pixmap = + XCreatePixmap(display, root, 128, 128, DefaultDepth(display, 0)); + GC gc = XCreateGC(display, pixmap, 0, NULL); + XSetForeground(display, gc, 0xFFFF0000); + bench_many_small_draw_arcs(display, pixmap, gc); + bench_large_fill_arc(display, pixmap, gc); + bench_convex_polygon(display, pixmap, gc); + bench_self_intersecting_polygon(display, pixmap, gc); + bench_wide_line(display, pixmap, gc); + XFreeGC(display, gc); + XFreePixmap(display, pixmap); + XCloseDisplay(display); + return 0; +} diff --git a/tests/check.c b/tests/check.c index e2e2e6b..dddfa8c 100644 --- a/tests/check.c +++ b/tests/check.c @@ -19,6 +19,9 @@ #include "drawing.h" #include "gc.h" #include "image.h" +#include "path/compose.h" +#include "path/edges.h" +#include "path/path.h" #include "util.h" int convertEvent(Display *display, @@ -28,12 +31,17 @@ int convertEvent(Display *display, extern Bool mouseFrozen; #include +#include #include #include #include #include #include +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + static int failures = 0; #define CHECK(condition, message) \ @@ -117,6 +125,13 @@ static int pixel_is_rgb(SDL_Surface *surface, Uint8 red, Uint8 green, Uint8 blue); +static int pixel_is_rgba(SDL_Surface *surface, + int x, + int y, + Uint8 red, + Uint8 green, + Uint8 blue, + Uint8 alpha); static int count_open_file_descriptors(void) { @@ -634,6 +649,17 @@ static int pixel_is_rgb(SDL_Surface *surface, Uint8 red, Uint8 green, Uint8 blue) +{ + return pixel_is_rgba(surface, x, y, red, green, blue, 255); +} + +static int pixel_is_rgba(SDL_Surface *surface, + int x, + int y, + Uint8 red, + Uint8 green, + Uint8 blue, + Uint8 alpha) { Uint8 gotRed = 0; Uint8 gotGreen = 0; @@ -641,7 +667,8 @@ static int pixel_is_rgb(SDL_Surface *surface, Uint8 gotAlpha = 0; SDL_GetRGBA(getPixel(surface, (unsigned int) x, (unsigned int) y), surface->format, &gotRed, &gotGreen, &gotBlue, &gotAlpha); - return gotRed == red && gotGreen == green && gotBlue == blue; + return gotRed == red && gotGreen == green && gotBlue == blue && + gotAlpha == alpha; } static int test_pixmaps(Display *display) @@ -830,6 +857,562 @@ static int test_pixmaps(Display *display) return 1; } +static int test_drawables_and_gcs(Display *display) +{ + Window root = RootWindow(display, DefaultScreen(display)); + Pixmap pixmap = + XCreatePixmap(display, root, 32, 32, DefaultDepth(display, 0)); + CHECK(pixmap != None, "drawables pixmap creation failed"); + GC gc = XCreateGC(display, pixmap, 0, NULL); + CHECK(gc, "drawables GC creation failed"); + + CHECK(XSetForeground(display, gc, 0xFF000000), "set black failed"); + CHECK(XFillRectangle(display, pixmap, gc, 0, 0, 32, 32), + "clear drawable failed"); + + XPoint triangle[] = {{2, 2}, {14, 2}, {2, 14}}; + CHECK(XSetForeground(display, gc, 0xFFFF0000), "set red failed"); + CHECK( + XFillPolygon(display, pixmap, gc, triangle, 3, Convex, CoordModeOrigin), + "XFillPolygon convex triangle failed"); + + XPoint previousTriangle[] = {{18, 2}, {12, 0}, {-12, 12}}; + CHECK(XSetForeground(display, gc, 0xFF0000FF), "set blue failed"); + CHECK(XFillPolygon(display, pixmap, gc, previousTriangle, 3, Convex, + CoordModePrevious), + "XFillPolygon CoordModePrevious failed"); + + XPoint bowtie[] = {{4, 18}, {14, 30}, {14, 18}, {4, 30}}; + CHECK(XSetFillRule(display, gc, EvenOddRule), "set even-odd failed"); + CHECK(XSetForeground(display, gc, 0xFFFFFF00), "set yellow failed"); + CHECK( + XFillPolygon(display, pixmap, gc, bowtie, 4, Complex, CoordModeOrigin), + "XFillPolygon bowtie failed"); + + XRectangle clip = {.x = 24, .y = 24, .width = 4, .height = 4}; + CHECK(XSetClipRectangles(display, gc, 0, 0, &clip, 1, Unsorted), + "set polygon clip failed"); + XPoint clippedPoly[] = {{22, 22}, {31, 22}, {31, 31}, {22, 31}}; + CHECK(XSetForeground(display, gc, 0xFF00FF00), "set green failed"); + CHECK(XFillPolygon(display, pixmap, gc, clippedPoly, 4, Convex, + CoordModeOrigin), + "XFillPolygon clipped square failed"); + CHECK(XSetClipMask(display, gc, None), "clear polygon clip failed"); + + XArc fillArcs[] = { + {.x = 18, + .y = 18, + .width = 8, + .height = 8, + .angle1 = 0, + .angle2 = 360 * 64}, + {.x = 24, + .y = 2, + .width = 6, + .height = 6, + .angle1 = 0, + .angle2 = 360 * 64}, + }; + CHECK(XSetForeground(display, gc, 0xFFFF00FF), "set magenta failed"); + CHECK(XFillArcs(display, pixmap, gc, fillArcs, 2), "XFillArcs failed"); + XArc drawArcs[] = { + {.x = 2, + .y = 22, + .width = 8, + .height = 8, + .angle1 = 0, + .angle2 = 90 * 64}, + {.x = 16, + .y = 2, + .width = 8, + .height = 8, + .angle1 = 0, + .angle2 = 90 * 64}, + }; + CHECK(XSetForeground(display, gc, 0xFFFFFFFF), "set white failed"); + CHECK(XDrawArcs(display, pixmap, gc, drawArcs, 2), "XDrawArcs failed"); + + CHECK(XSetForeground(display, gc, 0xFF112233), "set base failed"); + CHECK(XSetFunction(display, gc, GXcopy), "set GXcopy failed"); + CHECK(XFillRectangle(display, pixmap, gc, 0, 0, 3, 3), + "raster base fill failed"); + CHECK(XSetForeground(display, gc, 0xFF010203), "set xor source failed"); + CHECK(XSetFunction(display, gc, GXxor), "set GXxor failed"); + CHECK(XFillRectangle(display, pixmap, gc, 0, 0, 1, 1), + "GXxor fill rectangle failed"); + CHECK(XDrawPoint(display, pixmap, gc, 1, 0), "GXxor draw point failed"); + CHECK(XDrawLine(display, pixmap, gc, 0, 1, 2, 1), "GXxor draw line failed"); + CHECK(XSetFunction(display, gc, GXnoop), "set GXnoop failed"); + CHECK(XSetForeground(display, gc, 0xFFFFFFFF), "set noop source failed"); + CHECK(XFillRectangle(display, pixmap, gc, 2, 2, 1, 1), + "GXnoop fill rectangle failed"); + CHECK(XSetFunction(display, gc, GXcopy), "set mask base copy failed"); + CHECK(XSetForeground(display, gc, 0xFF112233), "set mask base failed"); + CHECK(XFillRectangle(display, pixmap, gc, 3, 0, 1, 1), + "plane-mask base fill failed"); + CHECK(XSetPlaneMask(display, gc, 0x000000FF), "set plane mask failed"); + CHECK(XSetForeground(display, gc, 0xFF010203), + "set mask xor source failed"); + CHECK(XSetFunction(display, gc, GXxor), "set masked GXxor failed"); + CHECK(XFillRectangle(display, pixmap, gc, 3, 0, 1, 1), + "plane-masked GXxor fill rectangle failed"); + CHECK(XSetPlaneMask(display, gc, 0xFFFFFFFF), "reset plane mask failed"); + CHECK(XSetFunction(display, gc, GXcopy), "reset GXcopy failed"); + XPoint alphaPoly[] = {{4, 0}, {8, 0}, {4, 4}}; + CHECK(XSetForeground(display, gc, 0x80112233), "set alpha polygon failed"); + CHECK(XFillPolygon(display, pixmap, gc, alphaPoly, 3, Convex, + CoordModeOrigin), + "alpha XFillPolygon failed"); + CHECK(XSetForeground(display, gc, 0x80667788), "set alpha arc failed"); + CHECK(XFillArc(display, pixmap, gc, 10, 0, 6, 6, 0, 360 * 64), + "alpha XFillArc failed"); + + SDL_Renderer *renderer = NULL; + GET_RENDERER(pixmap, renderer); + SDL_Surface *surface = getRenderSurface(renderer); + CHECK(surface, "getRenderSurface for drawables failed"); + int bowtieYellowPixels = 0; + for (int by = 18; by < 31; by++) { + for (int bx = 4; bx < 15; bx++) { + if (pixel_is_rgb(surface, bx, by, 255, 255, 0)) + bowtieYellowPixels++; + } + } + CHECK(pixel_is_rgb(surface, 4, 4, 255, 0, 0), + "convex triangle did not fill expected pixel"); + CHECK(pixel_is_rgb(surface, 22, 4, 0, 0, 255), + "CoordModePrevious polygon did not fill expected pixel"); + CHECK(bowtieYellowPixels > 0, "even-odd bowtie produced no filled pixels"); + CHECK(pixel_is_rgb(surface, 25, 25, 0, 255, 0), + "clipped polygon did not fill inside clip"); + CHECK(pixel_is_rgb(surface, 28, 28, 0, 0, 0), + "clipped polygon wrote outside clip"); + CHECK(pixel_is_rgb(surface, 22, 22, 255, 0, 255), + "first XFillArcs arc did not land"); + CHECK(pixel_is_rgb(surface, 27, 5, 255, 0, 255), + "second XFillArcs arc did not land"); + CHECK(pixel_is_rgb(surface, 10, 26, 255, 255, 255), + "first XDrawArcs arc did not land"); + CHECK(pixel_is_rgb(surface, 24, 6, 255, 255, 255), + "second XDrawArcs arc did not land"); + CHECK(pixel_is_rgb(surface, 0, 0, 0x10, 0x20, 0x30), + "GXxor fill rectangle produced wrong pixel"); + CHECK(pixel_is_rgb(surface, 1, 0, 0x10, 0x20, 0x30), + "GXxor draw point produced wrong pixel"); + CHECK(pixel_is_rgb(surface, 2, 1, 0x10, 0x20, 0x30), + "GXxor draw line produced wrong pixel"); + CHECK(pixel_is_rgba(surface, 0, 0, 0x10, 0x20, 0x30, 255), + "GXxor fill rectangle did not preserve alpha"); + CHECK(pixel_is_rgba(surface, 1, 0, 0x10, 0x20, 0x30, 255), + "GXxor draw point did not preserve alpha"); + CHECK(pixel_is_rgb(surface, 2, 2, 0x11, 0x22, 0x33), + "GXnoop changed the destination pixel"); + CHECK(pixel_is_rgba(surface, 3, 0, 0x11, 0x22, 0x30, 255), + "GXxor ignored the GC plane mask"); + CHECK(pixel_is_rgba(surface, 5, 1, 0x11, 0x22, 0x33, 0x80), + "GXcopy polygon fill blended instead of replacing"); + CHECK(pixel_is_rgba(surface, 13, 3, 0x66, 0x77, 0x88, 0x80), + "GXcopy arc fill blended instead of replacing"); + SDL_FreeSurface(surface); + + Pixmap pathArcPixmap = + XCreatePixmap(display, root, 40, 40, DefaultDepth(display, 0)); + CHECK(pathArcPixmap != None, "path arc pixmap creation failed"); + GC pathArcGc = XCreateGC(display, pathArcPixmap, 0, NULL); + CHECK(pathArcGc, "path arc GC creation failed"); + CHECK(XSetForeground(display, pathArcGc, 0xFF000000), + "path arc black failed"); + CHECK(XFillRectangle(display, pathArcPixmap, pathArcGc, 0, 0, 40, 40), + "path arc clear failed"); + CHECK(XSetForeground(display, pathArcGc, 0xFF00FFFF), + "path arc cyan failed"); + CHECK( + XFillArc(display, pathArcPixmap, pathArcGc, 8, 8, 24, 24, 0, 360 * 64), + "large path XFillArc failed"); + CHECK(XSetForeground(display, pathArcGc, 0xFFFFFFFF), + "path arc white failed"); + CHECK(XDrawArc(display, pathArcPixmap, pathArcGc, 2, 2, 24, 24, 0, 90 * 64), + "large path XDrawArc failed"); + XArc overlappingArcs[] = { + {.x = 4, + .y = 16, + .width = 20, + .height = 20, + .angle1 = 0, + .angle2 = 360 * 64}, + {.x = 14, + .y = 16, + .width = 20, + .height = 20, + .angle1 = 0, + .angle2 = 360 * 64}, + }; + CHECK(XSetForeground(display, pathArcGc, 0xFFFFAA00), + "path arc orange failed"); + CHECK(XFillArcs(display, pathArcPixmap, pathArcGc, overlappingArcs, 2), + "overlapping path XFillArcs failed"); + GET_RENDERER(pathArcPixmap, renderer); + surface = getRenderSurface(renderer); + CHECK(surface, "getRenderSurface for path arcs failed"); + CHECK(pixel_is_rgb(surface, 20, 12, 0, 255, 255), + "path XFillArc did not fill center"); + CHECK(pixel_is_rgb(surface, 26, 14, 255, 255, 255), + "path XDrawArc did not draw expected point"); + CHECK(pixel_is_rgb(surface, 19, 26, 255, 170, 0), + "overlapping XFillArcs canceled the overlap"); + SDL_FreeSurface(surface); + XFreeGC(display, pathArcGc); + XFreePixmap(display, pathArcPixmap); + + Pixmap widePixmap = + XCreatePixmap(display, root, 48, 48, DefaultDepth(display, 0)); + CHECK(widePixmap != None, "wide stroke pixmap creation failed"); + GC wideGc = XCreateGC(display, widePixmap, 0, NULL); + CHECK(wideGc, "wide stroke GC creation failed"); + CHECK(XSetForeground(display, wideGc, 0xFF000000), "wide black failed"); + CHECK(XFillRectangle(display, widePixmap, wideGc, 0, 0, 48, 48), + "wide clear failed"); + CHECK(XSetForeground(display, wideGc, 0xFFFF0000), "wide red failed"); + CHECK(XSetLineAttributes(display, wideGc, 6, LineSolid, CapButt, JoinMiter), + "wide line attributes failed"); + CHECK(XDrawLine(display, widePixmap, wideGc, 4, 8, 28, 8), + "wide XDrawLine failed"); + CHECK(XSetForeground(display, wideGc, 0xFF00FF00), "wide green failed"); + CHECK( + XSetLineAttributes(display, wideGc, 6, LineSolid, CapRound, JoinRound), + "round line attributes failed"); + XPoint widePoints[] = {{8, 20}, {20, 32}, {32, 20}}; + CHECK( + XDrawLines(display, widePixmap, wideGc, widePoints, 3, CoordModeOrigin), + "wide XDrawLines failed"); + CHECK(XSetForeground(display, wideGc, 0xFF0000FF), "wide blue failed"); + CHECK(XSetLineAttributes(display, wideGc, 6, LineSolid, CapProjecting, + JoinBevel), + "projecting line attributes failed"); + CHECK(XDrawRectangle(display, widePixmap, wideGc, 34, 4, 8, 8), + "wide XDrawRectangle failed"); + CHECK(XSetForeground(display, wideGc, 0xFFFFFF00), "wide yellow failed"); + CHECK( + XSetLineAttributes(display, wideGc, 6, LineSolid, CapRound, JoinRound), + "zero-length round attributes failed"); + CHECK(XDrawLine(display, widePixmap, wideGc, 8, 40, 8, 40), + "zero-length round XDrawLine failed"); + CHECK(XSetForeground(display, wideGc, 0xFFFF00FF), "wide magenta failed"); + CHECK(XSetLineAttributes(display, wideGc, 6, LineSolid, CapProjecting, + JoinBevel), + "zero-length projecting attributes failed"); + XSegment zeroSegment = {28, 40, 28, 40}; + CHECK(XDrawSegments(display, widePixmap, wideGc, &zeroSegment, 1), + "zero-length projecting XDrawSegments failed"); + char zeroDash[] = {4, 4}; + CHECK(XSetDashes(display, wideGc, 0, zeroDash, 2), + "zero-length dash setup failed"); + CHECK(XSetForeground(display, wideGc, 0xFF00FFFF), "wide cyan failed"); + CHECK(XSetLineAttributes(display, wideGc, 6, LineOnOffDash, CapRound, + JoinRound), + "zero-length dashed round attributes failed"); + CHECK(XDrawLine(display, widePixmap, wideGc, 40, 40, 40, 40), + "zero-length dashed round XDrawLine failed"); + GET_RENDERER(widePixmap, renderer); + surface = getRenderSurface(renderer); + CHECK(surface, "getRenderSurface for wide strokes failed"); + CHECK(pixel_is_rgb(surface, 12, 8, 255, 0, 0), + "wide butt line missed center"); + CHECK(pixel_is_rgb(surface, 12, 10, 255, 0, 0), + "wide butt line missed half-width pixel"); + CHECK(pixel_is_rgb(surface, 20, 28, 0, 255, 0), + "wide round join missed interior"); + CHECK(pixel_is_rgb(surface, 42, 8, 0, 0, 255), + "wide rectangle missed right edge"); + CHECK(pixel_is_rgb(surface, 8, 40, 255, 255, 0), + "zero-length round line was dropped"); + CHECK(pixel_is_rgb(surface, 28, 40, 255, 0, 255), + "zero-length projecting segment was dropped"); + CHECK(pixel_is_rgb(surface, 40, 40, 0, 255, 255), + "zero-length dashed round line was dropped"); + SDL_FreeSurface(surface); + XFreeGC(display, wideGc); + XFreePixmap(display, widePixmap); + + Pixmap dashPixmap = + XCreatePixmap(display, root, 32, 20, DefaultDepth(display, 0)); + CHECK(dashPixmap != None, "dash pixmap creation failed"); + GC dashGc = XCreateGC(display, dashPixmap, 0, NULL); + CHECK(dashGc, "dash GC creation failed"); + CHECK(XSetForeground(display, dashGc, 0xFF000000), "dash black failed"); + CHECK(XFillRectangle(display, dashPixmap, dashGc, 0, 0, 32, 20), + "dash clear failed"); + char dashList[] = {4, 4}; + CHECK(XSetDashes(display, dashGc, 0, dashList, 2), "XSetDashes failed"); + CHECK(XSetForeground(display, dashGc, 0xFFFF0000), "dash red failed"); + CHECK(XSetLineAttributes(display, dashGc, 1, LineOnOffDash, CapButt, + JoinMiter), + "on-off dash attributes failed"); + CHECK(XDrawLine(display, dashPixmap, dashGc, 2, 4, 24, 4), + "LineOnOffDash draw failed"); + CHECK(XSetForeground(display, dashGc, 0xFF00FF00), "dash green failed"); + CHECK(XSetBackground(display, dashGc, 0xFF0000FF), "dash blue failed"); + CHECK(XSetLineAttributes(display, dashGc, 1, LineDoubleDash, CapButt, + JoinMiter), + "double dash attributes failed"); + CHECK(XDrawLine(display, dashPixmap, dashGc, 2, 12, 24, 12), + "LineDoubleDash draw failed"); + GET_RENDERER(dashPixmap, renderer); + surface = getRenderSurface(renderer); + CHECK(surface, "getRenderSurface for dashed strokes failed"); + CHECK(pixel_is_rgb(surface, 3, 4, 255, 0, 0), + "LineOnOffDash missed on dash"); + CHECK(pixel_is_rgb(surface, 7, 4, 0, 0, 0), "LineOnOffDash drew off dash"); + CHECK(pixel_is_rgb(surface, 3, 12, 0, 255, 0), + "LineDoubleDash missed foreground dash"); + CHECK(pixel_is_rgb(surface, 7, 12, 0, 0, 255), + "LineDoubleDash missed background dash"); + SDL_FreeSurface(surface); + XFreeGC(display, dashGc); + XFreePixmap(display, dashPixmap); + + XFreeGC(display, gc); + XFreePixmap(display, pixmap); + return 1; +} + +/* Regression coverage for the fixes that closed out the Drawables / Pixmaps / + * GC review pass: small-arc ArcChord routing, batched point and rectangle + * primitives, non-GXcopy line batches, and dashed small arcs. */ +static int test_drawing_coverage(Display *display) +{ + Window root = RootWindow(display, DefaultScreen(display)); + + Pixmap arcPx = + XCreatePixmap(display, root, 24, 12, DefaultDepth(display, 0)); + CHECK(arcPx != None, "arc-mode pixmap creation failed"); + GC arcGc = XCreateGC(display, arcPx, 0, NULL); + CHECK(arcGc, "arc-mode GC creation failed"); + CHECK(XSetForeground(display, arcGc, 0xFF000000), "arc-mode black failed"); + CHECK(XFillRectangle(display, arcPx, arcGc, 0, 0, 24, 12), + "arc-mode clear failed"); + CHECK(XSetForeground(display, arcGc, 0xFFFF0000), "arc-mode red failed"); + /* Width 10 keeps us under the legacy 16-pixel cutoff. Before the fix, + * the small-arc fallback always rendered as ArcPieSlice and would have + * filled the center pixel for both modes. After the fix the path + * accelerator handles ArcChord regardless of size, so the center pixel + * stays unfilled for chord but stays filled for pie. */ + CHECK(XSetArcMode(display, arcGc, ArcChord), "set ArcChord failed"); + CHECK(XFillArc(display, arcPx, arcGc, 0, 1, 10, 10, 0, 90 * 64), + "small ArcChord fill failed"); + CHECK(XSetArcMode(display, arcGc, ArcPieSlice), "set ArcPieSlice failed"); + CHECK(XFillArc(display, arcPx, arcGc, 12, 1, 10, 10, 0, 90 * 64), + "small ArcPieSlice fill failed"); + SDL_Renderer *arcRenderer; + GET_RENDERER(arcPx, arcRenderer); + SDL_Surface *arcSurface = getRenderSurface(arcRenderer); + CHECK(arcSurface, "arc-mode getRenderSurface failed"); + /* Pixel (cx+2, cy-1) relative to each arc lies inside the pie wedge + * triangle but on the center side of the chord, i.e. inside pie and + * outside chord. Chord arc is at (0,1); pie arc is at (12,1). */ + CHECK(pixel_is_rgb(arcSurface, 7, 5, 0, 0, 0), + "small ArcChord still filled the triangle (legacy pie fallback?)"); + CHECK(pixel_is_rgb(arcSurface, 19, 5, 255, 0, 0), + "small ArcPieSlice failed to fill the wedge triangle"); + SDL_FreeSurface(arcSurface); + XFreeGC(display, arcGc); + XFreePixmap(display, arcPx); + + /* XDrawPoints / XDrawRectangles were previously WARN_UNIMPLEMENTED + * stubs in missing.c. Exercise both batched forms and verify the + * pixels actually land. */ + Pixmap batchPx = + XCreatePixmap(display, root, 16, 16, DefaultDepth(display, 0)); + CHECK(batchPx != None, "batch pixmap creation failed"); + GC batchGc = XCreateGC(display, batchPx, 0, NULL); + CHECK(batchGc, "batch GC creation failed"); + CHECK(XSetForeground(display, batchGc, 0xFF000000), "batch black failed"); + CHECK(XFillRectangle(display, batchPx, batchGc, 0, 0, 16, 16), + "batch clear failed"); + CHECK(XSetForeground(display, batchGc, 0xFFFFFFFF), "batch white failed"); + XPoint originPts[] = {{1, 1}, {2, 2}, {3, 3}}; + CHECK(XDrawPoints(display, batchPx, batchGc, originPts, 3, CoordModeOrigin), + "XDrawPoints CoordModeOrigin failed"); + XPoint prevPts[] = {{10, 1}, {1, 1}, {1, 1}}; + CHECK(XDrawPoints(display, batchPx, batchGc, prevPts, 3, CoordModePrevious), + "XDrawPoints CoordModePrevious failed"); + CHECK(XSetForeground(display, batchGc, 0xFF0000FF), "batch blue failed"); + XRectangle rects[] = {{5, 8, 3, 3}, {10, 10, 4, 4}}; + CHECK(XDrawRectangles(display, batchPx, batchGc, rects, 2), + "XDrawRectangles failed"); + SDL_Renderer *batchRenderer; + GET_RENDERER(batchPx, batchRenderer); + SDL_Surface *batchSurface = getRenderSurface(batchRenderer); + CHECK(batchSurface, "batch getRenderSurface failed"); + CHECK(pixel_is_rgb(batchSurface, 1, 1, 255, 255, 255), + "XDrawPoints origin point 0 missing"); + CHECK(pixel_is_rgb(batchSurface, 3, 3, 255, 255, 255), + "XDrawPoints origin point 2 missing"); + CHECK(pixel_is_rgb(batchSurface, 10, 1, 255, 255, 255), + "XDrawPoints previous start point missing"); + CHECK(pixel_is_rgb(batchSurface, 11, 2, 255, 255, 255), + "XDrawPoints CoordModePrevious accumulation broke"); + CHECK(pixel_is_rgb(batchSurface, 12, 3, 255, 255, 255), + "XDrawPoints CoordModePrevious second accumulation broke"); + /* XDrawRectangles per X11 spec outlines (w+1)x(h+1): rect {5,8,3,3} + * has corners (5,8) and (8,11); rect {10,10,4,4} has corners + * (10,10) and (14,14). The far corner check would fail under the + * old SDL w-by-h behavior, which only reached (13,13). */ + CHECK(pixel_is_rgb(batchSurface, 5, 8, 0, 0, 255), + "XDrawRectangles first rect missing top-left corner"); + CHECK(pixel_is_rgb(batchSurface, 8, 11, 0, 0, 255), + "XDrawRectangles first rect missing bottom-right corner"); + CHECK(pixel_is_rgb(batchSurface, 14, 14, 0, 0, 255), + "XDrawRectangles second rect missing X11-spec bottom-right corner"); + /* Interior must stay background to confirm the call outlined, not filled. + */ + CHECK(pixel_is_rgb(batchSurface, 6, 9, 0, 0, 0), + "XDrawRectangles filled instead of outlined"); + CHECK(pixel_is_rgb(batchSurface, 12, 12, 0, 0, 0), + "XDrawRectangles filled second rect instead of outlining"); + SDL_FreeSurface(batchSurface); + XFreeGC(display, batchGc); + XFreePixmap(display, batchPx); + + /* Non-GXcopy XDrawSegments and XDrawLines: before the fix, batched + * line primitives fell through to plain SDL blending and silently + * dropped the GC function. With the software walker the XOR pattern + * applies to every span pixel. */ + Pixmap xorPx = + XCreatePixmap(display, root, 16, 16, DefaultDepth(display, 0)); + CHECK(xorPx != None, "xor pixmap creation failed"); + GC xorGc = XCreateGC(display, xorPx, 0, NULL); + CHECK(xorGc, "xor GC creation failed"); + CHECK(XSetForeground(display, xorGc, 0xFF112233), "xor base failed"); + CHECK(XFillRectangle(display, xorPx, xorGc, 0, 0, 16, 16), + "xor clear failed"); + CHECK(XSetForeground(display, xorGc, 0xFF010203), "xor source failed"); + CHECK(XSetFunction(display, xorGc, GXxor), "xor set GXxor failed"); + XSegment segments[] = {{0, 4, 5, 4}, {8, 4, 12, 4}}; + CHECK(XDrawSegments(display, xorPx, xorGc, segments, 2), + "GXxor XDrawSegments failed"); + XPoint linePts[] = {{0, 8}, {5, 8}, {10, 8}}; + CHECK(XDrawLines(display, xorPx, xorGc, linePts, 3, CoordModeOrigin), + "GXxor XDrawLines failed"); + SDL_Renderer *xorRenderer; + GET_RENDERER(xorPx, xorRenderer); + SDL_Surface *xorSurface = getRenderSurface(xorRenderer); + CHECK(xorSurface, "xor getRenderSurface failed"); + /* 0x11^0x01=0x10, 0x22^0x02=0x20, 0x33^0x03=0x30. */ + CHECK(pixel_is_rgb(xorSurface, 0, 4, 0x10, 0x20, 0x30), + "GXxor XDrawSegments left endpoint not XORed"); + CHECK(pixel_is_rgb(xorSurface, 12, 4, 0x10, 0x20, 0x30), + "GXxor XDrawSegments right endpoint not XORed"); + /* Gap between the two segments stays at the base color. */ + CHECK(pixel_is_rgb(xorSurface, 6, 4, 0x11, 0x22, 0x33), + "GXxor XDrawSegments filled the gap between disjoint segments"); + CHECK(pixel_is_rgb(xorSurface, 0, 8, 0x10, 0x20, 0x30), + "GXxor XDrawLines start not XORed"); + CHECK(pixel_is_rgb(xorSurface, 5, 8, 0x10, 0x20, 0x30), + "GXxor XDrawLines join was XORed twice"); + CHECK(pixel_is_rgb(xorSurface, 10, 8, 0x10, 0x20, 0x30), + "GXxor XDrawLines end not XORed"); + SDL_FreeSurface(xorSurface); + XFreeGC(display, xorGc); + XFreePixmap(display, xorPx); + + Pixmap dashSegPx = + XCreatePixmap(display, root, 16, 16, DefaultDepth(display, 0)); + CHECK(dashSegPx != None, "dash segment pixmap creation failed"); + GC dashSegGc = XCreateGC(display, dashSegPx, 0, NULL); + CHECK(dashSegGc, "dash segment GC creation failed"); + CHECK(XSetForeground(display, dashSegGc, 0xFF000000), + "dash segment black failed"); + CHECK(XFillRectangle(display, dashSegPx, dashSegGc, 0, 0, 16, 16), + "dash segment clear failed"); + /* Pattern {2, 2} with length-8 segments gives a verifiable on/off/on/off + * sequence. The off-dash pixels (3 and 7 along each segment) must be + * background; pass a length where only an end-pixel check would pass + * trivially even if dashes were ignored. */ + char dashSegmentsPattern[] = {2, 2}; + CHECK(XSetDashes(display, dashSegGc, 0, dashSegmentsPattern, 2), + "dash segment XSetDashes failed"); + CHECK(XSetForeground(display, dashSegGc, 0xFFFFFFFF), + "dash segment white failed"); + CHECK(XSetLineAttributes(display, dashSegGc, 1, LineOnOffDash, CapButt, + JoinMiter), + "dash segment attributes failed"); + XSegment dashSegments[] = {{0, 12, 8, 12}, {0, 14, 8, 14}}; + CHECK(XDrawSegments(display, dashSegPx, dashSegGc, dashSegments, 2), + "dashed XDrawSegments failed"); + SDL_Renderer *dashSegRenderer; + GET_RENDERER(dashSegPx, dashSegRenderer); + SDL_Surface *dashSegSurface = getRenderSurface(dashSegRenderer); + CHECK(dashSegSurface, "dash segment getRenderSurface failed"); + CHECK(pixel_is_rgb(dashSegSurface, 0, 12, 255, 255, 255), + "dashed XDrawSegments first segment did not start on"); + CHECK(pixel_is_rgb(dashSegSurface, 3, 12, 0, 0, 0), + "dashed XDrawSegments off-gap pixel was drawn"); + CHECK(pixel_is_rgb(dashSegSurface, 4, 12, 255, 255, 255), + "dashed XDrawSegments second on-dash missing"); + /* Segment 2 must restart the dash pattern — same gap at the same + * relative offset confirms the per-segment phase reset. */ + CHECK(pixel_is_rgb(dashSegSurface, 0, 14, 255, 255, 255), + "dashed XDrawSegments did not reset dash phase"); + CHECK(pixel_is_rgb(dashSegSurface, 3, 14, 0, 0, 0), + "dashed XDrawSegments reset broke off-dash on segment 2"); + SDL_FreeSurface(dashSegSurface); + XFreeGC(display, dashSegGc); + XFreePixmap(display, dashSegPx); + + /* Dashed small arcs: before shouldUsePathArc gated on lineStyle, a + * sub-16 arc with LineOnOffDash routed to the legacy point spray + * which ignores the dash list entirely. Now the dashed arc must + * leave visible gaps. We compare against the same arc drawn solid + * and require the dashed pixel count to be strictly smaller. */ + Pixmap dashArcPx = + XCreatePixmap(display, root, 32, 14, DefaultDepth(display, 0)); + CHECK(dashArcPx != None, "dash-arc pixmap creation failed"); + GC dashArcGc = XCreateGC(display, dashArcPx, 0, NULL); + CHECK(dashArcGc, "dash-arc GC creation failed"); + CHECK(XSetForeground(display, dashArcGc, 0xFF000000), + "dash-arc black failed"); + CHECK(XFillRectangle(display, dashArcPx, dashArcGc, 0, 0, 32, 14), + "dash-arc clear failed"); + CHECK(XSetForeground(display, dashArcGc, 0xFFFFFFFF), + "dash-arc white failed"); + CHECK(XDrawArc(display, dashArcPx, dashArcGc, 1, 1, 12, 12, 0, 360 * 64), + "solid small XDrawArc failed"); + char dashPattern[] = {2, 2}; + CHECK(XSetDashes(display, dashArcGc, 0, dashPattern, 2), + "dash-arc XSetDashes failed"); + CHECK(XSetLineAttributes(display, dashArcGc, 1, LineOnOffDash, CapButt, + JoinMiter), + "dash-arc XSetLineAttributes failed"); + CHECK(XDrawArc(display, dashArcPx, dashArcGc, 17, 1, 12, 12, 0, 360 * 64), + "dashed small XDrawArc failed"); + SDL_Renderer *dashArcRenderer; + GET_RENDERER(dashArcPx, dashArcRenderer); + SDL_Surface *dashArcSurface = getRenderSurface(dashArcRenderer); + CHECK(dashArcSurface, "dash-arc getRenderSurface failed"); + int solidCount = 0; + int dashedCount = 0; + for (int yy = 0; yy < 14; yy++) { + for (int xx = 0; xx < 16; xx++) { + if (pixel_is_rgb(dashArcSurface, xx, yy, 255, 255, 255)) + solidCount++; + } + for (int xx = 16; xx < 32; xx++) { + if (pixel_is_rgb(dashArcSurface, xx, yy, 255, 255, 255)) + dashedCount++; + } + } + CHECK(solidCount > 0, "solid small XDrawArc rendered nothing"); + CHECK(dashedCount > 0, "dashed small XDrawArc rendered nothing"); + CHECK(dashedCount < solidCount, + "dashed XDrawArc covered as many pixels as solid (legacy fallback?)"); + SDL_FreeSurface(dashArcSurface); + XFreeGC(display, dashArcGc); + XFreePixmap(display, dashArcPx); + + return 1; +} + static int test_images(Display *display) { CHECK(XImageByteOrder(display) == ImageByteOrder(display), @@ -1004,6 +1587,117 @@ static int test_images(Display *display) return 1; } +static int test_path_accelerator(Display *display) +{ + (void) display; + + Path cubic; + CHECK(pathInit(&cubic), "pathInit failed"); + CHECK(pathMoveTo(&cubic, 0.0, 0.0), "pathMoveTo failed"); + CHECK(pathCubicTo(&cubic, 0.0, 10.0, 10.0, 10.0, 10.0, 0.0), + "pathCubicTo failed"); + PathPoint *points = NULL; + size_t count = 0; + CHECK(pathFlatten(&cubic, 0.25, &points, &count), "pathFlatten failed"); + CHECK(count > 2, "cubic flattened to too few points"); + for (size_t i = 0; i < count; i++) { + CHECK(points[i].x >= -0.001 && points[i].x <= 10.001, + "flattened x left expected bounds"); + CHECK(points[i].y >= -0.001 && points[i].y <= 10.001, + "flattened y left expected bounds"); + } + free(points); + pathFree(&cubic); + + Path cusp; + CHECK(pathInit(&cusp), "cusp pathInit failed"); + CHECK(pathMoveTo(&cusp, 0.0, 0.0), "cusp pathMoveTo failed"); + CHECK(pathCubicTo(&cusp, 1000.0, 0.001, -1000.0, -0.001, 1.0, 0.0), + "cusp pathCubicTo failed"); + points = NULL; + count = 0; + CHECK(pathFlatten(&cusp, 0.25, &points, &count), + "near-cusp cubic should flatten with default tolerance"); + CHECK(count < 65536, "near-cusp cubic exceeded output cap"); + free(points); + pathFree(&cusp); + + Path overdeep; + CHECK(pathInit(&overdeep), "overdeep pathInit failed"); + CHECK(pathMoveTo(&overdeep, 0.0, 0.0), "overdeep pathMoveTo failed"); + CHECK(pathCubicTo(&overdeep, 0.0, 100.0, 100.0, -100.0, 100.0, 0.0), + "overdeep pathCubicTo failed"); + points = NULL; + count = 0; + CHECK(!pathFlatten(&overdeep, 1e-30, &points, &count), + "overdeep flatten unexpectedly succeeded"); + CHECK(points == NULL && count == 0, "failed flatten returned output"); + pathFree(&overdeep); + + Path pie; + CHECK(pathInit(&pie), "pie pathInit failed"); + CHECK(pathAddArc(&pie, 10.0, 10.0, 5.0, 5.0, 0.0, M_PI / 2.0, ArcPieSlice), + "pie pathAddArc failed"); + CHECK(pie.commandCount >= 4, "pie arc emitted too few commands"); + CHECK(pie.commands[0] == PATH_CMD_MOVE && pie.commands[1] == PATH_CMD_LINE, + "pie arc did not start through center"); + CHECK(pie.commands[pie.commandCount - 1] == PATH_CMD_CLOSE, + "pie arc did not close"); + pathFree(&pie); + + Path chord; + CHECK(pathInit(&chord), "chord pathInit failed"); + CHECK(pathAddArc(&chord, 10.0, 10.0, 5.0, 5.0, 0.0, M_PI / 2.0, ArcChord), + "chord pathAddArc failed"); + CHECK(chord.commandCount >= 3, "chord arc emitted too few commands"); + CHECK(chord.commands[0] == PATH_CMD_MOVE && + chord.commands[1] == PATH_CMD_CUBIC, + "chord arc should connect endpoints directly"); + CHECK(chord.commands[chord.commandCount - 1] == PATH_CMD_CLOSE, + "chord arc did not close"); + pathFree(&chord); + + Uint32 buffer[4] = {0}; + PathSpan composeSpans[] = { + {.y = 0, .xStart = 0, .xEnd = 1, .coverage = 255}, + {.y = 1, .xStart = 1, .xEnd = 1, .coverage = 255}, + }; + PathSpanList composeList = { + .spans = composeSpans, + .count = 2, + .capacity = 2, + }; + CHECK(pathComposeSpansToBuffer(buffer, 2, 2, &composeList, 0xFF112233), + "pathComposeSpansToBuffer failed"); + CHECK(buffer[0] == 0x112233FF && buffer[1] == 0x112233FF, + "compose did not write RGBA8888 pixels"); + CHECK(buffer[2] == 0 && buffer[3] == 0x112233FF, + "compose wrote the wrong span locations"); + + PathPoint huge[] = { + {.x = 0.0, .y = 0.0}, + {.x = 100000000000.0, .y = 1.0}, + }; + PathEdgeList edges; + CHECK(!pathBuildEdges(huge, 2, &edges), + "pathBuildEdges accepted out-of-range fixed coordinates"); + CHECK(edges.edges == NULL && edges.count == 0, + "failed edge build leaked partial storage"); + + Uint32 pixel = 0; + PathSpan singleSpan = {.y = 0, .xStart = 0, .xEnd = 0, .coverage = 255}; + PathSpanList singleSpanList = { + .spans = &singleSpan, + .count = 1, + .capacity = 1, + }; + CHECK(!pathComposeSpansToBuffer(&pixel, -1, 1, &singleSpanList, 0xFFFFFFFF), + "compose accepted negative width"); + CHECK(!pathComposeSpansToBuffer(NULL, 1, 1, &singleSpanList, 0xFFFFFFFF), + "compose accepted NULL buffer"); + return 1; +} + static int test_regions(Display *display) { (void) display; @@ -2788,7 +3482,10 @@ int main(void) run_test("compat_stubs", test_compat_stubs); run_test("colors", test_colors); run_test("pixmaps", test_pixmaps); + run_test("drawables_and_gcs", test_drawables_and_gcs); + run_test("drawing_coverage", test_drawing_coverage); run_test("images", test_images); + run_test("path_accelerator", test_path_accelerator); run_test("regions", test_regions); run_test("events", test_events); run_test("windows", test_windows);