Skip to content

Correctly crop bitmap updates when TS_BITMAP_DATA.width is padded - #1436

Closed
Eugene (Eugeny) wants to merge 1 commit into
Devolutions:masterfrom
warp-tech:bitmap-shear
Closed

Correctly crop bitmap updates when TS_BITMAP_DATA.width is padded#1436
Eugene (Eugeny) wants to merge 1 commit into
Devolutions:masterfrom
warp-tech:bitmap-shear

Conversation

@Eugeny

Copy link
Copy Markdown

Downstream issue: warp-tech/warpgate#2173

TL;DR: IronRDP decodes the bitmap incorrectly when update.width and update_rectangle.width() differ

AI disclosure: AI was used to investigate and fix this.


process_bitmap_update decodes each bitmap at update.width/update.height, but DecodedImage::apply_*_bitmap re-chunk the buffer at update_rectangle.width(). When a server pads the header width beyond the rectangle (spec-legal; xrdp always pads to a multiple of 4 pixels, e = (4 - width) & 3), each row is offset by the padding → diagonal shear. Flat fills look fine; text/images/the xrdp login dialog hatch. Affects 0.9.0 and master, all depths, both compressed and raw paths (apply_rgb16/15_bitmap, apply_bgr24_bitmap, apply_rgb32_bitmap, apply_rgb8_with_palette, apply_rgb24).

Fix — treat the header width as source stride; crop each row to rectangle.width() and the row count to rectangle.height() before applying (as FreeRDP/mstsc do). Patch leaves apply_* untouched — feeding them a tightly-packed rectangle-sized buffer makes their existing chunks_exact(rectangle_width * …) correct.

Copilot AI review requested due to automatic review settings July 9, 2026 19:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a bitmap rendering corruption (diagonal "shear") in ironrdp-session that occurs when an RDP server pads TS_BITMAP_DATA.width beyond the destination rectangle width (spec-legal; xrdp pads to a multiple of 4 pixels). Previously, process_bitmap_update decoded each bitmap at update.width stride, but the DecodedImage::apply_* functions re-chunk the source buffer at update_rectangle.width(), so any width/row padding offset every subsequent row. The fix introduces a repack_bitmap_to_rectangle helper that crops each row to the rectangle width (and the row count to the rectangle height) before the buffer reaches the apply_* functions, covering both the compressed (RLE + RDP6) and uncompressed paths that were previously affected.

Changes:

  • Added repack_bitmap_to_rectangle, an overflow-guarded helper that re-packs padded pixel data to a tightly-packed, rectangle-sized buffer (returns None when no re-packing is needed).
  • Applied cropping via two closures (crop_tight for compressed streams, crop_padded for uncompressed rows padded to 4-byte boundaries) across all depths and both compressed/raw code paths.
  • Consolidated the previous uncompressed padded/unpadded branches into a single match now fed the cropped buffer.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

) {
Ok(()) => image.apply_rgb24(&buf, &update.rectangle, true)?,
Ok(()) => {
let c = crop_tight(&buf, 3);
Marc-André Moreau (mamoreau-devolutions) added a commit that referenced this pull request Jul 30, 2026
## Summary

- Preserve `TS_BITMAP_DATA` source stride independently of destination
bounds for raw, Interleaved RLE, and RDP6 bitmap updates.
- Remove raw 4-byte scanline padding without collapsing padded source
columns into following rows.
- Crop only the source extent beyond the destination and reject empty
dimensions explicitly; do not add framebuffer bounds suppression.
- Render decoded RDP6 RGB data top-down and retain bottom-up rendering
for raw and RLE data.

## Why this supersedes the overlapping proposals

- #1252 identifies both dimensions but applies a clipped stride, which
still misaddresses padded source rows; its broad framebuffer-clipping
changes are intentionally excluded.
- #1398 correctly identifies the RDP6 stride/orientation issue, but
leaves raw and RLE paths unresolved.
- #1408 includes the related stride issue but also mixes unrelated Win7
activation and web changes; its allocation/inference normalization is
unnecessary once source stride is explicit.
- #1436 correctly crops padded rows but repacks each path into temporary
buffers; this PR preserves the source stride directly and covers all
bitmap codecs.

## Specification basis

- MS-RDPBCGR 2.2.9.1.1.3.1.2.2 (`TS_BITMAP_DATA`): separate destination
bounds, dimensions, and bottom-up raw rows with 4-byte row padding.
- MS-RDPBCGR 2.2.9.1.1.3.1.2.3 and 2.2.9.1.1.3.1.2.4: compressed bitmap
header and Interleaved RLE stream.
- MS-RDPEGDI 2.2.2.5.1: RDP 6.0 bitmap stream.

## Validation

- `cargo test -p ironrdp-session --lib`
- `cargo check -p ironrdp-session --all-features`
- `cargo clippy -p ironrdp-session --all-targets -- -D warnings`
- `cargo xtask check fmt -v`

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@mamoreau-devolutions

Copy link
Copy Markdown
Contributor

superseded by #1486

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants