Skip to content

LibGfx: Implement scale() support in Gfx::Painter - #27096

Open
jmalcolm137 wants to merge 4 commits into
SerenityOS:masterfrom
jmalcolm137:libgfx-painter-scaling
Open

jmalcolm137 wants to merge 4 commits into
SerenityOS:masterfrom
jmalcolm137:libgfx-painter-scaling

Conversation

@jmalcolm137

Copy link
Copy Markdown

Several Gfx::Painter methods still VERIFY() that the target bitmap's
scale() is 1. Some primitives already support a non-unit scale
(draw_rect(), draw_line(), draw_bitmap(GlyphBitmap), ...), but the
rest do not. This series implements the missing scaled paths so a
Painter can render into a bitmap with scale() > 1 (for example, into a
client-side HiDPI backing store).

Commits

  1. LibGfx: Keep Gfx::Painter::fill_physical_rect() within the bitmap
    draw_line() can pass a rect that overruns the bitmap's edge (a thick
    horizontal or vertical line ending on the clip boundary), which
    previously wrote out of bounds. Clip against the bitmap's physical size.

  2. LibGfx: Draw all rows of Gfx::Painter::draw_rect() sides at scale > 1
    The side loops iterated up to max_y * scale inclusive, which covers
    only the first physical row of the final logical row, leaving a one pixel
    gap just above the bottom edge at scale > 1.

  3. LibGfx: Implement scale() support in Gfx::Painter
    Adds the scaled paths for the rect fills (including dither and
    checkerboard patterns), draw_bitmap(CharacterBitmap),
    draw_focus_rect(), the draw_text() overloads, ellipse/bezier/arc
    by lifting their guards (they delegate to the scale-aware
    draw_line()), and get_region_bitmap(). Also adds the tests.

The scaled paths keep the existing unscaled fast paths intact, so 1x
output is unchanged by construction.

Testing

Tests/LibGfx/TestPainter gains:

  • painter_scale_matches_pixel_doubled_unscaled: renders the same scene
    (fills, checkerboard, dither pattern, draw_rect(), draw_focus_rect(),
    horizontal and vertical lines, and a CharacterBitmap) into a 1x and a
    2x bitmap, and asserts the 2x result is exactly the pixel-doubled image of
    the 1x result.
  • painter_scale_get_region_bitmap: checks get_region_bitmap() returns a
    bitmap at the painter's scale.

ninja TestPainter builds and all tests pass.

Notes

  • Run clang-format (version 20) on the changed files before pushing. The
    changes were written to match the surrounding style.
  • The draw_text() overloads no longer reject scale() > 1; glyphs are
    placed through the already scale-aware glyph drawing paths. Rasterizing
    glyphs at the scaled size for higher quality HiDPI text is left as a
    follow-up.

fill_physical_rect()'s comment says that callers do clipping, but
draw_line() passes rects that can extend past the bottom or right edge
of the bitmap. A thick horizontal or vertical line ending on the clip
boundary, for example, fills from its start position for `thickness`
pixels, which writes out of bounds. Intersect the requested rect with
the bitmap's physical size before touching pixels.
draw_rect()'s side loops iterate from min_y * scale to max_y * scale
inclusive. At scale > 1 that covers only the first physical row of the
final logical row, so the bottom physical row of each side is left
undrawn, leaving a one pixel gap just above the bottom edge. Iterate up
to (max_y + 1) * scale instead.
Several Painter methods still VERIFY() that the target bitmap's scale is
1. Some drawing primitives already handle a non-unit scale (draw_rect(),
draw_line(), draw_bitmap(GlyphBitmap), ...), but the rest do not.

Implement the missing scaled paths:

- fill_rect_with_draw_op(), fill_rect_with_dither_pattern() and
  draw_bitmap(CharacterBitmap) expand every logical pixel into a scale x
  scale block.
- fill_rect_with_checkerboard() scales the rects and the cell size
  before running the existing run-based algorithm, leaving the unscaled
  path unchanged.
- draw_focus_rect() fills a 1x1 rect instead of setting a single
  physical pixel.
- fill_ellipse(), draw_ellipse_intersecting(),
  draw_quadratic_bezier_curve() and draw_elliptical_arc() already
  delegate to the scale-aware draw_line(), so their VERIFY() can go.
- draw_text() places glyphs through scale-aware glyph drawing paths, so
  the text overloads no longer have to reject scaling.
- get_region_bitmap() returns a bitmap at the painter's scale.

Add a test asserting that drawing into a 2x bitmap produces the
pixel-doubled image of drawing the same scene into a 1x bitmap.
@github-actions github-actions Bot added the 👀 pr-needs-review PR needs review from a maintainer or community member label Oct 11, 2026
@BuggieBot

Copy link
Copy Markdown
Member

Hello!

One or more of the commit messages in this PR do not match the SerenityOS code submission policy, please check the lint_commits CI job for more details on which commits were flagged and why.
Please do not close this PR and open another, instead modify your commit message(s) with git commit --amend and force push those changes to update this PR.

Filling a path with a painter whose scale is > 1 hit the
VERIFY(painter.scale() == 1) FIXME, which aborted clients that
draw paths at 2x -- e.g. a graph's filled area, which crashed on
repaint.

Rasterise in device space: map logical coordinates to device as
(logical + translation) * scale, scale the clip rect, grow the
scanline buffer to the device extent, and scale the path edges by
scale (subpixel samples by samples_per_pixel * scale). scale == 1
is unchanged.
@jmalcolm137
jmalcolm137 force-pushed the libgfx-painter-scaling branch from 83a63fe to 073b01e Compare October 11, 2026 07:40
@nico

nico commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

What's the motivation for this? Usually we only add code that has in-tree callers.

@jmalcolm137

Copy link
Copy Markdown
Author

What's the motivation for this? Usually we only add code that has in-tree callers.

I have been using LibGUI to build applications on Linux. My MacBook has a Retina display.

@nico

nico commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

So this is for out-of-tree clients?

@jmalcolm137

Copy link
Copy Markdown
Author

So this is for out-of-tree clients?

Well, I am primarily using this to run all the in-tree clients. But, I am running them on Linux. And yes, I am writing out-of-tree clients as well.

This will of course be useful / necessary for running SerenityOS native at HiDPI. But that is not my current use case.

@jmalcolm137

Copy link
Copy Markdown
Author

These were labelled as FIXME. There is also an existing bug that mixes logical and physical which gets hidden when those are the same.

@MacDue

MacDue commented Oct 15, 2026

Copy link
Copy Markdown
Member

These were labelled as FIXME

FYI: Some of these FIXMEs were/are "I guess this could be handled, but it's not needed, so I'll leave a FIXME".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

👀 pr-needs-review PR needs review from a maintainer or community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants