From 5e46feaeeae8ae977f395ee17009a8033ed0bd2e Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sun, 31 Aug 2025 12:27:41 +0200 Subject: [PATCH] it: add more tests --- src/ifs/wl_surface.rs | 10 + src/it/test_ifs/test_surface.rs | 30 ++ src/it/tests.rs | 4 + src/it/tests/t0046_buffer_release.rs | 78 +++++ src/it/tests/t0047_surface_damage.rs | 501 +++++++++++++++++++++++++++ 5 files changed, 623 insertions(+) create mode 100644 src/it/tests/t0046_buffer_release.rs create mode 100644 src/it/tests/t0047_surface_damage.rs diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs index 6d1bb32b..b3115d0e 100644 --- a/src/ifs/wl_surface.rs +++ b/src/ifs/wl_surface.rs @@ -997,6 +997,16 @@ impl WlSurface { .push(XWaylandEvent::Configure(window)); } } + + #[cfg(feature = "it")] + pub fn get_pending_damage(&self) -> (Vec, Vec, bool) { + let pending = self.pending.borrow(); + ( + pending.surface_damage.clone(), + pending.buffer_damage.clone(), + pending.damage_full, + ) + } } const MAX_DAMAGE: usize = 32; diff --git a/src/it/test_ifs/test_surface.rs b/src/it/test_ifs/test_surface.rs index ea828b4d..6f15dbd1 100644 --- a/src/it/test_ifs/test_surface.rs +++ b/src/it/test_ifs/test_surface.rs @@ -59,6 +59,36 @@ impl TestSurface { Ok(()) } + pub fn damage(&self, x: i32, y: i32, width: i32, height: i32) -> Result<(), TestError> { + self.tran.send(Damage { + self_id: self.id, + x, + y, + width, + height, + })?; + Ok(()) + } + + pub fn damage_buffer(&self, x: i32, y: i32, width: i32, height: i32) -> Result<(), TestError> { + self.tran.send(DamageBuffer { + self_id: self.id, + x, + y, + width, + height, + })?; + Ok(()) + } + + pub fn set_buffer_transform(&self, transform: i32) -> Result<(), TestError> { + self.tran.send(SetBufferTransform { + self_id: self.id, + transform, + })?; + Ok(()) + } + pub fn commit(&self) -> Result<(), TestError> { self.tran.send(Commit { self_id: self.id })?; Ok(()) diff --git a/src/it/tests.rs b/src/it/tests.rs index dbf3ff79..84e59eed 100644 --- a/src/it/tests.rs +++ b/src/it/tests.rs @@ -76,6 +76,8 @@ mod t0042_toplevel_select; mod t0043_destroy_registry; mod t0044_stacked_focus; mod t0045_content_type; +mod t0046_buffer_release; +mod t0047_surface_damage; pub trait TestCase: Sync { fn name(&self) -> &'static str; @@ -140,5 +142,7 @@ pub fn tests() -> Vec<&'static dyn TestCase> { t0043_destroy_registry, t0044_stacked_focus, t0045_content_type, + t0046_buffer_release, + t0047_surface_damage, } } diff --git a/src/it/tests/t0046_buffer_release.rs b/src/it/tests/t0046_buffer_release.rs new file mode 100644 index 00000000..908bc0d6 --- /dev/null +++ b/src/it/tests/t0046_buffer_release.rs @@ -0,0 +1,78 @@ +use { + crate::{ + it::{test_error::TestResult, testrun::TestRun}, + theme::Color, + wire::WlBufferId, + }, + std::rc::Rc, +}; + +testcase!(); + +/// Test wl_buffer.release event functionality +async fn test(run: Rc) -> TestResult { + let client = run.create_client().await?; + + // Create a surface and buffer + let surface = client.comp.create_surface().await?; + let buffer1 = client.spbm.create_buffer(Color::from_srgb(255, 0, 0))?; + let buffer2 = client.spbm.create_buffer(Color::from_srgb(0, 255, 0))?; + + // Initially both buffers should be marked as released (not in use) + tassert!(buffer1.released.get()); + tassert!(buffer2.released.get()); + + // Attach the first buffer and commit + surface.attach(buffer1.id)?; + surface.commit()?; + + // The buffer should now be in use, so released should be false + buffer1.released.set(false); // Reset to track actual release event + + client.sync().await; + + // Attach a different buffer and commit - this should cause the first buffer to be released + surface.attach(buffer2.id)?; + surface.commit()?; + + buffer2.released.set(false); // Reset to track release of second buffer + + client.sync().await; + + // Buffer1 should now be released since buffer2 is attached + tassert!(buffer1.released.get()); + + // Create a third buffer and attach it + let buffer3 = client.spbm.create_buffer(Color::from_srgb(0, 0, 255))?; + surface.attach(buffer3.id)?; + surface.commit()?; + + buffer3.released.set(false); + + client.sync().await; + + // Buffer2 should now be released since buffer3 is attached + tassert!(buffer2.released.get()); + + // Test buffer reuse - we should be able to reuse buffer1 now that it's released + surface.attach(buffer1.id)?; + surface.commit()?; + + buffer1.released.set(false); + + client.sync().await; + + // Buffer3 should now be released since buffer1 is attached again + tassert!(buffer3.released.get()); + + // Finally, detach the buffer (attach NULL) - this should release buffer1 + surface.attach(WlBufferId::NONE)?; + surface.commit()?; + + client.sync().await; + + // Buffer1 should now be released since no buffer is attached + tassert!(buffer1.released.get()); + + Ok(()) +} diff --git a/src/it/tests/t0047_surface_damage.rs b/src/it/tests/t0047_surface_damage.rs new file mode 100644 index 00000000..d9760bc8 --- /dev/null +++ b/src/it/tests/t0047_surface_damage.rs @@ -0,0 +1,501 @@ +use { + crate::{ + it::{test_error::TestResult, testrun::TestRun}, + rect::Rect, + }, + std::rc::Rc, +}; + +testcase!(); + +/// Test wl_surface.damage and wl_surface.damage_buffer requests +/// This test verifies that the compositor correctly handles damage requests according to the Wayland protocol +/// and creates appropriate output damage when surface damage is committed. +async fn test(run: Rc) -> TestResult { + run.backend.install_default()?; + let client = run.create_client().await?; + + // Get connector for tracking output damage + let connector_id = run.backend.default_connector.id; + let connector_data = run.state.connectors.get(&connector_id).unwrap(); + + // Create a simple surface with a buffer + let surface = client.comp.create_surface().await?; + let buffer = client + .spbm + .create_buffer(crate::theme::Color::from_srgb(255, 0, 0))?; + surface.attach(buffer.id)?; + surface.commit()?; // Initial commit to attach buffer + client.sync().await; + + // Test 1: wl_surface.damage - basic functionality and damage clearing + surface.damage(10, 10, 50, 50)?; + client.sync().await; + + // Verify damage is pending + { + let (surface_damage, buffer_damage, damage_full) = surface.server.get_pending_damage(); + tassert_eq!(surface_damage.len(), 1); + tassert_eq!(surface_damage[0], Rect::new_sized(10, 10, 50, 50).unwrap()); + tassert!(buffer_damage.is_empty()); + tassert!(!damage_full); + } + + // Critical test: Commit should clear pending damage + surface.commit()?; + client.sync().await; + + { + let (surface_damage, buffer_damage, damage_full) = surface.server.get_pending_damage(); + tassert!(surface_damage.is_empty()); + tassert!(buffer_damage.is_empty()); + tassert!(!damage_full); + } + + // Test 2: wl_surface.damage_buffer functionality + surface.damage_buffer(20, 20, 30, 30)?; + client.sync().await; + + // Verify buffer damage is pending + { + let (surface_damage, buffer_damage, damage_full) = surface.server.get_pending_damage(); + tassert!(surface_damage.is_empty()); + tassert_eq!(buffer_damage.len(), 1); + tassert_eq!(buffer_damage[0], Rect::new_sized(20, 20, 30, 30).unwrap()); + tassert!(!damage_full); + } + + // Commit should clear pending buffer damage + surface.commit()?; + client.sync().await; + + { + let (surface_damage, buffer_damage, damage_full) = surface.server.get_pending_damage(); + tassert!(surface_damage.is_empty()); + tassert!(buffer_damage.is_empty()); + tassert!(!damage_full); + } + + // Test 3: Mixed surface and buffer damage + surface.damage(5, 5, 10, 10)?; + surface.damage_buffer(15, 15, 10, 10)?; + surface.damage(25, 25, 10, 10)?; + client.sync().await; + + { + let (surface_damage, buffer_damage, damage_full) = surface.server.get_pending_damage(); + tassert_eq!(surface_damage.len(), 2); + tassert_eq!(buffer_damage.len(), 1); + tassert!(!damage_full); + } + + surface.commit()?; + client.sync().await; + + { + let (surface_damage, buffer_damage, damage_full) = surface.server.get_pending_damage(); + tassert!(surface_damage.is_empty()); + tassert!(buffer_damage.is_empty()); + tassert!(!damage_full); + } + + // Test 4: damage_full optimization - many small damage rects should trigger damage_full + for i in 0..40 { + // More than MAX_DAMAGE (32) to trigger damage_full + surface.damage(i * 2, i * 2, 1, 1)?; + } + client.sync().await; + + { + let (_surface_damage, _buffer_damage, damage_full) = surface.server.get_pending_damage(); + tassert!(damage_full); // Should have triggered damage_full optimization + } + + // Critical: damage_full should still clear pending damage after commit + surface.commit()?; + client.sync().await; + + { + let (surface_damage, buffer_damage, damage_full) = surface.server.get_pending_damage(); + tassert!(surface_damage.is_empty()); + tassert!(buffer_damage.is_empty()); + tassert!(!damage_full); + } + + // Test 5: Verify output damage creation and values + // For this test we need a visible surface to generate actual output damage + let window = client.create_window().await?; + window.surface.attach(buffer.id)?; + window.map().await?; + client.sync().await; + + // Get the surface's absolute position for damage calculation + let surface_pos = window.surface.server.buffer_abs_pos.get(); + + // Clear any existing output damage + connector_data.damage.borrow_mut().clear(); + + // Add specific damage and commit + let client_damage = Rect::new_sized(10, 10, 20, 20).unwrap(); + window.surface.damage( + client_damage.x1(), + client_damage.y1(), + client_damage.width(), + client_damage.height(), + )?; + window.surface.commit()?; + client.sync().await; + + // Verify output damage was created with exact correct values + { + let output_damage = connector_data.damage.borrow(); + tassert!(!output_damage.is_empty()); + + // The surface damage should be transformed to output coordinates + // Surface damage is moved by the surface's absolute position + let expected_damage = client_damage.move_(surface_pos.x1(), surface_pos.y1()); + + // Verify the exact output damage coordinates + let mut found_exact_damage = false; + for &actual_damage in output_damage.iter() { + // Check if this output damage exactly matches our expected damage + if actual_damage.x1() == expected_damage.x1() + && actual_damage.y1() == expected_damage.y1() + && actual_damage.x2() == expected_damage.x2() + && actual_damage.y2() == expected_damage.y2() + { + found_exact_damage = true; + break; + } + } + + if !found_exact_damage { + // If exact match not found, provide detailed debugging info + run.errors.push(format!( + "Expected output damage: x1={}, y1={}, x2={}, y2={} ({}x{})", + expected_damage.x1(), + expected_damage.y1(), + expected_damage.x2(), + expected_damage.y2(), + expected_damage.width(), + expected_damage.height() + )); + run.errors.push(format!( + "Surface position: x1={}, y1={}", + surface_pos.x1(), + surface_pos.y1() + )); + run.errors.push(format!( + "Client damage: x1={}, y1={}, x2={}, y2={} ({}x{})", + client_damage.x1(), + client_damage.y1(), + client_damage.x2(), + client_damage.y2(), + client_damage.width(), + client_damage.height() + )); + run.errors.push("Actual output damage:".to_string()); + for (i, &actual_damage) in output_damage.iter().enumerate() { + run.errors.push(format!( + " [{}]: x1={}, y1={}, x2={}, y2={} ({}x{})", + i, + actual_damage.x1(), + actual_damage.y1(), + actual_damage.x2(), + actual_damage.y2(), + actual_damage.width(), + actual_damage.height() + )); + } + } + + tassert!(found_exact_damage); + } + + // Test 5b: Verify multiple surface damage rectangles create correct output damage + connector_data.damage.borrow_mut().clear(); + + // Add multiple damage rectangles + let damage1 = Rect::new_sized(5, 5, 10, 10).unwrap(); + let damage2 = Rect::new_sized(20, 25, 15, 8).unwrap(); + + window.surface.damage( + damage1.x1(), + damage1.y1(), + damage1.width(), + damage1.height(), + )?; + window.surface.damage( + damage2.x1(), + damage2.y1(), + damage2.width(), + damage2.height(), + )?; + window.surface.commit()?; + client.sync().await; + + // Verify both damage rectangles are transformed correctly + { + let output_damage = connector_data.damage.borrow(); + tassert!(!output_damage.is_empty()); + + let expected_damage1 = damage1.move_(surface_pos.x1(), surface_pos.y1()); + let expected_damage2 = damage2.move_(surface_pos.x1(), surface_pos.y1()); + + let mut found_damage1 = false; + let mut found_damage2 = false; + + for &actual_damage in output_damage.iter() { + if actual_damage.x1() == expected_damage1.x1() + && actual_damage.y1() == expected_damage1.y1() + && actual_damage.x2() == expected_damage1.x2() + && actual_damage.y2() == expected_damage1.y2() + { + found_damage1 = true; + } + if actual_damage.x1() == expected_damage2.x1() + && actual_damage.y1() == expected_damage2.y1() + && actual_damage.x2() == expected_damage2.x2() + && actual_damage.y2() == expected_damage2.y2() + { + found_damage2 = true; + } + } + + if !found_damage1 || !found_damage2 { + run.errors.push(format!("Multiple damage test failed:")); + run.errors.push(format!( + "Expected damage1: x1={}, y1={}, x2={}, y2={}", + expected_damage1.x1(), + expected_damage1.y1(), + expected_damage1.x2(), + expected_damage1.y2() + )); + run.errors.push(format!( + "Expected damage2: x1={}, y1={}, x2={}, y2={}", + expected_damage2.x1(), + expected_damage2.y1(), + expected_damage2.x2(), + expected_damage2.y2() + )); + run.errors.push(format!( + "Found damage1: {}, Found damage2: {}", + found_damage1, found_damage2 + )); + } + + tassert!(found_damage1); + tassert!(found_damage2); + } + + // Test 6: Verify buffer damage creates correct output damage with exact coordinates + connector_data.damage.borrow_mut().clear(); + + // Add buffer damage within the buffer bounds (spbm creates 1x1 pixel buffers) + // Buffer damage must be within buffer.buffer.rect or it gets clipped to empty + let buffer_damage = Rect::new_sized(0, 0, 1, 1).unwrap(); // Entire 1x1 buffer + window.surface.damage_buffer( + buffer_damage.x1(), + buffer_damage.y1(), + buffer_damage.width(), + buffer_damage.height(), + )?; + window.surface.commit()?; + client.sync().await; + + // Verify buffer damage was transformed correctly to output coordinates + { + let output_damage = connector_data.damage.borrow(); + tassert!(!output_damage.is_empty()); + + // Buffer damage is transformed by the damage matrix which includes the surface position + // The buffer damage (0,0,1,1) should be transformed to surface coordinates + let expected_buffer_damage = buffer_damage.move_(surface_pos.x1(), surface_pos.y1()); + + // Find the exact output damage that matches our expected buffer damage + let mut found_exact_buffer_damage = false; + for &actual_damage in output_damage.iter() { + if actual_damage.x1() == expected_buffer_damage.x1() + && actual_damage.y1() == expected_buffer_damage.y1() + && actual_damage.x2() == expected_buffer_damage.x2() + && actual_damage.y2() == expected_buffer_damage.y2() + { + found_exact_buffer_damage = true; + break; + } + } + + tassert!(found_exact_buffer_damage); + } + + // Test 7: Check output damage from existing window's viewport (which already has scaling) + connector_data.damage.borrow_mut().clear(); + + // The existing window was created with create_surface_ext() which automatically creates a viewport + // Let's verify that the viewport's existing scaling affects buffer damage correctly + // First, let's modify the viewport scaling that already exists on the window + window.surface.viewport.set_destination(150, 100)?; // Change scaling to 150x100 + + // Add buffer damage to test viewport scaling coordinate transformation + window.surface.damage_buffer(0, 0, 1, 1)?; // Damage entire 1x1 buffer + window.surface.commit()?; + client.sync().await; + + // Verify the created output damage from viewporter coordinate transformation + { + let output_damage = connector_data.damage.borrow(); + tassert!(!output_damage.is_empty()); + + // With viewporter scaling, the 1x1 buffer damage should scale to 150x100 + // and be moved by surface position (0, 36) to get output coordinates (0, 36, 150, 136) + let expected_scaled_damage = Rect::new_sized(0, 0, 150, 100).unwrap(); + let expected_output_damage = + expected_scaled_damage.move_(surface_pos.x1(), surface_pos.y1()); + + // Find the exact scaled buffer damage in the output damage + let mut found_viewport_scaled_damage = false; + for &actual_damage in output_damage.iter() { + if actual_damage.x1() == expected_output_damage.x1() + && actual_damage.y1() == expected_output_damage.y1() + && actual_damage.x2() == expected_output_damage.x2() + && actual_damage.y2() == expected_output_damage.y2() + { + found_viewport_scaled_damage = true; + break; + } + } + + if !found_viewport_scaled_damage { + run.errors + .push("Viewport-scaled buffer damage verification failed:".to_string()); + run.errors.push(format!( + "Expected output damage: x1={}, y1={}, x2={}, y2={} ({}x{})", + expected_output_damage.x1(), + expected_output_damage.y1(), + expected_output_damage.x2(), + expected_output_damage.y2(), + expected_output_damage.width(), + expected_output_damage.height() + )); + run.errors.push("Actual output damage:".to_string()); + for (i, &actual_damage) in output_damage.iter().enumerate() { + run.errors.push(format!( + " [{}]: x1={}, y1={}, x2={}, y2={} ({}x{})", + i, + actual_damage.x1(), + actual_damage.y1(), + actual_damage.x2(), + actual_damage.y2(), + actual_damage.width(), + actual_damage.height() + )); + } + } + + tassert!(found_viewport_scaled_damage); + } + + // Test 8: Verify buffer transform rotation integrates with damage coordinate transformation + // Create a surface with buffer transform rotation to test coordinate transformation + let rotation_window = client.create_window().await?; + + rotation_window.map().await?; + client.sync().await; + + // Disable viewporter by setting destination to 0x0 to rely purely on buffer dimensions + rotation_window.surface.viewport.set_destination(0, 0)?; // Disable viewporter + + // Use a rectangular buffer (4x2) so rotation has a visible geometric effect + // Attach AFTER mapping to avoid being overwritten by map()'s single-pixel buffer + let rotation_buffer = client.shm.create_buffer(4, 2)?; + rotation_window.surface.attach(rotation_buffer.buffer.id)?; + rotation_window.surface.set_buffer_transform(1)?; // TF_90 = 1 (90 degrees rotation) + rotation_window.surface.commit()?; // Commit the new buffer and transform + client.sync().await; + + // Get the rotated surface position for damage coordinate verification + let rotation_surface_pos = rotation_window.surface.server.buffer_abs_pos.get(); + + // Clear damage immediately before the commit we want to test + connector_data.damage.borrow_mut().clear(); + + // Test buffer damage on rotated surface - damage entire buffer + rotation_window.surface.damage_buffer(0, 0, 4, 2)?; // Damage entire 4x2 buffer + rotation_window.surface.commit()?; + client.sync().await; + + // Verify buffer damage creates exact output damage coordinates with buffer transform applied + { + let output_damage = connector_data.damage.borrow(); + tassert!(!output_damage.is_empty()); + + // With buffer transform (90° rotation) and no viewporter: + // Original 4x2 buffer becomes 2x4 after 90° rotation + // Full buffer damage should result in full surface damage (2x4) + // This verifies that rotation transforms the buffer dimensions correctly + let expected_rotated_damage = Rect::new_sized(0, 0, 2, 4).unwrap(); // 4x2 buffer rotated to 2x4 + let expected_output_damage = + expected_rotated_damage.move_(rotation_surface_pos.x1(), rotation_surface_pos.y1()); + + // Find the exact transformed buffer damage in the output damage + let mut found_exact_rotation_damage = false; + for &actual_damage in output_damage.iter() { + if actual_damage.x1() == expected_output_damage.x1() + && actual_damage.y1() == expected_output_damage.y1() + && actual_damage.x2() == expected_output_damage.x2() + && actual_damage.y2() == expected_output_damage.y2() + { + found_exact_rotation_damage = true; + break; + } + } + + if !found_exact_rotation_damage { + run.errors + .push("Buffer transform rotation exact coordinate test failed:".to_string()); + run.errors.push(format!( + "Expected exact output damage: x1={}, y1={}, x2={}, y2={} ({}x{})", + expected_output_damage.x1(), + expected_output_damage.y1(), + expected_output_damage.x2(), + expected_output_damage.y2(), + expected_output_damage.width(), + expected_output_damage.height() + )); + run.errors.push(format!( + "Rotated surface position: x1={}, y1={}", + rotation_surface_pos.x1(), + rotation_surface_pos.y1() + )); + run.errors + .push("Applied: 4x2 buffer + 90-degree rotation (no viewporter)".to_string()); + run.errors + .push("Actual output damage from buffer transform rotation:".to_string()); + for (i, &actual_damage) in output_damage.iter().enumerate() { + run.errors.push(format!( + " [{}]: x1={}, y1={}, x2={}, y2={} ({}x{})", + i, + actual_damage.x1(), + actual_damage.y1(), + actual_damage.x2(), + actual_damage.y2(), + actual_damage.width(), + actual_damage.height() + )); + } + } + + tassert!(found_exact_rotation_damage); + } + + // Test 9: Empty damage rectangles (edge case) + connector_data.damage.borrow_mut().clear(); + window.surface.damage(0, 0, 0, 0)?; // Empty rect + window.surface.commit()?; + client.sync().await; + + // Empty damage should not crash the compositor (main requirement) + // Whether it creates output damage or not is implementation-defined + + Ok(()) +}