feat: add QOIZ image codec

Add a new QOIZ codec (UUID 229cc6dc-a860-4b52-b4d8-053a22b3892b) for
SetSurface command. The PDU data contains the same data as the QOI
codec, with zstd compression.

Some benchmarks showing interesting results (using ironrdp/perfenc)

QOI: 10s user CPU, 96.20% compression
QOIZ: 11s user CPU, 99.76% compression

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
This commit is contained in:
Marc-André Lureau
2025-07-24 06:53:10 -04:00
committed by Benoît Cortier
parent 613fd51f26
commit 87df67fdc7
15 changed files with 243 additions and 35 deletions
Generated
+21
View File
@@ -2723,6 +2723,7 @@ dependencies = [
"tracing",
"visibility",
"x509-cert",
"zstd-safe",
]
[[package]]
@@ -2739,6 +2740,7 @@ dependencies = [
"ironrdp-svc",
"qoicoubeh",
"tracing",
"zstd-safe",
]
[[package]]
@@ -6971,3 +6973,22 @@ dependencies = [
"quote",
"syn",
]
[[package]]
name = "zstd-safe"
version = "7.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.15+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
dependencies = [
"cc",
"pkg-config",
]
+2 -1
View File
@@ -10,8 +10,9 @@ name = "perfenc"
path = "src/perfenc.rs"
[features]
default = ["qoi"]
default = ["qoi", "qoiz"]
qoi = ["ironrdp/qoi"]
qoiz = ["ironrdp/qoiz"]
[dependencies]
anyhow = "1.0.98"
+7 -1
View File
@@ -28,7 +28,7 @@ async fn main() -> Result<(), anyhow::Error> {
println!(" --width <WIDTH> Width of the display (default: 3840)");
println!(" --height <HEIGHT> Height of the display (default: 2400)");
println!(" --codec <CODEC> Codec to use (default: remotefx)");
println!(" Valid values: qoi, remotefx, bitmap, none");
println!(" Valid values: qoi, qoiz, remotefx, bitmap, none");
println!(" --fps <FPS> Frames per second (default: none)");
std::process::exit(0);
}
@@ -54,6 +54,8 @@ async fn main() -> Result<(), anyhow::Error> {
OptCodec::None => {}
#[cfg(feature = "qoi")]
OptCodec::Qoi => update_codecs.set_qoi(Some(0)),
#[cfg(feature = "qoiz")]
OptCodec::QoiZ => update_codecs.set_qoiz(Some(0)),
};
let mut encoder = UpdateEncoder::new(DesktopSize { width, height }, flags, update_codecs);
@@ -176,6 +178,8 @@ enum OptCodec {
None,
#[cfg(feature = "qoi")]
Qoi,
#[cfg(feature = "qoiz")]
QoiZ,
}
impl Default for OptCodec {
@@ -194,6 +198,8 @@ impl core::str::FromStr for OptCodec {
"none" => Ok(Self::None),
#[cfg(feature = "qoi")]
"qoi" => Ok(Self::Qoi),
#[cfg(feature = "qoiz")]
"qoiz" => Ok(Self::QoiZ),
_ => Err(anyhow::anyhow!("unknown codec: {}", s)),
}
}
+1
View File
@@ -28,6 +28,7 @@ default = ["rustls"]
rustls = ["ironrdp-tls/rustls", "tokio-tungstenite/rustls-tls-native-roots"]
native-tls = ["ironrdp-tls/native-tls", "tokio-tungstenite/native-tls"]
qoi = ["ironrdp/qoi"]
qoiz = ["ironrdp/qoiz"]
[dependencies]
# Protocols
+1
View File
@@ -19,6 +19,7 @@ test = false
default = []
arbitrary = ["dep:arbitrary"]
qoi = ["ironrdp-pdu/qoi"]
qoiz = ["ironrdp-pdu/qoiz"]
[dependencies]
ironrdp-svc = { path = "../ironrdp-svc", version = "0.4" } # public
+1
View File
@@ -20,6 +20,7 @@ default = []
std = ["alloc", "ironrdp-error/std", "ironrdp-core/std"]
alloc = ["ironrdp-core/alloc", "ironrdp-error/alloc"]
qoi = []
qoiz = ["qoi"]
[dependencies]
bitflags = "2.9"
@@ -34,7 +34,7 @@ pub use self::bitmap_cache::{
pub use self::bitmap_codecs::{
client_codecs_capabilities, server_codecs_capabilities, BitmapCodecs, CaptureFlags, Codec, CodecId, CodecProperty,
EntropyBits, Guid, NsCodec, RemoteFxContainer, RfxCaps, RfxCapset, RfxClientCapsContainer, RfxICap, RfxICapFlags,
CODEC_ID_NONE, CODEC_ID_QOI, CODEC_ID_REMOTEFX,
CODEC_ID_NONE, CODEC_ID_QOI, CODEC_ID_QOIZ, CODEC_ID_REMOTEFX,
};
pub use self::brush::{Brush, SupportLevel};
pub use self::frame_acknowledge::FrameAcknowledge;
@@ -43,6 +43,9 @@ const GUID_IGNORE: Guid = Guid(0x9c43_51a6, 0x3535, 0x42ae, 0x91, 0x0c, 0xcd, 0x
#[rustfmt::skip]
#[cfg(feature="qoi")]
const GUID_QOI: Guid = Guid(0x4dae_9af8, 0xb399, 0x4df6, 0xb4, 0x3a, 0x66, 0x2f, 0xd9, 0xc0, 0xf5, 0xd6);
#[rustfmt::skip]
#[cfg(feature="qoiz")]
const GUID_QOIZ: Guid = Guid(0x229c_c6dc, 0xa860, 0x4b52, 0xb4, 0xd8, 0x05, 0x3a, 0x22, 0xb3, 0x89, 0x2b);
#[derive(Debug, PartialEq, Eq)]
pub struct Guid(u32, u16, u16, u8, u8, u8, u8, u8, u8, u8, u8);
@@ -172,6 +175,8 @@ impl Encode for Codec {
CodecProperty::Ignore => GUID_IGNORE,
#[cfg(feature = "qoi")]
CodecProperty::Qoi => GUID_QOI,
#[cfg(feature = "qoiz")]
CodecProperty::QoiZ => GUID_QOIZ,
_ => return Err(other_err!("invalid codec")),
};
guid.encode(dst)?;
@@ -211,6 +216,8 @@ impl Encode for Codec {
}
#[cfg(feature = "qoi")]
CodecProperty::Qoi => dst.write_u16(0),
#[cfg(feature = "qoiz")]
CodecProperty::QoiZ => dst.write_u16(0),
CodecProperty::Ignore => dst.write_u16(0),
CodecProperty::None => dst.write_u16(0),
};
@@ -236,6 +243,8 @@ impl Encode for Codec {
},
#[cfg(feature = "qoi")]
CodecProperty::Qoi => 0,
#[cfg(feature = "qoiz")]
CodecProperty::QoiZ => 0,
CodecProperty::Ignore => 0,
CodecProperty::None => 0,
}
@@ -280,6 +289,13 @@ impl<'de> Decode<'de> for Codec {
}
CodecProperty::Qoi
}
#[cfg(feature = "qoiz")]
GUID_QOIZ => {
if !property_buffer.is_empty() {
return Err(invalid_field_err!("qoi property", "must be empty"));
}
CodecProperty::QoiZ
}
_ => CodecProperty::None,
};
@@ -301,6 +317,8 @@ pub enum CodecProperty {
Ignore,
#[cfg(feature = "qoi")]
Qoi,
#[cfg(feature = "qoiz")]
QoiZ,
None,
}
@@ -639,6 +657,7 @@ pub struct CodecId(u8);
pub const CODEC_ID_NONE: CodecId = CodecId(0);
pub const CODEC_ID_REMOTEFX: CodecId = CodecId(3);
pub const CODEC_ID_QOI: CodecId = CodecId(0x0A);
pub const CODEC_ID_QOIZ: CodecId = CodecId(0x0B);
impl Debug for CodecId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -646,6 +665,7 @@ impl Debug for CodecId {
0 => "None",
3 => "RemoteFx",
0x0A => "QOI",
0x0B => "QOIZ",
_ => "unknown",
};
write!(f, "CodecId({name})")
@@ -658,6 +678,7 @@ impl CodecId {
0 => Some(CODEC_ID_NONE),
3 => Some(CODEC_ID_REMOTEFX),
0x0A => Some(CODEC_ID_QOI),
0x0B => Some(CODEC_ID_QOIZ),
_ => None,
}
}
@@ -700,6 +721,7 @@ fn parse_codecs_config<'a>(codecs: &'a [&'a str]) -> Result<HashMap<&'a str, boo
///
/// * `remotefx` (on by default)
/// * `qoi` (on by default, when feature "qoi")
/// * `qoiz` (on by default, when feature "qoiz")
///
/// # Returns
///
@@ -711,6 +733,7 @@ pub fn client_codecs_capabilities(config: &[&str]) -> Result<BitmapCodecs, Strin
List of codecs:
- `remotefx` (on by default)
- `qoi` (on by default, when feature "qoi")
- `qoiz` (on by default, when feature "qoiz")
"#
.to_owned());
}
@@ -739,6 +762,14 @@ List of codecs:
});
}
#[cfg(feature = "qoiz")]
if config.remove("qoiz").unwrap_or(true) {
codecs.push(Codec {
id: CODEC_ID_QOIZ.0,
property: CodecProperty::QoiZ,
});
}
let codec_names = config.keys().copied().collect::<Vec<_>>().join(", ");
if !codec_names.is_empty() {
return Err(format!("Unknown codecs: {codec_names}"));
@@ -760,6 +791,7 @@ List of codecs:
///
/// * `remotefx` (on by default)
/// * `qoi` (on by default, when feature "qoi")
/// * `qoiz` (on by default, when feature "qoiz")
///
/// # Returns
///
@@ -771,6 +803,7 @@ pub fn server_codecs_capabilities(config: &[&str]) -> Result<BitmapCodecs, Strin
List of codecs:
- `remotefx` (on by default)
- `qoi` (on by default, when feature "qoi")
- `qoiz` (on by default, when feature "qoiz")
"#
.to_owned());
}
@@ -797,6 +830,14 @@ List of codecs:
});
}
#[cfg(feature = "qoiz")]
if config.remove("qoiz").unwrap_or(true) {
codecs.push(Codec {
id: 0,
property: CodecProperty::QoiZ,
});
}
let codec_names = config.keys().copied().collect::<Vec<_>>().join(", ");
if !codec_names.is_empty() {
return Err(format!("Unknown codecs: {codec_names}"));
+3 -1
View File
@@ -16,10 +16,11 @@ doctest = true
test = false
[features]
default = ["rayon", "qoi"]
default = ["rayon", "qoi", "qoiz"]
helper = ["dep:x509-cert", "dep:rustls-pemfile"]
rayon = ["dep:rayon"]
qoi = ["dep:qoicoubeh", "ironrdp-pdu/qoi"]
qoiz = ["dep:zstd-safe", "qoi", "ironrdp-pdu/qoiz"]
# Internal (PRIVATE!) features used to aid testing.
# Don't rely on these whatsoever. They may disappear at any time.
@@ -49,6 +50,7 @@ rayon = { version = "1.10.0", optional = true }
bytes = "1"
visibility = { version = "0.1", optional = true }
qoicoubeh = { version = "0.5", optional = true }
zstd-safe = { version = "7.2", optional = true }
[dev-dependencies]
tokio = { version = "1", features = ["sync"] }
+95 -19
View File
@@ -1,7 +1,8 @@
use core::fmt;
use core::num::NonZeroU16;
use std::sync::{Arc, Mutex};
use anyhow::{Context, Result};
use anyhow::{anyhow, Context, Result};
use ironrdp_acceptor::DesktopSize;
use ironrdp_graphics::diff::{find_different_rects_sub, Rect};
use ironrdp_pdu::encode_vec;
@@ -35,6 +36,8 @@ pub(crate) struct UpdateEncoderCodecs {
remotefx: Option<(EntropyBits, u8)>,
#[cfg(feature = "qoi")]
qoi: Option<u8>,
#[cfg(feature = "qoiz")]
qoiz: Option<u8>,
}
impl UpdateEncoderCodecs {
@@ -44,6 +47,8 @@ impl UpdateEncoderCodecs {
remotefx: None,
#[cfg(feature = "qoi")]
qoi: None,
#[cfg(feature = "qoiz")]
qoiz: None,
}
}
@@ -57,6 +62,12 @@ impl UpdateEncoderCodecs {
pub(crate) fn set_qoi(&mut self, qoi: Option<u8>) {
self.qoi = qoi
}
#[cfg(feature = "qoiz")]
#[cfg_attr(feature = "__bench", visibility::make(pub))]
pub(crate) fn set_qoiz(&mut self, qoiz: Option<u8>) {
self.qoiz = qoiz
}
}
impl Default for UpdateEncoderCodecs {
@@ -94,6 +105,10 @@ impl UpdateEncoder {
if let Some(id) = codecs.qoi {
bitmap = BitmapUpdater::Qoi(QoiHandler::new(id));
}
#[cfg(feature = "qoiz")]
if let Some(id) = codecs.qoiz {
bitmap = BitmapUpdater::Qoiz(QoizHandler::new(id));
}
bitmap
} else {
@@ -306,6 +321,8 @@ enum BitmapUpdater {
RemoteFx(RemoteFxHandler),
#[cfg(feature = "qoi")]
Qoi(QoiHandler),
#[cfg(feature = "qoiz")]
Qoiz(QoizHandler),
}
impl BitmapUpdater {
@@ -316,6 +333,8 @@ impl BitmapUpdater {
Self::RemoteFx(up) => up.handle(bitmap),
#[cfg(feature = "qoi")]
Self::Qoi(up) => up.handle(bitmap),
#[cfg(feature = "qoiz")]
Self::Qoiz(up) => up.handle(bitmap),
}
}
@@ -445,28 +464,85 @@ impl QoiHandler {
#[cfg(feature = "qoi")]
impl BitmapUpdateHandler for QoiHandler {
fn handle(&mut self, bitmap: &BitmapUpdate) -> Result<UpdateFragmenter> {
use ironrdp_graphics::image_processing::PixelFormat::*;
let raw_channels = match bitmap.format {
ARgb32 => qoi::RawChannels::Argb,
XRgb32 => qoi::RawChannels::Xrgb,
ABgr32 => qoi::RawChannels::Abgr,
XBgr32 => qoi::RawChannels::Xbgr,
BgrA32 => qoi::RawChannels::Bgra,
BgrX32 => qoi::RawChannels::Bgrx,
RgbA32 => qoi::RawChannels::Rgba,
RgbX32 => qoi::RawChannels::Rgbx,
};
let enc = qoi::EncoderBuilder::new(&bitmap.data, bitmap.width.get().into(), bitmap.height.get().into())
.stride(bitmap.stride)
.raw_channels(raw_channels)
.build()?;
let data = enc.encode_to_vec()?;
let data = qoi_encode(bitmap)?;
set_surface(bitmap, self.codec_id, &data)
}
}
#[cfg(feature = "qoiz")]
#[derive(Clone)]
struct QoizHandler {
codec_id: u8,
zctxt: Arc<Mutex<zstd_safe::CCtx<'static>>>,
}
#[cfg(feature = "qoiz")]
impl fmt::Debug for QoizHandler {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("QoizHandler").field("codec_id", &self.codec_id).finish()
}
}
#[cfg(feature = "qoiz")]
impl QoizHandler {
fn new(codec_id: u8) -> Self {
let mut zctxt = zstd_safe::CCtx::default();
zctxt.set_parameter(zstd_safe::CParameter::CompressionLevel(3)).unwrap();
zctxt
.set_parameter(zstd_safe::CParameter::EnableLongDistanceMatching(true))
.unwrap();
let zctxt = Arc::new(Mutex::new(zctxt));
Self { codec_id, zctxt }
}
}
#[cfg(feature = "qoiz")]
impl BitmapUpdateHandler for QoizHandler {
fn handle(&mut self, bitmap: &BitmapUpdate) -> Result<UpdateFragmenter> {
let qoi = qoi_encode(bitmap)?;
let mut inb = zstd_safe::InBuffer::around(&qoi);
let mut data = vec![0; qoi.len()];
let mut outb = zstd_safe::OutBuffer::around(data.as_mut_slice());
let mut zctxt = self.zctxt.lock().unwrap();
let res = zctxt
.compress_stream2(
&mut outb,
&mut inb,
zstd_safe::zstd_sys::ZSTD_EndDirective::ZSTD_e_flush,
)
.map_err(zstd_safe::get_error_name)
.unwrap();
if res != 0 {
return Err(anyhow!("Failed to zstd compress"));
}
set_surface(bitmap, self.codec_id, outb.as_slice())
}
}
#[cfg(feature = "qoi")]
fn qoi_encode(bitmap: &BitmapUpdate) -> Result<Vec<u8>> {
use ironrdp_graphics::image_processing::PixelFormat::*;
let raw_channels = match bitmap.format {
ARgb32 => qoi::RawChannels::Argb,
XRgb32 => qoi::RawChannels::Xrgb,
ABgr32 => qoi::RawChannels::Abgr,
XBgr32 => qoi::RawChannels::Xbgr,
BgrA32 => qoi::RawChannels::Bgra,
BgrX32 => qoi::RawChannels::Bgrx,
RgbA32 => qoi::RawChannels::Rgba,
RgbX32 => qoi::RawChannels::Rgbx,
};
let enc = qoi::EncoderBuilder::new(&bitmap.data, bitmap.width.get().into(), bitmap.height.get().into())
.stride(bitmap.stride)
.raw_channels(raw_channels)
.build()?;
Ok(enc.encode_to_vec()?)
}
fn set_surface(bitmap: &BitmapUpdate, codec_id: u8, data: &[u8]) -> Result<UpdateFragmenter> {
let destination = ExclusiveRectangle {
left: bitmap.x,
+12
View File
@@ -63,6 +63,14 @@ impl RdpServerOptions {
.iter()
.any(|codec| matches!(codec.property, CodecProperty::Qoi))
}
#[cfg(feature = "qoiz")]
fn has_qoiz(&self) -> bool {
self.codecs
.0
.iter()
.any(|codec| matches!(codec.property, CodecProperty::QoiZ))
}
}
#[derive(Clone)]
@@ -754,6 +762,10 @@ impl RdpServer {
CodecProperty::Qoi if self.opts.has_qoi() => {
update_codecs.set_qoi(Some(codec.id));
}
#[cfg(feature = "qoiz")]
CodecProperty::QoiZ if self.opts.has_qoiz() => {
update_codecs.set_qoiz(Some(codec.id));
}
_ => (),
}
}
+2
View File
@@ -18,6 +18,7 @@ test = false
[features]
default = []
qoi = ["dep:qoicoubeh", "ironrdp-pdu/qoi"]
qoiz = ["dep:zstd-safe", "qoi"]
[dependencies]
ironrdp-core = { path = "../ironrdp-core", version = "0.1" } # public
@@ -30,6 +31,7 @@ ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.5", features = ["std"] } #
ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.3" }
tracing = { version = "0.1", features = ["log"] }
qoicoubeh = { version = "0.5", optional = true }
zstd-safe = { version = "7.2", optional = true, features = ["std"] }
[lints]
workspace = true
+54 -12
View File
@@ -37,6 +37,8 @@ pub struct Processor {
mouse_pos_update: Option<(u16, u16)>,
no_server_pointer: bool,
pointer_software_rendering: bool,
#[cfg(feature = "qoiz")]
zdctx: zstd_safe::DCtx<'static>,
}
impl Processor {
@@ -363,20 +365,34 @@ impl Processor {
}
#[cfg(feature = "qoi")]
ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOI => {
let (header, decoded) = qoi::decode_to_vec(bits.extended_bitmap_data.data)
.map_err(|e| reason_err!("QOI decode", "{}", e))?;
match header.channels {
qoi::Channels::Rgb => {
let rectangle = image.apply_rgb24(&decoded, &destination, false)?;
update_rectangle = update_rectangle
.map(|rect: InclusiveRectangle| rect.union(&rectangle))
.or(Some(rectangle));
}
qoi::Channels::Rgba => {
warn!("Unsupported RGBA QOI data");
qoi_apply(
image,
destination,
bits.extended_bitmap_data.data,
&mut update_rectangle,
)?;
}
#[cfg(feature = "qoiz")]
ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOIZ => {
let compressed = &bits.extended_bitmap_data.data;
let mut input = zstd_safe::InBuffer::around(compressed);
let mut data = vec![0; compressed.len() * 4];
let mut pos = 0;
loop {
let mut output = zstd_safe::OutBuffer::around_pos(data.as_mut_slice(), pos);
self.zdctx
.decompress_stream(&mut output, &mut input)
.map_err(zstd_safe::get_error_name)
.map_err(|e| reason_err!("zstd", "{}", e))?;
pos = output.pos();
if pos == output.capacity() {
data.resize(data.capacity() * 2, 0);
} else {
break;
}
}
qoi_apply(image, destination, &data, &mut update_rectangle)?;
}
_ => {
warn!("Unsupported codec ID: {}", bits.extended_bitmap_data.codec_id);
@@ -398,6 +414,30 @@ impl Processor {
}
}
#[cfg(feature = "qoi")]
fn qoi_apply(
image: &mut DecodedImage,
destination: InclusiveRectangle,
data: &[u8],
update_rectangle: &mut Option<InclusiveRectangle>,
) -> SessionResult<()> {
let (header, decoded) = qoi::decode_to_vec(data).map_err(|e| reason_err!("QOI decode", "{}", e))?;
match header.channels {
qoi::Channels::Rgb => {
let rectangle = image.apply_rgb24(&decoded, &destination, false)?;
*update_rectangle = update_rectangle
.as_ref()
.map(|rect: &InclusiveRectangle| rect.union(&rectangle))
.or(Some(rectangle));
}
qoi::Channels::Rgba => {
warn!("Unsupported RGBA QOI data");
}
}
Ok(())
}
pub struct ProcessorBuilder {
pub io_channel_id: u16,
pub user_channel_id: u16,
@@ -421,6 +461,8 @@ impl ProcessorBuilder {
mouse_pos_update: None,
no_server_pointer: self.no_server_pointer,
pointer_software_rendering: self.pointer_software_rendering,
#[cfg(feature = "qoiz")]
zdctx: zstd_safe::DCtx::default(),
}
}
}
+1
View File
@@ -21,6 +21,7 @@ crate-type = ["cdylib", "rlib"]
default = ["panic_hook"]
panic_hook = ["iron-remote-desktop/panic_hook"]
qoi = ["ironrdp/qoi"]
qoiz = ["ironrdp/qoiz"]
[dependencies]
# Protocols
+1
View File
@@ -32,6 +32,7 @@ rdpdr = ["dep:ironrdp-rdpdr"]
rdpsnd = ["dep:ironrdp-rdpsnd"]
displaycontrol = ["dep:ironrdp-displaycontrol"]
qoi = ["ironrdp-server?/qoi", "ironrdp-pdu?/qoi", "ironrdp-connector?/qoi", "ironrdp-session?/qoi"]
qoiz = ["ironrdp-server?/qoiz", "ironrdp-pdu?/qoiz", "ironrdp-connector?/qoiz", "ironrdp-session?/qoiz"]
# Internal (PRIVATE!) features used to aid testing.
# Don't rely on these whatsoever. They may disappear at any time.
__bench = ["ironrdp-server/__bench"]