Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46b9aa8cf0 | |||
| 372b27540b | |||
| db4d15bf8b | |||
| 8e24ea9ed7 | |||
| 73c0125843 |
@@ -207,10 +207,20 @@ jobs:
|
|||||||
# (Config/Punktfunk-macOS.entitlements) — mandatory for the Mac App Store.
|
# (Config/Punktfunk-macOS.entitlements) — mandatory for the Mac App Store.
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
# Separate archive from the Developer ID one above: App Store needs a profile-signed
|
# Separate archive from the Developer ID one above: App Store needs a signed, entitled
|
||||||
# archive (manual signing), not the unsigned-then-codesign DMG path. Same App-Manager
|
# archive that -exportArchive can re-sign for distribution, not the unsigned-then-codesign
|
||||||
# ASC-key constraint as iOS/tvOS — MANUAL signing, NOT -allowProvisioningUpdates
|
# DMG path. Archive with AUTOMATIC signing (development). Why not a manually-specified
|
||||||
# (cloud signing the key can't do). Quit Xcode so it can't prune the dropped profile.
|
# profile (as this step used to do): the in-app license screens added a SwiftPM resource
|
||||||
|
# bundle (PunktfunkKit_PunktfunkKit), and a resource bundle is a product type that cannot
|
||||||
|
# carry a provisioning profile — a global PROVISIONING_PROFILE_SPECIFIER (here) or an
|
||||||
|
# sdk-scoped one (iOS/tvOS) lands on it and fails the archive ("does not support
|
||||||
|
# provisioning profiles"). Automatic signing assigns a profile only to the app and leaves
|
||||||
|
# the resource bundle (and the macOS-host macro plugins) alone, and bakes the sandbox
|
||||||
|
# entitlements in. No -allowProvisioningUpdates → it stays OFFLINE and never cloud-signs
|
||||||
|
# (the App-Manager ASC key can't), so the runner must have a macOS *development* profile
|
||||||
|
# for io.unom.punktfunk installed. DISTRIBUTION signing happens in the export step below
|
||||||
|
# (manual, via the plist). Quit Xcode so it can't prune the manually-installed App Store
|
||||||
|
# distribution profile that export needs.
|
||||||
osascript -e 'tell application "Xcode" to quit' >/dev/null 2>&1 || true
|
osascript -e 'tell application "Xcode" to quit' >/dev/null 2>&1 || true
|
||||||
pkill -x Xcode 2>/dev/null || true
|
pkill -x Xcode 2>/dev/null || true
|
||||||
PROFILE="Punktfunk macOS App Store Distribution"
|
PROFILE="Punktfunk macOS App Store Distribution"
|
||||||
@@ -218,11 +228,10 @@ jobs:
|
|||||||
-project "$PROJECT" -scheme Punktfunk \
|
-project "$PROJECT" -scheme Punktfunk \
|
||||||
-destination 'generic/platform=macOS' \
|
-destination 'generic/platform=macOS' \
|
||||||
-archivePath "$RUNNER_TEMP/Punktfunk-macos-appstore.xcarchive" \
|
-archivePath "$RUNNER_TEMP/Punktfunk-macos-appstore.xcarchive" \
|
||||||
|
-skipMacroValidation -skipPackagePluginValidation \
|
||||||
MARKETING_VERSION="$VERSION" CURRENT_PROJECT_VERSION="$BUILD_NUM" \
|
MARKETING_VERSION="$VERSION" CURRENT_PROJECT_VERSION="$BUILD_NUM" \
|
||||||
CODE_SIGN_STYLE=Manual \
|
CODE_SIGN_STYLE=Automatic \
|
||||||
CODE_SIGN_IDENTITY="Apple Distribution" \
|
DEVELOPMENT_TEAM="$TEAM_ID"
|
||||||
DEVELOPMENT_TEAM="$TEAM_ID" \
|
|
||||||
PROVISIONING_PROFILE_SPECIFIER="$PROFILE"
|
|
||||||
cat > "$RUNNER_TEMP/export-macos-appstore.plist" <<EOF
|
cat > "$RUNNER_TEMP/export-macos-appstore.plist" <<EOF
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
@@ -252,35 +261,27 @@ jobs:
|
|||||||
# Best-effort until the App Store Connect app record for io.unom.punktfunk exists.
|
# Best-effort until the App Store Connect app record for io.unom.punktfunk exists.
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
# MANUAL App Store signing: the local (valid) Apple Distribution identity + the App
|
# Archive with AUTOMATIC signing (development) — see the macOS App Store step for the full
|
||||||
# Store provisioning profile. NOT -allowProvisioningUpdates — with an App-Manager-role
|
# rationale. The SwiftPM resource bundle (PunktfunkKit_PunktfunkKit, added with the in-app
|
||||||
# ASC key that forces Xcode's CLOUD-managed signing, which the role can't do ("Cloud
|
# license screens) builds for iphoneos, so even the sdk-scoped PROVISIONING_PROFILE_SPECIFIER
|
||||||
# signing permission error"). The profile must be installed on the runner under
|
# this step used to set matched it and failed the archive ("does not support provisioning
|
||||||
# ~/Library/Developer/Xcode/UserData/Provisioning Profiles/ (install it once with
|
# profiles"). Automatic signing profiles only the app and leaves the resource bundle (and
|
||||||
# Xcode.app quit, or it prunes the manually-dropped distribution profile).
|
# the macOS-host macro plugins) alone. No -allowProvisioningUpdates → OFFLINE, never
|
||||||
# A running Xcode.app prunes unrecognized profiles from that dir — quit it so the App
|
# cloud-signs (the App-Manager ASC key can't), so the runner needs an iOS *development*
|
||||||
# Store profile survives this build; headless xcodebuild doesn't need the GUI app.
|
# profile for io.unom.punktfunk installed. DISTRIBUTION signing is the export step below
|
||||||
|
# (manual, via the plist). A running Xcode.app prunes unrecognized profiles — quit it so the
|
||||||
|
# manually-installed App Store distribution profile survives for export.
|
||||||
osascript -e 'tell application "Xcode" to quit' >/dev/null 2>&1 || true
|
osascript -e 'tell application "Xcode" to quit' >/dev/null 2>&1 || true
|
||||||
pkill -x Xcode 2>/dev/null || true
|
pkill -x Xcode 2>/dev/null || true
|
||||||
PROFILE="Punktfunk iOS App Store Distribution"
|
PROFILE="Punktfunk iOS App Store Distribution"
|
||||||
# Scope signing to the iOS device SDK via an xcconfig — see the tvOS step below for the
|
|
||||||
# full rationale. A global (CLI) profile specifier would also be forced onto the shared
|
|
||||||
# macOS-host SwiftPM macro plugins, which reject it and fail the archive; [sdk=iphoneos*]
|
|
||||||
# in an xcconfig lands it on the app/framework slices only.
|
|
||||||
SIGN_XCCONFIG="$RUNNER_TEMP/sign-ios.xcconfig"
|
|
||||||
cat > "$SIGN_XCCONFIG" <<XCCONF
|
|
||||||
CODE_SIGN_STYLE = Manual
|
|
||||||
DEVELOPMENT_TEAM = $TEAM_ID
|
|
||||||
CODE_SIGN_IDENTITY[sdk=iphoneos*] = Apple Distribution
|
|
||||||
PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*] = $PROFILE
|
|
||||||
XCCONF
|
|
||||||
DEVELOPER_DIR="$XCODE_DEV_DIR" xcodebuild archive \
|
DEVELOPER_DIR="$XCODE_DEV_DIR" xcodebuild archive \
|
||||||
-project "$PROJECT" -scheme Punktfunk-iOS \
|
-project "$PROJECT" -scheme Punktfunk-iOS \
|
||||||
-destination 'generic/platform=iOS' \
|
-destination 'generic/platform=iOS' \
|
||||||
-archivePath "$RUNNER_TEMP/Punktfunk-ios.xcarchive" \
|
-archivePath "$RUNNER_TEMP/Punktfunk-ios.xcarchive" \
|
||||||
-skipMacroValidation -skipPackagePluginValidation \
|
-skipMacroValidation -skipPackagePluginValidation \
|
||||||
-xcconfig "$SIGN_XCCONFIG" \
|
MARKETING_VERSION="$VERSION" CURRENT_PROJECT_VERSION="$BUILD_NUM" \
|
||||||
MARKETING_VERSION="$VERSION" CURRENT_PROJECT_VERSION="$BUILD_NUM"
|
CODE_SIGN_STYLE=Automatic \
|
||||||
|
DEVELOPMENT_TEAM="$TEAM_ID"
|
||||||
cat > "$RUNNER_TEMP/export-appstore.plist" <<EOF
|
cat > "$RUNNER_TEMP/export-appstore.plist" <<EOF
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
@@ -312,33 +313,24 @@ jobs:
|
|||||||
# on the runner (xcodebuild -downloadPlatform tvOS).
|
# on the runner (xcodebuild -downloadPlatform tvOS).
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
# Same manual App Store signing as iOS (the App-Manager ASC key can't cloud-sign).
|
# Archive with AUTOMATIC signing (development) — see the macOS App Store step. The SwiftPM
|
||||||
|
# resource bundle (PunktfunkKit_PunktfunkKit) builds for appletvos and rejected the
|
||||||
|
# sdk-scoped profile this step used to set; Automatic signing profiles only the app and
|
||||||
|
# leaves the resource bundle + the macOS-host macro plugins (OnceMacro/SwizzlingMacro/
|
||||||
|
# AssociationMacro) alone. No -allowProvisioningUpdates → OFFLINE, never cloud-signs (the
|
||||||
|
# App-Manager ASC key can't), so the runner needs a tvOS *development* profile for
|
||||||
|
# io.unom.punktfunk installed. DISTRIBUTION signing is the export step below (manual, plist).
|
||||||
osascript -e 'tell application "Xcode" to quit' >/dev/null 2>&1 || true
|
osascript -e 'tell application "Xcode" to quit' >/dev/null 2>&1 || true
|
||||||
pkill -x Xcode 2>/dev/null || true
|
pkill -x Xcode 2>/dev/null || true
|
||||||
PROFILE="Punktfunk tvOS App Store Distribution"
|
PROFILE="Punktfunk tvOS App Store Distribution"
|
||||||
# Scope signing to the tvOS device SDK via an xcconfig. A global (CLI) profile specifier
|
|
||||||
# hits EVERY target, including the shared SwiftPM macro plugins (OnceMacro/SwizzlingMacro/
|
|
||||||
# AssociationMacro) which build for the macOS host and reject a provisioning profile
|
|
||||||
# ("<macro> does not support provisioning profiles"), failing the archive. Conditionals
|
|
||||||
# work only in an xcconfig (xcodebuild mis-parses a CLI "SETTING[sdk=..]=val"), and a
|
|
||||||
# command-line -xcconfig outranks target settings, so [sdk=appletvos*] puts the profile on
|
|
||||||
# the app/framework slices only — the macosx-host macros get nothing. (The macOS archive
|
|
||||||
# above is immune: its host-SDK macros are CODE_SIGNING_ALLOWED=NO, so a global specifier
|
|
||||||
# is ignored there.)
|
|
||||||
SIGN_XCCONFIG="$RUNNER_TEMP/sign-tvos.xcconfig"
|
|
||||||
cat > "$SIGN_XCCONFIG" <<XCCONF
|
|
||||||
CODE_SIGN_STYLE = Manual
|
|
||||||
DEVELOPMENT_TEAM = $TEAM_ID
|
|
||||||
CODE_SIGN_IDENTITY[sdk=appletvos*] = Apple Distribution
|
|
||||||
PROVISIONING_PROFILE_SPECIFIER[sdk=appletvos*] = $PROFILE
|
|
||||||
XCCONF
|
|
||||||
DEVELOPER_DIR="$XCODE_DEV_DIR" xcodebuild archive \
|
DEVELOPER_DIR="$XCODE_DEV_DIR" xcodebuild archive \
|
||||||
-project "$PROJECT" -scheme Punktfunk-tvOS \
|
-project "$PROJECT" -scheme Punktfunk-tvOS \
|
||||||
-destination 'generic/platform=tvOS' \
|
-destination 'generic/platform=tvOS' \
|
||||||
-archivePath "$RUNNER_TEMP/Punktfunk-tvos.xcarchive" \
|
-archivePath "$RUNNER_TEMP/Punktfunk-tvos.xcarchive" \
|
||||||
-skipMacroValidation -skipPackagePluginValidation \
|
-skipMacroValidation -skipPackagePluginValidation \
|
||||||
-xcconfig "$SIGN_XCCONFIG" \
|
MARKETING_VERSION="$VERSION" CURRENT_PROJECT_VERSION="$BUILD_NUM" \
|
||||||
MARKETING_VERSION="$VERSION" CURRENT_PROJECT_VERSION="$BUILD_NUM"
|
CODE_SIGN_STYLE=Automatic \
|
||||||
|
DEVELOPMENT_TEAM="$TEAM_ID"
|
||||||
cat > "$RUNNER_TEMP/export-tvos.plist" <<EOF
|
cat > "$RUNNER_TEMP/export-tvos.plist" <<EOF
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
"name": "MIT OR Apache-2.0",
|
"name": "MIT OR Apache-2.0",
|
||||||
"identifier": "MIT OR Apache-2.0"
|
"identifier": "MIT OR Apache-2.0"
|
||||||
},
|
},
|
||||||
"version": "0.0.1"
|
"version": "0.3.0"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/api/v1/clients": {
|
"/api/v1/clients": {
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ struct AcknowledgementsView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
// Top-level LazyVStack so the third-party-notices chunks (Licenses.thirdPartyNoticesChunks,
|
||||||
|
// ~885 KB total) load lazily as they scroll into view — a single Text that large overshoots
|
||||||
|
// the text-rendering height limit (blank below the limit + very slow). spacing 0 keeps the
|
||||||
|
// notice chunks visually continuous; the header block carries its own spacing + bottom pad.
|
||||||
|
LazyVStack(alignment: .leading, spacing: 0) {
|
||||||
VStack(alignment: .leading, spacing: 18) {
|
VStack(alignment: .leading, spacing: 18) {
|
||||||
Text("punktfunk")
|
Text("punktfunk")
|
||||||
.font(.title2).bold()
|
.font(.title2).bold()
|
||||||
@@ -33,10 +38,17 @@ struct AcknowledgementsView: View {
|
|||||||
)
|
)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
Text(Licenses.thirdPartyNotices)
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.bottom, 18)
|
||||||
|
|
||||||
|
ForEach(Licenses.thirdPartyNoticesChunks.indices, id: \.self) { i in
|
||||||
|
Text(Licenses.thirdPartyNoticesChunks[i])
|
||||||
.font(.caption2.monospaced())
|
.font(.caption2.monospaced())
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.modifier(SelectableText())
|
.modifier(SelectableText())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.frame(maxWidth: 900, alignment: .leading)
|
.frame(maxWidth: 900, alignment: .leading)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding()
|
.padding()
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ struct AddHostSheet: View {
|
|||||||
}
|
}
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.formStyle(.grouped)
|
.formStyle(.grouped)
|
||||||
|
#endif
|
||||||
|
#if os(iOS)
|
||||||
|
// The detent below is sized to fit all 3 rows + the action button exactly, so the
|
||||||
|
// Form must NOT scroll/bounce inside it — lock it. (iOS 16+; safe at iOS 17.)
|
||||||
|
.scrollDisabled(true)
|
||||||
#endif
|
#endif
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
// macOS: UNCHANGED — Cancel + Spacer + Add in an HStack, both wired to the
|
// macOS: UNCHANGED — Cancel + Spacer + Add in an HStack, both wired to the
|
||||||
@@ -120,8 +125,8 @@ struct AddHostSheet: View {
|
|||||||
// Form + the full-width action row, instead of the half-screen .medium it used to rest
|
// Form + the full-width action row, instead of the half-screen .medium it used to rest
|
||||||
// at. A single fixed detent is enough: the system keeps the content above the keyboard
|
// at. A single fixed detent is enough: the system keeps the content above the keyboard
|
||||||
// when Address/Port is focused, and on iPadOS this renders as a short bottom sheet (not a
|
// when Address/Port is focused, and on iPadOS this renders as a short bottom sheet (not a
|
||||||
// centered formSheet card). If Dynamic Type grows the rows past this height the Form just
|
// centered formSheet card). The Form itself is .scrollDisabled (above) so it can't
|
||||||
// scrolls inside the detent — nothing is clipped. (.height(_:) is iOS 16+, safe at iOS 17.)
|
// bounce/scroll inside this fixed detent. (.height(_:) is iOS 16+, safe at iOS 17.)
|
||||||
.presentationDetents([.height(320)])
|
.presentationDetents([.height(320)])
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -33,4 +33,18 @@ public enum Licenses {
|
|||||||
let text = resource("THIRD-PARTY-NOTICES")
|
let text = resource("THIRD-PARTY-NOTICES")
|
||||||
return text.isEmpty ? "Third-party notices unavailable." : text
|
return text.isEmpty ? "Third-party notices unavailable." : text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `thirdPartyNotices` pre-split into render-sized line chunks. The full notices are ~885 KB /
|
||||||
|
/// 16k lines; a single SwiftUI `Text` that large overshoots CoreText/CoreAnimation's max
|
||||||
|
/// renderable height — it lays out for ages and draws blank past the limit — so the
|
||||||
|
/// Acknowledgements screen renders these chunks in a `LazyVStack` (only on-screen chunks lay
|
||||||
|
/// out, and no chunk is tall enough to clip). Split at line boundaries and joined with "\n";
|
||||||
|
/// the inter-chunk break is the `LazyVStack` row boundary, so no text is lost. Computed once.
|
||||||
|
public static let thirdPartyNoticesChunks: [String] = {
|
||||||
|
let lines = thirdPartyNotices.split(separator: "\n", omittingEmptySubsequences: false)
|
||||||
|
let chunkSize = 200
|
||||||
|
return stride(from: 0, to: lines.count, by: chunkSize).map { start in
|
||||||
|
lines[start..<min(start + chunkSize, lines.count)].joined(separator: "\n")
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -327,17 +327,17 @@ impl VirtualDisplayManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Windows defaults a new IddCx monitor into CLONE mode when a physical display is already
|
// Resolve the capture target — wait for Windows to auto-activate the freshly-ADDed IDD into its
|
||||||
// active (a laptop panel, an attached monitor): the cloned IDD shares that display's source, so
|
// OWN display path (it comes up EXTENDED alongside any existing/basic display; `set_active_mode`
|
||||||
// the OS never commits a distinct path for it and capture sees no frames. Force EXTEND first so
|
// below then promotes it to primary and `isolate_displays_ccd` makes it the sole composited
|
||||||
// the IDD comes up as its OWN active path; the resolve loop below then finds it. Idempotent /
|
// desktop — the proven flow). May be None on a GPU-less box (target added but not WDDM-activated);
|
||||||
// no-op on a sole-display box, so it's safe on the headless single-GPU path too.
|
|
||||||
// SAFETY: `force_extend_topology` only calls `SetDisplayConfig` (a CCD topology apply) with no
|
|
||||||
// borrowed caller memory; it runs under the manager `state` lock, the sole topology mutator.
|
|
||||||
unsafe { force_extend_topology() };
|
|
||||||
|
|
||||||
// Resolve the capture target. May be None on a GPU-less box (target added but not WDDM-activated);
|
|
||||||
// the capture backend re-resolves once a GPU is present.
|
// the capture backend re-resolves once a GPU is present.
|
||||||
|
//
|
||||||
|
// We do NOT force a topology change FIRST: the bare `SDC_TOPOLOGY_EXTEND` preset is ACCESS_DENIED
|
||||||
|
// from our Session-0 service context on a headless box and BREAKS this auto-activate (it regressed
|
||||||
|
// the headless path — the IDD then never gets its own path → "not an active display path" → black).
|
||||||
|
// force-EXTEND is only the FALLBACK below, for an integrated-screen box where a fresh IDD is CLONED
|
||||||
|
// onto the panel (shares its source) instead of getting its own path.
|
||||||
let mut gdi_name = None;
|
let mut gdi_name = None;
|
||||||
for _ in 0..15 {
|
for _ in 0..15 {
|
||||||
thread::sleep(Duration::from_millis(200));
|
thread::sleep(Duration::from_millis(200));
|
||||||
@@ -349,6 +349,32 @@ impl VirtualDisplayManager {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback for an integrated-screen box (e.g. a laptop panel): Windows CLONES a freshly-added
|
||||||
|
// IDD onto the existing display, sharing its source, so it never gets its own committed path. On
|
||||||
|
// the IddCx clone behaviour observed live (commit 8e87e61, an Intel-iGPU + NVIDIA-Optimus laptop)
|
||||||
|
// `resolve_gdi_name` then stays None — so this `is_none()` fallback fires, force-EXTENDs to
|
||||||
|
// de-clone, and the second resolve finds the now-committed path. Headless/extended boxes already
|
||||||
|
// resolved above (the IDD auto-activates with its OWN source) and skip this — which is the whole
|
||||||
|
// point, since force-EXTEND's bare preset is ACCESS_DENIED from our service context there.
|
||||||
|
//
|
||||||
|
// CAVEAT (unobserved for IddCx, untested across GPU/driver/OS): textbook CCD also lets a clone
|
||||||
|
// appear as a *shared-source ACTIVE* path (resolve → Some), which this `is_none()` gate would NOT
|
||||||
|
// catch. If that ever shows up, widen the gate to also fire when the IDD target's source is shared
|
||||||
|
// with another active path (a `target_is_cloned` helper) — needs on-laptop validation first.
|
||||||
|
if gdi_name.is_none() {
|
||||||
|
// SAFETY: as above — `force_extend_topology` only calls `SetDisplayConfig` (CCD) with no
|
||||||
|
// borrowed caller memory, under the `state` lock.
|
||||||
|
unsafe { force_extend_topology() };
|
||||||
|
for _ in 0..15 {
|
||||||
|
thread::sleep(Duration::from_millis(200));
|
||||||
|
// SAFETY: as the resolve loop above.
|
||||||
|
if let Some(n) = unsafe { resolve_gdi_name(added.target_id) } {
|
||||||
|
gdi_name = Some(n);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut ccd_saved: Option<SavedConfig> = None;
|
let mut ccd_saved: Option<SavedConfig> = None;
|
||||||
match &gdi_name {
|
match &gdi_name {
|
||||||
Some(n) => {
|
Some(n) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user