feat(apple): iPad ⌃⌥⇧Q release chord + click-to-recapture, pixel-grid snap, match-window opt-in
ci / docs-site (push) Successful in 49s
ci / rust (push) Failing after 51s
ci / web (push) Successful in 52s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
decky / build-publish (push) Successful in 16s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
deb / build-publish (push) Failing after 4m20s
apple / swift (push) Successful in 4m43s
arch / build-publish (push) Failing after 4m56s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 4m8s
ci / bench (push) Successful in 5m43s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6m21s
docker / deploy-docs (push) Successful in 21s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 3m33s
windows-host / package (push) Successful in 9m2s
flatpak / build-publish (push) Failing after 8m11s
android / android (push) Successful in 12m4s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m54s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m39s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 5m3s
release / apple (push) Successful in 21m56s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 5m31s
apple / screenshots (push) Successful in 19m11s
ci / docs-site (push) Successful in 49s
ci / rust (push) Failing after 51s
ci / web (push) Successful in 52s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
decky / build-publish (push) Successful in 16s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
deb / build-publish (push) Failing after 4m20s
apple / swift (push) Successful in 4m43s
arch / build-publish (push) Failing after 4m56s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 4m8s
ci / bench (push) Successful in 5m43s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6m21s
docker / deploy-docs (push) Successful in 21s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 3m33s
windows-host / package (push) Successful in 9m2s
flatpak / build-publish (push) Failing after 8m11s
android / android (push) Successful in 12m4s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m54s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m39s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 5m3s
release / apple (push) Successful in 21m56s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 5m31s
apple / screenshots (push) Successful in 19m11s
- InputCapture / StreamViewIOS: iPad ⌃⌥⇧Q un-capture chord, recognized from the GCKeyboard HID stream (no NSEvent monitor on iOS) for cross-client parity with the macOS/Windows/Linux combo; and a click into the video re-engages capture — the iPad analogue of macOS mouseDown → engageCapture(fromClick:), with the engaging click suppressed toward the host. - SessionPresenter: snap the aspect-fit sublayer frame to the backing pixel grid. AVMakeRect centers the fit rect at fractional points, so the compositor resampled the layer — a uniform "everything soft" blur even when the drawable was pixel-exact 1:1. Rounding origin + size to device pixels makes the composite a true 1:1 blit; idempotent when already aligned. - MetalVideoPresenter: PUNKTFUNK_BILINEAR_LUMA=1 A/B lever — compiles the shader with Catmull-Rom luma off (plain bilinear) to isolate bicubic overshoot from upstream fringing. - SettingsView / StreamView / StreamViewIOS: match-window reverted to opt-in (default OFF) — the explicit mode is used and never auto-resized unless enabled.
This commit is contained in:
@@ -124,7 +124,16 @@ float2 chromaUV(texture2d<float> lumaTex, texture2d<float> chromaTex, float2 uv)
|
||||
float3 sampleRgb(texture2d<float> lumaTex, texture2d<float> chromaTex, float2 uv,
|
||||
constant CscUniform& csc) {
|
||||
constexpr sampler s(filter::linear, address::clamp_to_edge);
|
||||
float3 yuv = float3(catmullRomLuma(lumaTex, s, uv),
|
||||
#ifdef PF_BILINEAR_LUMA
|
||||
// DEBUG (PUNKTFUNK_BILINEAR_LUMA=1): plain bilinear luma — Catmull-Rom OFF. A/B lever to see if
|
||||
// the bicubic overshoot contributes to edge fringing. NOTE: at a true 1:1 present both paths
|
||||
// reduce to the identity texel, so if this toggle VISIBLY changes the picture, the present is
|
||||
// NOT 1:1 (there's a resample); if it looks identical, the fringing is upstream (codec/source/OS).
|
||||
float lumaY = lumaTex.sample(s, uv).r;
|
||||
#else
|
||||
float lumaY = catmullRomLuma(lumaTex, s, uv);
|
||||
#endif
|
||||
float3 yuv = float3(lumaY,
|
||||
chromaTex.sample(s, chromaUV(lumaTex, chromaTex, uv)).rg);
|
||||
return saturate(float3(dot(csc.r0.xyz, yuv) + csc.r0.w,
|
||||
dot(csc.r1.xyz, yuv) + csc.r1.w,
|
||||
@@ -250,7 +259,16 @@ public final class MetalVideoPresenter {
|
||||
let pipelineHDR: MTLRenderPipelineState
|
||||
let pipelineHDRToneMap: MTLRenderPipelineState?
|
||||
do {
|
||||
let library = try device.makeLibrary(source: shaderSource, options: nil)
|
||||
// DEBUG A/B lever: PUNKTFUNK_BILINEAR_LUMA=1 compiles the shader with Catmull-Rom OFF
|
||||
// (plain bilinear luma) by prepending a #define ahead of the source. Default (unset) is
|
||||
// the normal bicubic path. Read at presenter creation — set it in the environment and
|
||||
// relaunch to flip; the log line confirms which path built.
|
||||
let bilinearLuma = ProcessInfo.processInfo.environment["PUNKTFUNK_BILINEAR_LUMA"] == "1"
|
||||
let source = (bilinearLuma ? "#define PF_BILINEAR_LUMA 1\n" : "") + shaderSource
|
||||
if bilinearLuma {
|
||||
presenterLog.info("stage2: PUNKTFUNK_BILINEAR_LUMA=1 — Catmull-Rom luma DISABLED (bilinear)")
|
||||
}
|
||||
let library = try device.makeLibrary(source: source, options: nil)
|
||||
let vtx = library.makeFunction(name: "pf_vtx")
|
||||
let sdr = MTLRenderPipelineDescriptor()
|
||||
sdr.vertexFunction = vtx
|
||||
@@ -590,8 +608,17 @@ public final class MetalVideoPresenter {
|
||||
let sig = "\(Int(decoded.width))x\(Int(decoded.height))→\(Int(drawable.width))x\(Int(drawable.height))|hdr\(hdrActive ? 1 : 0)"
|
||||
if sig != lastSizeSig {
|
||||
lastSizeSig = sig
|
||||
// Explicit verdict: is the shader presenting 1:1 (decoded == drawable) or resampling? The
|
||||
// scale ratio makes a residual match-window mismatch obvious. If this says 1:1 but the
|
||||
// picture is still soft, the resample is downstream of us (macOS compositor — a scaled
|
||||
// display mode, or a fractional-pixel window position), not the shader.
|
||||
let sx = decoded.width > 0 ? drawable.width / decoded.width : 0
|
||||
let sy = decoded.height > 0 ? drawable.height / decoded.height : 0
|
||||
let verdict = decoded == drawable
|
||||
? "1:1 (no resample)"
|
||||
: String(format: "RESAMPLE scale=%.4fx%.4f", sx, sy)
|
||||
let msg =
|
||||
"stage2: decoded \(Int(decoded.width))x\(Int(decoded.height)) → drawable \(Int(drawable.width))x\(Int(drawable.height)) hdr=\(hdrActive)"
|
||||
"stage2: decoded \(Int(decoded.width))x\(Int(decoded.height)) → drawable \(Int(drawable.width))x\(Int(drawable.height)) [\(verdict)] hdr=\(hdrActive)"
|
||||
presenterLog.info("\(msg, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,17 +205,33 @@ final class SessionPresenter {
|
||||
return nil
|
||||
}()
|
||||
let fit: CGRect = aspect.map { AVMakeRect(aspectRatio: $0, insideRect: bounds) } ?? bounds
|
||||
// Snap the sublayer frame to the BACKING PIXEL GRID. AVMakeRect centers the aspect-fit rect,
|
||||
// so its origin/size are usually fractional points; a metal sublayer whose frame doesn't land
|
||||
// on whole device pixels is RESAMPLED by the macOS/UIKit compositor during composite — a
|
||||
// uniform "everything looks soft" blur — even when the drawable itself is pixel-exact 1:1
|
||||
// (verified via the stage2 "[1:1 (no resample)]" log while the picture was still soft). Round
|
||||
// origin AND size to device pixels so the composite is a true 1:1 blit. Idempotent when the
|
||||
// frame is already aligned (e.g. fullscreen fit == integer bounds), so it's a no-op there.
|
||||
let scale = contentsScale > 0 ? contentsScale : 1
|
||||
let snapped = CGRect(
|
||||
x: (fit.origin.x * scale).rounded() / scale,
|
||||
y: (fit.origin.y * scale).rounded() / scale,
|
||||
width: (fit.width * scale).rounded() / scale,
|
||||
height: (fit.height * scale).rounded() / scale)
|
||||
// No implicit resize animation; contentsScale tracks the view's backing/display scale.
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
metalLayer.contentsScale = contentsScale
|
||||
metalLayer.frame = fit
|
||||
metalLayer.frame = snapped
|
||||
CATransaction.commit()
|
||||
// Hand the resulting pixel size to the render thread (it must not read layer geometry
|
||||
// cross-thread) — this is what the presenter sizes its drawable to.
|
||||
// cross-thread) — this is what the presenter sizes its drawable to. Uses the SNAPPED size so
|
||||
// the drawable's texel count equals the on-screen device-pixel count exactly (1 texel ↔ 1
|
||||
// device pixel); with the frame snapped, this equals the pre-snap rounded value, so the
|
||||
// decoded↔drawable 1:1 the log confirmed is preserved.
|
||||
stage2?.setDrawableTarget(CGSize(
|
||||
width: (fit.width * contentsScale).rounded(),
|
||||
height: (fit.height * contentsScale).rounded()))
|
||||
width: (snapped.width * scale).rounded(),
|
||||
height: (snapped.height * scale).rounded()))
|
||||
#if os(tvOS)
|
||||
// Push the display's live EDR headroom alongside: > 1 means the TV is composited in an
|
||||
// HDR mode (the session's AVDisplayManager request landed — see StreamViewIOS), and HDR
|
||||
|
||||
Reference in New Issue
Block a user