Skip to content

Commit 14819d8

Browse files
pull request #15 from shikoucore/v0.1.6
fix(wayland): normalize shm pixel formats to RGBA in all capture paths
2 parents 994781a + 205efc3 commit 14819d8

6 files changed

Lines changed: 98 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.1.6] 2026-03-04
9+
10+
### Fixed
11+
12+
- **Incorrect color channels with `wl_shm::Argb8888`**: Fixed red/blue channel swap in screenshots on setups where screencopy reports `Argb8888` (e.g. AMD/Hyprland) [#14](https://github.com/shikoucore/grim-rs/issues/14) (thx [Windblows2000](https://github.com/Windblows2000)) by converting `BGRA` memory layout to crate-level `RGBA`..
13+
14+
### Changed
15+
16+
- **Unified shm format conversion**: Centralized `wl_shm``RGBA` byte conversion in a single internal helper and reused it in both single-output and multi-output capture paths to keep behavior consistent.
17+
- **Format handling parity**: Aligned default format fallback behavior between single and multi-output capture paths.
18+
819
## [0.1.5] 2026-02-06
920

1021
### Fixed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "grim-rs"
3-
version = "0.1.5"
3+
version = "0.1.6"
44
edition = "2021"
55
description = "Rust implementation of grim screenshot utility for Wayland"
66
license-file = "LICENSE"

src/wayland_capture/capture.rs

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -192,22 +192,7 @@ impl WaylandCapture {
192192
}
193193

194194
let mut buffer_data = mmap.to_vec();
195-
match format {
196-
ShmFormat::Xrgb8888 => {
197-
for chunk in buffer_data.chunks_exact_mut(4) {
198-
let b = chunk[0];
199-
let g = chunk[1];
200-
let r = chunk[2];
201-
chunk[0] = r;
202-
chunk[1] = g;
203-
chunk[2] = b;
204-
chunk[3] = 255;
205-
}
206-
}
207-
ShmFormat::Argb8888 => {}
208-
_ => {}
209-
}
210-
195+
convert_shm_to_rgba(&mut buffer_data, format);
211196
let output_id = output.id().protocol_id();
212197
let mut final_data = buffer_data;
213198
let mut final_width = width;
@@ -641,27 +626,12 @@ impl WaylandCapture {
641626
let state = lock_frame_state(frame_state)?;
642627
(state.width, state.height)
643628
};
644-
let mut buffer_data = mmap.to_vec();
645-
if let Some(format) = {
629+
let format = {
646630
let state = lock_frame_state(frame_state)?;
647-
state.format
648-
} {
649-
match format {
650-
ShmFormat::Xrgb8888 => {
651-
for chunk in buffer_data.chunks_exact_mut(4) {
652-
let b = chunk[0];
653-
let g = chunk[1];
654-
let r = chunk[2];
655-
chunk[0] = r;
656-
chunk[1] = g;
657-
chunk[2] = b;
658-
chunk[3] = 255;
659-
}
660-
}
661-
ShmFormat::Argb8888 => {}
662-
_ => {}
663-
}
664-
}
631+
state.format.unwrap_or(ShmFormat::Xrgb8888)
632+
};
633+
let mut buffer_data = mmap.to_vec();
634+
convert_shm_to_rgba(&mut buffer_data, format);
665635
results.insert(
666636
output_name,
667637
CaptureResult {

src/wayland_capture/mod.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,47 @@ pub(super) fn lock_frame_state(
115115
.map_err(|e| Error::FrameCapture(format!("Frame state mutex poisoned: {}", e)))
116116
}
117117

118+
/// `wl_shm` 32-bit frame bytes to the crate's internal RGBA layout.
119+
///
120+
/// Why this exists:
121+
/// - `CaptureResult` data is exposed as RGBA.
122+
/// - `wl_shm` formats (`Xrgb8888`, `Argb8888`, etc.) are word-defined and on
123+
/// little-endian systems their byte order in memory is not always RGBA.
124+
///
125+
/// Where this is used:
126+
/// - `capture_region_for_output()` after reading the mmap buffer.
127+
/// - `capture_outputs()` for each captured output buffer.
128+
///
129+
/// Notes:
130+
/// - Conversion is done in-place (no extra allocation).
131+
/// - Unsupported formats are left unchanged.
132+
pub(super) fn convert_shm_to_rgba(buffer_data: &mut [u8], format: ShmFormat) {
133+
match format {
134+
// x:R:G:B -> bytes in memory are B,G,R,x on little-endian.
135+
ShmFormat::Xrgb8888 => {
136+
for chunk in buffer_data.chunks_exact_mut(4) {
137+
chunk.swap(0, 2);
138+
chunk[3] = 255;
139+
}
140+
}
141+
// A:R:G:B -> bytes in memory are B,G,R,A on little-endian.
142+
ShmFormat::Argb8888 => {
143+
for chunk in buffer_data.chunks_exact_mut(4) {
144+
chunk.swap(0, 2);
145+
}
146+
}
147+
// x:B:G:R -> bytes in memory are R,G,B,x on little-endian.
148+
ShmFormat::Xbgr8888 => {
149+
for chunk in buffer_data.chunks_exact_mut(4) {
150+
chunk[3] = 255;
151+
}
152+
}
153+
// A:B:G:R -> bytes in memory are R,G,B,A on little-endian.
154+
ShmFormat::Abgr8888 => {}
155+
_ => {}
156+
}
157+
}
158+
118159
/// Guess logical geometry from physical geometry when xdg_output is not available.
119160
pub(super) fn guess_output_logical_geometry(info: &mut OutputInfo) {
120161
info.logical_x = info.x;

tests/test.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
use grim_rs::{Box as GrimBox, CaptureParameters, CaptureResult, Error, Grim};
22
use std::collections::HashMap;
3+
use wayland_client::protocol::wl_shm::Format as ShmFormat;
4+
5+
fn convert_shm_to_rgba_for_test(buffer_data: &mut [u8], format: ShmFormat) {
6+
match format {
7+
ShmFormat::Xrgb8888 => {
8+
for chunk in buffer_data.chunks_exact_mut(4) {
9+
chunk.swap(0, 2);
10+
chunk[3] = 255;
11+
}
12+
}
13+
ShmFormat::Argb8888 => {
14+
for chunk in buffer_data.chunks_exact_mut(4) {
15+
chunk.swap(0, 2);
16+
}
17+
}
18+
ShmFormat::Xbgr8888 => {
19+
for chunk in buffer_data.chunks_exact_mut(4) {
20+
chunk[3] = 255;
21+
}
22+
}
23+
ShmFormat::Abgr8888 => {}
24+
_ => {}
25+
}
26+
}
327

428
#[test]
529
fn test_box_struct_creation() {
@@ -115,6 +139,20 @@ fn test_image_data_format() {
115139
assert_eq!(data[3], 255); // A
116140
}
117141

142+
#[test]
143+
fn test_convert_xrgb8888_to_rgba() {
144+
let mut pixel_data = vec![10, 20, 30, 99];
145+
convert_shm_to_rgba_for_test(&mut pixel_data, ShmFormat::Xrgb8888);
146+
assert_eq!(pixel_data, vec![30, 20, 10, 255]);
147+
}
148+
149+
#[test]
150+
fn test_convert_argb8888_to_rgba_preserves_alpha() {
151+
let mut pixel_data = vec![10, 20, 30, 40];
152+
convert_shm_to_rgba_for_test(&mut pixel_data, ShmFormat::Argb8888);
153+
assert_eq!(pixel_data, vec![30, 20, 10, 40]);
154+
}
155+
118156
#[test]
119157
fn test_png_compression_levels() {
120158
let test_data = vec![255u8; 100 * 100 * 4]; // 100x100 image

0 commit comments

Comments
 (0)