Integrate native Rime and stabilize input, output, and rendering
- Add optional modifier-only Super binding whose tap is cancelled by other input, focus changes, grabs, locks, and session teardown. - Ship the native ferese-rime user service, Rime defaults, installer inventory, package dependencies, session environment, and docs. - Vendor Smithay 8a27023 with the SHM texture-cache format fix and add GPU regressions for format changes and resize snapshots. - Release reused CRTC owners before staging output modes, then restore changed outputs before former owners when a transaction fails. - Wait for resize-capture GPU completion, expand filtered surface damage, and preserve IME popup origin under fractional scaling. - Mark weekends like holidays in both calendar layouts.
This commit is contained in:
1 parent
a2be421974
commit
993bc0f88f
391 files changed
+130908
-76
No files matched your search
@@ -1,4 +1,9 @@
|
||||
//! Private protocol clients and offscreen EGL; no host desktop or DRM required.
|
||||
#[path = "resize_scene_gpu_tests.rs"]
|
||||
mod resize_scene_gpu_tests;
|
||||
#[path = "partial_shm_gpu_tests.rs"]
|
||||
mod partial_shm_gpu_tests;
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::os::fd::{AsRawFd, RawFd};
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
//! Streaming SHM updates compared with independent full imports and full repaint.
|
||||
use super::*;
|
||||
use std::borrow::Cow;
|
||||
use std::fs::File;
|
||||
use std::os::unix::fs::FileExt;
|
||||
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::element::{Element, RenderElement};
|
||||
use smithay::backend::renderer::{Color32F, Frame, Renderer};
|
||||
use smithay::utils::{Buffer, Physical, Size};
|
||||
|
||||
const WIDTH: u32 = 193;
|
||||
const HEIGHT: u32 = 137;
|
||||
const STRIDE: u32 = (WIDTH + 7) * 4;
|
||||
const OFFSET: u32 = 64;
|
||||
|
||||
struct ReusableShm {
|
||||
file: File,
|
||||
buffer: u32,
|
||||
version: usize,
|
||||
format: u32,
|
||||
}
|
||||
|
||||
fn buffer_pixels(pixels: &[u8], format: u32, switching_formats: bool) -> Cow<'_, [u8]> {
|
||||
if !switching_formats {
|
||||
return Cow::Borrowed(pixels);
|
||||
}
|
||||
let mut encoded = pixels.to_vec();
|
||||
for pixel in encoded.chunks_exact_mut(4) {
|
||||
if format == 1 {
|
||||
// XRGB's unused high byte may legally be zero.
|
||||
pixel[3] = 0;
|
||||
} else {
|
||||
// A valid premultiplied translucent ARGB surface makes a stale
|
||||
// opaque shader variant visibly differ in RGB as well as alpha.
|
||||
for channel in &mut pixel[..3] {
|
||||
*channel = (u16::from(*channel) * 128 / 255) as u8;
|
||||
}
|
||||
pixel[3] = 128;
|
||||
}
|
||||
}
|
||||
Cow::Owned(encoded)
|
||||
}
|
||||
|
||||
fn write_region(file: &File, pixels: &[u8], rect: Rectangle<i32, Buffer>) {
|
||||
for y in rect.loc.y..rect.loc.y + rect.size.h {
|
||||
let source = (y as u32 * WIDTH + rect.loc.x as u32) as usize * 4;
|
||||
let target = u64::from(OFFSET + y as u32 * STRIDE + rect.loc.x as u32 * 4);
|
||||
file.write_all_at(&pixels[source..source + rect.size.w as usize * 4], target)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn pixels(renderer: &mut GlesRenderer, texture: &mut GlesTexture) -> Vec<u8> {
|
||||
let size = smithay::backend::renderer::Texture::size(texture);
|
||||
let target = renderer.bind(texture).unwrap();
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888)
|
||||
.unwrap();
|
||||
renderer.map_texture(&mapping).unwrap().to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a private Wayland socket and an EGL rendering device"]
|
||||
fn reused_shm_partial_damage_matches_fresh_full_uploads_at_fractional_scale() {
|
||||
const TEST: &str = "state::capture_privacy::tests::partial_shm_gpu_tests::reused_shm_partial_damage_matches_fresh_full_uploads_at_fractional_scale";
|
||||
if !crate::startup_tests::private_runtime(TEST) {
|
||||
return;
|
||||
}
|
||||
run_shm_scenarios(&[(1, None), (3, None)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a private Wayland socket and an EGL rendering device"]
|
||||
fn same_sized_argb_xrgb_switches_match_fresh_full_uploads() {
|
||||
const TEST: &str = "state::capture_privacy::tests::partial_shm_gpu_tests::same_sized_argb_xrgb_switches_match_fresh_full_uploads";
|
||||
if !crate::startup_tests::private_runtime(TEST) {
|
||||
return;
|
||||
}
|
||||
run_shm_scenarios(&[(2, Some(0)), (2, Some(1))]);
|
||||
}
|
||||
|
||||
fn run_shm_scenarios(scenarios: &[(usize, Option<u32>)]) {
|
||||
let software = std::env::var_os("FERESE_TEST_EGL_SOFTWARE").is_some();
|
||||
let device = EGLDevice::enumerate()
|
||||
.unwrap()
|
||||
.find(|device| device.is_software() == software)
|
||||
.expect("requested EGL device");
|
||||
eprintln!(
|
||||
"partial SHM EGL software={software}, render node={:?}",
|
||||
device.render_device_path()
|
||||
);
|
||||
// SAFETY: The display owns the enumerated device; each renderer below owns
|
||||
// a fresh, unshared context used only by this test thread.
|
||||
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||
let mut renderer = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
|
||||
let scale = 1.5;
|
||||
let physical: Size<i32, Physical> = (480, 360).into();
|
||||
let mut mismatching_frames = 0;
|
||||
|
||||
for &(buffer_count, first_format) in scenarios {
|
||||
let switching_formats = first_format.is_some();
|
||||
let mut events = EventLoop::try_new().unwrap();
|
||||
let config = crate::config::Config::parse_source(
|
||||
"animations { enabled #false; }\nlayout { inner-gap 4; outer-gap 4; }\nwindow-rule transient=#false floating=#true\n",
|
||||
)
|
||||
.unwrap()
|
||||
.runtime_config()
|
||||
.unwrap();
|
||||
let mut state = Ferese::new(&mut events, Display::new().unwrap(), config).unwrap();
|
||||
state.animation_test_time = Some(Duration::ZERO);
|
||||
state.focus_effect.active_opacity = 1.0;
|
||||
state.focus_effect.inactive_opacity = 1.0;
|
||||
state.focus_effect.inactive_dim = 0.0;
|
||||
state.theme_settings.window_radius = 12.0;
|
||||
let output = Output::new(
|
||||
"partial-shm".into(),
|
||||
PhysicalProperties {
|
||||
serial_number: String::new(),
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: "test".into(),
|
||||
model: "test".into(),
|
||||
},
|
||||
);
|
||||
output.change_current_state(
|
||||
Some(Mode {
|
||||
size: physical,
|
||||
refresh: 60_000,
|
||||
}),
|
||||
Some(Transform::Normal),
|
||||
Some(Scale::Fractional(scale)),
|
||||
None,
|
||||
);
|
||||
state.space.map_output(&output, (0, 0));
|
||||
state.register_output(&output, "partial-shm".into());
|
||||
let (window, mut wire) = window(&mut state, &mut events, 0xff203c5c);
|
||||
let id = state.windows.ids()[&window];
|
||||
state.focused_window = Some(id);
|
||||
state.set_floating_window_geometry(
|
||||
&window,
|
||||
(56, 64).into(),
|
||||
(WIDTH as i32, HEIGHT as i32).into(),
|
||||
);
|
||||
state.display_handle.flush_clients().unwrap();
|
||||
ack_configure(&mut wire, 3);
|
||||
let client = window.toplevel().unwrap().wl_surface().client().unwrap();
|
||||
let shm = client
|
||||
.create_resource::<WlShm, smithay::wayland::GlobalData, Ferese>(
|
||||
&state.display_handle,
|
||||
1,
|
||||
smithay::wayland::GlobalData,
|
||||
)
|
||||
.unwrap();
|
||||
let compositor = client
|
||||
.create_resource::<WlCompositor, smithay::wayland::GlobalData, Ferese>(
|
||||
&state.display_handle,
|
||||
6,
|
||||
smithay::wayland::GlobalData,
|
||||
)
|
||||
.unwrap();
|
||||
let mut raster = 0xff203c5cu32
|
||||
.to_ne_bytes()
|
||||
.repeat((WIDTH * HEIGHT) as usize);
|
||||
let full: Rectangle<i32, Buffer> =
|
||||
Rectangle::from_size((WIDTH as i32, HEIGHT as i32).into());
|
||||
let mut buffers = Vec::new();
|
||||
for slot in 0..buffer_count {
|
||||
let format = first_format.map_or(0, |first| first ^ (slot as u32 % 2));
|
||||
let file = tempfile::tempfile().unwrap();
|
||||
let len = OFFSET + STRIDE * HEIGHT;
|
||||
file.set_len(u64::from(len)).unwrap();
|
||||
// Nontrivial offset/stride/padding exercise pixel-store state too.
|
||||
file.write_all_at(&vec![0xa5; len as usize], 0).unwrap();
|
||||
write_region(
|
||||
&file,
|
||||
&buffer_pixels(&raster, format, switching_formats),
|
||||
full,
|
||||
);
|
||||
let pool = 7 + slot as u32 * 2;
|
||||
let buffer = pool + 1;
|
||||
request(
|
||||
&mut wire,
|
||||
shm.id().protocol_id(),
|
||||
0,
|
||||
&[pool, len],
|
||||
Some(file.as_raw_fd()),
|
||||
);
|
||||
request(
|
||||
&mut wire,
|
||||
pool,
|
||||
0,
|
||||
&[buffer, OFFSET, WIDTH, HEIGHT, STRIDE, format],
|
||||
None,
|
||||
);
|
||||
buffers.push(ReusableShm {
|
||||
file,
|
||||
buffer,
|
||||
version: 0,
|
||||
format,
|
||||
});
|
||||
}
|
||||
let region = 7 + buffer_count as u32 * 2;
|
||||
request(&mut wire, compositor.id().protocol_id(), 1, &[region], None);
|
||||
if switching_formats {
|
||||
// ARGB is genuinely translucent in this scenario; XRGB receives
|
||||
// automatic opacity from its declared SHM format.
|
||||
request(&mut wire, 2, 4, &[0], None);
|
||||
} else {
|
||||
request(&mut wire, region, 1, &[0, 0, WIDTH, HEIGHT], None);
|
||||
request(&mut wire, 2, 4, &[region], None);
|
||||
}
|
||||
request(&mut wire, region, 0, &[], None);
|
||||
request(&mut wire, 3, 3, &[0, 0, WIDTH, HEIGHT], None);
|
||||
request(&mut wire, 2, 1, &[buffers[0].buffer, 0, 0], None);
|
||||
request(&mut wire, 2, 9, &[0, 0, WIDTH, HEIGHT], None);
|
||||
request(&mut wire, 2, 6, &[], None);
|
||||
dispatch(&mut events, &mut state);
|
||||
state.windows.update(id, |record| record.opening = None);
|
||||
state.animations_enabled = true;
|
||||
state.advance_animations_at(Duration::from_secs(1), Duration::from_secs(1));
|
||||
let fixed_geometry = state.windows.geometry(&id).unwrap().visual.current;
|
||||
assert_eq!(window.geometry().size, (WIDTH as i32, HEIGHT as i32).into());
|
||||
|
||||
let mut tracker = OutputDamageTracker::from_output(&output);
|
||||
let mut targets: Vec<GlesTexture> = (0..3)
|
||||
.map(|_| {
|
||||
renderer
|
||||
.create_buffer(Fourcc::Abgr8888, (physical.w, physical.h).into())
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
let mut history = Vec::new();
|
||||
let mut total_changed_bytes = 0;
|
||||
let mut partial_frames = 0;
|
||||
for frame_index in 0..24 {
|
||||
let mut changed = None;
|
||||
if frame_index > 0 {
|
||||
let rect = if switching_formats {
|
||||
full
|
||||
} else {
|
||||
Rectangle::<i32, Buffer>::new(
|
||||
(
|
||||
(frame_index * 37 % (WIDTH as usize - 29)) as i32,
|
||||
(frame_index * 23 % (HEIGHT as usize - 13)) as i32,
|
||||
)
|
||||
.into(),
|
||||
(29, 13).into(),
|
||||
)
|
||||
};
|
||||
let color = 0xff000000u32
|
||||
| (((frame_index * 41) % 220 + 20) as u32) << 16
|
||||
| (((frame_index * 67) % 220 + 20) as u32) << 8
|
||||
| (((frame_index * 97) % 220 + 20) as u32);
|
||||
for y in rect.loc.y..rect.loc.y + rect.size.h {
|
||||
for x in rect.loc.x..rect.loc.x + rect.size.w {
|
||||
let at = (y as u32 * WIDTH + x as u32) as usize * 4;
|
||||
raster[at..at + 4].copy_from_slice(&color.to_ne_bytes());
|
||||
}
|
||||
}
|
||||
history.push(rect);
|
||||
let buffer = &mut buffers[frame_index % buffer_count];
|
||||
let encoded = buffer_pixels(&raster, buffer.format, switching_formats);
|
||||
// A recycled client buffer catches up only the rectangles changed
|
||||
// since its last use. Surface damage describes this frame's delta
|
||||
// from the preceding surface commit, independent of buffer age.
|
||||
for pending in &history[buffer.version..] {
|
||||
write_region(&buffer.file, &encoded, *pending);
|
||||
total_changed_bytes += pending.size.w * pending.size.h * 4;
|
||||
}
|
||||
buffer.version = history.len();
|
||||
request(&mut wire, 2, 1, &[buffer.buffer, 0, 0], None);
|
||||
request(
|
||||
&mut wire,
|
||||
2,
|
||||
9,
|
||||
&[
|
||||
rect.loc.x as u32,
|
||||
rect.loc.y as u32,
|
||||
rect.size.w as u32,
|
||||
rect.size.h as u32,
|
||||
],
|
||||
None,
|
||||
);
|
||||
request(&mut wire, 2, 6, &[], None);
|
||||
dispatch(&mut events, &mut state);
|
||||
changed = Some((rect, color));
|
||||
partial_frames += 1;
|
||||
}
|
||||
state.advance_animations_at(
|
||||
Duration::from_millis(16),
|
||||
Duration::from_millis(1016 + frame_index as u64 * 16),
|
||||
);
|
||||
let scene = state.sample_frame(&output, Duration::ZERO);
|
||||
assert_eq!(
|
||||
scene.windows[&id].presentation.bounds.current, fixed_geometry,
|
||||
"streaming moved the window"
|
||||
);
|
||||
assert_eq!(scene.windows[&id].presentation.alpha(), 1.0);
|
||||
let elements = crate::render::sampled_output_elements(
|
||||
&mut state,
|
||||
&mut renderer,
|
||||
&output,
|
||||
false,
|
||||
&scene,
|
||||
);
|
||||
assert!(
|
||||
elements
|
||||
.iter()
|
||||
.all(|element| !element.is_framebuffer_effect())
|
||||
);
|
||||
let slot = frame_index % 3;
|
||||
{
|
||||
let mut target = renderer.bind(&mut targets[slot]).unwrap();
|
||||
tracker
|
||||
.render_output(
|
||||
&mut renderer,
|
||||
&mut target,
|
||||
if frame_index < 3 { 0 } else { 3 },
|
||||
&elements,
|
||||
Color32F::new(0.035, 0.04, 0.055, 1.0),
|
||||
)
|
||||
.unwrap()
|
||||
.sync
|
||||
.wait()
|
||||
.unwrap();
|
||||
}
|
||||
let actual = pixels(&mut renderer, &mut targets[slot]);
|
||||
let cached_format = smithay::backend::renderer::utils::with_renderer_surface_state(
|
||||
window.toplevel().unwrap().wl_surface(),
|
||||
|surface| {
|
||||
surface
|
||||
.texture::<GlesTexture>(renderer.context_id())
|
||||
.and_then(smithay::backend::renderer::Texture::format)
|
||||
},
|
||||
)
|
||||
.flatten();
|
||||
|
||||
// A fresh, unshared context cannot reuse the display path's imported
|
||||
// SHM texture. Smithay creates a new texture with a complete TexImage2D
|
||||
// upload regardless of partial damage, then this oracle repaints every
|
||||
// layer without damage history or opaque-region culling.
|
||||
let mut oracle =
|
||||
unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
|
||||
let oracle_context = oracle.context_id().erased();
|
||||
let mut reference: GlesTexture = oracle
|
||||
.create_buffer(Fourcc::Abgr8888, (physical.w, physical.h).into())
|
||||
.unwrap();
|
||||
let full_elements = crate::render::sampled_output_elements(
|
||||
&mut state,
|
||||
&mut oracle,
|
||||
&output,
|
||||
false,
|
||||
&scene,
|
||||
);
|
||||
let fresh_format = smithay::backend::renderer::utils::with_renderer_surface_state(
|
||||
window.toplevel().unwrap().wl_surface(),
|
||||
|surface| {
|
||||
surface
|
||||
.texture::<GlesTexture>(oracle.context_id())
|
||||
.and_then(smithay::backend::renderer::Texture::format)
|
||||
},
|
||||
)
|
||||
.flatten();
|
||||
{
|
||||
let mut target = oracle.bind(&mut reference).unwrap();
|
||||
let mut frame = oracle
|
||||
.render(&mut target, physical, Transform::Normal)
|
||||
.unwrap();
|
||||
frame
|
||||
.clear(
|
||||
Color32F::new(0.035, 0.04, 0.055, 1.0),
|
||||
&[Rectangle::from_size(physical)],
|
||||
)
|
||||
.unwrap();
|
||||
for element in full_elements.iter().rev() {
|
||||
let rect = element.geometry(scale.into());
|
||||
element
|
||||
.draw(
|
||||
&mut frame,
|
||||
element.src(),
|
||||
rect,
|
||||
&[Rectangle::from_size(rect.size)],
|
||||
&[],
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
frame.finish().unwrap().wait().unwrap();
|
||||
}
|
||||
let expected = pixels(&mut oracle, &mut reference);
|
||||
let differing = actual
|
||||
.iter()
|
||||
.zip(&expected)
|
||||
.filter(|(a, b)| a.abs_diff(**b) > 1)
|
||||
.count();
|
||||
if differing > 0 {
|
||||
mismatching_frames += 1;
|
||||
let differences = actual
|
||||
.chunks_exact(4)
|
||||
.zip(expected.chunks_exact(4))
|
||||
.enumerate()
|
||||
.filter(|(_, (a, b))| a.iter().zip(*b).any(|(x, y)| x.abs_diff(*y) > 1))
|
||||
.map(|(at, (a, b))| {
|
||||
(
|
||||
(at % physical.w as usize, at / physical.w as usize),
|
||||
a.to_vec(),
|
||||
b.to_vec(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let bounds = differences.iter().fold(
|
||||
(usize::MAX, usize::MAX, 0, 0),
|
||||
|(l, t, r, b), ((x, y), _, _)| (l.min(*x), t.min(*y), r.max(*x), b.max(*y)),
|
||||
);
|
||||
eprintln!(
|
||||
"reused client buffers={buffer_count}, partial frame={frame_index}, different channels={differing}, pixel bounds={bounds:?}, samples={:?}",
|
||||
&differences[..differences.len().min(8)]
|
||||
);
|
||||
if switching_formats {
|
||||
let rgb_differing = actual
|
||||
.chunks_exact(4)
|
||||
.zip(expected.chunks_exact(4))
|
||||
.filter(|(a, b)| {
|
||||
a[..3].iter().zip(&b[..3]).any(|(x, y)| x.abs_diff(*y) > 1)
|
||||
})
|
||||
.count();
|
||||
eprintln!(
|
||||
"same-size SHM first_format={first_format:?} attached_format={} cached_texture_format={cached_format:?} fresh_texture_format={fresh_format:?} RGB-mismatching-pixels={rgb_differing}",
|
||||
buffers[frame_index % buffer_count].format
|
||||
);
|
||||
}
|
||||
}
|
||||
if !switching_formats && let Some((rect, color)) = changed {
|
||||
let x = ((fixed_geometry.x + f64::from(rect.loc.x) + f64::from(rect.size.w) / 2.)
|
||||
* scale) as usize;
|
||||
let y = ((fixed_geometry.y + f64::from(rect.loc.y) + f64::from(rect.size.h) / 2.)
|
||||
* scale) as usize;
|
||||
let at = (y * physical.w as usize + x) * 4;
|
||||
let cpu_rgba = [(color >> 16) as u8, (color >> 8) as u8, color as u8, 255];
|
||||
assert_eq!(
|
||||
&actual[at..at + 4],
|
||||
&cpu_rgba,
|
||||
"partial upload missed CPU raster at frame={frame_index}"
|
||||
);
|
||||
}
|
||||
state.render.forget_context(&oracle_context);
|
||||
}
|
||||
assert_eq!(partial_frames, 23);
|
||||
if !switching_formats {
|
||||
assert!(total_changed_bytes < (WIDTH * HEIGHT * 4 * partial_frames as u32) as i32);
|
||||
}
|
||||
eprintln!(
|
||||
"stable scale={scale}, reused wl_buffers={buffer_count}: 24 age-3 frames, {partial_frames} partial commits, {total_changed_bytes} locally written SHM bytes, 24 fresh-context full-upload oracle frames"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
mismatching_frames, 0,
|
||||
"partial SHM frames differ from complete fresh uploads"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
//! Exercise the display scene's damage history, independently of capture repainting.
|
||||
use super::*;
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::element::{Element, RenderElement};
|
||||
use smithay::backend::renderer::{Color32F, Frame, Renderer, Texture};
|
||||
use smithay::utils::{Physical, Size};
|
||||
|
||||
fn commit_raster(
|
||||
state: &mut Ferese,
|
||||
events: &mut EventLoop<'static, Ferese>,
|
||||
window: &Window,
|
||||
wire: &mut UnixStream,
|
||||
object: &mut u32,
|
||||
size: (u32, u32),
|
||||
phase: u32,
|
||||
) {
|
||||
let client = window.toplevel().unwrap().wl_surface().client().unwrap();
|
||||
let shm = client
|
||||
.create_resource::<WlShm, smithay::wayland::GlobalData, Ferese>(
|
||||
&state.display_handle,
|
||||
1,
|
||||
smithay::wayland::GlobalData,
|
||||
)
|
||||
.unwrap();
|
||||
let compositor = client
|
||||
.create_resource::<WlCompositor, smithay::wayland::GlobalData, Ferese>(
|
||||
&state.display_handle,
|
||||
6,
|
||||
smithay::wayland::GlobalData,
|
||||
)
|
||||
.unwrap();
|
||||
let (width, height) = size;
|
||||
// A high-contrast grid catches stale/cropped pixels, unlike a solid window.
|
||||
let mut raster = Vec::with_capacity((width * height * 4) as usize);
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let color = if (x / 13 + y / 11 + phase) % 2 == 0 {
|
||||
0xff26b976_u32
|
||||
} else {
|
||||
0xffd44381_u32
|
||||
};
|
||||
raster.extend_from_slice(&color.to_ne_bytes());
|
||||
}
|
||||
}
|
||||
let mut file = tempfile::tempfile().unwrap();
|
||||
file.write_all(&raster).unwrap();
|
||||
request(
|
||||
wire,
|
||||
shm.id().protocol_id(),
|
||||
0,
|
||||
&[*object, width * height * 4],
|
||||
Some(file.as_raw_fd()),
|
||||
);
|
||||
request(
|
||||
wire,
|
||||
*object,
|
||||
0,
|
||||
&[*object + 1, 0, width, height, width * 4, 0],
|
||||
None,
|
||||
);
|
||||
request(wire, 3, 3, &[0, 0, width, height], None);
|
||||
request(wire, 2, 1, &[*object + 1, 0, 0], None);
|
||||
// Real opaque desktop clients declare an opaque region. Without it the
|
||||
// damage tracker cannot exercise the rounded element's occlusion path.
|
||||
request(wire, compositor.id().protocol_id(), 1, &[*object + 2], None);
|
||||
request(wire, *object + 2, 1, &[0, 0, width, height], None);
|
||||
request(wire, 2, 4, &[*object + 2], None);
|
||||
request(wire, *object + 2, 0, &[], None);
|
||||
request(wire, 2, 2, &[0, 0, width, height], None);
|
||||
request(wire, 2, 6, &[], None);
|
||||
*object += 3;
|
||||
dispatch(events, state);
|
||||
}
|
||||
|
||||
fn read_pixels(renderer: &mut GlesRenderer, texture: &mut GlesTexture) -> Vec<u8> {
|
||||
let size = texture.size();
|
||||
let target = renderer.bind(texture).unwrap();
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888)
|
||||
.unwrap();
|
||||
renderer.map_texture(&mapping).unwrap().to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a private Wayland socket and an EGL rendering device"]
|
||||
fn floating_maximize_handoff_matches_full_repaint_at_buffer_age_three() {
|
||||
const TEST: &str = "state::capture_privacy::tests::resize_scene_gpu_tests::floating_maximize_handoff_matches_full_repaint_at_buffer_age_three";
|
||||
if !crate::startup_tests::private_runtime(TEST) {
|
||||
return;
|
||||
}
|
||||
let software = std::env::var_os("FERESE_TEST_EGL_SOFTWARE").is_some();
|
||||
let device = EGLDevice::enumerate()
|
||||
.unwrap()
|
||||
.find(|device| device.is_software() == software)
|
||||
.expect("requested EGL rendering device");
|
||||
eprintln!(
|
||||
"resize scene EGL software={software}, render node={:?}",
|
||||
device.render_device_path()
|
||||
);
|
||||
// SAFETY: This test owns the enumerated device and the fresh EGL context.
|
||||
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||
// SAFETY: GL work remains on this renderer's owning test thread.
|
||||
let mut renderer = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
|
||||
|
||||
for scale in [1.0, 1.5] {
|
||||
let mut events = EventLoop::try_new().unwrap();
|
||||
let config = crate::config::Config::parse_source(
|
||||
"animations { enabled #false; }\nlayout { inner-gap 4; outer-gap 4; }\nwindow-rule transient=#false floating=#true\n",
|
||||
)
|
||||
.unwrap()
|
||||
.runtime_config()
|
||||
.unwrap();
|
||||
let mut state = Ferese::new(&mut events, Display::new().unwrap(), config).unwrap();
|
||||
state.animation_test_time = Some(Duration::ZERO);
|
||||
state.focus_effect.active_opacity = 1.0;
|
||||
state.focus_effect.inactive_opacity = 1.0;
|
||||
state.focus_effect.inactive_dim = 0.0;
|
||||
state.theme_settings.window_radius = 12.0;
|
||||
let output = Output::new(
|
||||
"resize-age-three".into(),
|
||||
PhysicalProperties {
|
||||
serial_number: String::new(),
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: "test".into(),
|
||||
model: "test".into(),
|
||||
},
|
||||
);
|
||||
let physical: Size<i32, Physical> = ((320.0 * scale) as i32, (240.0 * scale) as i32).into();
|
||||
output.change_current_state(
|
||||
Some(Mode {
|
||||
size: physical,
|
||||
refresh: 60_000,
|
||||
}),
|
||||
Some(Transform::Normal),
|
||||
Some(Scale::Fractional(scale)),
|
||||
None,
|
||||
);
|
||||
state.space.map_output(&output, (0, 0));
|
||||
state.register_output(&output, "resize-age-three".into());
|
||||
let (window, mut wire) = window(&mut state, &mut events, 0xff26b976);
|
||||
let id = state.windows.ids()[&window];
|
||||
state.focused_window = Some(id);
|
||||
state.set_floating_window_geometry(&window, (106, 92).into(), (128, 80).into());
|
||||
state.display_handle.flush_clients().unwrap();
|
||||
ack_configure(&mut wire, 3);
|
||||
let mut object = 7;
|
||||
commit_raster(
|
||||
&mut state,
|
||||
&mut events,
|
||||
&window,
|
||||
&mut wire,
|
||||
&mut object,
|
||||
(128, 80),
|
||||
0,
|
||||
);
|
||||
state.windows.update(id, |record| record.opening = None);
|
||||
state.animations_enabled = true;
|
||||
|
||||
let mut tracker = OutputDamageTracker::from_output(&output);
|
||||
let mut targets: Vec<GlesTexture> = (0..3)
|
||||
.map(|_| {
|
||||
renderer
|
||||
.create_buffer(Fourcc::Abgr8888, (physical.w, physical.h).into())
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
let mut reference: GlesTexture = renderer
|
||||
.create_buffer(Fourcc::Abgr8888, (physical.w, physical.h).into())
|
||||
.unwrap();
|
||||
let mut frame_index = 0;
|
||||
let mut now = Duration::ZERO;
|
||||
let mut snapshot_frames = 0;
|
||||
let mut fill_frames = 0;
|
||||
let mut moving_frames = 0;
|
||||
let mut repaint_frames = 0;
|
||||
let mut native_draw_frames = 0;
|
||||
let mut opaque_frames = 0;
|
||||
|
||||
// Delayed commits exercise both old-frame cropping on shrink and neutral
|
||||
// fill on grow. Repeating in both directions also reuses every target.
|
||||
for (cycle, delay) in [3, 1, 5, 0].into_iter().enumerate() {
|
||||
state.set_window_maximized(id, cycle % 2 == 0);
|
||||
state.display_handle.flush_clients().unwrap();
|
||||
let (_, configured) = ack_configure_with_size(&mut wire, 3, Some(4));
|
||||
let requested = configured.expect("maximize/restore configure");
|
||||
for tick in 0..28 {
|
||||
if tick == delay {
|
||||
// The offscreen fixture has no DirectBackend. Invoke the
|
||||
// same capture-before-release operation used by that backend.
|
||||
let snapshot = crate::render::capture_resize_snapshot(
|
||||
&mut renderer,
|
||||
&window,
|
||||
window.geometry(),
|
||||
scale,
|
||||
32 * 1024 * 1024,
|
||||
)
|
||||
.unwrap()
|
||||
.expect("old client pixels");
|
||||
state.render.set_snapshot(id, snapshot);
|
||||
commit_raster(
|
||||
&mut state,
|
||||
&mut events,
|
||||
&window,
|
||||
&mut wire,
|
||||
&mut object,
|
||||
requested,
|
||||
cycle as u32 + 1,
|
||||
);
|
||||
repaint_frames += 1;
|
||||
} else if tick > delay && tick % 2 == 0 {
|
||||
// IDE repainting alternates with unchanged client buffers,
|
||||
// so both client damage and compositor-only motion matter.
|
||||
commit_raster(
|
||||
&mut state,
|
||||
&mut events,
|
||||
&window,
|
||||
&mut wire,
|
||||
&mut object,
|
||||
requested,
|
||||
cycle as u32 + tick as u32,
|
||||
);
|
||||
repaint_frames += 1;
|
||||
}
|
||||
now += Duration::from_millis(16);
|
||||
state.advance_animations_at(Duration::from_millis(16), now);
|
||||
let scene = state.sample_frame(&output, Duration::from_millis(5));
|
||||
let sample = &scene.windows[&id];
|
||||
assert_eq!(sample.presentation.alpha(), 1.0, "ordinary opaque window");
|
||||
if sample.geometry.visual.current != sample.geometry.visual.target {
|
||||
moving_frames += 1;
|
||||
}
|
||||
snapshot_frames += usize::from(state.render.snapshot(&id).is_some());
|
||||
let source = window.geometry().size;
|
||||
let visual = sample.presentation.bounds.current;
|
||||
fill_frames += usize::from(
|
||||
f64::from(source.w) < visual.width || f64::from(source.h) < visual.height,
|
||||
);
|
||||
let elements = crate::render::sampled_output_elements(
|
||||
&mut state,
|
||||
&mut renderer,
|
||||
&output,
|
||||
false,
|
||||
&scene,
|
||||
);
|
||||
native_draw_frames +=
|
||||
usize::from(state.render.snapshot(&id).is_some_and(|snapshot| {
|
||||
elements.iter().any(|element| element.id() == &snapshot.id)
|
||||
}));
|
||||
opaque_frames += usize::from(
|
||||
elements
|
||||
.iter()
|
||||
.any(|element| !element.opaque_regions(scale.into()).is_empty()),
|
||||
);
|
||||
assert!(
|
||||
elements
|
||||
.iter()
|
||||
.all(|element| !element.is_framebuffer_effect()),
|
||||
"this regression must cover opaque resize without backdrop blur"
|
||||
);
|
||||
let slot = frame_index % 3;
|
||||
{
|
||||
let mut target = renderer.bind(&mut targets[slot]).unwrap();
|
||||
tracker
|
||||
.render_output(
|
||||
&mut renderer,
|
||||
&mut target,
|
||||
if frame_index < 3 { 0 } else { 3 },
|
||||
&elements,
|
||||
Color32F::new(0.035, 0.04, 0.055, 1.0),
|
||||
)
|
||||
.unwrap()
|
||||
.sync
|
||||
.wait()
|
||||
.unwrap();
|
||||
}
|
||||
// Independent oracle: no damage history, no opaque-region
|
||||
// culling, and every layer painted back-to-front into a clean FBO.
|
||||
{
|
||||
let mut target = renderer.bind(&mut reference).unwrap();
|
||||
let mut frame = renderer
|
||||
.render(&mut target, physical, Transform::Normal)
|
||||
.unwrap();
|
||||
frame
|
||||
.clear(
|
||||
Color32F::new(0.035, 0.04, 0.055, 1.0),
|
||||
&[Rectangle::from_size(physical)],
|
||||
)
|
||||
.unwrap();
|
||||
for element in elements.iter().rev() {
|
||||
let rect = element.geometry(scale.into());
|
||||
element
|
||||
.draw(
|
||||
&mut frame,
|
||||
element.src(),
|
||||
rect,
|
||||
&[Rectangle::from_size(rect.size)],
|
||||
&[],
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
frame.finish().unwrap().wait().unwrap();
|
||||
}
|
||||
let actual = read_pixels(&mut renderer, &mut targets[slot]);
|
||||
let expected = read_pixels(&mut renderer, &mut reference);
|
||||
let differing = actual
|
||||
.iter()
|
||||
.zip(&expected)
|
||||
.filter(|(a, b)| a.abs_diff(**b) > 1)
|
||||
.count();
|
||||
assert_eq!(
|
||||
differing, 0,
|
||||
"scale={scale} cycle={cycle} tick={tick} frame={frame_index} source={source:?} visual={visual:?}"
|
||||
);
|
||||
frame_index += 1;
|
||||
}
|
||||
}
|
||||
assert!(snapshot_frames > 20, "resize handoff was not exercised");
|
||||
assert!(
|
||||
fill_frames > 0,
|
||||
"growing native content never needed resize fill"
|
||||
);
|
||||
eprintln!(
|
||||
"opaque resize scale={scale}: {frame_index} age-3 frames, {snapshot_frames} snapshot frames, {native_draw_frames} snapshot draw frames, {fill_frames} fill frames, {moving_frames} moving frames, {repaint_frames} client repaint frames, {opaque_frames} opaque frames"
|
||||
);
|
||||
assert!(moving_frames > 20, "maximize/restore animation did not run");
|
||||
assert!(
|
||||
repaint_frames > 40,
|
||||
"client repainting did not overlap motion"
|
||||
);
|
||||
assert!(
|
||||
native_draw_frames > 20,
|
||||
"retained snapshots were not in the display scene"
|
||||
);
|
||||
assert!(
|
||||
opaque_frames > 20,
|
||||
"opaque-region culling was not exercised"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user