Reflect the video below the hinge in the tabletop layout

The iPhone Duo tabletop layout mirrors every presented frame into the
lower half, blurred and fading out, like a screen standing on a glossy
table. The render view blits a quarter-resolution copy of the display
framebuffer into a ring of IOSurface-backed buffers (no Y flip, so the
surface is already mirrored) and hands it to a CALayer hosted by
MPVReflectionView; the simulator's software renderer feeds the same
layer a flipped CGImage. Off during PiP, for audio-only streams, and
via the new Playback → Foldable → "Reflection Below Video" setting.
This commit is contained in:
Arkadiusz Fal
2026-10-01 20:59:48 +02:00
parent 2fd52fff27
commit 83895a1fd9
11 changed files with 431 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
//
// MPVReflectionLayer.swift
// Yattee
//
// Layer that shows a vertically mirrored copy of the rendered video frame,
// used for the "reflection" below the video in the iPhone Duo tabletop
// layout. The render view feeds it every presented frame.
//
import Foundation
import QuartzCore
import IOSurface
#if canImport(UIKit)
import UIKit
final class MPVReflectionLayer: CALayer {
override init() {
super.init()
contentsGravity = .resizeAspect
isOpaque = false
// Frame updates must not trigger implicit fades.
actions = ["contents": NSNull(), "bounds": NSNull(), "position": NSNull(), "transform": NSNull()]
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
/// Show a surface whose rows are already mirrored (GL blit without Y flip).
func display(_ surface: IOSurface) {
contents = surface
}
/// Show an upright image; the layer mirrors it itself.
func displayMirrored(_ image: CGImage) {
if CATransform3DIsIdentity(transform) {
transform = CATransform3DMakeScale(1, -1, 1)
}
contents = image
}
func clear() {
contents = nil
}
}
#endif