Skip to content

Commit 2afccde

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 b163a6f commit 2afccde

File tree

17 files changed

+194
-14
lines changed

17 files changed

+194
-14
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/bitmap_codecs.rs

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

4346
#[derive(Debug, PartialEq, Eq)]
4447
pub struct Guid(u32, u16, u16, u8, u8, u8, u8, u8, u8, u8, u8);
@@ -166,6 +169,8 @@ impl Encode for Codec {
166169
CodecProperty::RemoteFx(_) => GUID_REMOTEFX,
167170
CodecProperty::ImageRemoteFx(_) => GUID_IMAGE_REMOTEFX,
168171
CodecProperty::Ignore => GUID_IGNORE,
172+
#[cfg(feature = "qoi")]
173+
CodecProperty::Qoi => GUID_QOI,
169174
_ => return Err(other_err!("invalid codec")),
170175
};
171176
guid.encode(dst)?;
@@ -203,6 +208,8 @@ impl Encode for Codec {
203208
}
204209
};
205210
}
211+
#[cfg(feature = "qoi")]
212+
CodecProperty::Qoi => dst.write_u16(0),
206213
CodecProperty::Ignore => dst.write_u16(0),
207214
CodecProperty::None => dst.write_u16(0),
208215
};
@@ -226,6 +233,8 @@ impl Encode for Codec {
226233
RemoteFxContainer::ClientContainer(container) => container.size(),
227234
RemoteFxContainer::ServerContainer(size) => *size,
228235
},
236+
#[cfg(feature = "qoi")]
237+
CodecProperty::Qoi => 0,
229238
CodecProperty::Ignore => 0,
230239
CodecProperty::None => 0,
231240
}
@@ -263,6 +272,13 @@ impl<'de> Decode<'de> for Codec {
263272
}
264273
}
265274
GUID_IGNORE => CodecProperty::Ignore,
275+
#[cfg(feature = "qoi")]
276+
GUID_QOI => {
277+
if !property_buffer.is_empty() {
278+
return Err(invalid_field_err!("qoi property", "must be empty"));
279+
}
280+
CodecProperty::Qoi
281+
}
266282
_ => CodecProperty::None,
267283
};
268284

@@ -282,6 +298,8 @@ pub enum CodecProperty {
282298
RemoteFx(RemoteFxContainer),
283299
ImageRemoteFx(RemoteFxContainer),
284300
Ignore,
301+
#[cfg(feature = "qoi")]
302+
Qoi,
285303
None,
286304
}
287305

@@ -617,15 +635,19 @@ bitflags! {
617635
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
618636
#[repr(u8)]
619637
pub enum CodecId {
620-
None = 0x0,
621-
RemoteFx = 0x3,
638+
None = 0x00,
639+
RemoteFx = 0x03,
640+
#[cfg(feature = "qoi")]
641+
Qoi = 0x0A,
622642
}
623643

624644
impl CodecId {
625645
pub const fn from_u8(value: u8) -> Option<Self> {
626646
match value {
627-
0 => Some(Self::None),
628-
3 => Some(Self::RemoteFx),
647+
0x00 => Some(Self::None),
648+
0x03 => Some(Self::RemoteFx),
649+
#[cfg(feature = "qoi")]
650+
0x0A => Some(Self::Qoi),
629651
_ => None,
630652
}
631653
}
@@ -667,6 +689,7 @@ fn parse_codecs_config<'a>(codecs: &'a [&'a str]) -> Result<HashMap<&'a str, boo
667689
/// # List of codecs
668690
///
669691
/// * `remotefx` (on by default)
692+
/// * `qoi` (on by default, when feature "qoi")
670693
///
671694
/// # Returns
672695
///
@@ -677,6 +700,7 @@ pub fn client_codecs_capabilities(config: &[&str]) -> Result<BitmapCodecs, Strin
677700
return Err(r#"
678701
List of codecs:
679702
- `remotefx` (on by default)
703+
- `qoi` (on by default, when feature "qoi")
680704
"#
681705
.to_owned());
682706
}
@@ -697,6 +721,14 @@ List of codecs:
697721
});
698722
}
699723

724+
#[cfg(feature = "qoi")]
725+
if config.remove("qoi").unwrap_or(true) {
726+
codecs.push(Codec {
727+
id: CodecId::Qoi as u8,
728+
property: CodecProperty::Qoi,
729+
});
730+
}
731+
700732
let codec_names = config.keys().copied().collect::<Vec<_>>().join(", ");
701733
if !codec_names.is_empty() {
702734
return Err(format!("Unknown codecs: {}", codec_names));
@@ -718,6 +750,7 @@ List of codecs:
718750
/// # List of codecs
719751
///
720752
/// * `remotefx` (on by default)
753+
/// * `qoi` (on by default, when feature "qoi")
721754
///
722755
/// # Returns
723756
///
@@ -727,6 +760,7 @@ pub fn server_codecs_capabilities(config: &[&str]) -> Result<BitmapCodecs, Strin
727760
return Err(r#"
728761
List of codecs:
729762
- `remotefx` (on by default)
763+
- `qoi` (on by default, when feature "qoi")
730764
"#
731765
.to_owned());
732766
}
@@ -745,6 +779,14 @@ List of codecs:
745779
});
746780
}
747781

782+
#[cfg(feature = "qoi")]
783+
if config.remove("qoi").unwrap_or(true) {
784+
codecs.push(Codec {
785+
id: 0,
786+
property: CodecProperty::Qoi,
787+
});
788+
}
789+
748790
let codec_names = config.keys().copied().collect::<Vec<_>>().join(", ");
749791
if !codec_names.is_empty() {
750792
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)