Skip to content

Commit 8e2fac6

Browse files
committed
Introduce automatic pixel shifting
The panel in the touchbar is an OLED screen and pixels on it will suffer degradation over time. Degradation effects are going to be worst on bright, white pixels that remain at the same position forever. This applies to the icons of our buttons. To lower the possibilty of a burn-in here as much as we can, introduce some subtle pixel shifting. Here's how it works: - Use the existing TIMEOUT_MS poll() timeout and (temporarily) allow smaller timeouts for smooth sub-pixel transitions to make the pixel shift completely invisible - We have lots of space in the horizontal direction, so there we shift pixels by the whole amount that's necessary to ensure no pixel remains "always on" - The background boxes around each button are also shifted in horizontal direction as otherwise the icons appear off-center horizontally, for this the boxes need to be reduced in width a bit so they don't overflow the panel - In the vertical direction, icons appear off-center on the whole touchbar really quickly, so only do very limited movement here, that's not very effective but better than nothing - Hitboxes for touch input remain the same and are not affected by pixel shifting This starts a pixel shift by one full pixel in x and y direction every 10 seconds, transitioning slowly between the states on a sub-pixel level for 4 seconds. Once the pixel shift reaches the final left or right edge of the shifting area, we'll leave it at that for 50 seconds to reduce stress on pixels in the center of the shifting area a bit more.
1 parent 7960118 commit 8e2fac6

5 files changed

Lines changed: 149 additions & 14 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ input-linux = "0.6"
2020
input-linux-sys = "0.8"
2121
nix = "0.26"
2222
privdrop = "0.5.3"
23+
rand = "0.8.5"

src/backlight.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use input::event::{
1010
};
1111
use crate::TIMEOUT_MS;
1212

13-
const BRIGHTNESS_DIM_TIMEOUT: i32 = TIMEOUT_MS * 1; // should be a multiple of TIMEOUT_MS
14-
const BRIGHTNESS_OFF_TIMEOUT: i32 = TIMEOUT_MS * 2; // should be a multiple of TIMEOUT_MS
13+
const BRIGHTNESS_DIM_TIMEOUT: i32 = TIMEOUT_MS * 3; // should be a multiple of TIMEOUT_MS
14+
const BRIGHTNESS_OFF_TIMEOUT: i32 = TIMEOUT_MS * 6; // should be a multiple of TIMEOUT_MS
1515
const DEFAULT_BRIGHTNESS: u32 = 128;
1616
const DIMMED_BRIGHTNESS: u32 = 1;
1717

src/main.rs

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,18 @@ use privdrop::PrivDrop;
3030

3131
mod backlight;
3232
mod display;
33+
mod pixel_shift;
3334

3435
use backlight::BacklightManager;
3536
use display::DrmBackend;
37+
use pixel_shift::PixelShiftManager;
38+
use pixel_shift::PIXEL_SHIFT_WIDTH_PX;
3639

3740
const DFR_WIDTH: i32 = 2008;
3841
const DFR_HEIGHT: i32 = 64;
3942
const BUTTON_COLOR_INACTIVE: f64 = 0.0;
4043
const BUTTON_COLOR_ACTIVE: f64 = 0.400;
41-
const TIMEOUT_MS: i32 = 30 * 1000;
44+
const TIMEOUT_MS: i32 = 10 * 1000;
4245

4346
enum ButtonImage {
4447
Text(&'static str),
@@ -62,12 +65,12 @@ impl Button {
6265
action, image: ButtonImage::Svg(svg)
6366
}
6467
}
65-
fn render(&self, c: &Context, left_edge: f64, button_width: f64) {
68+
fn render(&self, c: &Context, button_left_edge: f64, button_width: f64, y_shift: f64) {
6669
match &self.image {
6770
ButtonImage::Text(text) => {
6871
let extents = c.text_extents(text).unwrap();
6972
c.move_to(
70-
left_edge + button_width / 2.0 - extents.width() / 2.0,
73+
button_left_edge + button_width / 2.0 - extents.width() / 2.0,
7174
DFR_HEIGHT as f64 / 2.0 + extents.height() / 2.0
7275
);
7376
c.show_text(text).unwrap();
@@ -76,9 +79,10 @@ impl Button {
7679
let renderer = CairoRenderer::new(&svg);
7780
let y = 0.12 * DFR_HEIGHT as f64;
7881
let size = DFR_HEIGHT as f64 - y * 2.0;
79-
let x = left_edge + button_width / 2.0 - size / 2.0;
82+
let x = button_left_edge + button_width / 2.0 - size / 2.0;
83+
8084
renderer.render_document(c,
81-
&Rectangle::new(x, y, size, size)
85+
&Rectangle::new(x, y + y_shift, size, size)
8286
).unwrap();
8387
}
8488
}
@@ -90,21 +94,23 @@ struct FunctionLayer {
9094
}
9195

9296
impl FunctionLayer {
93-
fn draw(&self, surface: &Surface, active_buttons: &[bool]) {
97+
fn draw(&self, surface: &Surface, active_buttons: &[bool], pixel_shift: (f64, f64)) {
9498
let c = Context::new(&surface).unwrap();
9599
c.translate(DFR_HEIGHT as f64, 0.0);
96100
c.rotate((90.0f64).to_radians());
97-
let button_width = DFR_WIDTH as f64 / (self.buttons.len() + 1) as f64;
98-
let spacing_width = (DFR_WIDTH as f64 - self.buttons.len() as f64 * button_width) / (self.buttons.len() - 1) as f64;
101+
let button_width = (DFR_WIDTH as u64 - PIXEL_SHIFT_WIDTH_PX) as f64 / (self.buttons.len() + 1) as f64;
102+
let spacing_width = ((DFR_WIDTH as u64 - PIXEL_SHIFT_WIDTH_PX) as f64 - self.buttons.len() as f64 * button_width) / (self.buttons.len() - 1) as f64;
99103
let radius = 8.0f64;
100104
let bot = (DFR_HEIGHT as f64) * 0.2;
101105
let top = (DFR_HEIGHT as f64) * 0.85;
106+
let (pixel_shift_x, pixel_shift_y) = pixel_shift;
107+
102108
c.set_source_rgb(0.0, 0.0, 0.0);
103109
c.paint().unwrap();
104110
c.select_font_face("sans-serif", FontSlant::Normal, FontWeight::Bold);
105111
c.set_font_size(32.0);
106112
for (i, button) in self.buttons.iter().enumerate() {
107-
let left_edge = i as f64 * (button_width + spacing_width);
113+
let left_edge = i as f64 * (button_width + spacing_width) + pixel_shift_x + (PIXEL_SHIFT_WIDTH_PX / 2) as f64;
108114
let color = if active_buttons[i] { BUTTON_COLOR_ACTIVE } else { BUTTON_COLOR_INACTIVE };
109115
c.set_source_rgb(color, color, color);
110116
// draw box with rounded corners
@@ -143,7 +149,7 @@ impl FunctionLayer {
143149

144150
c.fill().unwrap();
145151
c.set_source_rgb(1.0, 1.0, 1.0);
146-
button.render(&c, left_edge, button_width);
152+
button.render(&c, left_edge, button_width, pixel_shift_y);
147153
}
148154
}
149155
}
@@ -198,6 +204,7 @@ fn toggle_key<F>(uinput: &mut UInputHandle<F>, code: Key, value: i32) where F: A
198204
fn main() {
199205
let mut uinput = UInputHandle::new(OpenOptions::new().write(true).open("/dev/uinput").unwrap());
200206
let mut backlight = BacklightManager::new();
207+
let mut pixel_shift = PixelShiftManager::new();
201208

202209
// drop privileges to input and video group
203210
let groups = ["input", "video"];
@@ -280,14 +287,23 @@ fn main() {
280287
let mut digitizer: Option<InputDevice> = None;
281288
let mut touches = HashMap::new();
282289
loop {
290+
let mut next_timeout_ms = TIMEOUT_MS;
291+
292+
let (pixel_shift_needs_redraw, pixel_shift_next_timeout_ms) = pixel_shift.update_pixel_shift();
293+
if pixel_shift_needs_redraw {
294+
needs_redraw = true; }
295+
if pixel_shift_next_timeout_ms != -1 {
296+
next_timeout_ms = pixel_shift_next_timeout_ms; }
297+
283298
if needs_redraw {
284299
needs_redraw = false;
285-
layers[active_layer].draw(&surface, &button_states[active_layer]);
300+
layers[active_layer].draw(&surface, &button_states[active_layer], pixel_shift.get_pixel_shift());
286301
let data = surface.data().unwrap();
287302
drm.map().unwrap().as_mut()[..data.len()].copy_from_slice(&data);
288303
drm.dirty(&[ClipRect{x1: 0, y1: 0, x2: DFR_HEIGHT as u16, y2: DFR_WIDTH as u16}]).unwrap();
289304
}
290-
poll(&mut [pollfd_tb, pollfd_main], TIMEOUT_MS).unwrap();
305+
306+
poll(&mut [pollfd_tb, pollfd_main], next_timeout_ms).unwrap();
291307
input_tb.dispatch().unwrap();
292308
input_main.dispatch().unwrap();
293309
for event in &mut input_tb.clone().chain(input_main.clone()) {

src/pixel_shift.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use rand::Rng;
2+
use std::{
3+
time::Instant,
4+
};
5+
use crate::TIMEOUT_MS;
6+
7+
const INTERVAL_MS: i32 = TIMEOUT_MS * 1; // should be a multiple of TIMEOUT_MS
8+
const PROLONGED_INTERVAL_MS: i32 = TIMEOUT_MS * 5; // should be a multiple of TIMEOUT_MS and more than INTERVAL_MS
9+
const ANIMATION_INTERVAL_MS: i32 = 200; // should be less than TIMEOUT_MS
10+
const ANIMATION_DURATION_MS: i32 = 4000; // should be a multiple of ANIMATION_INTERVAL_MS
11+
12+
// This is the total range on the x-axis that pixels will shift by over time, ie. they will shift by
13+
// PIXEL_SHIFT_WIDTH_PX / 2 to the right and to the left.
14+
// To make sure that no pixel ends up being always on, the minimum value to be safe here is the
15+
// size of the largest continuous colored line in the x-direction. The higher this value, the less
16+
// strain is put on the panel.
17+
pub const PIXEL_SHIFT_WIDTH_PX: u64 = 22; // should be divisible by 2
18+
// in y direction we can't really shift by a lot since icons still need to appear centered,
19+
// 2 pixels in each direction seems to be the maximum before it gets really visible.
20+
const PIXEL_SHIFT_HEIGHT_PX: u64 = 4; // should be divisible by 2
21+
22+
fn get_pixel_shift(x_progress: f64, y_constant: f64) -> (f64, f64) {
23+
let half_width = (PIXEL_SHIFT_WIDTH_PX / 2) as f64;
24+
let half_height = (PIXEL_SHIFT_HEIGHT_PX / 2) as f64;
25+
let mut shift_x = x_progress % (PIXEL_SHIFT_WIDTH_PX * 2) as f64;
26+
if shift_x <= half_width {
27+
shift_x = shift_x;
28+
} else if shift_x <= half_width * 2.0 {
29+
shift_x = half_width - (shift_x - half_width);
30+
} else if shift_x <= half_width * 3.0 {
31+
shift_x = 0.0 - (shift_x - half_width * 2.0);
32+
} else if shift_x <= half_width * 4.0 {
33+
shift_x = -half_width + (shift_x - half_width * 3.0);
34+
}
35+
36+
let mut shift_y = (shift_x.abs() + y_constant) % (PIXEL_SHIFT_HEIGHT_PX * 2) as f64;
37+
if shift_y <= half_height {
38+
shift_y = shift_y;
39+
} else if shift_y <= half_height * 2.0 {
40+
shift_y = half_height - (shift_y - half_height);
41+
} else if shift_y <= half_height * 3.0 {
42+
shift_y = 0.0 - (shift_y - half_height * 2.0);
43+
} else if shift_y <= half_height * 4.0 {
44+
shift_y = -half_height + (shift_y - half_height * 3.0);
45+
}
46+
47+
(shift_x, shift_y)
48+
}
49+
50+
pub struct PixelShiftManager {
51+
last_active: Instant,
52+
x_progress: f64,
53+
y_constant: f64,
54+
in_animation: bool,
55+
in_prolonged_timeout: bool,
56+
}
57+
58+
impl PixelShiftManager {
59+
pub fn new() -> PixelShiftManager {
60+
let x_progress: f64 = rand::thread_rng().gen_range(0..PIXEL_SHIFT_WIDTH_PX * 2) as f64;
61+
62+
// add some randomness to the relationship between shifting on the x and y axis
63+
// so that pixel shifting doesn't follow the same 2d pattern every time
64+
let y_constant: f64 = rand::thread_rng().gen_range(0..PIXEL_SHIFT_HEIGHT_PX * 2) as f64;
65+
66+
PixelShiftManager {
67+
last_active: Instant::now(),
68+
x_progress,
69+
y_constant,
70+
in_animation: false,
71+
in_prolonged_timeout: false
72+
}
73+
}
74+
75+
pub fn update_pixel_shift(&mut self) -> (bool, i32) {
76+
let mut pixels_changed = false;
77+
let mut next_timeout_ms = -1;
78+
let time_now = Instant::now();
79+
let since_last_pixel_shift = (time_now - self.last_active).as_millis() as i32;
80+
81+
if (self.in_animation && since_last_pixel_shift >= ANIMATION_INTERVAL_MS) ||
82+
(self.in_prolonged_timeout && since_last_pixel_shift >= PROLONGED_INTERVAL_MS) ||
83+
(!self.in_animation && !self.in_prolonged_timeout && since_last_pixel_shift >= INTERVAL_MS) {
84+
if ANIMATION_INTERVAL_MS == 0 || ANIMATION_DURATION_MS == 0 {
85+
self.x_progress += 1.0;
86+
} else {
87+
self.x_progress += ANIMATION_INTERVAL_MS as f64 / ANIMATION_DURATION_MS as f64;
88+
}
89+
self.last_active = time_now;
90+
pixels_changed = true;
91+
92+
if (self.x_progress % 1.0).abs() <= 0.01 || (self.x_progress % 1.0).abs() >= 0.99 {
93+
self.x_progress = self.x_progress.round();
94+
self.in_animation = false;
95+
self.in_prolonged_timeout = false;
96+
//println!("finished pixel shift, now {:?}", get_pixel_shift(self.x_progress, self.y_constant));
97+
98+
if self.x_progress as u64 % (PIXEL_SHIFT_WIDTH_PX * 2) == 0 {
99+
self.x_progress = 0.0;
100+
} else if self.x_progress as u64 % (PIXEL_SHIFT_WIDTH_PX) == 0 {
101+
} else if self.x_progress as u64 % (PIXEL_SHIFT_WIDTH_PX / 2) == 0 {
102+
self.in_prolonged_timeout = true;
103+
//println!("pixel shift reached left or right edge, prolonging timeout");
104+
}
105+
} else {
106+
next_timeout_ms = ANIMATION_INTERVAL_MS;
107+
self.in_animation = true;
108+
}
109+
}
110+
111+
(pixels_changed, next_timeout_ms)
112+
}
113+
114+
pub fn get_pixel_shift(&self) -> (f64, f64) {
115+
get_pixel_shift(self.x_progress, self.y_constant)
116+
}
117+
}

0 commit comments

Comments
 (0)