Skip to content

Commit 304f21e

Browse files
committed
feat: add QOI image codec
The Quite OK Image format ([1]) losslessly compresses images to a similar size of PNG, while offering 20x-50x faster encoding and 3x-4x faster decoding. Add a new QOI codec (UUID 4dae9af8-b399-4df6-b43a-662fd9c0f5d6) for SetSurface command. The PDU data contains the QOI header (14 bytes) + data "chunks" and the end marker (8 bytes). Some benchmarks showing interesting results (using ironrdp/perfenc) Bitmap: 74s user CPU, 92.5% compression RemoteFx (lossy): 201s user CPU, 96.72% compression QOI: 10s user CPU, 96.20% compression [1]: https://qoiformat.org/ Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
1 parent 2f60ab0 commit 304f21e

File tree

18 files changed

+192
-11
lines changed

18 files changed

+192
-11
lines changed

.cargo/config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
[alias]
22
xtask = "run --package xtask --"
3+
4+
[patch.crates-io]
5+
qoi = { git = "https://github.com/elmarco/qoi-rust.git", branch = "raw" }

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ironrdp-client/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ test = false
2727
default = ["rustls"]
2828
rustls = ["ironrdp-tls/rustls"]
2929
native-tls = ["ironrdp-tls/native-tls"]
30+
qoi = ["ironrdp/qoi"]
3031

3132
[dependencies]
3233
# Protocols

crates/ironrdp-connector/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ doctest = false
1616
test = false
1717

1818
[features]
19+
default = []
1920
arbitrary = ["dep:arbitrary"]
21+
qoi = ["ironrdp-pdu/qoi"]
2022

2123
[dependencies]
2224
ironrdp-svc = { path = "../ironrdp-svc", version = "0.3" } # public

crates/ironrdp-pdu/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ doctest = false
1919
default = []
2020
std = ["alloc", "ironrdp-error/std", "ironrdp-core/std"]
2121
alloc = ["ironrdp-core/alloc", "ironrdp-error/alloc"]
22+
qoi = []
2223

2324
[dependencies]
2425
bitflags = "2.4"

crates/ironrdp-pdu/src/rdp/capability_sets.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pub use self::bitmap_cache::{
3434
pub use self::bitmap_codecs::{
3535
client_codecs_capabilities, server_codecs_capabilities, BitmapCodecs, CaptureFlags, Codec, CodecId, CodecProperty,
3636
EntropyBits, Guid, NsCodec, RemoteFxContainer, RfxCaps, RfxCapset, RfxClientCapsContainer, RfxICap, RfxICapFlags,
37-
CODEC_ID_NONE, CODEC_ID_REMOTEFX,
37+
CODEC_ID_NONE, CODEC_ID_QOI, CODEC_ID_REMOTEFX,
3838
};
3939
pub use self::brush::{Brush, SupportLevel};
4040
pub use self::frame_acknowledge::FrameAcknowledge;

crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ const GUID_REMOTEFX: Guid = Guid(0x7677_2f12, 0xbd72, 0x4463, 0xaf, 0xb3, 0xb7,
4040
const GUID_IMAGE_REMOTEFX: Guid = Guid(0x2744_ccd4, 0x9d8a, 0x4e74, 0x80, 0x3c, 0x0e, 0xcb, 0xee, 0xa1, 0x9c, 0x54);
4141
#[rustfmt::skip]
4242
const GUID_IGNORE: Guid = Guid(0x9c43_51a6, 0x3535, 0x42ae, 0x91, 0x0c, 0xcd, 0xfc, 0xe5, 0x76, 0x0b, 0x58);
43+
#[rustfmt::skip]
44+
#[cfg(feature="qoi")]
45+
const GUID_QOI: Guid = Guid(0x4dae_9af8, 0xb399, 0x4df6, 0xb4, 0x3a, 0x66, 0x2f, 0xd9, 0xc0, 0xf5, 0xd6);
4346

4447
#[derive(Debug, PartialEq, Eq)]
4548
pub struct Guid(u32, u16, u16, u8, u8, u8, u8, u8, u8, u8, u8);
@@ -167,6 +170,8 @@ impl Encode for Codec {
167170
CodecProperty::RemoteFx(_) => GUID_REMOTEFX,
168171
CodecProperty::ImageRemoteFx(_) => GUID_IMAGE_REMOTEFX,
169172
CodecProperty::Ignore => GUID_IGNORE,
173+
#[cfg(feature = "qoi")]
174+
CodecProperty::Qoi => GUID_QOI,
170175
_ => return Err(other_err!("invalid codec")),
171176
};
172177
guid.encode(dst)?;
@@ -204,6 +209,8 @@ impl Encode for Codec {
204209
}
205210
};
206211
}
212+
#[cfg(feature = "qoi")]
213+
CodecProperty::Qoi => dst.write_u16(0),
207214
CodecProperty::Ignore => dst.write_u16(0),
208215
CodecProperty::None => dst.write_u16(0),
209216
};
@@ -227,6 +234,8 @@ impl Encode for Codec {
227234
RemoteFxContainer::ClientContainer(container) => container.size(),
228235
RemoteFxContainer::ServerContainer(size) => *size,
229236
},
237+
#[cfg(feature = "qoi")]
238+
CodecProperty::Qoi => 0,
230239
CodecProperty::Ignore => 0,
231240
CodecProperty::None => 0,
232241
}
@@ -264,6 +273,13 @@ impl<'de> Decode<'de> for Codec {
264273
}
265274
}
266275
GUID_IGNORE => CodecProperty::Ignore,
276+
#[cfg(feature = "qoi")]
277+
GUID_QOI => {
278+
if !property_buffer.is_empty() {
279+
return Err(invalid_field_err!("qoi property", "must be empty"));
280+
}
281+
CodecProperty::Qoi
282+
}
267283
_ => CodecProperty::None,
268284
};
269285

@@ -283,6 +299,8 @@ pub enum CodecProperty {
283299
RemoteFx(RemoteFxContainer),
284300
ImageRemoteFx(RemoteFxContainer),
285301
Ignore,
302+
#[cfg(feature = "qoi")]
303+
Qoi,
286304
None,
287305
}
288306

@@ -620,12 +638,14 @@ pub struct CodecId(u8);
620638

621639
pub const CODEC_ID_NONE: CodecId = CodecId(0);
622640
pub const CODEC_ID_REMOTEFX: CodecId = CodecId(3);
641+
pub const CODEC_ID_QOI: CodecId = CodecId(0x0A);
623642

624643
impl Debug for CodecId {
625644
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
626645
let name = match self.0 {
627646
0 => "None",
628647
3 => "RemoteFx",
648+
0x0A => "QOI",
629649
_ => "unknown",
630650
};
631651
write!(f, "CodecId({})", name)
@@ -637,6 +657,7 @@ impl CodecId {
637657
match value {
638658
0 => Some(CODEC_ID_NONE),
639659
3 => Some(CODEC_ID_REMOTEFX),
660+
0x0A => Some(CODEC_ID_QOI),
640661
_ => None,
641662
}
642663
}
@@ -678,6 +699,7 @@ fn parse_codecs_config<'a>(codecs: &'a [&'a str]) -> Result<HashMap<&'a str, boo
678699
/// # List of codecs
679700
///
680701
/// * `remotefx` (on by default)
702+
/// * `qoi` (on by default, when feature "qoi")
681703
///
682704
/// # Returns
683705
///
@@ -688,6 +710,7 @@ pub fn client_codecs_capabilities(config: &[&str]) -> Result<BitmapCodecs, Strin
688710
return Err(r#"
689711
List of codecs:
690712
- `remotefx` (on by default)
713+
- `qoi` (on by default, when feature "qoi")
691714
"#
692715
.to_owned());
693716
}
@@ -708,6 +731,14 @@ List of codecs:
708731
});
709732
}
710733

734+
#[cfg(feature = "qoi")]
735+
if config.remove("qoi").unwrap_or(true) {
736+
codecs.push(Codec {
737+
id: CODEC_ID_QOI.0,
738+
property: CodecProperty::Qoi,
739+
});
740+
}
741+
711742
let codec_names = config.keys().copied().collect::<Vec<_>>().join(", ");
712743
if !codec_names.is_empty() {
713744
return Err(format!("Unknown codecs: {}", codec_names));
@@ -729,6 +760,7 @@ List of codecs:
729760
/// # List of codecs
730761
///
731762
/// * `remotefx` (on by default)
763+
/// * `qoi` (on by default, when feature "qoi")
732764
///
733765
/// # Returns
734766
///
@@ -738,6 +770,7 @@ pub fn server_codecs_capabilities(config: &[&str]) -> Result<BitmapCodecs, Strin
738770
return Err(r#"
739771
List of codecs:
740772
- `remotefx` (on by default)
773+
- `qoi` (on by default, when feature "qoi")
741774
"#
742775
.to_owned());
743776
}
@@ -756,6 +789,14 @@ List of codecs:
756789
});
757790
}
758791

792+
#[cfg(feature = "qoi")]
793+
if config.remove("qoi").unwrap_or(true) {
794+
codecs.push(Codec {
795+
id: 0,
796+
property: CodecProperty::Qoi,
797+
});
798+
}
799+
759800
let codec_names = config.keys().copied().collect::<Vec<_>>().join(", ");
760801
if !codec_names.is_empty() {
761802
return Err(format!("Unknown codecs: {}", codec_names));

crates/ironrdp-server/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ name = "perfenc"
1919
required-features = ["__bench"]
2020

2121
[features]
22-
default = ["rayon"]
22+
default = ["rayon", "qoi"]
2323
helper = ["dep:x509-cert", "dep:rustls-pemfile"]
2424
rayon = ["dep:rayon"]
25+
qoi = ["dep:qoi", "ironrdp-pdu/qoi"]
2526

2627
# Internal (PRIVATE!) features used to aid testing.
2728
# Don't rely on these whatsoever. They may disappear at any time.
@@ -50,6 +51,7 @@ rustls-pemfile = { version = "2.2.0", optional = true }
5051
rayon = { version = "1.10.0", optional = true }
5152
bytes = "1"
5253
visibility = { version = "0.1", optional = true }
54+
qoi = { version = "0.4", optional = true }
5355

5456
[dev-dependencies]
5557
tokio = { version = "1", features = ["sync", "fs"] }

crates/ironrdp-server/examples/perfenc.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ async fn main() -> Result<(), anyhow::Error> {
2727
println!(" --width <WIDTH> Width of the display (default: 3840)");
2828
println!(" --height <HEIGHT> Height of the display (default: 2400)");
2929
println!(" --codec <CODEC> Codec to use (default: remotefx)");
30-
println!(" Valid values: remotefx, bitmap, none");
30+
println!(" Valid values: qoi, remotefx, bitmap, none");
3131
println!(" --fps <FPS> Frames per second (default: none)");
3232
std::process::exit(0);
3333
}
@@ -51,6 +51,8 @@ async fn main() -> Result<(), anyhow::Error> {
5151
flags -= CmdFlags::SET_SURFACE_BITS;
5252
}
5353
OptCodec::None => {}
54+
#[cfg(feature = "qoi")]
55+
OptCodec::Qoi => update_codecs.set_qoi(Some(0)),
5456
};
5557

5658
let mut encoder = UpdateEncoder::new(DesktopSize { width, height }, flags, update_codecs);
@@ -171,6 +173,8 @@ enum OptCodec {
171173
RemoteFX,
172174
Bitmap,
173175
None,
176+
#[cfg(feature = "qoi")]
177+
Qoi,
174178
}
175179

176180
impl Default for OptCodec {
@@ -187,6 +191,8 @@ impl core::str::FromStr for OptCodec {
187191
"remotefx" => Ok(Self::RemoteFX),
188192
"bitmap" => Ok(Self::Bitmap),
189193
"none" => Ok(Self::None),
194+
#[cfg(feature = "qoi")]
195+
"qoi" => Ok(Self::Qoi),
190196
_ => Err(anyhow::anyhow!("unknown codec: {}", s)),
191197
}
192198
}

crates/ironrdp-server/src/encoder/mod.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,30 @@ enum CodecId {
3737
#[derive(Debug)]
3838
pub(crate) struct UpdateEncoderCodecs {
3939
remotefx: Option<(EntropyBits, u8)>,
40+
#[cfg(feature = "qoi")]
41+
qoi: Option<u8>,
4042
}
4143

4244
impl UpdateEncoderCodecs {
4345
#[cfg_attr(feature = "__bench", visibility::make(pub))]
4446
pub(crate) fn new() -> Self {
45-
Self { remotefx: None }
47+
Self {
48+
remotefx: None,
49+
#[cfg(feature = "qoi")]
50+
qoi: None,
51+
}
4652
}
4753

4854
#[cfg_attr(feature = "__bench", visibility::make(pub))]
4955
pub(crate) fn set_remotefx(&mut self, remotefx: Option<(EntropyBits, u8)>) {
5056
self.remotefx = remotefx
5157
}
58+
59+
#[cfg(feature = "qoi")]
60+
#[cfg_attr(feature = "__bench", visibility::make(pub))]
61+
pub(crate) fn set_qoi(&mut self, qoi: Option<u8>) {
62+
self.qoi = qoi
63+
}
5264
}
5365

5466
impl Default for UpdateEncoderCodecs {
@@ -87,6 +99,11 @@ impl UpdateEncoder {
8799
video = Some(bitmap.clone());
88100
}
89101

102+
#[cfg(feature = "qoi")]
103+
if let Some(id) = codecs.qoi {
104+
bitmap = BitmapUpdater::Qoi(QoiHandler::new(id));
105+
}
106+
90107
(bitmap, video)
91108
} else {
92109
(BitmapUpdater::Bitmap(BitmapHandler::new()), None)
@@ -361,6 +378,8 @@ enum BitmapUpdater {
361378
None(NoneHandler),
362379
Bitmap(BitmapHandler),
363380
RemoteFx(RemoteFxHandler),
381+
#[cfg(feature = "qoi")]
382+
Qoi(QoiHandler),
364383
}
365384

366385
impl BitmapUpdater {
@@ -369,6 +388,8 @@ impl BitmapUpdater {
369388
Self::None(up) => up.handle(bitmap),
370389
Self::Bitmap(up) => up.handle(bitmap),
371390
Self::RemoteFx(up) => up.handle(bitmap),
391+
#[cfg(feature = "qoi")]
392+
Self::Qoi(up) => up.handle(bitmap),
372393
}
373394
}
374395

@@ -482,6 +503,47 @@ impl BitmapUpdateHandler for RemoteFxHandler {
482503
}
483504
}
484505

506+
#[cfg(feature = "qoi")]
507+
#[derive(Clone, Debug)]
508+
struct QoiHandler {
509+
codec_id: u8,
510+
}
511+
512+
#[cfg(feature = "qoi")]
513+
impl QoiHandler {
514+
fn new(codec_id: u8) -> Self {
515+
Self { codec_id }
516+
}
517+
}
518+
519+
#[cfg(feature = "qoi")]
520+
impl BitmapUpdateHandler for QoiHandler {
521+
fn handle(&mut self, bitmap: &BitmapUpdate) -> Result<UpdateFragmenter> {
522+
use ironrdp_graphics::image_processing::PixelFormat::*;
523+
524+
let channels = match bitmap.format {
525+
ARgb32 => qoi::RawChannels::Argb,
526+
XRgb32 => qoi::RawChannels::Xrgb,
527+
ABgr32 => qoi::RawChannels::Abgr,
528+
XBgr32 => qoi::RawChannels::Xbgr,
529+
BgrA32 => qoi::RawChannels::Bgra,
530+
BgrX32 => qoi::RawChannels::Bgrx,
531+
RgbA32 => qoi::RawChannels::Rgba,
532+
RgbX32 => qoi::RawChannels::Rgbx,
533+
};
534+
535+
let enc = qoi::Encoder::new_raw(
536+
&bitmap.data,
537+
bitmap.width.get().into(),
538+
bitmap.height.get().into(),
539+
bitmap.stride,
540+
channels,
541+
)?;
542+
let data = enc.encode_to_vec()?;
543+
set_surface(bitmap, self.codec_id, &data)
544+
}
545+
}
546+
485547
fn set_surface(bitmap: &BitmapUpdate, codec_id: u8, data: &[u8]) -> Result<UpdateFragmenter> {
486548
let destination = ExclusiveRectangle {
487549
left: bitmap.x,

0 commit comments

Comments
 (0)