[PATCH v2 0/2] 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. -- v2: gdiplus: Implement software region fill for bitmap images. gdiplus: Refactor SOFTWARE_GdipFillRegion() to separate bitmap handling. 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 | 90 ++++++++++- dlls/gdiplus/region.c | 272 ++++++++++++++++++++++++++++++++- 3 files changed, 365 insertions(+), 13 deletions(-) diff --git a/dlls/gdiplus/gdiplus_private.h b/dlls/gdiplus/gdiplus_private.h index 9d0a166fdeb..908e9c7aa1e 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, struct span_list *spans, const RECT *bounds); + 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..c1932f49b90 100644 --- a/dlls/gdiplus/graphics.c +++ b/dlls/gdiplus/graphics.c @@ -4544,6 +4544,38 @@ 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 premult) +{ + 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); + if (premult) + GdipBitmapSetPixel(dst_bitmap, x, y, color_over_fgpremult(dst_color, src_color)); + else + 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 +4965,50 @@ 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, &spans, &bounds); + + if (stat == Ok) + { + GpBitmap *dst_bitmap = (GpBitmap*)graphics->image; + CompositingMode comp_mode = graphics->compmode; + int premult = dst_bitmap->format & PixelFormatPAlpha; + 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, premult); + } + } + + 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 +5032,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 +5068,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..bb625925591 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) @@ -2206,3 +2212,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, struct span_list *spans, const RECT *bounds) +{ + 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, &spans_left, bounds); + + if (stat == Ok) + stat = region_element_to_spans(right, &spans_right, bounds); + + 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)) + 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)) + 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; + } + } + + return stat; +} + +/* Objects in the region element may be modified */ +GpStatus region_element_to_spans(const struct region_element *element, struct span_list *spans, const RECT *bounds) +{ + GpStatus stat = Ok; + + switch (element->type) + { + case RegionDataRect: + { + const GpRectF *rect = &element->elementdata.rect; + RECT rc; + + 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); + + 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, spans, bounds); + break; + } + + return stat; +} -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10359
Implemented region fill. We could modify `struct span` to include y0 and y1 for rectangles only, but I'm not sure it would buy much. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10359#note_133382
participants (2)
-
Conor McCarthy -
Conor McCarthy (@cmccarthy)