Full project rename, decided 2026-06-10: - Crates/binaries: punktfunk-core / punktfunk-host / punktfunk-client-rs. - C ABI: punktfunk_* symbols, Punktfunk* types, include/punktfunk_core.h, PUNKTFUNK_FEATURE_QUIC guard (header regenerated; cbindgen renames updated, incl. PUNKTFUNK_BTN_*/PUNKTFUNK_AXIS_* wire constants). - Protocol: punktfunk/1 — control-plane magic LMN1 → PKF1, nonce salt lmn1 → pkf1. WIRE BREAK: clients must be rebuilt from this revision. - Env knobs: PUNKTFUNK_VIDEO_SOURCE / PUNKTFUNK_COMPOSITOR / PUNKTFUNK_ZEROCOPY / …. - Host config dir: ~/.config/punktfunk (the box's dir was migrated in place — the persistent identity is unchanged, pinned fingerprints stay valid). - Swift package: PunktfunkKit + PunktfunkCore.xcframework + PunktfunkConnection (Sources/PunktfunkClient app + tests renamed with it); build-xcframework.sh updated. - scripts/: 60-punktfunk.rules, punktfunk-host.service; OpenAPI doc regenerated. Also: scripts/headless/run-headless-kde.sh — full headless Plasma bringup. Root cause of "desktop but no apps/settings" over the stream: plasmashell launched without XDG_MENU_PREFIX=plasma-, so the launcher resolved a nonexistent applications.menu and rendered an empty menu. The script sets the complete KDE session env (menu prefix, KDE_FULL_SESSION, session version) and rebuilds ksycoca before starting plasmashell. Gate: 97/97 tests, clippy -D warnings (both feature sets), fmt, C-ABI harness PASS, zero lumen references left outside .git. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-13
@@ -1,16 +1,16 @@
|
||||
// Connect form ⇄ live stream. Stage-1 UX: pick host + mode, see frames, type/aim.
|
||||
|
||||
import AppKit
|
||||
import LumenKit
|
||||
import PunktfunkKit
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@StateObject private var model = SessionModel()
|
||||
@AppStorage("lumen.host") private var host = "192.168.1.70"
|
||||
@AppStorage("lumen.port") private var port = 9777
|
||||
@AppStorage("lumen.width") private var width = 1920
|
||||
@AppStorage("lumen.height") private var height = 1080
|
||||
@AppStorage("lumen.hz") private var hz = 60
|
||||
@AppStorage("punktfunk.host") private var host = "192.168.1.70"
|
||||
@AppStorage("punktfunk.port") private var port = 9777
|
||||
@AppStorage("punktfunk.width") private var width = 1920
|
||||
@AppStorage("punktfunk.height") private var height = 1080
|
||||
@AppStorage("punktfunk.hz") private var hz = 60
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
@@ -24,17 +24,17 @@ struct ContentView: View {
|
||||
.onDisappear { model.disconnect() } // window closed mid-session (Cmd+N spawns more)
|
||||
}
|
||||
|
||||
/// Development hook: LUMEN_AUTOCONNECT=host[:port] connects immediately at the saved
|
||||
/// (or LUMEN_MODE=WxHxHz) mode — lets scripts drive first-light runs. (IPv4/hostname
|
||||
/// Development hook: PUNKTFUNK_AUTOCONNECT=host[:port] connects immediately at the saved
|
||||
/// (or PUNKTFUNK_MODE=WxHxHz) mode — lets scripts drive first-light runs. (IPv4/hostname
|
||||
/// only; an IPv6 literal would need bracket parsing.)
|
||||
private func autoConnectIfAsked() {
|
||||
guard let target = ProcessInfo.processInfo.environment["LUMEN_AUTOCONNECT"],
|
||||
guard let target = ProcessInfo.processInfo.environment["PUNKTFUNK_AUTOCONNECT"],
|
||||
!target.isEmpty, model.connection == nil, !model.connecting
|
||||
else { return }
|
||||
let parts = target.split(separator: ":")
|
||||
host = String(parts[0])
|
||||
if parts.count == 2, let p = Int(parts[1]) { port = p }
|
||||
if let mode = ProcessInfo.processInfo.environment["LUMEN_MODE"] {
|
||||
if let mode = ProcessInfo.processInfo.environment["PUNKTFUNK_MODE"] {
|
||||
let dims = mode.split(separator: "x").compactMap { Int($0) }
|
||||
if dims.count == 3 {
|
||||
width = dims[0]
|
||||
@@ -48,7 +48,7 @@ struct ContentView: View {
|
||||
hz: UInt32(clamping: hz))
|
||||
}
|
||||
|
||||
private func stream(_ conn: LumenConnection) -> some View {
|
||||
private func stream(_ conn: PunktfunkConnection) -> some View {
|
||||
StreamView(
|
||||
connection: conn,
|
||||
onFrame: { [meter = model.meter] au in meter.note(byteCount: au.data.count) },
|
||||
@@ -61,7 +61,7 @@ struct ContentView: View {
|
||||
.background(Color.black)
|
||||
}
|
||||
|
||||
private func hud(_ conn: LumenConnection) -> some View {
|
||||
private func hud(_ conn: PunktfunkConnection) -> some View {
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Text("\(conn.width)×\(conn.height)@\(conn.refreshHz) \(model.fps) fps \(model.mbps, specifier: "%.1f") Mb/s")
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
@@ -76,7 +76,7 @@ struct ContentView: View {
|
||||
|
||||
private var connectForm: some View {
|
||||
VStack(spacing: 14) {
|
||||
Text("lumen").font(.largeTitle.weight(.semibold))
|
||||
Text("punktfunk").font(.largeTitle.weight(.semibold))
|
||||
Form {
|
||||
TextField("Host", text: $host)
|
||||
TextField("Port", value: $port, format: .number.grouping(.never))
|
||||
+3
-3
@@ -1,15 +1,15 @@
|
||||
// LumenClient — development app shell around LumenKit (swift run LumenClient).
|
||||
// PunktfunkClient — development app shell around PunktfunkKit (swift run PunktfunkClient).
|
||||
// Connect form → StreamView (AVSampleBufferDisplayLayer HEVC) + InputCapture.
|
||||
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct LumenClientApp: App {
|
||||
struct PunktfunkClientApp: App {
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup("lumen") {
|
||||
WindowGroup("punktfunk") {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -2,7 +2,7 @@
|
||||
// pump-thread → main-actor stats relay.
|
||||
|
||||
import Foundation
|
||||
import LumenKit
|
||||
import PunktfunkKit
|
||||
import SwiftUI
|
||||
|
||||
/// Pump-thread-side frame counters; a 1 Hz main-actor timer drains them into @Published
|
||||
@@ -35,7 +35,7 @@ final class FrameMeter: @unchecked Sendable {
|
||||
|
||||
@MainActor
|
||||
final class SessionModel: ObservableObject {
|
||||
@Published var connection: LumenConnection?
|
||||
@Published var connection: PunktfunkConnection?
|
||||
@Published var connecting = false
|
||||
@Published var errorMessage: String?
|
||||
@Published var fps = 0
|
||||
@@ -51,8 +51,8 @@ final class SessionModel: ObservableObject {
|
||||
connecting = true
|
||||
errorMessage = nil
|
||||
Task.detached(priority: .userInitiated) {
|
||||
// LumenConnection.init blocks on the QUIC handshake — keep it off the main actor.
|
||||
let result = Result { try LumenConnection(
|
||||
// PunktfunkConnection.init blocks on the QUIC handshake — keep it off the main actor.
|
||||
let result = Result { try PunktfunkConnection(
|
||||
host: host, port: port, width: width, height: height, refreshHz: hz) }
|
||||
await MainActor.run { [weak self] in
|
||||
guard let self else { return }
|
||||
@@ -64,7 +64,7 @@ final class SessionModel: ObservableObject {
|
||||
self.startStatsTimer()
|
||||
case .failure:
|
||||
self.errorMessage = "Connection failed — is the host running? " +
|
||||
"(lumen-host m3-host on \(host):\(port))"
|
||||
"(punktfunk-host m3-host on \(host):\(port))"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ final class SessionModel: ObservableObject {
|
||||
errorMessage = "Session ended by host."
|
||||
}
|
||||
|
||||
private func startInput(_ conn: LumenConnection) {
|
||||
private func startInput(_ conn: PunktfunkConnection) {
|
||||
let capture = InputCapture(connection: conn)
|
||||
capture.start()
|
||||
inputCapture = capture
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// Annex-B HEVC → CoreMedia plumbing.
|
||||
//
|
||||
// The lumen host emits Annex-B access units with in-band VPS/SPS/PPS on every IDR
|
||||
// The punktfunk host emits Annex-B access units with in-band VPS/SPS/PPS on every IDR
|
||||
// (deliberately — the client needs no out-of-band extradata). VideoToolbox wants the AVCC
|
||||
// flavor instead: a CMVideoFormatDescription built from the parameter sets, and sample
|
||||
// buffers whose NALs are 4-byte-length-prefixed. This file converts between the two.
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
// Input capture → lumen/1 datagrams, via the GameController framework.
|
||||
// Input capture → punktfunk/1 datagrams, via the GameController framework.
|
||||
//
|
||||
// GCMouse delivers RAW deltas (not the accelerated cursor) — exactly what the host-side
|
||||
// injector expects for relative motion. GCKeyboard gives HID keycodes which we map to the
|
||||
@@ -22,12 +22,12 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import GameController
|
||||
import LumenCore
|
||||
import PunktfunkCore
|
||||
|
||||
public final class InputCapture {
|
||||
private static weak var activeCapture: InputCapture?
|
||||
|
||||
private let connection: LumenConnection
|
||||
private let connection: PunktfunkConnection
|
||||
private var observers: [NSObjectProtocol] = []
|
||||
private var mice: [GCMouse] = []
|
||||
private var keyboards: [GCKeyboard] = []
|
||||
@@ -40,7 +40,7 @@ public final class InputCapture {
|
||||
private var pressedVKs: Set<UInt32> = []
|
||||
private var pressedButtons: Set<UInt32> = []
|
||||
|
||||
public init(connection: LumenConnection) {
|
||||
public init(connection: PunktfunkConnection) {
|
||||
self.connection = connection
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ public final class InputCapture {
|
||||
}
|
||||
|
||||
/// HID usage (GCKeyCode raw) → Windows VK (the host maps VK → evdev; every VK emitted
|
||||
/// here exists in lumen-host/src/inject.rs::vk_to_evdev — extend the two together).
|
||||
/// here exists in punktfunk-host/src/inject.rs::vk_to_evdev — extend the two together).
|
||||
static let hidToVK: [Int: UInt32] = {
|
||||
var m: [Int: UInt32] = [:]
|
||||
// a–z: HID 0x04..0x1D → VK 'A'..'Z'.
|
||||
+48
-48
@@ -1,6 +1,6 @@
|
||||
// Swift wrapper around the lumen-core C ABI's lumen/1 connection API.
|
||||
// Swift wrapper around the punktfunk-core C ABI's punktfunk/1 connection API.
|
||||
//
|
||||
// Threading contract (mirrors the C header): one LumenConnection is pumped from a single
|
||||
// Threading contract (mirrors the C header): one PunktfunkConnection is pumped from a single
|
||||
// video thread via nextAU(); nextAudio()/nextRumble() may each run on their own (single)
|
||||
// drain thread — the core keeps per-plane borrow slots, so the planes never alias;
|
||||
// send() is enqueue-only and safe alongside all of them. The pointers inside an AU/audio
|
||||
@@ -18,14 +18,14 @@
|
||||
// close, the pull methods throw `.closed` and the threads unwind on their own.
|
||||
|
||||
import Foundation
|
||||
import LumenCore
|
||||
import PunktfunkCore
|
||||
|
||||
// cbindgen's C17-compatible header spells the typedefs as plain integers
|
||||
// (`typedef int32_t LumenStatus`, `typedef uint8_t LumenInputKind`) while the enum
|
||||
// (`typedef int32_t PunktfunkStatus`, `typedef uint8_t PunktfunkInputKind`) while the enum
|
||||
// constants import as a distinct same-named Swift type — bridge by raw value once here.
|
||||
private let statusOK: Int32 = LUMEN_STATUS_OK.rawValue
|
||||
private let statusNoFrame: Int32 = LUMEN_STATUS_NO_FRAME.rawValue
|
||||
private let statusClosed: Int32 = LUMEN_STATUS_CLOSED.rawValue
|
||||
private let statusOK: Int32 = PUNKTFUNK_STATUS_OK.rawValue
|
||||
private let statusNoFrame: Int32 = PUNKTFUNK_STATUS_NO_FRAME.rawValue
|
||||
private let statusClosed: Int32 = PUNKTFUNK_STATUS_CLOSED.rawValue
|
||||
|
||||
/// One reassembled, FEC-recovered, decrypted access unit (Annex-B HEVC from the host).
|
||||
public struct AccessUnit: Sendable {
|
||||
@@ -43,7 +43,7 @@ public struct AudioPacket: Sendable {
|
||||
public let seq: UInt32
|
||||
}
|
||||
|
||||
public enum LumenClientError: Error {
|
||||
public enum PunktfunkClientError: Error {
|
||||
/// Connect failed — wrong host/port, timeout, or a certificate-pin mismatch.
|
||||
case connectFailed
|
||||
/// `pinSHA256` was non-nil but not exactly 32 bytes. Failing closed: connecting
|
||||
@@ -53,7 +53,7 @@ public enum LumenClientError: Error {
|
||||
case status(Int32)
|
||||
}
|
||||
|
||||
public final class LumenConnection {
|
||||
public final class PunktfunkConnection {
|
||||
private var handle: OpaquePointer?
|
||||
/// Set by close() before it contends for the plane locks: the pullers see it at their
|
||||
/// next poll boundary and exit, so close() can't be starved by back-to-back polls
|
||||
@@ -88,22 +88,22 @@ public final class LumenConnection {
|
||||
pinSHA256: Data? = nil,
|
||||
timeoutMs: UInt32 = 10_000
|
||||
) throws {
|
||||
if let pin = pinSHA256, pin.count != 32 { throw LumenClientError.invalidPin }
|
||||
if let pin = pinSHA256, pin.count != 32 { throw PunktfunkClientError.invalidPin }
|
||||
var observed = [UInt8](repeating: 0, count: 32)
|
||||
handle = host.withCString { cs in
|
||||
if let pin = pinSHA256 {
|
||||
return pin.withUnsafeBytes { p in
|
||||
lumen_connect(
|
||||
punktfunk_connect(
|
||||
cs, port, width, height, refreshHz,
|
||||
p.bindMemory(to: UInt8.self).baseAddress, &observed, timeoutMs)
|
||||
}
|
||||
}
|
||||
return lumen_connect(cs, port, width, height, refreshHz, nil, &observed, timeoutMs)
|
||||
return punktfunk_connect(cs, port, width, height, refreshHz, nil, &observed, timeoutMs)
|
||||
}
|
||||
guard handle != nil else { throw LumenClientError.connectFailed }
|
||||
guard handle != nil else { throw PunktfunkClientError.connectFailed }
|
||||
hostFingerprint = Data(observed)
|
||||
var w: UInt32 = 0, h: UInt32 = 0, hz: UInt32 = 0
|
||||
_ = lumen_connection_mode(handle, &w, &h, &hz)
|
||||
_ = punktfunk_connection_mode(handle, &w, &h, &hz)
|
||||
self.width = w
|
||||
self.height = h
|
||||
self.refreshHz = hz
|
||||
@@ -114,10 +114,10 @@ public final class LumenConnection {
|
||||
public func nextAU(timeoutMs: UInt32 = 100) throws -> AccessUnit? {
|
||||
pumpLock.lock()
|
||||
defer { pumpLock.unlock() }
|
||||
guard let h = liveHandle() else { throw LumenClientError.closed }
|
||||
guard let h = liveHandle() else { throw PunktfunkClientError.closed }
|
||||
|
||||
var frame = LumenFrame()
|
||||
let rc = lumen_connection_next_au(h, &frame, timeoutMs)
|
||||
var frame = PunktfunkFrame()
|
||||
let rc = punktfunk_connection_next_au(h, &frame, timeoutMs)
|
||||
switch rc {
|
||||
case statusOK:
|
||||
guard let base = frame.data, frame.len > 0 else { return nil }
|
||||
@@ -128,9 +128,9 @@ public final class LumenConnection {
|
||||
case statusNoFrame:
|
||||
return nil
|
||||
case statusClosed:
|
||||
throw LumenClientError.closed
|
||||
throw PunktfunkClientError.closed
|
||||
default:
|
||||
throw LumenClientError.status(rc)
|
||||
throw PunktfunkClientError.status(rc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,10 +140,10 @@ public final class LumenConnection {
|
||||
public func nextAudio(timeoutMs: UInt32 = 100) throws -> AudioPacket? {
|
||||
audioLock.lock()
|
||||
defer { audioLock.unlock() }
|
||||
guard let h = liveHandle() else { throw LumenClientError.closed }
|
||||
guard let h = liveHandle() else { throw PunktfunkClientError.closed }
|
||||
|
||||
var pkt = LumenAudioPacket()
|
||||
let rc = lumen_connection_next_audio(h, &pkt, timeoutMs)
|
||||
var pkt = PunktfunkAudioPacket()
|
||||
let rc = punktfunk_connection_next_audio(h, &pkt, timeoutMs)
|
||||
switch rc {
|
||||
case statusOK:
|
||||
guard let base = pkt.data, pkt.len > 0 else { return nil }
|
||||
@@ -152,9 +152,9 @@ public final class LumenConnection {
|
||||
case statusNoFrame:
|
||||
return nil
|
||||
case statusClosed:
|
||||
throw LumenClientError.closed
|
||||
throw PunktfunkClientError.closed
|
||||
default:
|
||||
throw LumenClientError.status(rc)
|
||||
throw PunktfunkClientError.status(rc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,30 +164,30 @@ public final class LumenConnection {
|
||||
public func nextRumble(timeoutMs: UInt32 = 0) throws -> (pad: UInt16, low: UInt16, high: UInt16)? {
|
||||
audioLock.lock()
|
||||
defer { audioLock.unlock() }
|
||||
guard let h = liveHandle() else { throw LumenClientError.closed }
|
||||
guard let h = liveHandle() else { throw PunktfunkClientError.closed }
|
||||
|
||||
var pad: UInt16 = 0, low: UInt16 = 0, high: UInt16 = 0
|
||||
let rc = lumen_connection_next_rumble(h, &pad, &low, &high, timeoutMs)
|
||||
let rc = punktfunk_connection_next_rumble(h, &pad, &low, &high, timeoutMs)
|
||||
switch rc {
|
||||
case statusOK:
|
||||
return (pad, low, high)
|
||||
case statusNoFrame:
|
||||
return nil
|
||||
case statusClosed:
|
||||
throw LumenClientError.closed
|
||||
throw PunktfunkClientError.closed
|
||||
default:
|
||||
throw LumenClientError.status(rc)
|
||||
throw PunktfunkClientError.status(rc)
|
||||
}
|
||||
}
|
||||
|
||||
/// Send one input event (delivered to the host as a QUIC datagram). Thread-safe;
|
||||
/// silently dropped after close.
|
||||
public func send(_ event: LumenInputEvent) {
|
||||
public func send(_ event: PunktfunkInputEvent) {
|
||||
var ev = event
|
||||
abiLock.lock()
|
||||
defer { abiLock.unlock() }
|
||||
guard let h = handle, !closeRequested else { return }
|
||||
_ = lumen_connection_send_input(h, &ev)
|
||||
_ = punktfunk_connection_send_input(h, &ev)
|
||||
}
|
||||
|
||||
/// Close the connection and free the handle. Safe from any thread, idempotent; waits
|
||||
@@ -205,7 +205,7 @@ public final class LumenConnection {
|
||||
audioLock.unlock()
|
||||
pumpLock.unlock()
|
||||
if let h {
|
||||
lumen_connection_close(h) // joins the connection's internal Rust threads
|
||||
punktfunk_connection_close(h) // joins the connection's internal Rust threads
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,46 +220,46 @@ public final class LumenConnection {
|
||||
}
|
||||
|
||||
// Convenience constructors for the wire input events (field semantics match
|
||||
// lumen_core::input::InputEvent; see lumen_core.h).
|
||||
public extension LumenInputEvent {
|
||||
// punktfunk_core::input::InputEvent; see punktfunk_core.h).
|
||||
public extension PunktfunkInputEvent {
|
||||
private static func make(
|
||||
_ kind: UInt32, code: UInt32, x: Int32, y: Int32, flags: UInt32 = 0
|
||||
) -> LumenInputEvent {
|
||||
LumenInputEvent(kind: UInt8(kind), _pad: (0, 0, 0), code: code, x: x, y: y, flags: flags)
|
||||
) -> PunktfunkInputEvent {
|
||||
PunktfunkInputEvent(kind: UInt8(kind), _pad: (0, 0, 0), code: code, x: x, y: y, flags: flags)
|
||||
}
|
||||
static func mouseMove(dx: Int32, dy: Int32) -> LumenInputEvent {
|
||||
make(LUMEN_INPUT_KIND_MOUSE_MOVE.rawValue, code: 0, x: dx, y: dy)
|
||||
static func mouseMove(dx: Int32, dy: Int32) -> PunktfunkInputEvent {
|
||||
make(PUNKTFUNK_INPUT_KIND_MOUSE_MOVE.rawValue, code: 0, x: dx, y: dy)
|
||||
}
|
||||
/// GameStream button ids: 1=left 2=middle 3=right 4=X1 5=X2 (host maps to evdev BTN_*).
|
||||
static func mouseButton(_ button: UInt32, down: Bool) -> LumenInputEvent {
|
||||
static func mouseButton(_ button: UInt32, down: Bool) -> PunktfunkInputEvent {
|
||||
make(
|
||||
(down ? LUMEN_INPUT_KIND_MOUSE_BUTTON_DOWN : LUMEN_INPUT_KIND_MOUSE_BUTTON_UP).rawValue,
|
||||
(down ? PUNKTFUNK_INPUT_KIND_MOUSE_BUTTON_DOWN : PUNKTFUNK_INPUT_KIND_MOUSE_BUTTON_UP).rawValue,
|
||||
code: button, x: 0, y: 0)
|
||||
}
|
||||
/// `vk` is a Windows virtual-key code (the host's vk_to_evdev table consumes these).
|
||||
static func key(_ vk: UInt32, down: Bool) -> LumenInputEvent {
|
||||
make((down ? LUMEN_INPUT_KIND_KEY_DOWN : LUMEN_INPUT_KIND_KEY_UP).rawValue, code: vk, x: 0, y: 0)
|
||||
static func key(_ vk: UInt32, down: Bool) -> PunktfunkInputEvent {
|
||||
make((down ? PUNKTFUNK_INPUT_KIND_KEY_DOWN : PUNKTFUNK_INPUT_KIND_KEY_UP).rawValue, code: vk, x: 0, y: 0)
|
||||
}
|
||||
/// WHEEL_DELTA(120)-scaled; positive = up (vertical) / right (horizontal) — the
|
||||
/// convention Moonlight/SDL use; the host maps onto the ei/wl axes.
|
||||
static func scroll(_ delta: Int32, horizontal: Bool = false) -> LumenInputEvent {
|
||||
make(LUMEN_INPUT_KIND_MOUSE_SCROLL.rawValue, code: horizontal ? 1 : 0, x: delta, y: 0)
|
||||
static func scroll(_ delta: Int32, horizontal: Bool = false) -> PunktfunkInputEvent {
|
||||
make(PUNKTFUNK_INPUT_KIND_MOUSE_SCROLL.rawValue, code: horizontal ? 1 : 0, x: delta, y: 0)
|
||||
}
|
||||
|
||||
// Gamepad (wire contract in lumen_core::input::gamepad): one transition per event,
|
||||
// Gamepad (wire contract in punktfunk_core::input::gamepad): one transition per event,
|
||||
// `pad` = controller index, accumulated host-side into a virtual Xbox 360 pad.
|
||||
|
||||
/// `button` is a GameStream buttonFlags bit (A=0x1000 B=0x2000 X=0x4000 Y=0x8000,
|
||||
/// dpad=0x1/2/4/8, start=0x10 back=0x20 LS=0x40 RS=0x80 LB=0x100 RB=0x200 guide=0x400).
|
||||
static func gamepadButton(_ button: UInt32, down: Bool, pad: UInt32 = 0) -> LumenInputEvent {
|
||||
static func gamepadButton(_ button: UInt32, down: Bool, pad: UInt32 = 0) -> PunktfunkInputEvent {
|
||||
make(
|
||||
LUMEN_INPUT_KIND_GAMEPAD_BUTTON.rawValue,
|
||||
PUNKTFUNK_INPUT_KIND_GAMEPAD_BUTTON.rawValue,
|
||||
code: button, x: down ? 1 : 0, y: 0, flags: pad)
|
||||
}
|
||||
|
||||
/// Axis ids: 0=LSX 1=LSY 2=RSX 3=RSY (−32768...32767, XInput convention: +y = UP —
|
||||
/// `GCControllerDirectionPad.yAxis` already matches, no flip), 4=LT 5=RT (0...255).
|
||||
static func gamepadAxis(_ axis: UInt32, value: Int32, pad: UInt32 = 0) -> LumenInputEvent {
|
||||
make(LUMEN_INPUT_KIND_GAMEPAD_AXIS.rawValue, code: axis, x: value, y: 0, flags: pad)
|
||||
static func gamepadAxis(_ axis: UInt32, value: Int32, pad: UInt32 = 0) -> PunktfunkInputEvent {
|
||||
make(PUNKTFUNK_INPUT_KIND_GAMEPAD_AXIS.rawValue, code: axis, x: value, y: 0, flags: pad)
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -1,4 +1,4 @@
|
||||
// SwiftUI presentation: AVSampleBufferDisplayLayer fed straight from the lumen/1 connection.
|
||||
// SwiftUI presentation: AVSampleBufferDisplayLayer fed straight from the punktfunk/1 connection.
|
||||
//
|
||||
// Stage-1 presenter (see README): the layer accepts *compressed* HEVC sample buffers and
|
||||
// does hardware decode + display itself — fastest path to pixels, IOSurface-backed
|
||||
@@ -13,13 +13,13 @@ import AVFoundation
|
||||
import SwiftUI
|
||||
|
||||
public struct StreamView: NSViewRepresentable {
|
||||
private let connection: LumenConnection
|
||||
private let connection: PunktfunkConnection
|
||||
private let onFrame: (@Sendable (AccessUnit) -> Void)?
|
||||
private let onSessionEnd: (@Sendable () -> Void)?
|
||||
|
||||
/// `onFrame`/`onSessionEnd` fire on the pump thread — hop to the main actor for UI.
|
||||
public init(
|
||||
connection: LumenConnection,
|
||||
connection: PunktfunkConnection,
|
||||
onFrame: (@Sendable (AccessUnit) -> Void)? = nil,
|
||||
onSessionEnd: (@Sendable () -> Void)? = nil
|
||||
) {
|
||||
@@ -67,7 +67,7 @@ public final class StreamLayerView: NSView {
|
||||
|
||||
private let displayLayer = AVSampleBufferDisplayLayer()
|
||||
private var token: PumpToken?
|
||||
public private(set) var connection: LumenConnection?
|
||||
public private(set) var connection: PunktfunkConnection?
|
||||
|
||||
public override init(frame: NSRect) {
|
||||
super.init(frame: frame)
|
||||
@@ -81,7 +81,7 @@ public final class StreamLayerView: NSView {
|
||||
/// Pump thread: pull AUs from the connection, wrap, enqueue. The first IDR yields the
|
||||
/// format description; non-IDR AUs before it are dropped (the host opens with an IDR).
|
||||
public func start(
|
||||
connection: LumenConnection,
|
||||
connection: PunktfunkConnection,
|
||||
onFrame: (@Sendable (AccessUnit) -> Void)? = nil,
|
||||
onSessionEnd: (@Sendable () -> Void)? = nil
|
||||
) {
|
||||
@@ -104,7 +104,7 @@ public final class StreamLayerView: NSView {
|
||||
if layer.status == .failed {
|
||||
// Decode wedged: flush and re-gate on the next in-band parameter
|
||||
// sets — resuming with a delta frame can't recover. (A
|
||||
// request-IDR channel on lumen/1 is a host-side TODO; with the
|
||||
// request-IDR channel on punktfunk/1 is a host-side TODO; with the
|
||||
// host's infinite GOP this may otherwise stay black until the
|
||||
// next recovery keyframe.)
|
||||
layer.flush()
|
||||
@@ -123,13 +123,13 @@ public final class StreamLayerView: NSView {
|
||||
}
|
||||
}
|
||||
}
|
||||
thread.name = "lumen-pump"
|
||||
thread.name = "punktfunk-pump"
|
||||
thread.qualityOfService = .userInteractive
|
||||
thread.start()
|
||||
}
|
||||
|
||||
/// Stop pumping (≤ one poll timeout). Does not close the connection — that stays with
|
||||
/// whoever owns it (LumenConnection.close() is safe alongside a draining pump).
|
||||
/// whoever owns it (PunktfunkConnection.close() is safe alongside a draining pump).
|
||||
public func stop() {
|
||||
token?.cancel()
|
||||
token = nil
|
||||
Reference in New Issue
Block a user