[PATCH v6 0/3] MR10359: gdiplus: Implement software path fill for images.
Implementation via region fill is relatively slow, and can cause issues in video playback where gdiplus paths are used to render a subtitle string once per frame. They Are Billions exhibits this issue. -- v6: gdiplus: Bypass span combination for an intersected rect. gdiplus: Implement software region fill for bitmap images. https://gitlab.winehq.org/wine/wine/-/merge_requests/10359
From: Conor McCarthy <cmccarthy@codeweavers.com> This make it relatively simple to add an alternative to alpha_blend_pixels_hrgn() for bitmap rendering. --- dlls/gdiplus/graphics.c | 73 +++++++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/dlls/gdiplus/graphics.c b/dlls/gdiplus/graphics.c index 2419eea94db..4ceaea2757e 100644 --- a/dlls/gdiplus/graphics.c +++ b/dlls/gdiplus/graphics.c @@ -4795,7 +4795,7 @@ GpStatus WINGDIPAPI GdipFillRectanglesI(GpGraphics *graphics, GpBrush *brush, GD return ret; } -static GpStatus get_clipped_region_hrgn(GpGraphics* graphics, GpRegion* region, HRGN *hrgn) +static GpStatus get_clipped_device_region(GpGraphics* graphics, GpRegion* region, GpRegion** clipped_region) { GpStatus status; GpRegion *tmp_region, *device_region; @@ -4860,7 +4860,24 @@ static GpStatus get_clipped_region_hrgn(GpGraphics* graphics, GpRegion* region, } if (status == Ok) - status = GdipGetRegionHRgn(device_region, NULL, hrgn); + *clipped_region = device_region; + else + GdipDeleteRegion(device_region); + } + + return status; +} + +static GpStatus get_clipped_region_hrgn(GpGraphics* graphics, GpRegion* region, HRGN *hrgn) +{ + GpStatus status; + GpRegion *device_region; + + status = get_clipped_device_region(graphics, region, &device_region); + + if (status == Ok) + { + status = GdipGetRegionHRgn(device_region, NULL, hrgn); GdipDeleteRegion(device_region); } @@ -4922,7 +4939,7 @@ static GpStatus SOFTWARE_GdipFillRegion(GpGraphics *graphics, GpBrush *brush, GpStatus stat; DWORD *pixel_data; HRGN hregion; - RECT bound_rect; + GpRegion* device_region = NULL; GpRect gp_bound_rect; if (!brush_can_fill_pixels(brush)) @@ -4931,22 +4948,49 @@ static GpStatus SOFTWARE_GdipFillRegion(GpGraphics *graphics, GpBrush *brush, stat = gdi_transform_acquire(graphics); if (stat == Ok) - stat = get_clipped_region_hrgn(graphics, region, &hregion); - - if (stat == Ok && GetRgnBox(hregion, &bound_rect) == NULLREGION) { - DeleteObject(hregion); - gdi_transform_release(graphics); - return Ok; + if (graphics->image && graphics->image->type == ImageTypeBitmap) + { + stat = get_clipped_device_region(graphics, region, &device_region); + + if (stat == Ok) + stat = GdipGetRegionBoundsI(device_region, graphics, &gp_bound_rect); + + if (stat == Ok && gp_bound_rect.Width == 0 && gp_bound_rect.Height == 0) + { + GdipDeleteRegion(device_region); + gdi_transform_release(graphics); + return Ok; + } + + if (stat == Ok) + stat = GdipGetRegionHRgn(device_region, NULL, &hregion); + } + else + { + RECT bound_rect; + + stat = get_clipped_region_hrgn(graphics, region, &hregion); + + if (stat == Ok && GetRgnBox(hregion, &bound_rect) == NULLREGION) + { + DeleteObject(hregion); + gdi_transform_release(graphics); + return Ok; + } + + if (stat == Ok) + { + gp_bound_rect.X = bound_rect.left; + gp_bound_rect.Y = bound_rect.top; + gp_bound_rect.Width = bound_rect.right - bound_rect.left; + gp_bound_rect.Height = bound_rect.bottom - bound_rect.top; + } + } } if (stat == Ok) { - gp_bound_rect.X = bound_rect.left; - gp_bound_rect.Y = bound_rect.top; - gp_bound_rect.Width = bound_rect.right - bound_rect.left; - gp_bound_rect.Height = bound_rect.bottom - bound_rect.top; - pixel_data = calloc(gp_bound_rect.Width * gp_bound_rect.Height, sizeof(*pixel_data)); if (!pixel_data) stat = OutOfMemory; @@ -4968,6 +5012,7 @@ static GpStatus SOFTWARE_GdipFillRegion(GpGraphics *graphics, GpBrush *brush, DeleteObject(hregion); } + GdipDeleteRegion(device_region); gdi_transform_release(graphics); return stat; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10359
From: Conor McCarthy <cmccarthy@codeweavers.com> Implementation via hregion fill is relatively slow, and can cause issues in video playback where gdiplus paths are used to render a subtitle string once per frame. They Are Billions exhibits this issue. --- dlls/gdiplus/gdiplus_private.h | 16 ++ dlls/gdiplus/graphics.c | 86 +++++++++- dlls/gdiplus/region.c | 280 ++++++++++++++++++++++++++++++++- 3 files changed, 369 insertions(+), 13 deletions(-) diff --git a/dlls/gdiplus/gdiplus_private.h b/dlls/gdiplus/gdiplus_private.h index 9d0a166fdeb..cc5f823214b 100644 --- a/dlls/gdiplus/gdiplus_private.h +++ b/dlls/gdiplus/gdiplus_private.h @@ -52,6 +52,20 @@ #define PIXELFORMATBPP(x) ((x) ? ((x) >> 8) & 255 : 24) +struct span +{ + /* Represents a horizontal span between two scanline intersections */ + int x[2]; + int y; +}; + +struct span_list +{ + struct span *spans; + size_t capacity; + size_t length; +}; + COLORREF ARGB2COLORREF(ARGB color); HBITMAP ARGB2BMP(ARGB color); extern INT arc2polybezier(GpPointF * points, REAL x1, REAL y1, REAL x2, REAL y2, @@ -153,6 +167,8 @@ extern GpStatus get_region_hrgn(struct region_element *element, const RECT *boun extern GpStatus get_hatch_data(GpHatchStyle hatchstyle, const unsigned char **result); +extern GpStatus region_element_to_spans(const struct region_element *element, const RECT *bounds, struct span_list *spans); + static inline INT gdip_round(REAL x) { return (INT) floorf(x + 0.5); diff --git a/dlls/gdiplus/graphics.c b/dlls/gdiplus/graphics.c index 4ceaea2757e..b278bfd1d94 100644 --- a/dlls/gdiplus/graphics.c +++ b/dlls/gdiplus/graphics.c @@ -4544,6 +4544,35 @@ end: return retval; } +static void bitmap_scanline_span_fill(GpBitmap *dst_bitmap, const DWORD *src_row, int row_x, + int start_x, int end_x, int y, CompositingMode comp_mode) +{ + int x; + + for (x = start_x; x < end_x; x++) + { + ARGB dst_color, src_color; + + src_color = src_row[x - row_x]; + + if (comp_mode == CompositingModeSourceCopy) + { + if (!(src_color & 0xff000000)) + GdipBitmapSetPixel(dst_bitmap, x, y, 0); + else + GdipBitmapSetPixel(dst_bitmap, x, y, src_color); + } + else + { + if (!(src_color & 0xff000000)) + continue; + + GdipBitmapGetPixel(dst_bitmap, x, y, &dst_color); + GdipBitmapSetPixel(dst_bitmap, x, y, color_over(dst_color, src_color)); + } + } +} + static GpStatus SOFTWARE_GdipFillPath(GpGraphics *graphics, GpBrush *brush, GpPath *path) { GpStatus stat; @@ -4933,12 +4962,49 @@ end: return status; } +static GpStatus alpha_blend_pixels_gpregion(GpGraphics *graphics, INT dst_x, INT dst_y, + const BYTE *src, INT src_width, INT src_height, INT src_stride, GpRegion* region) +{ + struct span_list spans = {0}; + RECT bounds; + GpStatus stat; + + bounds.left = dst_x; + bounds.top = dst_y; + bounds.right = dst_x + src_width; + bounds.bottom = dst_y + src_height; + stat = region_element_to_spans(®ion->node, &bounds, &spans); + + if (stat == Ok) + { + GpBitmap *dst_bitmap = (GpBitmap*)graphics->image; + CompositingMode comp_mode = graphics->compmode; + size_t i; + + for (i = 0; i < spans.length; i++) + { + struct span *span = &spans.spans[i]; + const BYTE *row; + + assert(span->y >= dst_y); + assert(span->y - dst_y < src_height); + + row = src + (span->y - dst_y) * src_stride; + bitmap_scanline_span_fill(dst_bitmap, (const DWORD *)row, dst_x, span->x[0], span->x[1], span->y, comp_mode); + } + } + + free(spans.spans); + + return stat; +} + static GpStatus SOFTWARE_GdipFillRegion(GpGraphics *graphics, GpBrush *brush, GpRegion* region) { GpStatus stat; DWORD *pixel_data; - HRGN hregion; + HRGN hregion = NULL; GpRegion* device_region = NULL; GpRect gp_bound_rect; @@ -4962,9 +5028,6 @@ static GpStatus SOFTWARE_GdipFillRegion(GpGraphics *graphics, GpBrush *brush, gdi_transform_release(graphics); return Ok; } - - if (stat == Ok) - stat = GdipGetRegionHRgn(device_region, NULL, &hregion); } else { @@ -5001,10 +5064,17 @@ static GpStatus SOFTWARE_GdipFillRegion(GpGraphics *graphics, GpBrush *brush, &gp_bound_rect, gp_bound_rect.Width); if (stat == Ok) - stat = alpha_blend_pixels_hrgn(graphics, gp_bound_rect.X, - gp_bound_rect.Y, (BYTE*)pixel_data, gp_bound_rect.Width, - gp_bound_rect.Height, gp_bound_rect.Width * 4, hregion, - PixelFormat32bppARGB); + { + if (hregion) + stat = alpha_blend_pixels_hrgn(graphics, gp_bound_rect.X, + gp_bound_rect.Y, (BYTE*)pixel_data, gp_bound_rect.Width, + gp_bound_rect.Height, gp_bound_rect.Width * 4, hregion, + PixelFormat32bppARGB); + else + stat = alpha_blend_pixels_gpregion(graphics, gp_bound_rect.X, + gp_bound_rect.Y, (BYTE*)pixel_data, gp_bound_rect.Width, + gp_bound_rect.Height, gp_bound_rect.Width * 4, device_region); + } free(pixel_data); } diff --git a/dlls/gdiplus/region.c b/dlls/gdiplus/region.c index c5b93e1ffa3..d0d911d2721 100644 --- a/dlls/gdiplus/region.c +++ b/dlls/gdiplus/region.c @@ -1003,6 +1003,16 @@ GpStatus WINGDIPAPI GdipGetRegionDataSize(GpRegion *region, UINT *needed) return Ok; } +static size_t grow_capacity_geometric(size_t capacity, size_t max_capacity, size_t count) +{ + size_t new_capacity = max(4, capacity); + while (new_capacity < count && new_capacity <= max_capacity / 2) + new_capacity *= 2; + if (new_capacity < count) + return max_capacity; + return new_capacity; +} + static GpStatus edge_list_reserve(struct edge_list *edges, size_t count) { size_t new_capacity, max_capacity; @@ -1015,11 +1025,7 @@ static GpStatus edge_list_reserve(struct edge_list *edges, size_t count) if (count > max_capacity) return OutOfMemory; - new_capacity = max(4, edges->capacity); - while (new_capacity < count && new_capacity <= max_capacity / 2) - new_capacity *= 2; - if (new_capacity < count) - new_capacity = max_capacity; + new_capacity = grow_capacity_geometric(edges->capacity, max_capacity, count); new_edges = realloc(edges->edges, new_capacity * sizeof(edges->edges[0])); if (!new_edges) @@ -1038,6 +1044,14 @@ static inline INT rgn_round(REAL x) return (INT) ceilf(x - RGN_ROUND_OFS); } +static inline void rect_round_from_gp_rect_f(RECT *rc, const GpRectF *rect) +{ + rc->left = rgn_round(rect->X); + rc->top = rgn_round(rect->Y); + rc->right = rgn_round(rect->X + rect->Width); + rc->bottom = rgn_round(rect->Y + rect->Height); +} + static GpStatus line_to_edge_list(GpPointF p1, GpPointF p2, const RECT *bounds, struct edge_list *edges) { GpStatus stat = Ok; @@ -2206,3 +2220,259 @@ GpStatus WINGDIPAPI GdipGetRegionScans(GpRegion *region, GpRectF *scans, INT *co return Ok; } + +static GpStatus span_list_reserve(struct span_list *spans, size_t count) +{ + size_t new_capacity, max_capacity; + struct span *new_spans; + + if (count <= spans->capacity) + return Ok; + + max_capacity = ~(SIZE_T)0 / sizeof(spans->spans[0]); + if (count > max_capacity) + return OutOfMemory; + + new_capacity = grow_capacity_geometric(spans->capacity, max_capacity, count); + + new_spans = realloc(spans->spans, new_capacity * sizeof(spans->spans[0])); + if (!new_spans) + return OutOfMemory; + + spans->spans = new_spans; + spans->capacity = new_capacity; + + return Ok; +} + +static GpStatus edge_list_to_spans_alternate(struct edge_list *edges, struct span_list *spans) +{ + GpStatus stat; + size_t i; + + stat = span_list_reserve(spans, spans->length + edges->length / 2u); + if (stat != Ok) + return stat; + + for (i = 0; i + 1 < edges->length; i += 2) + { + struct edge *edge = &edges->edges[i]; + struct span *span; + + assert(edge[0].y == edge[1].y); + + span = &spans->spans[spans->length++]; + span->x[0] = edge[0].x; + span->x[1] = edge[1].x; + span->y = edge[0].y; + } + + return Ok; +} + +static GpStatus edge_list_to_spans_winding(struct edge_list *edges, struct span_list *spans) +{ + int start_x = 0, winding_count = 0; + GpStatus stat = Ok; + size_t i; + + for (i = 0; i < edges->length; i++) + { + struct edge *edge = &edges->edges[i]; + + if (winding_count && start_x < edge->x) + { + struct span *span; + + stat = span_list_reserve(spans, spans->length + 1); + if (stat != Ok) + break; + + span = &spans->spans[spans->length++]; + span->x[0] = start_x; + span->x[1] = edge->x; + span->y = edge->y; + } + + start_x = edge->x; + winding_count += edge->rising ? 1 : -1; + } + + return stat; +} + +static GpStatus rect_to_spans(const RECT *rc, struct span_list *spans) +{ + GpStatus stat; + LONG y; + + stat = span_list_reserve(spans, spans->length + rc->bottom - rc->top); + if (stat != Ok) + return stat; + + for (y = rc->top; y < rc->bottom; ++y) + { + struct span *span = &spans->spans[spans->length++]; + span->x[0] = rc->left; + span->x[1] = rc->right; + span->y = y; + } + + return Ok; +} + +static GpStatus combine_regions_to_spans(const struct region_element *left, const struct region_element *right, + DWORD type, const RECT *bounds, struct span_list *spans) +{ + struct span_list spans_left = {0}, spans_right = {0}; + size_t i_left = 0, i_right = 0; + const struct span *cur_left, *cur_right; + BOOL in_left, in_right, in_result; + int x, y, x1_left, x1_right, x1_min; + GpStatus stat; + + stat = region_element_to_spans(left, bounds, &spans_left); + + if (stat == Ok) + stat = region_element_to_spans(right, bounds, &spans_right); + + cur_left = spans_left.length ? &spans_left.spans[0] : NULL; + cur_right = spans_right.length ? &spans_right.spans[0] : NULL; + + for (y = bounds->top; stat == Ok && y < bounds->bottom; ++y) + { + for (x = bounds->left; stat == Ok && x < bounds->right; ) + { + /* Update the current left and right spans */ + + if (cur_left && ((x >= cur_left->x[1] && y == cur_left->y) || y > cur_left->y)) + cur_left = (++i_left < spans_left.length) ? &spans_left.spans[i_left] : NULL; + + if (cur_right && ((x >= cur_right->x[1] && y == cur_right->y) || y > cur_right->y)) + cur_right = (++i_right < spans_right.length) ? &spans_right.spans[i_right] : NULL; + + assert(!cur_left || cur_left->y >= y); + assert(!cur_right || cur_right->y >= y); + + /* For both left and right, if x lies within a span, set the next x to its end, + * otherwise set the next x to the start of the next span, or the right boundary, + * to support combine modes Exclude and Complement. */ + + if (cur_left && y == cur_left->y) + { + in_left = x >= cur_left->x[0]; + x1_left = cur_left->x[in_left]; + } + else + { + in_left = FALSE; + x1_left = bounds->right; + } + + if (cur_right && y == cur_right->y) + { + in_right = x >= cur_right->x[0]; + x1_right = cur_right->x[in_right]; + } + else + { + in_right = FALSE; + x1_right = bounds->right; + } + + x1_min = min(x1_left, x1_right); + + switch (type) + { + case CombineModeIntersect: in_result = in_left & in_right; break; + case CombineModeUnion: in_result = in_left | in_right; break; + case CombineModeXor: in_result = in_left ^ in_right; break; + case CombineModeExclude: in_result = in_left & !in_right; break; + case CombineModeComplement: in_result = (!in_left) & in_right; break; + default: FIXME("Unhandled mode %lu.\n", type); in_result = FALSE; break; + } + + if (in_result) + { + struct span *result; + + stat = span_list_reserve(spans, spans->length + 1); + if (stat == Ok) + { + result = &spans->spans[spans->length++]; + result->x[0] = x; + result->x[1] = x1_min; + result->y = y; + } + } + + assert(x1_min > x); + x = x1_min; + } + } + + free(spans_left.spans); + free(spans_right.spans); + + return stat; +} + +/* Objects in the region element may be modified */ +GpStatus region_element_to_spans(const struct region_element *element, const RECT *bounds, struct span_list *spans) +{ + GpStatus stat = Ok; + + switch (element->type) + { + case RegionDataRect: + { + const GpRectF *rect = &element->elementdata.rect; + RECT rc; + + rect_round_from_gp_rect_f(&rc, rect); + + if (IntersectRect(&rc, &rc, bounds)) + stat = rect_to_spans(&rc, spans); + + break; + } + + case RegionDataPath: + { + GpPath *path = element->elementdata.path; + struct edge_list edge_list = { 0 }; + + stat = GdipFlattenPath(path, NULL, FlatnessDefault); + + if (stat == Ok) + stat = flat_path_to_edge_list(path, bounds, &edge_list); + + if (stat == Ok) + { + qsort(edge_list.edges, edge_list.length, sizeof(edge_list.edges[0]), cmp_edges); + + if (path->fill == FillModeWinding) + stat = edge_list_to_spans_winding(&edge_list, spans); + else + stat = edge_list_to_spans_alternate(&edge_list, spans); + } + + free(edge_list.edges); + break; + } + + case RegionDataEmptyRect: + break; + + case RegionDataInfiniteRect: + stat = rect_to_spans(bounds, spans); + break; + + default: + stat = combine_regions_to_spans(element->elementdata.combine.left, element->elementdata.combine.right, + element->type, bounds, spans); + break; + } + + return stat; +} -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10359
From: Conor McCarthy <cmccarthy@codeweavers.com> --- dlls/gdiplus/region.c | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/dlls/gdiplus/region.c b/dlls/gdiplus/region.c index d0d911d2721..d63f679c70d 100644 --- a/dlls/gdiplus/region.c +++ b/dlls/gdiplus/region.c @@ -2321,9 +2321,27 @@ static GpStatus rect_to_spans(const RECT *rc, struct span_list *spans) return Ok; } +static BOOL bounds_intersect_region_element_rect(RECT *bounds, const struct region_element *element) +{ + if (element->type == RegionDataInfiniteRect) + return TRUE; + + if (element->type == RegionDataRect) + { + RECT rc; + + rect_round_from_gp_rect_f(&rc, &element->elementdata.rect); + IntersectRect(bounds, bounds, &rc); + return TRUE; + } + + return FALSE; +} + static GpStatus combine_regions_to_spans(const struct region_element *left, const struct region_element *right, - DWORD type, const RECT *bounds, struct span_list *spans) + DWORD type, const RECT *bound_rect, struct span_list *spans) { + RECT bounds = *bound_rect; struct span_list spans_left = {0}, spans_right = {0}; size_t i_left = 0, i_right = 0; const struct span *cur_left, *cur_right; @@ -2331,17 +2349,27 @@ static GpStatus combine_regions_to_spans(const struct region_element *left, cons int x, y, x1_left, x1_right, x1_min; GpStatus stat; - stat = region_element_to_spans(left, bounds, &spans_left); + if (type == CombineModeIntersect) + { + /* In intersect mode, where one side is a rect, it is sufficient to intersect + * it with the bounds and convert the other side to spans */ + if (bounds_intersect_region_element_rect(&bounds, left)) + return region_element_to_spans(right, &bounds, spans); + if (bounds_intersect_region_element_rect(&bounds, right)) + return region_element_to_spans(left, &bounds, spans); + } + + stat = region_element_to_spans(left, &bounds, &spans_left); if (stat == Ok) - stat = region_element_to_spans(right, bounds, &spans_right); + stat = region_element_to_spans(right, &bounds, &spans_right); cur_left = spans_left.length ? &spans_left.spans[0] : NULL; cur_right = spans_right.length ? &spans_right.spans[0] : NULL; - for (y = bounds->top; stat == Ok && y < bounds->bottom; ++y) + for (y = bounds.top; stat == Ok && y < bounds.bottom; ++y) { - for (x = bounds->left; stat == Ok && x < bounds->right; ) + for (x = bounds.left; stat == Ok && x < bounds.right; ) { /* Update the current left and right spans */ @@ -2366,7 +2394,7 @@ static GpStatus combine_regions_to_spans(const struct region_element *left, cons else { in_left = FALSE; - x1_left = bounds->right; + x1_left = bounds.right; } if (cur_right && y == cur_right->y) @@ -2377,7 +2405,7 @@ static GpStatus combine_regions_to_spans(const struct region_element *left, cons else { in_right = FALSE; - x1_right = bounds->right; + x1_right = bounds.right; } x1_min = min(x1_left, x1_right); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10359
On Mon Mar 30 21:03:16 2026 +0000, Esme Povirk wrote:
I don't think we should even have to check the points in the path. If the clipping region (when accounting for device bounds) is a rectangle, then it should always contain the bounds we pass into alpha_blend_pixels_gpregion, and therefore we can treat it as infinite. The step of intersecting should already have been done implicitly by converting it to spans. I've revised it, but I'm not sure if the changes are what you intended.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10359#note_134523
On Tue Mar 31 14:03:33 2026 +0000, Conor McCarthy wrote:
I've revised it, but I'm not sure if the changes are what you intended. Can we have `bounds_intersect_region_element_rect` recurse if it encounters an intersect? That should cover the case where there's a rectangular clipping region on the Graphics object.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10359#note_134964
On Fri Apr 3 18:51:32 2026 +0000, Esme Povirk wrote:
Can we have `bounds_intersect_region_element_rect` recurse if it encounters an intersect? That should cover the case where there's a rectangular clipping region on the Graphics object. I think this is already covered. `combine_regions_to_spans` calls `region_element_to_spans` on `left`, `right` or `both`, then `region_element_to_spans` calls `combine_regions_to_spans` if it encounters an intersect, and then `combine_regions_to_spans` once again begins by calling `bounds_intersect_region_element_rect`.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10359#note_135087
On Mon Apr 6 06:19:23 2026 +0000, Conor McCarthy wrote:
I think this is already covered. `combine_regions_to_spans` calls `region_element_to_spans` on `left`, `right` or `both`, then `region_element_to_spans` calls `combine_regions_to_spans` if it encounters an intersect, and then `combine_regions_to_spans` once again begins by calling `bounds_intersect_region_element_rect`. Suppose our region looks like this:
- Intersect[A]
- Intersect[B]
- Rectangle[C] # graphics bounds
- Rectangle[D] # graphics clipping region
- Path[E] # what we're drawing
`combine_regions_to_spans` when called for [B] will use the optimized path to return a span array for the intersection of [C] and [D]. But, `combine_regions_to_spans` when called for [A] will have span lists for [B] and [E], which it must combine using a loop, even though it's easy to prove that [B] is a rectangle. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10359#note_135706
participants (3)
-
Conor McCarthy -
Conor McCarthy (@cmccarthy) -
Esme Povirk (@madewokherd)