@@ -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 ,
0 commit comments