Skip to content

feat(egfx): decode Planar bitmaps in the client - #1507

Merged
Benoît Cortier (CBenoit) merged 1 commit into
Devolutions:masterfrom
lamco-admin:feat/egfx-planar-decode
Jul 31, 2026
Merged

Benoît Cortier (CBenoit) merged 1 commit into
Devolutions:masterfrom
lamco-admin:feat/egfx-planar-decode

Conversation

@glamberson

Copy link
Copy Markdown
Contributor

What

Wires RDPGFX_CODECID_PLANAR (0x000A) into the EGFX client's WireToSurface1 dispatch, alongside the existing ClearCodec and Uncompressed paths.

Why

Planar reached the dispatch and fell through to on_unhandled_pdu, so a server sending Planar frames got nothing drawn. The decoder already existed: [MS-RDPEGFX] 2.2.2.1 defines the payload for this codec ID as the Planar Codec from [MS-RDPEGDI] 2.2.2.5.1 and 3.1.9, which is the RDP6_BITMAP_STREAM that ironrdp-graphics decodes and that ironrdp-session already uses for the fast-path bitmap route.

This is the same decode-core-exists-but-is-not-wired situation ClearCodec was in before #1175. #1498 makes it reachable in-tree: send_planar_frame means an IronRDP server can now send frames an IronRDP client would discard.

ironrdp-egfx already depends on ironrdp-graphics and already pulls ClearCodecDecoder from it, so this reuses rdp6::BitmapStreamDecoder rather than adding a second implementation.

Three spec details that shape the decode

Dimensions come from destRect. [MS-RDPEGFX] 2.2.2.1 says the field carries both the target point and "the dimensions (width and height) of the bitmap data encapsulated in the bitmapData field", and that it "specifies a bounding rectangle if the codecId field contains the RDPGFX_CODECID_AVC420 (0x000B), RDPGFX_CODECID_AVC444 (0x000E), or the RDPGFX_CODECID_AVC444V2 (0x000F) identifier". Planar is not in that list, so the rectangle is exact and its extent is what the decoder is given.

Pixels are opaque, deliberately. An RDP6_BITMAP_STREAM can carry an alpha plane: [MS-RDPEGDI] 2.2.2.5.1 defines the NA bit as "Indicates if an alpha plane is present. If NA is equal to 1, there is no alpha plane... If NA is equal to 0, the alpha plane is sent as the first color plane." EGFX does not use that channel for opacity. It carries per-pixel opacity in a separate RDPGFX_CODECID_ALPHA (0x000C) command holding an ALPHACODEC_BITMAP_STREAM ([MS-RDPEGFX] 2.2.4.3), whose stated purpose is to specify "the opacity of each pixel in the encoded bitmap". So a Planar command contributes color only, and the decoder's RGB24 output is widened with a synthesized opaque alpha. That reasoning is in a comment at the conversion site so it does not read like an oversight.

No vertical flip. The RDP6 decoder emits top-down row-major, which is how surfaces are stored, so the rows are used as produced.

I also checked whether Planar needs capability negotiation before a server may send it. It does not: the only mentions of the codec in [MS-RDPEGFX] are the codec ID definition itself, unlike the AVC codecs which are capability-gated.

Tests

The round-trip test builds its payload with IronRDP's own RDP6 encoder and drives a real WireToSurface1 through the client, so it exercises the actual stream framing rather than a hand-built approximation. It covers both the RLE and raw plane forms, uses distinct per-channel values so a channel swap or row reversal fails, and asserts the update is not routed to on_unhandled_pdu. It fails against the previous code.

There is also a unit test for the RGB24 to RGBA widening.

Scope

No public API change; cargo semver-checks reports no version bump required. Related to #1464.

Verification

cargo xtask check fmt/lints/tests/typos/locks all pass.

RDPGFX_CODECID_PLANAR (0x000A) reached the client's WireToSurface1 dispatch
and fell through to on_unhandled_pdu, so a server sending Planar frames got
nothing drawn. The decoder for it already existed: [MS-RDPEGFX] 2.2.2.1
defines the payload as the Planar Codec from [MS-RDPEGDI] 2.2.2.5.1 and
3.1.9, which is the RDP6_BITMAP_STREAM that ironrdp-graphics decodes and that
ironrdp-session already uses for the fast-path bitmap route.

Wire it up alongside ClearCodec. ironrdp-egfx already depends on
ironrdp-graphics, so this reuses rdp6::BitmapStreamDecoder rather than adding
a second implementation.

Three details from the spec that shape the decode:

Dimensions come from destRect. [MS-RDPEGFX] 2.2.2.1 says the field carries
both the target point and "the dimensions (width and height) of the bitmap
data", and is a bounding rectangle only for AVC420, AVC444 and AVC444V2, so
for Planar the rectangle is exact.

Pixels are opaque. An RDP6_BITMAP_STREAM can carry an alpha plane when the
FormatHeader NA bit is clear, but EGFX conveys per-pixel opacity in a separate
RDPGFX_CODECID_ALPHA (0x000C) command holding an ALPHACODEC_BITMAP_STREAM
([MS-RDPEGFX] 2.2.4.3). A Planar command therefore contributes color only, so
the decoder's RGB24 output is widened with a synthesized opaque alpha.

No vertical flip. The decoder emits top-down row-major, matching how surfaces
are stored.

The test encodes with IronRDP's own RDP6 encoder and drives a real
WireToSurface1 through the client, covering both the RLE and raw plane forms,
so it exercises the actual stream framing rather than a hand-built
approximation. It fails without the dispatch arm.

@CBenoit Benoît Cortier (CBenoit) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@CBenoit
Benoît Cortier (CBenoit) merged commit 66c8a81 into Devolutions:master Jul 31, 2026
24 of 25 checks passed

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

Adds Planar bitmap decoding to the EGFX client.

Changes:

  • Dispatches Planar WireToSurface1 PDUs to the RDP6 decoder.
  • Converts decoded RGB24 pixels to opaque RGBA.
  • Adds raw/RLE decoding tests.

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

height: dest_height,
};

self.handler.on_bitmap_updated(&update);
///
/// ClearCodec produces BGRA output per [MS-RDPEGFX 2.2.4.1]. Reorder to
/// [R, G, B, A] for the uniform `BitmapUpdate` pixel format.
/// Widen tightly-packed RGB24 to RGBA, marking every pixel opaque.
Marc-André Moreau (mamoreau-devolutions) pushed a commit that referenced this pull request Aug 2, 2026
Wires `RDPGFX_CODECID_PLANAR` (0x000A) into the EGFX client's
`WireToSurface1` dispatch, alongside the existing ClearCodec and
Uncompressed paths.
truebest pushed a commit to truebest/IronRDP that referenced this pull request Aug 16, 2026
Wires `RDPGFX_CODECID_PLANAR` (0x000A) into the EGFX client's
`WireToSurface1` dispatch, alongside the existing ClearCodec and
Uncompressed paths.

(cherry picked from commit 66c8a81)
@glamberson
Greg Lamberson (glamberson) deleted the feat/egfx-planar-decode branch August 30, 2026 15:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Size: up to 449 counted lines and 10 files; exceeds S in either measure

Development

Successfully merging this pull request may close these issues.

3 participants