Skip to content

Commit a99c82b

Browse files
author
Dorinda Bassey
committed
vhost-device-gpu: Refactor vhost-device-gpu
This commit refactors vhost-device-gpu by separating virglrenderer from rutabaga, and using gfxstream via rutabaga, Simplifying future backend development. This commit introduces a significant refactor of the virtio-gpu backend architecture: - Transition `gfxstream` support to use `Rutabaga` abstraction. - Decouple `virglrenderer` from `Rutabaga`, allowing it to be used as standalone. - Unify backend handling with runtime dispatch for `gfxstream` (via TLS) and `virglrenderer` (via trait object). Key Changes: VirglRenderer Backend: - `virgl.rs` is now a standalone backend that directly calls `libvirglrenderer` functions. - Removed reliance on `rutabaga` for virgl path. Gfxstream Backend via Rutabaga: - Introduced `gfxstream.rs` backend using `rutabaga` - Thread-local `GfxstreamAdapter` manages its own `Rutabaga` instance, initialized lazily. - Preserved internal `GfxstreamResource` tracking with scanout support and memory handling. Renderer Selection Logic: - In `device.rs`, `lazy_init_and_handle_event()` now: - Dispatches `GfxstreamAdapter` using thread-local storage (TLS) for `Gfxstream`. - Retains trait-based dynamic dispatch for `VirglRenderer`. - Introduced `extract_backend_and_vring()` helper for reusing backend setup logic. Code Deduplication: - Abstracted common logic for both backends to common.rs. - Shared helpers reused between gfxstream and virgl. - Improved modularity with fewer duplicated error handling branches. Testing and Validation: - Replaced `virtio_gpu.rs` testing paths with new unit tests for `gfxstream.rs` and `virgl.rs`. - Added code coverage for the new refactored crate. Signed-off-by: Dorinda Bassey <[email protected]>
1 parent b6a1543 commit a99c82b

File tree

13 files changed

+3238
-1749
lines changed

13 files changed

+3238
-1749
lines changed

vhost-device-gpu/Cargo.toml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,22 @@ edition = "2021"
1414
resolver = "2"
1515

1616
[features]
17-
default = ["gfxstream"]
17+
default = ["backend-virgl", "backend-gfxstream"]
1818
xen = ["vm-memory/xen", "vhost/xen", "vhost-user-backend/xen"]
19-
gfxstream = ["rutabaga_gfx/gfxstream"]
19+
backend-gfxstream = ["rutabaga_gfx/gfxstream"]
20+
backend-virgl = ["dep:virglrenderer"]
2021

2122
[dependencies]
2223
clap = { version = "4.5", features = ["derive"] }
2324
env_logger = "0.11.6"
2425
libc = "0.2"
2526
log = "0.4"
27+
once_cell = "1.21.3"
2628

2729
[target.'cfg(not(target_env = "musl"))'.dependencies]
28-
rutabaga_gfx = { version = "0.1.71", features = ["virgl_renderer"] }
29-
thiserror = "2.0.17"
30+
virglrenderer = {version = "0.1.2", optional = true }
31+
rutabaga_gfx = { version = "0.1.71"}
32+
thiserror = "2.0.12"
3033
vhost = { version = "0.14.0", features = ["vhost-user-backend"] }
3134
vhost-user-backend = "0.20"
3235
virtio-bindings = "0.2.5"
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
// src/backend/common.rs
2+
3+
use std::sync::{Arc, Mutex};
4+
5+
use log::{debug, error};
6+
use vhost::vhost_user::{
7+
gpu_message::{VhostUserGpuCursorPos, VhostUserGpuCursorUpdate, VhostUserGpuEdidRequest},
8+
GpuBackend,
9+
};
10+
11+
use crate::{
12+
gpu_types::{FenceDescriptor, FenceState, VirtioGpuRing},
13+
protocol::{
14+
GpuResponse,
15+
GpuResponse::{ErrUnspec, OkDisplayInfo, OkEdid, OkNoData},
16+
VirtioGpuResult, VIRTIO_GPU_MAX_SCANOUTS,
17+
},
18+
};
19+
20+
#[derive(Debug, Clone)]
21+
pub struct VirtioGpuScanout {
22+
pub resource_id: u32,
23+
}
24+
25+
#[derive(Copy, Clone, Debug, Default)]
26+
pub struct AssociatedScanouts(u32);
27+
28+
impl AssociatedScanouts {
29+
pub const fn enable(&mut self, scanout_id: u32) {
30+
self.0 |= 1 << scanout_id;
31+
}
32+
33+
pub const fn disable(&mut self, scanout_id: u32) {
34+
self.0 ^= 1 << scanout_id;
35+
}
36+
37+
pub const fn has_any_enabled(self) -> bool {
38+
self.0 != 0
39+
}
40+
41+
pub fn iter_enabled(self) -> impl Iterator<Item = u32> {
42+
(0..VIRTIO_GPU_MAX_SCANOUTS).filter(move |i| ((self.0 >> i) & 1) == 1)
43+
}
44+
}
45+
46+
pub const VHOST_USER_GPU_MAX_CURSOR_DATA_SIZE: usize = 16384; // 4*4*1024
47+
pub const READ_RESOURCE_BYTES_PER_PIXEL: usize = 4;
48+
49+
#[derive(Copy, Clone, Debug, Default)]
50+
pub struct CursorConfig {
51+
pub width: u32,
52+
pub height: u32,
53+
}
54+
55+
impl CursorConfig {
56+
pub const fn expected_buffer_len(&self) -> usize {
57+
self.width as usize * self.height as usize * READ_RESOURCE_BYTES_PER_PIXEL
58+
}
59+
}
60+
61+
pub fn common_display_info(gpu_backend: &GpuBackend) -> VirtioGpuResult {
62+
let backend_display_info = gpu_backend.get_display_info().map_err(|e| {
63+
error!("Failed to get display info: {e:?}");
64+
ErrUnspec
65+
})?;
66+
let display_info = backend_display_info
67+
.pmodes
68+
.iter()
69+
.map(|display| (display.r.width, display.r.height, display.enabled == 1))
70+
.collect::<Vec<_>>();
71+
debug!("Displays: {:?}", display_info);
72+
Ok(OkDisplayInfo(display_info))
73+
}
74+
75+
pub fn common_get_edid(
76+
gpu_backend: &GpuBackend,
77+
edid_req: VhostUserGpuEdidRequest,
78+
) -> VirtioGpuResult {
79+
debug!("edid request: {edid_req:?}");
80+
let edid = gpu_backend.get_edid(&edid_req).map_err(|e| {
81+
error!("Failed to get edid from frontend: {}", e);
82+
ErrUnspec
83+
})?;
84+
Ok(OkEdid {
85+
blob: Box::from(&edid.edid[..edid.size as usize]),
86+
})
87+
}
88+
89+
pub fn common_process_fence(
90+
fence_state: &Arc<Mutex<FenceState>>,
91+
ring: VirtioGpuRing,
92+
fence_id: u64,
93+
desc_index: u16,
94+
len: u32,
95+
) -> bool {
96+
// In case the fence is signaled immediately after creation, don't add a return
97+
// FenceDescriptor.
98+
let mut fence_state = fence_state.lock().unwrap();
99+
if fence_id > *fence_state.completed_fences.get(&ring).unwrap_or(&0) {
100+
fence_state.descs.push(FenceDescriptor {
101+
ring,
102+
fence_id,
103+
desc_index,
104+
len,
105+
});
106+
107+
false
108+
} else {
109+
true
110+
}
111+
}
112+
113+
pub fn common_move_cursor(
114+
gpu_backend: &GpuBackend,
115+
resource_id: u32,
116+
cursor: VhostUserGpuCursorPos,
117+
) -> VirtioGpuResult {
118+
if resource_id == 0 {
119+
gpu_backend.cursor_pos_hide(&cursor).map_err(|e| {
120+
error!("Failed to set cursor pos from frontend: {}", e);
121+
ErrUnspec
122+
})?;
123+
} else {
124+
gpu_backend.cursor_pos(&cursor).map_err(|e| {
125+
error!("Failed to set cursor pos from frontend: {}", e);
126+
ErrUnspec
127+
})?;
128+
}
129+
130+
Ok(GpuResponse::OkNoData)
131+
}
132+
133+
pub fn common_update_cursor(
134+
gpu_backend: &GpuBackend,
135+
cursor_pos: VhostUserGpuCursorPos,
136+
hot_x: u32,
137+
hot_y: u32,
138+
data: Box<[u8]>,
139+
config: CursorConfig,
140+
) -> VirtioGpuResult {
141+
let expected_len = config.expected_buffer_len();
142+
143+
if data.len() != expected_len {
144+
error!(
145+
"Mismatched cursor data size: expected {}, got {}",
146+
expected_len,
147+
data.len()
148+
);
149+
return Err(ErrUnspec);
150+
}
151+
152+
let data_ref: &[u8] = &data;
153+
let cursor_update = VhostUserGpuCursorUpdate {
154+
pos: cursor_pos,
155+
hot_x,
156+
hot_y,
157+
};
158+
let mut padded_data = [0u8; VHOST_USER_GPU_MAX_CURSOR_DATA_SIZE];
159+
padded_data[..data_ref.len()].copy_from_slice(data_ref);
160+
161+
gpu_backend
162+
.cursor_update(&cursor_update, &padded_data)
163+
.map_err(|e| {
164+
error!("Failed to update cursor: {}", e);
165+
ErrUnspec
166+
})?;
167+
168+
Ok(OkNoData)
169+
}
170+
171+
pub fn common_set_scanout_disable(scanouts: &mut [Option<VirtioGpuScanout>], scanout_idx: usize) {
172+
scanouts[scanout_idx] = None;
173+
debug!("Disabling scanout scanout_id={scanout_idx}");
174+
}
175+
176+
#[cfg(test)]
177+
mod tests {
178+
use std::{
179+
os::unix::net::UnixStream,
180+
sync::{Arc, Mutex},
181+
};
182+
183+
use assert_matches::assert_matches;
184+
185+
use super::*;
186+
use crate::{
187+
gpu_types::VirtioGpuRing,
188+
protocol::{GpuResponse::ErrUnspec, VIRTIO_GPU_MAX_SCANOUTS},
189+
};
190+
191+
const CURSOR_POS: VhostUserGpuCursorPos = VhostUserGpuCursorPos {
192+
scanout_id: 0,
193+
x: 0,
194+
y: 0,
195+
};
196+
const CURSOR_CONFIG: CursorConfig = CursorConfig {
197+
width: 4,
198+
height: 4,
199+
};
200+
const BYTES_PER_PIXEL: usize = 4;
201+
const EXPECTED_LEN: usize =
202+
(CURSOR_CONFIG.width as usize) * (CURSOR_CONFIG.height as usize) * BYTES_PER_PIXEL;
203+
204+
fn dummy_gpu_backend() -> GpuBackend {
205+
let (_, backend) = UnixStream::pair().unwrap();
206+
GpuBackend::from_stream(backend)
207+
}
208+
209+
// AssociatedScanouts
210+
// Test that enabling, disabling, iterating, and checking any enabled works as
211+
// expected.
212+
#[test]
213+
fn associated_scanouts_enable_disable_iter_and_any() {
214+
let mut assoc = AssociatedScanouts::default();
215+
216+
// No scanouts initially
217+
assert!(!assoc.has_any_enabled());
218+
assert_eq!(assoc.iter_enabled().count(), 0);
219+
220+
// Enable a couple
221+
assoc.enable(0);
222+
assoc.enable(3);
223+
assert!(assoc.has_any_enabled());
224+
assert_eq!(assoc.iter_enabled().collect::<Vec<u32>>(), vec![0u32, 3u32]);
225+
226+
// Disable one
227+
assoc.disable(3);
228+
assert!(assoc.has_any_enabled());
229+
assert_eq!(assoc.iter_enabled().collect::<Vec<u32>>(), vec![0u32]);
230+
231+
// Disable last
232+
assoc.disable(0);
233+
assert!(!assoc.has_any_enabled());
234+
assert_eq!(assoc.iter_enabled().count(), 0);
235+
}
236+
237+
// CursorConfig
238+
// Test that expected_buffer_len computes the correct size.
239+
#[test]
240+
fn cursor_config_expected_len() {
241+
let cfg = CursorConfig {
242+
width: 64,
243+
height: 64,
244+
};
245+
assert_eq!(
246+
cfg.expected_buffer_len(),
247+
64 * 64 * READ_RESOURCE_BYTES_PER_PIXEL
248+
);
249+
}
250+
251+
// Update cursor
252+
// Test that updating the cursor with mismatched data size fails.
253+
#[test]
254+
fn update_cursor_mismatched_data_size_fails() {
255+
let gpu_backend = dummy_gpu_backend();
256+
257+
// Data has length 1 (expected is 64)
258+
let bad_data = Box::from([0u8]);
259+
260+
let result = common_update_cursor(&gpu_backend, CURSOR_POS, 0, 0, bad_data, CURSOR_CONFIG);
261+
262+
assert_matches!(result, Err(ErrUnspec), "Should fail due to mismatched size");
263+
}
264+
265+
// Test that updating the cursor with correct data size but backend failure
266+
// returns ErrUnspec.
267+
#[test]
268+
fn update_cursor_backend_failure() {
269+
let gpu_backend = dummy_gpu_backend();
270+
271+
// Data has the correct length (64 bytes)
272+
let correct_data = vec![0u8; EXPECTED_LEN].into_boxed_slice();
273+
274+
let result =
275+
common_update_cursor(&gpu_backend, CURSOR_POS, 0, 0, correct_data, CURSOR_CONFIG);
276+
277+
assert_matches!(
278+
result,
279+
Err(ErrUnspec),
280+
"Should fail due to failure to update cursor"
281+
);
282+
}
283+
284+
// Fence handling
285+
// Test that processing a fence pushes a descriptor when the fence is new.
286+
#[test]
287+
fn process_fence_pushes_descriptor_when_new() {
288+
let fence_state = Arc::new(Mutex::new(FenceState::default()));
289+
let ring = VirtioGpuRing::Global;
290+
291+
// Clone because common_process_fence takes ownership of ring
292+
let ret = common_process_fence(&fence_state, ring.clone(), 42, 7, 512);
293+
assert!(!ret, "New fence should not complete immediately");
294+
295+
let st = fence_state.lock().unwrap();
296+
assert_eq!(st.descs.len(), 1);
297+
assert_eq!(st.descs[0].ring, ring);
298+
assert_eq!(st.descs[0].fence_id, 42);
299+
assert_eq!(st.descs[0].desc_index, 7);
300+
assert_eq!(st.descs[0].len, 512);
301+
}
302+
303+
// Test that processing a fence that is already completed returns true
304+
// immediately.
305+
#[test]
306+
fn process_fence_immediately_completes_when_already_done() {
307+
let ring = VirtioGpuRing::Global;
308+
309+
// Seed state so that ring's 100 is already completed.
310+
let mut seeded = FenceState::default();
311+
seeded.completed_fences.insert(ring.clone(), 100);
312+
let fence_state = Arc::new(Mutex::new(seeded));
313+
314+
let ret = common_process_fence(&fence_state, ring, 100, 1, 4);
315+
assert!(ret, "already-completed fence should return true");
316+
317+
let st = fence_state.lock().unwrap();
318+
assert!(st.descs.is_empty());
319+
}
320+
321+
// Test that disabling a scanout clears the corresponding slot.
322+
#[test]
323+
fn set_scanout_disable_clears_slot() {
324+
const N: usize = VIRTIO_GPU_MAX_SCANOUTS as usize;
325+
let mut scanouts: [Option<VirtioGpuScanout>; N] = Default::default();
326+
327+
scanouts[5] = Some(VirtioGpuScanout { resource_id: 123 });
328+
common_set_scanout_disable(&mut scanouts, 5);
329+
assert!(scanouts[5].is_none());
330+
}
331+
}

0 commit comments

Comments
 (0)