Skip to content

Commit 1b28a11

Browse files
authored
Merge pull request #459 from cosmic-utils/fix/blur-rotation-aspect-letterbox
fix(blur): correct aspect on rotated sensors and paint letterbox
2 parents 8148312 + 82726b9 commit 1b28a11

8 files changed

Lines changed: 93 additions & 5 deletions

File tree

src/app/camera_preview/widget.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ impl AppModel {
125125
let zoom_level = self.current_zoom_level();
126126
let scroll_zoom_enabled = self.mode.supports_fit_and_zoom();
127127

128+
// Look up the COSMIC theme's window background color. The blur
129+
// shader paints letterbox areas with this so the preview's
130+
// out-of-image region matches the app background instead of
131+
// letting the window bg leak through (issue: white letterbox
132+
// during Fit-mode blur transitions).
133+
let bg = cosmic::theme::active().cosmic().bg_color();
134+
let letterbox_color = [bg.red, bg.green, bg.blue, 1.0];
135+
128136
let video_elem = video_widget::video_widget(
129137
frame.clone(),
130138
video_widget::VideoWidgetConfig {
@@ -140,6 +148,7 @@ impl AppModel {
140148
cover_blend: Some(cover_blend),
141149
bar_top_px: self.top_ui_height(),
142150
bar_bottom_px: self.bottom_ui_height(),
151+
letterbox_color,
143152
},
144153
);
145154

src/app/filter_picker/view.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ impl AppModel {
8888
cover_blend: None,
8989
bar_top_px: 0.0,
9090
bar_bottom_px: 0.0,
91+
// Filter previews don't use blur, so this is only here
92+
// to satisfy the struct — value is ignored downstream.
93+
letterbox_color: [0.0, 0.0, 0.0, 1.0],
9194
},
9295
)
9396
} else {

src/app/handlers/capture.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,16 @@ impl AppModel {
559559
"Burst mode frames collected, starting processing"
560560
);
561561

562+
// Capture blur state up front so the HDR+ processing blur shows the
563+
// burst still frame with the rotation/mirror of the camera that
564+
// produced it. Without this, `blur_frame_rotation` keeps whatever
565+
// value was set the last time a transition fired — or `None` if the
566+
// user never switched cameras this session — and the burst frame
567+
// renders rotated wrong on 90°/270° sensors. Mirrors the existing
568+
// capture in `handle_burst_mode_complete`.
569+
self.blur_frame_rotation = self.current_frame_rotation;
570+
self.blur_frame_mirror = self.should_mirror_preview();
571+
562572
// Turn off flash now that capture is complete (before processing)
563573
self.turn_off_flash_hardware();
564574
if self.flash.active {

src/app/video_primitive.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,17 @@ struct ViewportUniform {
120120
bar_top_height: f32,
121121
/// Bottom bar height in pixels
122122
bar_bottom_height: f32,
123+
/// Padding to align `letterbox_color` (vec4) to 16 bytes — required for the
124+
/// struct to round-trip through wgpu's std140-ish uniform layout. Two
125+
/// `f32`s after the three trailing `f32` fields above bring us to offset
126+
/// 80, which is a vec4 boundary.
127+
_pad0: f32,
128+
_pad1: f32,
129+
/// Theme background color (RGBA, sRGB straight) used by the blur shader
130+
/// to fill letterbox areas instead of returning transparent (which would
131+
/// let the COSMIC window background show through during Fit-mode blur
132+
/// transitions). Other shaders accept the field but ignore it.
133+
letterbox_color: [f32; 4],
123134
}
124135

125136
/// Combined frame and viewport data to reduce mutex contention
@@ -156,6 +167,10 @@ pub struct VideoPrimitive {
156167
pub crop_uv: Option<(f32, f32, f32, f32)>,
157168
/// Zoom level (1.0 = no zoom, 2.0 = 2x zoom, etc.)
158169
pub zoom_level: f32,
170+
/// Theme background color (sRGB straight, RGBA) — passed to the blur
171+
/// shader so the letterbox in Contain / Fit mode is painted with the
172+
/// app background instead of leaking through to the COSMIC window bg.
173+
pub letterbox_color: [f32; 4],
159174
}
160175

161176
/// Video texture (shared across filter variations)
@@ -272,6 +287,10 @@ impl VideoPrimitive {
272287
rotation: 0,
273288
crop_uv: None,
274289
zoom_level: 1.0,
290+
// Black is a sensible default if no widget overrides it (e.g.
291+
// headless tests). The real bg color is plumbed in via
292+
// `VideoWidgetConfig::letterbox_color` from the active theme.
293+
letterbox_color: [0.0, 0.0, 0.0, 1.0],
275294
}
276295
}
277296

@@ -396,14 +415,28 @@ impl PrimitiveTrait for VideoPrimitive {
396415

397416
// For blur video (VIDEO_ID_BLUR), ensure intermediate textures exist
398417
// and invalidate the blur cache so the new frame gets blurred.
418+
//
419+
// The blur pass 1 shader is configured with viewport_size in
420+
// **display orientation** (sensor w/h swapped for 90°/270°
421+
// rotation) so its Contain math computes the right aspect.
422+
// The intermediate render target must match that orientation
423+
// — otherwise the shader thinks it's drawing into a portrait
424+
// viewport while wgpu rasterises to a landscape target, and
425+
// the blur frame comes out stretched on rotated sensors
426+
// (visible on the Pixel 3a / 90° mount).
399427
if self.video_id == VIDEO_ID_BLUR {
400428
pipeline
401429
.blur_cached
402430
.store(false, std::sync::atomic::Ordering::Relaxed);
431+
let (int_w, int_h) = if self.rotation == 1 || self.rotation == 3 {
432+
(frame.height, frame.width)
433+
} else {
434+
(frame.width, frame.height)
435+
};
403436
pipeline.ensure_intermediate_textures(
404437
device,
405-
frame.width,
406-
frame.height,
438+
int_w,
439+
int_h,
407440
pipeline.output_format,
408441
);
409442
}
@@ -484,6 +517,9 @@ impl PrimitiveTrait for VideoPrimitive {
484517
rotation: self.rotation,
485518
bar_top_height: 0.0,
486519
bar_bottom_height: 0.0,
520+
_pad0: 0.0,
521+
_pad1: 0.0,
522+
letterbox_color: self.letterbox_color,
487523
};
488524
queue.write_buffer(
489525
&binding.viewport_buffer,
@@ -507,6 +543,9 @@ impl PrimitiveTrait for VideoPrimitive {
507543
rotation: self.rotation,
508544
bar_top_height: bar_top,
509545
bar_bottom_height: bar_bottom,
546+
_pad0: 0.0,
547+
_pad1: 0.0,
548+
letterbox_color: self.letterbox_color,
510549
};
511550
queue.write_buffer(
512551
&binding.viewport_buffer,
@@ -586,6 +625,9 @@ impl PrimitiveTrait for VideoPrimitive {
586625
rotation: 0, // Already rotated in preblur
587626
bar_top_height: 0.0,
588627
bar_bottom_height: 0.0,
628+
_pad0: 0.0,
629+
_pad1: 0.0,
630+
letterbox_color: self.letterbox_color,
589631
};
590632
queue.write_buffer(
591633
&pb_binding.viewport_buffer,
@@ -614,6 +656,9 @@ impl PrimitiveTrait for VideoPrimitive {
614656
rotation: 0, // Already rotated in pass 1
615657
bar_top_height: 0.0,
616658
bar_bottom_height: 0.0,
659+
_pad0: 0.0,
660+
_pad1: 0.0,
661+
letterbox_color: self.letterbox_color,
617662
};
618663
queue.write_buffer(
619664
&intermediate_1.viewport_buffer,
@@ -639,6 +684,9 @@ impl PrimitiveTrait for VideoPrimitive {
639684
rotation: 0, // Already rotated in pass 1
640685
bar_top_height: bar_top,
641686
bar_bottom_height: bar_bottom,
687+
_pad0: 0.0,
688+
_pad1: 0.0,
689+
letterbox_color: self.letterbox_color,
642690
};
643691
queue.write_buffer(
644692
&intermediate_2.viewport_buffer,

src/app/video_shader.wgsl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ struct ViewportUniform {
2222
rotation: u32, // Sensor rotation: 0=None, 1=90CW, 2=180, 3=270CW
2323
bar_top_height: f32, // Top bar height in pixels (for contain centering)
2424
bar_bottom_height: f32, // Bottom bar height in pixels
25+
_pad0: f32, // pad to vec4 alignment for letterbox_color
26+
_pad1: f32,
27+
letterbox_color: vec4<f32>, // RGBA — only used by the blur pass; declared here so the struct matches
2528
}
2629

2730
@group(0) @binding(2)

src/app/video_shader_blur.wgsl

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ struct ViewportUniform {
2121
rotation: u32, // Sensor rotation: 0=None, 1=90CW, 2=180, 3=270CW
2222
bar_top_height: f32, // Top bar height in pixels
2323
bar_bottom_height: f32, // Bottom bar height in pixels
24+
_pad0: f32, // pad to vec4 alignment for letterbox_color
25+
_pad1: f32,
26+
letterbox_color: vec4<f32>, // RGBA fill for letterbox in blur pass (alpha unused)
2427
}
2528

2629
@group(0) @binding(2)
@@ -117,10 +120,14 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
117120
tex_coords = (tex_coords - pivot) * scale + vec2<f32>(0.5, 0.5);
118121
}
119122

120-
// Discard letterbox before the crop remap — see video_shader.wgsl for the
121-
// rationale (post-remap check can falsely keep fragments inside the crop).
123+
// For the blur backdrop, paint letterbox with the theme's background
124+
// color (opaque) instead of discarding to transparent — the previous
125+
// discard let the COSMIC window background show through in Contain /
126+
// Fit mode. Return *before* the crop remap so we can short-circuit;
127+
// the regular post-remap letterbox check is unnecessary here because
128+
// anything out of [0,1] is letterbox by definition.
122129
if (tex_coords.x < 0.0 || tex_coords.x > 1.0 || tex_coords.y < 0.0 || tex_coords.y > 1.0) {
123-
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
130+
return vec4<f32>(viewport.letterbox_color.rgb, 1.0);
124131
}
125132

126133
// Apply the blended crop remap

src/app/video_shader_preblur.wgsl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ struct ViewportUniform {
3131
rotation: u32,
3232
bar_top_height: f32,
3333
bar_bottom_height: f32,
34+
_pad0: f32,
35+
_pad1: f32,
36+
letterbox_color: vec4<f32>, // unused here; struct must match the shared ViewportUniform
3437
}
3538

3639
@group(0) @binding(2)

src/app/video_widget.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ pub struct VideoWidgetConfig {
8181
/// Bottom UI bar height in pixels — matches the actual UI footprint
8282
/// (capture button + bottom bar + optional zoom row).
8383
pub bar_bottom_px: f32,
84+
/// Theme background color (sRGB straight, RGBA). Used by the blur shader
85+
/// to fill the letterbox in Contain / Fit mode instead of returning
86+
/// transparent — otherwise the COSMIC window background leaks through.
87+
pub letterbox_color: [f32; 4],
8488
}
8589

8690
/// Video widget that renders camera frames using a custom GPU primitive
@@ -114,6 +118,7 @@ impl VideoWidget {
114118
primitive.rotation = config.rotation;
115119
primitive.crop_uv = config.crop_uv;
116120
primitive.zoom_level = config.zoom_level;
121+
primitive.letterbox_color = config.letterbox_color;
117122

118123
// Calculate aspect ratio from frame dimensions, adjusted for crop and rotation
119124
// For 90° and 270° rotations, swap width and height

0 commit comments

Comments
 (0)