Phase 1: Parse SpiceCursor image data¶
Overview¶
Parse the cursor image data from CURSOR_INIT and CURSOR_SET
messages, convert it to RGBA, cache by unique_id, and
forward to the app via a new CursorShape channel event.
Current state¶
CursorInit::read()parses 9 bytes (position, trail, visible) but ignores any cursor data that follows.CursorSet::read()parses 5 bytes (position, visible) but ignores any cursor data that follows.- The cursor channel sends
CursorPositionevents with (x, y, visible) only — no image data. - There is no cursor image cache.
Wire format¶
SpiceCursor structure (follows position data)¶
Offset Size Field
------ ---- -----------
0 4 flags (u32 LE)
bit 0: (unused)
bit 1: CACHE_ME — client should cache this
bit 2: FROM_CACHE — no pixel data follows;
look up by unique_id
4 8 unique_id (u64 LE)
12 2 cursor_type (u16 LE)
14 2 width (u16 LE)
16 2 height (u16 LE)
18 2 hot_spot_x (u16 LE)
20 2 hot_spot_y (u16 LE)
22 var pixel_data (only if FROM_CACHE is NOT set)
Total header size: 22 bytes.
Where SpiceCursor appears¶
- CURSOR_INIT (opcode 101): 9 bytes of position/trail/ visible, then SpiceCursor if payload > 9 + 22 bytes.
- CURSOR_SET (opcode 103): 5 bytes of position/visible, then SpiceCursor if payload > 5 + 22 bytes.
Pixel data formats (by cursor_type)¶
| Type | Name | Bytes/pixel | Conversion to RGBA |
|---|---|---|---|
| 0 | Alpha | 4 | ARGB → RGBA (swap A to end) |
| 1 | Mono | 1-bit | Not planned initially |
| 5 | Color24 | 3 | BGR → RGBA (add A=255) |
| 6 | Color32 | 4 | xRGB → RGBA (set A=255) |
Type 0 (Alpha) is the most common from QEMU/KVM.
Implementation steps¶
Step 1: Add SpiceCursorHeader struct¶
In protocol/messages.rs, add:
pub struct SpiceCursorHeader {
pub flags: u32,
pub unique_id: u64,
pub cursor_type: u16,
pub width: u16,
pub height: u16,
pub hot_spot_x: u16,
pub hot_spot_y: u16,
}
With const SIZE: usize = 22 and a standard read()
method. Define flag constants:
Step 2: Add CursorImage type and CursorShape event¶
In channels/mod.rs, add:
pub struct CursorImage {
pub width: u16,
pub height: u16,
pub hot_spot_x: u16,
pub hot_spot_y: u16,
pub pixels: Vec<u8>, // RGBA
}
Add a new event variant:
Step 3: Add cursor image cache to CursorChannel¶
In channels/cursor.rs, add fields:
Step 4: Parse SpiceCursor in INIT and SET handlers¶
After reading the position/visible fields, check whether the payload is large enough to contain a SpiceCursor header (22 more bytes). If so:
- Parse
SpiceCursorHeaderfrom the remaining payload. - If
FROM_CACHEflag is set: look upunique_idincursor_cacheand emitCursorShapewith the cached image. If not found, log a warning. - If
FROM_CACHEis NOT set: read pixel data after the header, convert to RGBA based oncursor_type, emitCursorShape. - If
CACHE_MEflag is set: store a clone of the image incursor_cachekeyed byunique_id.
Step 5: Pixel data conversion¶
Implement conversion for the common types:
- Alpha (type 0): Read 4 bytes per pixel as ARGB, emit
as RGBA:
[A, R, G, B] → [R, G, B, A] - Color32 (type 6): Read 4 bytes per pixel as xRGB,
emit as RGBA:
[x, R, G, B] → [R, G, B, 255] - Color24 (type 5): Read 3 bytes per pixel as BGR,
emit as RGBA:
[B, G, R] → [R, G, B, 255] - Other types: Log a warning and skip.
Use checked arithmetic for width * height * bytes_per_pixel
as per the STYLEGUIDE.md dimension safety rule.
Step 6: Handle INVALIDATE_ONE and INVALIDATE_ALL¶
- INVALIDATE_ONE (opcode 107): payload is 8 bytes
(u64 unique_id). Remove from
cursor_cache. - INVALIDATE_ALL (opcode 108): clear
cursor_cache.
These are already defined as constants but currently fall through to the unknown handler.
Step 7: Unit test¶
Add a test that constructs a minimal CURSOR_SET payload with a 2x2 Alpha cursor, parses it, and verifies the RGBA output has A moved from the first byte to the last.
Files to modify¶
| File | Changes |
|---|---|
src/protocol/messages.rs |
Add SpiceCursorHeader struct |
src/protocol/constants.rs |
Add cursor flag constants |
src/channels/mod.rs |
Add CursorImage, CursorShape event |
src/channels/cursor.rs |
Parse SpiceCursor, cache, convert pixels |
Success criteria¶
- Running with
-vagainst a real server showscursor: set: ... cursor_type=0, 24x24(or similar) instead of just position. CursorShapeevents appear in/tmp/ryll.log.- FROM_CACHE lookups succeed when the server reuses a cursor shape.
pre-commit run --all-filespasses.