Skip to content

feat(egfx): wire RemoteFX Progressive decode into WireToSurface2 dispatch#1443

Open
truebest wants to merge 3 commits into
Devolutions:masterfrom
truebest:feat/egfx-progressive-client-decode
Open

feat(egfx): wire RemoteFX Progressive decode into WireToSurface2 dispatch#1443
truebest wants to merge 3 commits into
Devolutions:masterfrom
truebest:feat/egfx-progressive-client-decode

Conversation

@truebest

Copy link
Copy Markdown

Problem

GraphicsPipelineClient::handle_pdu currently only forwards WireToSurface2 (the RemoteFX Progressive codec) to handler.on_wire_to_surface2() and returns, without decoding it. This differs from the AVC420 path (decode_avc420) and from ClearCodec's WireToSurface1 dispatch (#1175), both of which decode into BitmapUpdate callbacks through on_bitmap_updated.

The ProgressiveDecoder itself already exists (landed via #1197) — this PR just wires it into the client dispatch, the way #1175 did for ClearCodec.

Discussed with Greg Lamberson (@glamberson) on #1158 first to avoid duplicating in-flight Lamco Development work; the client-side dispatch turned out to have fallen off their TODO after #1197 landed, so this fills that gap.

Changes

  • GraphicsPipelineClient gains a progressive_decoder: ProgressiveDecoder field.
  • WireToSurface2 now decodes through it and emits each updated 64x64 tile as a BitmapUpdate via the existing on_bitmap_updated path.
  • A decode failure now propagates as a terminal PduResult error instead of being logged and silently dropped — matching decode_avc420's behavior. Previously this would leave the session running with no further bitmap updates for the surface and no visible error.
  • ResetGraphics and DeleteEncodingContext now clear the decoder's per-context tile state, since both destroy the codec_context_id(s) scoped to them and a later reused id must not decode against stale tiles.

Testing

3 new integration tests: a malformed/first-frame-without-CONTEXT stream propagates an error; ResetGraphics clears context state (verified via a REGION-only continuation failing afterward); DeleteEncodingContext does the same for its specific context.

cargo test -p ironrdp-egfx: 17/17 passing. cargo fmt/cargo clippy --all-targets -- -D warnings: clean.

Found and fixed while running IronRDP against gnome-remote-desktop as an RDP server, which streams RemoteFX Progressive when AVC420 is unavailable.

…atch

GraphicsPipelineClient::handle_pdu currently only forwards WireToSurface2
(the progressive codec) to handler.on_wire_to_surface2() and returns,
without decoding it -- unlike AVC420 (decode_avc420) and ClearCode's
WireToSurface1 dispatch (Devolutions#1175), which both decode into BitmapUpdate
callbacks.

Add a progressive_decoder field to GraphicsPipelineClient, decode
WireToSurface2 streams through it, and emit each updated 64x64 tile as a
BitmapUpdate through the existing on_bitmap_updated path. A decode failure
now propagates as a terminal error instead of being logged and dropped,
matching decode_avc420's behavior. ResetGraphics and DeleteEncodingContext
now clear the decoder's per-context tile state, since both destroy the
codec_context_id(s) scoped to them and a later reused id must not decode
against stale tiles.
Copilot AI review requested due to automatic review settings July 13, 2026 03:38

Copilot AI left a comment

Copy link
Copy Markdown

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 wires the existing RemoteFX Progressive (WireToSurface2) decode path into GraphicsPipelineClient so progressive tiles are decoded client-side and emitted via the existing on_bitmap_updated RGBA callback path, matching the AVC420/ClearCodec dispatch model. It also ensures progressive per-context state is cleared on ResetGraphics and DeleteEncodingContext, and adds tests to verify error propagation and state clearing.

Changes:

  • Add a ProgressiveDecoder field to GraphicsPipelineClient and decode WireToSurface2 into BitmapUpdate callbacks.
  • Clear progressive decoder context state on ResetGraphics and DeleteEncodingContext.
  • Add integration tests validating decode-error propagation and context reset semantics.

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

Comment thread crates/ironrdp-egfx/src/client.rs Outdated
Comment on lines +744 to +747
let Some(surface) = self.surfaces.get(&pdu.surface_id) else {
warn!(surface_id = pdu.surface_id, "WireToSurface2 for unknown surface");
return Ok(());
};

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 8d71776 — now returns pdu_other_err!("unknown surface in WireToSurface2"), matching handle_wire_to_surface1.

Comment on lines +757 to +760
Err(e) => {
warn!(error = ?e, "RFX progressive decode failed");
return Err(pdu_other_err!("RFX progressive decode failed"));
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 8d71776 — lowercased to "rfx progressive decode failed".

Comment on lines +763 to +780
for tile in tiles {
let left = tile.x_idx.saturating_mul(64);
let top = tile.y_idx.saturating_mul(64);
let update = BitmapUpdate {
surface_id: pdu.surface_id,
destination_rectangle: ExclusiveRectangle {
left,
top,
right: left.saturating_add(64),
bottom: top.saturating_add(64),
},
codec_id: Codec1Type::Uncompressed,
data: tile.pixels,
width: 64,
height: 64,
};
self.handler.on_bitmap_updated(&update);
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 8d71776 — each tile's rectangle/width/height is now clamped to the surface bounds and the RGBA buffer cropped via the existing crop_decoded_frame helper for edge tiles.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b60ea0081

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

Comment thread crates/ironrdp-egfx/src/client.rs Outdated
Comment on lines +768 to +772
destination_rectangle: ExclusiveRectangle {
left,
top,
right: left.saturating_add(64),
bottom: top.saturating_add(64),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clip progressive edge tiles to surface bounds

For surfaces whose dimensions are not multiples of 64, the rightmost or bottommost progressive tile is still decoded as a padded 64x64 tile, but this reports the update rectangle as left + 64/top + 64. For example, a 100px-wide surface receiving tile x_idx = 1 produces right = 128, violating BitmapUpdate's “within the surface” contract and causing renderers that trust the rectangle to blit past their surface buffer or reject the update. Clip the rectangle/width/height to the surface dimensions and crop the tile data for edge tiles.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 8d71776 — same fix as the other clipping comment on this thread: rectangle/width/height clamped to surface bounds, buffer cropped via crop_decoded_frame.

…iles

- handle_wire_to_surface2 now returns a terminal error for an unknown
  surface instead of logging and silently returning Ok(()), matching
  handle_wire_to_surface1's existing behavior.
- Lowercase the progressive-decode-failure error message to match this
  file's convention.
- Clip each tile's destination_rectangle/width/height to the surface
  bounds and crop the RGBA buffer with the existing crop_decoded_frame
  helper, so a surface whose dimensions aren't a multiple of 64 no longer
  gets a BitmapUpdate claiming pixels outside the surface.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8d71776435

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

let (surface_width, surface_height) = (surface.width, surface.height);

let tiles = match self.progressive_decoder.decode_bitmap(
pdu.codec_context_id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Scope progressive contexts by surface

When a server reuses the same codec_context_id on two live surfaces, this stores both progressive streams in the same decoder context because the surface id is dropped here. WireToSurface2Pdu and DeleteEncodingContextPdu both carry surface_id, so a context is associated with a surface; with the current key, decoding the second surface can reallocate/overwrite the first surface's tile state, and deleting one surface's context removes the other. Key the progressive state by (surface_id, codec_context_id) and delete the matching surface-scoped context.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in d51fe30 — decode_bitmap()/delete_context() now also take surface_id and key contexts by (surface_id, codec_context_id). Added decoder_contexts_scoped_by_surface covering two surfaces reusing the same codec_context_id.

ProgressiveDecoder keyed its per-context tile state by codec_context_id
alone. Per MS-RDPEGFX, codec_context_id is scoped to the surface that owns
it (RDPGFX_DELETE_ENCODING_CONTEXT_PDU carries both surface_id and
codec_context_id together), so two surfaces reusing the same
codec_context_id value would collide: decoding the second surface could
overwrite the first's tile state, and deleting one surface's context would
remove the other's.

decode_bitmap() and delete_context() now also take surface_id and key
contexts by (surface_id, codec_context_id). Added
decoder_contexts_scoped_by_surface to cover it.
truebest added a commit to truebest/IronRDP that referenced this pull request Jul 13, 2026
…ntext scoping

Port the three issues found in code review of the upstream PR
(Devolutions#1443) to our own vendored copy, since this fork
already carries the same Progressive/WireToSurface2 client integration:

- handle_wire_to_surface2 now returns a terminal error for an unknown
  surface instead of silently dropping the update, matching
  handle_wire_to_surface1.
- Progressive tiles at the surface edge (dimensions not a multiple of 64,
  the common case) are now clipped to the surface bounds instead of
  claiming a BitmapUpdate rectangle that extends past it.
- ProgressiveDecoder now scopes context state by (surface_id,
  codec_context_id) instead of codec_context_id alone -- per MS-RDPEGFX,
  the id is scoped to its owning surface (RDPGFX_DELETE_ENCODING_CONTEXT_PDU
  carries both together), so two surfaces reusing the same id no longer
  collide.
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.

2 participants