Skip to main content
Associate
June 15, 2026
Question

rawPartialBitmap draws Y-overhang rows at dirty-rect top instead of screenY (ABGR2222 external bitmap, TouchGFX 4.26.1)

  • June 15, 2026
  • 1 reply
  • 53 views

Environment

  • TouchGFX 4.26.1 (Engine + prebuilt library)
  • LCD config: 8bpp, `LCD8bpp_ABGR2222`, 240x240
  • Reproduced on Windows simulator (MSVC), Linux simulator, and STM32U595 hardware. Same TouchGFX source across all three, so this is not platform-specific.

Summary

When an ABGR2222 dynamic external bitmap is drawn with `LCD::drawPartialBitmap` and the source rect extends past the dirty (clip) rect on the Y axis, the clipping path writes the overhanging rows starting at the dirty rect's top (`dirtyRect.y`) instead of at the requested `screenY`. The result is a duplicated copy of those rows at the wrong vertical position.

X-axis clipping is correct: `screenX` can be negative or off-screen with no issue. The defect is specific to vertical overhang.

 

Trigger conditions (all must hold)

  • Bitmap format `Bitmap::ABGR2222` (the other three 8bpp 2222 formats compile to identical code, so they are likely affected too)
  • Bitmap registered with `Bitmap::dynamicBitmapCreateExternal`
  • Drawn via `LCD::drawPartialBitmap`, or `LCD::blitCopy` with a full-bitmap blitRect
  • Source rect extends past the dirty rect on Y: `screenY + height > dirtyRect.y + dirtyRect.height`, or `screenY < dirtyRect.y`
  • `screenY != dirtyRect.y` (typically `screenY != 0`)

Non-trigger: a non-zero `screenY` on its own is fine when the whole bitmap fits inside the dirty rect (verified at `screenY = 1`).

 

Minimal reproduction

A 50x50 ABGR2222 external bitmap drawn at (60, 220) on a 240x240 screen, so it overhangs the bottom edge by 30 px.

// ABGR2222: 1 byte/pixel, bits MSB->LSB are AABBGGRR.
// Opaque red = alpha 3, blue 0, green 0, red 3 = 0b11000011 = 0xC3.
static uint8_t pattern[50 * 50];
static uint16_t pool[2048];

for (size_t i = 0; i < sizeof(pattern); ++i)
pattern[i] = 0xC3;

Bitmap::setCache(pool, sizeof(pool), 1);
BitmapId id = Bitmap::dynamicBitmapCreateExternal(
50, 50, pattern, Bitmap::ABGR2222);

// Inside a Widget::draw():
Bitmap bmp(id);
Rect src(0, 0, bmp.getWidth(), bmp.getHeight());
HAL::lcd().drawPartialBitmap(bmp, 60, 220, src, 255, true);

 

Expected: 

a single red strip about 20 rows tall near the bottom of the screen.

Observed

that strip, plus a spurious second red strip near the top. The bottom-overhang rows are written at `y = 0` (`dirtyRect.y`) instead of `y = 220` (`screenY`).

Impact

Any widget that scrolls a bitmap into or out of view, anchors one near a screen edge, or otherwise clips it vertically will show this artifact.

1 reply

ST Technical Moderator
June 25, 2026

Hello ​@bitter_laser.

Could you help clarify a bit further what you are trying to achieve? Is there e.g. a reason why you are not just using the Image widget?

The drawPartialBitmap() function expects the input rect to be constrained to the screen bounds. All TouchGFX widgets do this. I have included an example from Image.cpp below. The invalidatedArea rect is always clipped to framebuffer bounds.

void Image::draw(const Rect& invalidatedArea) const
{
Rect meAbs;
translateRectToAbsolute(meAbs); // To find our x and y coords in absolute.

// Calculate intersection between bitmap rect and invalidated area.
const Rect dirtyBitmapArea = bitmap.getRect() & invalidatedArea;

if (!dirtyBitmapArea.isEmpty())
{
HAL::lcd().drawPartialBitmap(bitmap, meAbs.x, meAbs.y, dirtyBitmapArea, alpha);
}
}

The drawPartialBitmap() function is mainly part of the public interface so that you can use it when implementing custom widgets. In that case, you should do something similar to what the Image widget does.

Best regards,
Johan

Associate
July 24, 2026

I think I’m either misunderstanding or there’s some confusion here.

The header in LCD.hpp spells this out:

* Draws all (or a part) of a bitmap. The coordinates of the corner of the bitmap is
* given in (x, y) and rect describes which part of the bitmap should be drawn.
*
* @param x The absolute x coordinate to place (0, 0) of the bitmap on the screen.
* @param y The absolute y coordinate to place (0, 0) of the bitmap on the screen.
* @param rect A rectangle describing what region of the bitmap is to be drawn

So by spec: (x, y) anchors the bitmap's origin, and rect selects which sub-region of the bitmap gets drawn. For a row at bitmap-local rect.y, the documented output position is screen-y + rect.y. Nowhere in that contract does dirtyRect.y (the invalidated redraw area) enter the placement formula at all. As I interpret it, it's supposed to only select what to draw, not where.

I supposed I’d push back with, given the contract from the code, what is the formula that correctly derives dirtyRect.y as the on-screen position for the overhanging rows, instead of y + rect.y? If there's a documented case where the destination y is supposed to come from the dirty/invalidated rect rather than from x/y + the source rect, can you point me to it?

AFAICT, the reproduction above follows the same pattern as Image::draw you shared. This isn’t a case of the caller failing to clip to the screen bounds, it’s the standard documented usage, I think. I’m open to totally misunderstanding what I’m doing/what ST wants this to look like, as I understand it this is a bug that any widget using Image with an ABGR2222 dynamic bitmap that scrolls or sits near a screen edge will hit this.

Is there e.g. a reason why you are not just using the Image widget?

Yes, two reasons: my use case composites multiple runtime-selected bitmpas in one draw call with shared pan/seam logic that `Image` can’t (currently, at least) express. Additionally, `Image::draw()` calls `drawPartialBitmap` internally which is this same problem, so it hits the defect whenever a tile is anchored off-screen or clipped vertically regardless.