atagen

monolith

public
Star0
Watch0

Commit 2e852b236c45

tarball

atagen <boss@atagen.co>2026-10-19 08:38 UTC

7 files changed+250-140
render: draw every element at the output's scale

Scene, layers, popups, the lock surface, the cursor, the drag icon and capture all hard-coded scale 1, so a fractional output scale would have drawn logical sizes as pixels. Window rects are snapped to the physical grid by edge, borders keep one width on every side, and the damage trackers for capture now take the target's scale.
Modified file: crates/monolith/src/capture.rs+39 additions−9 deletions
@@ -166,11 +166,30 @@ impl State {
166166 let mode = output.current_mode()?;
167167 output.current_transform().transform_size(mode.size).into()
168168 }
169- Target::Window(id) => self.wm.window(*id)?.geometry().size.into(),
169+ Target::Window(id) => {
170+ let size = self.wm.window(*id)?.geometry().size;
171+ size.to_physical_precise_round::<_, i32>(self.target_scale(target))
172+ .into()
173+ }
170174 };
171175 (size.0 > 0 && size.1 > 0).then_some(size)
172176 }
173177
178+ /// An output's own scale, and a window's is the one of the output it is on.
179+ fn target_scale(&self, target: &Target) -> f64 {
180+ let output = match target {
181+ Target::Output(name) => self.output_named(name).map(|(_, output)| output),
182+ Target::Window(id) => self
183+ .wm
184+ .get(*id)
185+ .and_then(|s| s.placed)
186+ .map(|(_, _, o)| o)
187+ .or_else(|| self.wm.layout.window(*id)?.output)
188+ .and_then(|o| self.backend.output(o)),
189+ };
190+ output.map_or(1.0, |o| o.current_scale().fractional_scale())
191+ }
192+
174193 fn constraints(&self, size: (i32, i32)) -> BufferConstraints {
175194 let dma = self.backend.render_node().map(|node| {
176195 let mut formats: Vec<(Fourcc, Vec<_>)> = Vec::new();
@@ -203,6 +222,7 @@ impl State {
203222 frame.fail(CaptureFailureReason::BufferConstraints);
204223 return;
205224 }
225+ let scale = self.target_scale(&target);
206226 let elements = match &target {
207227 Target::Output(name) => {
208228 let Some((o, output)) = self.output_named(name) else {
@@ -225,7 +245,7 @@ impl State {
225245 }
226246 Target::Window(id) => match self.wm.window(*id) {
227247 Some(_) if self.services.lock.active() => Vec::new(),
228- Some(window) => window_elements(&mut self.renderer, window),
248+ Some(window) => window_elements(&mut self.renderer, window, scale),
229249 None => {
230250 frame.fail(CaptureFailureReason::Stopped);
231251 return;
@@ -234,8 +254,8 @@ impl State {
234254 };
235255 let buffer = frame.buffer();
236256 let result = match get_dmabuf(&buffer) {
237- Ok(dmabuf) => self.capture_dmabuf(dmabuf.clone(), size, &elements),
238- Err(_) => self.capture_shm(&buffer, size, &elements),
257+ Ok(dmabuf) => self.capture_dmabuf(dmabuf.clone(), size, scale, &elements),
258+ Err(_) => self.capture_shm(&buffer, size, scale, &elements),
239259 };
240260 match result {
241261 Ok(()) => {
@@ -253,10 +273,11 @@ impl State {
253273 &mut self,
254274 mut dmabuf: Dmabuf,
255275 size: (i32, i32),
276+ scale: f64,
256277 elements: &[Draw],
257278 ) -> Result<(), Box<dyn std::error::Error>> {
258279 let mut fb = self.renderer.bind(&mut dmabuf)?;
259- let mut tracker = OutputDamageTracker::new(size, 1.0, Transform::Normal);
280+ let mut tracker = OutputDamageTracker::new(size, scale, Transform::Normal);
260281 let result = tracker.render_output(&mut self.renderer, &mut fb, 0, elements, CLEAR)?;
261282 result.sync.wait()?;
262283 Ok(())
@@ -266,13 +287,14 @@ impl State {
266287 &mut self,
267288 buffer: &WlBuffer,
268289 size: (i32, i32),
290+ scale: f64,
269291 elements: &[Draw],
270292 ) -> Result<(), Box<dyn std::error::Error>> {
271293 let format = with_buffer_contents_mut(buffer, |_, _, data| data.format)?;
272294 let fourcc = shm_format_to_fourcc(format).ok_or("unsupported shm format")?;
273295 let mut image: VulkanImage = self.renderer.create_buffer(fourcc, size.into())?;
274296 let mut fb = self.renderer.bind(&mut image)?;
275- let mut tracker = OutputDamageTracker::new(size, 1.0, Transform::Normal);
297+ let mut tracker = OutputDamageTracker::new(size, scale, Transform::Normal);
276298 let result = tracker.render_output(&mut self.renderer, &mut fb, 0, elements, CLEAR)?;
277299 result.sync.wait()?;
278300 let region = Rectangle::from_size(Size::from(size));
@@ -302,10 +324,18 @@ impl State {
302324 }
303325
304326 /// A window alone, with its popups, its geometry at the origin.
305-fn window_elements(renderer: &mut VulkanRenderer, window: &smithay::desktop::Window) -> Vec<Draw> {
306- let at = Point::from((-window.geometry().loc.x, -window.geometry().loc.y));
327+fn window_elements(
328+ renderer: &mut VulkanRenderer,
329+ window: &smithay::desktop::Window,
330+ scale: f64,
331+) -> Vec<Draw> {
332+ let inset = window
333+ .geometry()
334+ .loc
335+ .to_physical_precise_round::<_, i32>(scale);
336+ let at = Point::from((-inset.x, -inset.y));
307337 let elems: Vec<WaylandSurfaceRenderElement<VulkanRenderer>> =
308- window.render_elements(renderer, at, Scale::from(1.0), 1.0);
338+ window.render_elements(renderer, at, Scale::from(scale), 1.0);
309339 elems.into_iter().map(Draw::Surface).collect()
310340 }
311341
Modified file: crates/monolith/src/cursor.rs+12 additions−6 deletions
@@ -1,7 +1,7 @@
11 //! The pointer image on bare metal, where nothing else draws one. A client
22 //! cursor surface wins, otherwise the xcursor theme's default arrow.
33
4-use crate::scene::Draw;
4+use crate::scene::{Draw, to_physical};
55 use monolith_render_vk::VulkanRenderer;
66 use smithay::backend::allocator::Fourcc;
77 use smithay::backend::renderer::element::memory::{
@@ -87,10 +87,11 @@ impl Cursor {
8787 renderer: &mut VulkanRenderer,
8888 status: &CursorImageStatus,
8989 pos: Point<f64, Logical>,
90+ scale: f64,
9091 ) -> Vec<Draw> {
9192 let at = |hotspot: Point<i32, Logical>| -> Point<i32, Physical> {
9293 let p = pos - hotspot.to_f64();
93- (p.x.round() as i32, p.y.round() as i32).into()
94+ to_physical((p.x, p.y), scale)
9495 };
9596 match status {
9697 CursorImageStatus::Hidden => Vec::new(),
@@ -105,7 +106,7 @@ impl Cursor {
105106 renderer,
106107 surface,
107108 at(hotspot),
108- 1.0,
109+ scale,
109110 1.0,
110111 Kind::Cursor,
111112 )
@@ -162,13 +163,18 @@ pub struct DndIcon {
162163
163164 impl DndIcon {
164165 /// Elements for the icon with the pointer at `pos`, in output-local coordinates.
165- pub fn elements(&self, renderer: &mut VulkanRenderer, pos: Point<f64, Logical>) -> Vec<Draw> {
166+ pub fn elements(
167+ &self,
168+ renderer: &mut VulkanRenderer,
169+ pos: Point<f64, Logical>,
170+ scale: f64,
171+ ) -> Vec<Draw> {
166172 let p = pos + self.offset.to_f64();
167173 render_elements_from_surface_tree(
168174 renderer,
169175 &self.surface,
170- (p.x.round() as i32, p.y.round() as i32),
171- 1.0,
176+ to_physical((p.x, p.y), scale),
177+ scale,
172178 1.0,
173179 Kind::Unspecified,
174180 )
Modified file: crates/monolith/src/frame.rs+7 additions−2 deletions
@@ -38,6 +38,7 @@ pub fn compose(
3838 cursor: bool,
3939 ) -> Vec<Draw> {
4040 let mut elements = Vec::new();
41+ let scale = output.current_scale().fractional_scale();
4142 if let Some(area) = ctx.wm.layout.output(o).map(|out| out.area) {
4243 let pos = ctx.input.pointer_pos;
4344 if area.contains(pos.x, pos.y) {
@@ -45,10 +46,14 @@ pub fn compose(
4546 if cursor {
4647 let resize = ctx.input.resize_icon().map(CursorImageStatus::Named);
4748 let status = resize.as_ref().unwrap_or(&ctx.input.cursor_status);
48- elements.extend(ctx.input.cursor.elements(ctx.renderer, status, local));
49+ elements.extend(
50+ ctx.input
51+ .cursor
52+ .elements(ctx.renderer, status, local, scale),
53+ );
4954 }
5055 if let Some(icon) = &ctx.input.dnd_icon {
51- elements.extend(icon.elements(ctx.renderer, local));
56+ elements.extend(icon.elements(ctx.renderer, local, scale));
5257 }
5358 }
5459 }
Modified file: crates/monolith/src/layers.rs+8 additions−5 deletions
@@ -1,7 +1,7 @@
11 //! wlr-layer-shell, for bars, launchers, wallpapers and notifications.
22
33 use crate::focus::Focus;
4-use crate::scene::Draw;
4+use crate::scene::{Draw, to_physical};
55 use crate::state::State;
66 use monolith_layout::{Insets, OutputId};
77 use monolith_render_vk::VulkanRenderer;
@@ -255,24 +255,25 @@ impl State {
255255
256256 /// Draws an output's layer surfaces on `layer`, front to back.
257257 pub fn elements(out: &mut Vec<Draw>, renderer: &mut VulkanRenderer, output: &Output, layer: Layer) {
258+ let scale = output.current_scale().fractional_scale();
258259 let map = layer_map_for_output(output);
259260 for l in map.layers_on(layer).rev() {
260261 let Some(geo) = map.layer_geometry(l) else {
261262 continue;
262263 };
263- let at = geo.loc.to_physical(1);
264+ let at = to_physical((geo.loc.x.into(), geo.loc.y.into()), scale);
264265 let elems: Vec<WaylandSurfaceRenderElement<VulkanRenderer>> =
265266 if matches!(layer, Layer::Bottom | Layer::Background) {
266267 render_elements_from_surface_tree(
267268 renderer,
268269 l.wl_surface(),
269270 at,
270- 1.0,
271+ scale,
271272 1.0,
272273 Kind::Unspecified,
273274 )
274275 } else {
275- l.render_elements(renderer, at, Scale::from(1.0), 1.0)
276+ l.render_elements(renderer, at, Scale::from(scale), 1.0)
276277 };
277278 out.extend(elems.into_iter().map(Draw::Surface));
278279 }
@@ -281,11 +282,13 @@ pub fn elements(out: &mut Vec<Draw>, renderer: &mut VulkanRenderer, output: &Out
281282 /// The popups of the layer surfaces below the windows. Those surfaces draw
282283 /// without them, so a menu is not hidden by the window it opens over.
283284 pub fn lower_popups(out: &mut Vec<Draw>, renderer: &mut VulkanRenderer, output: &Output) {
285+ let scale = output.current_scale().fractional_scale();
284286 let map = layer_map_for_output(output);
285287 for layer in [Layer::Bottom, Layer::Background] {
286288 for l in map.layers_on(layer).rev() {
287289 if let Some(geo) = map.layer_geometry(l) {
288- crate::popups::elements(out, renderer, l.wl_surface(), geo.loc, 1.0);
290+ let at = to_physical((geo.loc.x.into(), geo.loc.y.into()), scale);
291+ crate::popups::elements(out, renderer, l.wl_surface(), at, scale, 1.0);
289292 }
290293 }
291294 }
Modified file: crates/monolith/src/lock.rs+1 additions−1 deletions
@@ -176,7 +176,7 @@ pub fn elements(lock: &Lock, renderer: &mut VulkanRenderer, output: &Output) ->
176176 renderer,
177177 surface.wl_surface(),
178178 (0, 0),
179- 1.0,
179+ output.current_scale().fractional_scale(),
180180 1.0,
181181 Kind::Unspecified,
182182 )
Modified file: crates/monolith/src/popups.rs+23 additions−13 deletions
@@ -128,13 +128,15 @@ pub fn placed(
128128 root: &WlSurface,
129129 geometry_at: Point<i32, Logical>,
130130 ) -> Vec<(WlSurface, Point<i32, Logical>)> {
131+ offsets(root)
132+ .into_iter()
133+ .map(|(popup, at)| (popup, geometry_at + at))
134+ .collect()
135+}
136+
137+fn offsets(root: &WlSurface) -> Vec<(WlSurface, Point<i32, Logical>)> {
131138 PopupManager::popups_for_surface(root)
132- .map(|(popup, at)| {
133- (
134- popup.wl_surface().clone(),
135- geometry_at + at - popup.geometry().loc,
136- )
137- })
139+ .map(|(popup, at)| (popup.wl_surface().clone(), at - popup.geometry().loc))
138140 .collect()
139141 }
140142
@@ -150,20 +152,28 @@ pub fn hit(
150152 })
151153 }
152154
153-/// Draws `root`'s popups with its window geometry at `geometry_at`.
155+/// Draws `root`'s popups with its window geometry at `geometry_at` pixels.
154156 pub fn elements(
155157 out: &mut Vec<Draw>,
156158 renderer: &mut VulkanRenderer,
157159 root: &WlSurface,
158- geometry_at: Point<i32, Logical>,
160+ geometry_at: Point<i32, Physical>,
161+ scale: f64,
159162 alpha: f32,
160163 ) {
161- for (popup, at) in placed(root, geometry_at) {
162- let at = Point::<i32, Physical>::from((at.x, at.y));
164+ for (popup, offset) in offsets(root) {
165+ let at = geometry_at + crate::scene::to_physical((offset.x.into(), offset.y.into()), scale);
163166 out.extend(
164- render_elements_from_surface_tree(renderer, &popup, at, 1.0, alpha, Kind::Unspecified)
165- .into_iter()
166- .map(Draw::Surface),
167+ render_elements_from_surface_tree(
168+ renderer,
169+ &popup,
170+ at,
171+ scale,
172+ alpha,
173+ Kind::Unspecified,
174+ )
175+ .into_iter()
176+ .map(Draw::Surface),
167177 );
168178 }
169179 }
Modified file: crates/monolith/src/scene.rs+160 additions−104 deletions
@@ -9,12 +9,11 @@ use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement
99 use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
1010 use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
1111 use smithay::backend::renderer::element::surface::render_elements_from_surface_tree;
12-use smithay::backend::renderer::element::texture::TextureRenderElement;
1312 use smithay::backend::renderer::element::utils::{CropRenderElement, RescaleRenderElement};
1413 use smithay::backend::renderer::element::{AsRenderElements, Element, Id, Kind, RenderElement};
1514 use smithay::backend::renderer::utils::with_renderer_surface_state;
1615 use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
17-use smithay::backend::renderer::{ContextId, Renderer, Texture};
16+use smithay::backend::renderer::{Frame, Renderer, Texture};
1817 use smithay::desktop::Window;
1918 use smithay::output::Output;
2019 use smithay::utils::user_data::UserDataMap;
@@ -26,8 +25,7 @@ smithay::backend::renderer::element::render_elements! {
2625 pub Draw<=VulkanRenderer>;
2726 Surface=WaylandSurfaceRenderElement<VulkanRenderer>,
2827 Cropped=CropRenderElement<WaylandSurfaceRenderElement<VulkanRenderer>>,
29- Texture=TextureRenderElement<VulkanTexture>,
30- Effect=EffectElement,
28+ Texture=PlacedTexture,
3129 Border=SolidColorRenderElement,
3230 Memory=MemoryRenderBufferRenderElement<VulkanRenderer>,
3331 Rescaled=RescaleRenderElement<WaylandSurfaceRenderElement<VulkanRenderer>>,
@@ -54,6 +52,7 @@ pub fn build(
5452 let Some(area) = layout.output(o).map(|out| out.area) else {
5553 return Vec::new();
5654 };
55+ let scale = output.map_or(1.0, |o| o.current_scale().fractional_scale());
5756 let local = |r: Rect| Rect::new(r.x - area.x, r.y - area.y, r.w, r.h);
5857 let mut ghosts = Vec::new();
5958 let mut tiled = Vec::new();
@@ -107,11 +106,21 @@ pub fn build(
107106 continue;
108107 };
109108 let loc = x.geometry().loc;
110- let at = Point::<i32, Physical>::from((loc.x - area.x as i32, loc.y - area.y as i32));
109+ let at = to_physical(
110+ (f64::from(loc.x) - area.x, f64::from(loc.y) - area.y),
111+ scale,
112+ );
111113 out.extend(
112- render_elements_from_surface_tree(renderer, &surface, at, 1.0, 1.0, Kind::Unspecified)
113- .into_iter()
114- .map(Draw::Surface),
114+ render_elements_from_surface_tree(
115+ renderer,
116+ &surface,
117+ at,
118+ scale,
119+ 1.0,
120+ Kind::Unspecified,
121+ )
122+ .into_iter()
123+ .map(Draw::Surface),
115124 );
116125 }
117126 if let Some(output) = output {
@@ -128,11 +137,18 @@ pub fn build(
128137 let Some(root) = windows.get(id).and_then(|s| s.window.toplevel()) else {
129138 continue;
130139 };
131- let Some(((x, y), _)) = pixels(r) else {
140+ let Some(((x, y), _)) = pixels(r, scale) else {
132141 continue;
133142 };
134143 let alpha = anim.effect(id, now).map_or(1.0, |e| e.progress);
135- crate::popups::elements(&mut out, renderer, root.wl_surface(), (x, y).into(), alpha);
144+ crate::popups::elements(
145+ &mut out,
146+ renderer,
147+ root.wl_surface(),
148+ (x, y).into(),
149+ scale,
150+ alpha,
151+ );
136152 }
137153 if let Some(output) = output
138154 && !fullscreen_at_rest
@@ -140,7 +156,13 @@ pub fn build(
140156 crate::layers::lower_popups(&mut out, renderer, output);
141157 }
142158 for (g, frame, r) in fading.into_iter().rev() {
143- out.extend(ghost_element(frame, r, anim.ghost_effect(g, now), deco));
159+ out.extend(ghost_element(
160+ frame,
161+ r,
162+ anim.ghost_effect(g, now),
163+ deco,
164+ scale,
165+ ));
144166 }
145167 for (id, r, framed, gliding) in tiled.into_iter().chain(above).rev() {
146168 let Some(state) = windows.get_mut(id) else {
@@ -148,8 +170,8 @@ pub fn build(
148170 };
149171 let effect = anim.effect(id, now);
150172 let first = out.len();
151- window_elements(&mut out, renderer, &state.window, r, effect, gliding);
152- let clip = framed.then(|| corner_clip(r, deco)).flatten();
173+ window_elements(&mut out, renderer, &state.window, r, effect, gliding, scale);
174+ let clip = framed.then(|| corner_clip(r, deco, scale)).flatten();
153175 if let Some(clip) = clip {
154176 clip_window(&mut out, first, clip);
155177 }
@@ -164,14 +186,20 @@ pub fn build(
164186 let alpha = effect.map_or(1.0, |e| e.progress);
165187 let borders = &mut state.borders;
166188 let first = out.len();
167- border_elements(&mut out, borders, r, deco, color, alpha);
189+ border_elements(&mut out, borders, r, deco, color, alpha, scale);
168190 if let Some(clip) = clip {
169- ring_borders(&mut out, first, clip, deco.border as i32);
191+ ring_borders(&mut out, first, clip, border_width(deco, scale));
170192 }
171193 }
172194 }
173195 for (g, frame, r) in ghosts.into_iter().rev() {
174- out.extend(ghost_element(frame, r, anim.ghost_effect(g, now), deco));
196+ out.extend(ghost_element(
197+ frame,
198+ r,
199+ anim.ghost_effect(g, now),
200+ deco,
201+ scale,
202+ ));
175203 }
176204 if let Some(output) = output {
177205 crate::layers::elements(&mut out, renderer, output, Layer::Bottom);
@@ -191,42 +219,53 @@ fn window_elements(
191219 r: Rect,
192220 effect: Option<Effect>,
193221 scaled: bool,
222+ scale: f64,
194223 ) {
195- let Some(((x, y), (w, h))) = pixels(r) else {
224+ let Some(((x, y), (w, h))) = pixels(r, scale) else {
196225 return;
197226 };
198227 let geo = window.geometry();
199- let loc = Point::<i32, Physical>::from((x - geo.loc.x, y - geo.loc.y));
228+ let inset = geo.loc.to_physical_precise_round::<_, i32>(scale);
229+ let drawn: Size<i32, Physical> = geo.size.to_physical_precise_round(scale);
230+ let loc = Point::<i32, Physical>::from((x - inset.x, y - inset.y));
200231 let alpha = effect.map_or(1.0, |e| e.progress);
201232 let elems: Vec<WaylandSurfaceRenderElement<VulkanRenderer>> = match window.toplevel() {
202233 Some(t) => render_elements_from_surface_tree(
203234 renderer,
204235 t.wl_surface(),
205236 loc,
206- 1.0,
237+ scale,
207238 alpha,
208239 Kind::Unspecified,
209240 ),
210- None => window.render_elements(renderer, loc, Scale::from(1.0), alpha),
241+ None => window.render_elements(renderer, loc, Scale::from(scale), alpha),
211242 };
212243 if let Some(effect) = effect {
213- effect_elements(out, renderer, window, elems, ((x, y), (w, h)), effect);
244+ effect_elements(
245+ out,
246+ renderer,
247+ window,
248+ elems,
249+ ((x, y), (w, h)),
250+ effect,
251+ scale,
252+ );
214253 return;
215254 }
216- if scaled && geo.size.w > 0 && geo.size.h > 0 {
217- let scale = Scale::from((
218- f64::from(w) / f64::from(geo.size.w),
219- f64::from(h) / f64::from(geo.size.h),
255+ if scaled && drawn.w > 0 && drawn.h > 0 {
256+ let factor = Scale::from((
257+ f64::from(w) / f64::from(drawn.w),
258+ f64::from(h) / f64::from(drawn.h),
220259 ));
221260 let origin = Point::<i32, Physical>::from((x, y));
222261 out.extend(
223262 elems
224263 .into_iter()
225- .map(|e| Draw::Rescaled(RescaleRenderElement::from_element(e, origin, scale))),
264+ .map(|e| Draw::Rescaled(RescaleRenderElement::from_element(e, origin, factor))),
226265 );
227266 return;
228267 }
229- if (w, h) == (geo.size.w, geo.size.h) {
268+ if (w, h) == (drawn.w, drawn.h) {
230269 out.extend(elems.into_iter().map(Draw::Surface));
231270 return;
232271 }
@@ -234,14 +273,14 @@ fn window_elements(
234273 out.extend(
235274 elems
236275 .into_iter()
237- .filter_map(|e| CropRenderElement::from_element(e, 1.0, crop))
276+ .filter_map(|e| CropRenderElement::from_element(e, scale, crop))
238277 .map(Draw::Cropped),
239278 );
240279 let Some(root) = crate::wm::root(window) else {
241280 return;
242281 };
243282 let context = renderer.context_id();
244- let Some((tex, scale, transform)) = with_renderer_surface_state(&root, |s| {
283+ let Some((tex, buffer_scale, transform)) = with_renderer_surface_state(&root, |s| {
245284 s.texture::<VulkanTexture>(context.clone())
246285 .cloned()
247286 .map(|t| (t, s.buffer_scale(), s.buffer_transform()))
@@ -249,28 +288,33 @@ fn window_elements(
249288 .flatten() else {
250289 return;
251290 };
252- let (gw, gh) = (geo.size.w, geo.size.h);
291+ let (gw, gh) = (drawn.w, drawn.h);
253292 let (cw, ch) = (w.min(gw), h.min(gh));
254- let (gx, gy) = (geo.loc.x as f64, geo.loc.y as f64);
293+ let (gx, gy) = (f64::from(geo.loc.x), f64::from(geo.loc.y));
255294 let mut strip = |src: Rectangle<f64, Logical>, at: (i32, i32), size: (i32, i32)| {
256295 if size.0 >= 1 && size.1 >= 1 {
257- out.push(texture(&context, &tex, scale, transform, src, at, size));
296+ let bounds = tex.size().to_logical(buffer_scale, transform).to_f64();
297+ let src = src.to_buffer(f64::from(buffer_scale), transform, &bounds);
298+ out.push(Draw::Texture(PlacedTexture::new(
299+ &tex, transform, src, at, size, None,
300+ )));
258301 }
259302 };
260- let right = gx + f64::from(gw) - 1.0;
261- let bottom = gy + f64::from(gh) - 1.0;
303+ let pixel = 1.0 / f64::from(buffer_scale);
304+ let right = gx + f64::from(geo.size.w) - pixel;
305+ let bottom = gy + f64::from(geo.size.h) - pixel;
262306 strip(
263- Rectangle::new((right, gy).into(), (1.0, f64::from(ch)).into()),
307+ Rectangle::new((right, gy).into(), (pixel, f64::from(ch) / scale).into()),
264308 (x + gw, y),
265309 (w - gw, ch),
266310 );
267311 strip(
268- Rectangle::new((gx, bottom).into(), (f64::from(cw), 1.0).into()),
312+ Rectangle::new((gx, bottom).into(), (f64::from(cw) / scale, pixel).into()),
269313 (x, y + gh),
270314 (cw, h - gh),
271315 );
272316 strip(
273- Rectangle::new((right, bottom).into(), (1.0, 1.0).into()),
317+ Rectangle::new((right, bottom).into(), (pixel, pixel).into()),
274318 (x + gw, y + gh),
275319 (w - gw, h - gh),
276320 );
@@ -286,12 +330,13 @@ fn border_elements(
286330 deco: &Decoration,
287331 color: [f32; 4],
288332 alpha: f32,
333+ scale: f64,
289334 ) {
290- let Some(((x, y), (w, h))) = pixels(r) else {
335+ let Some(((x, y), (w, h))) = pixels(r, scale) else {
291336 return;
292337 };
293- let b = deco.border as i32;
294- let reach = deco.radius.0.into_iter().max().unwrap_or(0) as i32;
338+ let b = border_width(deco, scale);
339+ let reach = (f64::from(deco.radius.0.into_iter().max().unwrap_or(0)) * scale).round() as i32;
295340 let reach = reach.min(w / 2).min(h / 2);
296341 let strips = [
297342 ((x - b, y - b), (w + 2 * b, b + reach)),
@@ -313,37 +358,25 @@ fn border_elements(
313358 }
314359 }
315360
316-/// Snaps a logical rect to whole pixels by rounding its edges, so neighbours
317-/// that share an edge stay flush.
318-fn pixels(r: Rect) -> Option<((i32, i32), (i32, i32))> {
319- let (x, y) = (r.x.round() as i32, r.y.round() as i32);
320- let w = r.right().round() as i32 - x;
321- let h = r.bottom().round() as i32 - y;
361+/// Snaps a logical rect to whole physical pixels by rounding its edges, so
362+/// neighbours that share an edge stay flush.
363+fn pixels(r: Rect, scale: f64) -> Option<((i32, i32), (i32, i32))> {
364+ let (x, y) = ((r.x * scale).round() as i32, (r.y * scale).round() as i32);
365+ let w = (r.right() * scale).round() as i32 - x;
366+ let h = (r.bottom() * scale).round() as i32 - y;
322367 (w >= 1 && h >= 1).then_some(((x, y), (w, h)))
323368 }
324369
325-fn texture(
326- context: &ContextId<VulkanTexture>,
327- texture: &VulkanTexture,
328- scale: i32,
329- transform: Transform,
330- src: Rectangle<f64, Logical>,
331- at: (i32, i32),
332- size: (i32, i32),
333-) -> Draw {
334- Draw::Texture(TextureRenderElement::from_static_texture(
335- Id::new(),
336- context.clone(),
337- Point::<f64, Physical>::from((f64::from(at.0), f64::from(at.1))),
338- texture.clone(),
339- scale,
340- transform,
341- None,
342- Some(src),
343- Some(Size::from(size)),
344- None,
345- Kind::Unspecified,
346- ))
370+pub fn to_physical(p: (f64, f64), scale: f64) -> Point<i32, Physical> {
371+ ((p.0 * scale).round() as i32, (p.1 * scale).round() as i32).into()
372+}
373+
374+/// The border width in physical pixels, the same on every side of a window.
375+fn border_width(deco: &Decoration, scale: f64) -> i32 {
376+ match deco.border {
377+ 0 => 0,
378+ b => ((f64::from(b) * scale).round() as i32).max(1),
379+ }
347380 }
348381
349382 /// A window in a spawn transition. Its root surface goes through the effect
@@ -355,6 +388,7 @@ fn effect_elements(
355388 elems: Vec<WaylandSurfaceRenderElement<VulkanRenderer>>,
356389 (at, size): ((i32, i32), (i32, i32)),
357390 effect: Effect,
391+ scale: f64,
358392 ) {
359393 let Some(root) = crate::wm::root(window) else {
360394 return;
@@ -372,46 +406,52 @@ fn effect_elements(
372406 elems
373407 .into_iter()
374408 .filter(|e| root_tex.is_none() || *e.id() != root_id)
375- .filter_map(|e| CropRenderElement::from_element(e, 1.0, crop))
409+ .filter_map(|e| CropRenderElement::from_element(e, scale, crop))
376410 .map(Draw::Cropped),
377411 );
378- if let Some((tex, scale, transform)) = root_tex {
412+ if let Some((tex, buffer_scale, transform)) = root_tex {
379413 let geo = window.geometry();
380414 let src = Rectangle::new(
381415 geo.loc.to_f64(),
382- (f64::from(size.0), f64::from(size.1)).into(),
416+ (f64::from(size.0) / scale, f64::from(size.1) / scale).into(),
383417 );
384- out.push(Draw::Effect(EffectElement::new(
385- &tex, scale, transform, src, at, size, effect,
418+ let bounds = tex.size().to_logical(buffer_scale, transform).to_f64();
419+ let src = src.to_buffer(f64::from(buffer_scale), transform, &bounds);
420+ out.push(Draw::Texture(PlacedTexture::new(
421+ &tex,
422+ transform,
423+ src,
424+ at,
425+ size,
426+ Some(effect),
386427 )));
387428 }
388429 }
389430
390-/// A texture drawn through the transition hook in the effect shader.
391-pub struct EffectElement {
431+/// A texture at a physical rectangle, drawn plainly or through the
432+/// transition hook in the effect shader.
433+pub struct PlacedTexture {
392434 id: Id,
393435 texture: VulkanTexture,
394436 src: Rectangle<f64, Buffer>,
395437 geometry: Rectangle<i32, Physical>,
396438 transform: Transform,
397- effect: Effect,
439+ effect: Option<Effect>,
398440 }
399441
400-impl EffectElement {
442+impl PlacedTexture {
401443 fn new(
402444 texture: &VulkanTexture,
403- scale: i32,
404445 transform: Transform,
405- src: Rectangle<f64, Logical>,
446+ src: Rectangle<f64, Buffer>,
406447 at: (i32, i32),
407448 size: (i32, i32),
408- effect: Effect,
449+ effect: Option<Effect>,
409450 ) -> Self {
410- let bounds = texture.size().to_logical(scale, transform).to_f64();
411451 Self {
412452 id: Id::new(),
413453 texture: texture.clone(),
414- src: src.to_buffer(f64::from(scale), transform, &bounds),
454+ src,
415455 geometry: Rectangle::new(at.into(), size.into()),
416456 transform,
417457 effect,
@@ -419,7 +459,7 @@ impl EffectElement {
419459 }
420460 }
421461
422-impl Element for EffectElement {
462+impl Element for PlacedTexture {
423463 fn id(&self) -> &Id {
424464 &self.id
425465 }
@@ -441,59 +481,75 @@ impl Element for EffectElement {
441481 }
442482 }
443483
444-impl RenderElement<VulkanRenderer> for EffectElement {
484+impl RenderElement<VulkanRenderer> for PlacedTexture {
445485 fn draw(
446486 &self,
447487 frame: &mut VulkanFrame<'_, '_>,
448488 src: Rectangle<f64, Buffer>,
449489 dst: Rectangle<i32, Physical>,
450490 damage: &[Rectangle<i32, Physical>],
451- _opaque_regions: &[Rectangle<i32, Physical>],
491+ opaque_regions: &[Rectangle<i32, Physical>],
452492 _cache: Option<&UserDataMap>,
453493 ) -> Result<(), monolith_render_vk::Error> {
454- frame.render_effect(
455- &self.texture,
456- src,
457- dst,
458- damage,
459- self.transform,
460- 1.0,
461- self.effect,
462- )
494+ match self.effect {
495+ Some(effect) => {
496+ frame.render_effect(&self.texture, src, dst, damage, self.transform, 1.0, effect)
497+ }
498+ None => frame.render_texture_from_to(
499+ &self.texture,
500+ src,
501+ dst,
502+ damage,
503+ opaque_regions,
504+ self.transform,
505+ 1.0,
506+ ),
507+ }
463508 }
464509 }
465510
466-fn ghost_element(frame: &LastFrame, r: Rect, effect: Effect, deco: &Decoration) -> Option<Draw> {
467- let (at, size) = pixels(r)?;
511+fn ghost_element(
512+ frame: &LastFrame,
513+ r: Rect,
514+ effect: Effect,
515+ deco: &Decoration,
516+ scale: f64,
517+) -> Option<Draw> {
518+ let (at, size) = pixels(r, scale)?;
468519 let src = Rectangle::new(
469520 frame.geometry.loc.to_f64(),
470- (f64::from(size.0), f64::from(size.1)).into(),
521+ (f64::from(size.0) / scale, f64::from(size.1) / scale).into(),
471522 );
472- let ghost = Draw::Effect(EffectElement::new(
523+ let bounds = frame
524+ .texture
525+ .size()
526+ .to_logical(frame.scale, frame.transform)
527+ .to_f64();
528+ let src = src.to_buffer(f64::from(frame.scale), frame.transform, &bounds);
529+ let ghost = Draw::Texture(PlacedTexture::new(
473530 &frame.texture,
474- frame.scale,
475531 frame.transform,
476532 src,
477533 at,
478534 size,
479- effect,
535+ Some(effect),
480536 ));
481- Some(match corner_clip(r, deco) {
537+ Some(match corner_clip(r, deco, scale) {
482538 Some(clip) => Draw::Clipped(ClipElement::new(ghost, clip)),
483539 None => ghost,
484540 })
485541 }
486542
487543 /// The rounded outline of a framed window at `r`, if it has one.
488-fn corner_clip(r: Rect, deco: &Decoration) -> Option<Clip> {
544+fn corner_clip(r: Rect, deco: &Decoration, scale: f64) -> Option<Clip> {
489545 let radii = deco.radius.0;
490546 if radii == [0; 4] {
491547 return None;
492548 }
493- let (at, size) = pixels(r)?;
549+ let (at, size) = pixels(r, scale)?;
494550 Some(Clip {
495551 rect: Rectangle::new(at.into(), size.into()),
496- radii: radii.map(|r| r as f32),
552+ radii: radii.map(|r| r as f32 * scale as f32),
497553 ring: 0.0,
498554 })
499555 }