Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
Config.xcconfig
.DS_Store
xcuserdata/
39 changes: 37 additions & 2 deletions Procyon/Components/GameOptionsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import SwiftUI
struct GameOptionsView: View {
@Binding var game: Game?
@EnvironmentObject var gameOptions: GameOptions
@EnvironmentObject var appGlobals: AppGlobals

var preferredMaxFrameRate: String {
$gameOptions.dxmtPreferredMaxFrameRate.wrappedValue < 20.0 ? "Disabled" : "\($gameOptions.dxmtPreferredMaxFrameRate.wrappedValue)"
Expand Down Expand Up @@ -90,6 +91,23 @@ struct GameOptionsView: View {
}
}
}
if(!game!.isNative) {
Divider()
Section("Resolution (Virtual Desktop)") {
Toggle("Enable Virtual Desktop", isOn: $gameOptions.virtualDesktopEnabled)
if(gameOptions.virtualDesktopEnabled) {
TextField("Resolution (e.g. 1920x1080)", text: $gameOptions.virtualDesktopResolution)
HStack {
ForEach(BottleResolutionManager.detectDisplayProfiles(), id: \.id) { profile in
Button("\(profile.name) (\(profile.resolution))") {
gameOptions.virtualDesktopResolution = profile.resolution
}
.buttonStyle(.bordered)
}
}
}
}
}
HStack {
Button("Save settings") {
console.log("saving")
Expand Down Expand Up @@ -118,14 +136,31 @@ struct GameOptionsView: View {
if let data: GameOptionsData = readUsrDefData(key: gameOptKey) {
self.gameOptions.set(data: data)
}
if(!game!.isNative) {
loadResolutionFromBottle()
}
}
}

private func loadResolutionFromBottle() {
guard !appGlobals.selectedBottle.isEmpty,
let bottleURL = URL(string: appGlobals.selectedBottle) else { return }
let manager = BottleResolutionManager(bottleURL: bottleURL)
do {
let state = try manager.loadCurrentState()
gameOptions.virtualDesktopEnabled = state.isVirtualDesktopEnabled
gameOptions.virtualDesktopResolution = state.resolution
} catch {
console.error("Failed to load resolution state: \(String(reflecting: error))")
}
}

}

#Preview {
@State @Previewable var game: Game? = .mock
@StateObject @Previewable var gameOptions: GameOptions = GameOptions(cxGraphicsBackend: "dxmt")

GameOptionsView(game: $game).environmentObject(gameOptions)
@StateObject @Previewable var appGlobals: AppGlobals = AppGlobals()

GameOptionsView(game: $game).environmentObject(gameOptions).environmentObject(appGlobals)
}
16 changes: 13 additions & 3 deletions Procyon/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ struct GameOptionsData: Codable { // this is used for reading saved properties
var envVariables: String
var sdlEnabled: Bool
var hidrawDisabled: Bool

var virtualDesktopEnabled: Bool
var virtualDesktopResolution: String

init(data: GameOptions) {
self.cxGraphicsBackend = data.cxGraphicsBackend
self.wineMSync = data.wineMSync
Expand All @@ -51,6 +53,8 @@ struct GameOptionsData: Codable { // this is used for reading saved properties
self.envVariables = data.envVariables
self.sdlEnabled = data.enableSDL
self.hidrawDisabled = data.disableHidraw
self.virtualDesktopEnabled = data.virtualDesktopEnabled
self.virtualDesktopResolution = data.virtualDesktopResolution
}
}

Expand All @@ -72,8 +76,10 @@ class GameOptions: ObservableObject { // this is used as form state
@Published var envVariables: String
@Published var enableSDL: Bool
@Published var disableHidraw: Bool

init(cxGraphicsBackend: String = "d3dmetal", wineMSync: Bool = true, mtlHudEnabled: Bool = false, x87PatchEnabled: Bool = false, dx9PatchEnabled: Bool = false, dxvk: String? = nil, wineEsync: String? = nil, d3dMEnableMetalFX: String? = nil, d3dSupportDXR: String? = nil, gameArguments: String = "", dxmtPreferredMaxFrameRate: Double = 0, dxmtMetalFXSpatial: Bool = false, dxmtMetalSpatialUpscaleFactor: Double = 1.0, advertiseAVX: Bool = true, envVariables: String = "", sdlEnabled: Bool = true, hidrawDisabled: Bool = false) {
@Published var virtualDesktopEnabled: Bool
@Published var virtualDesktopResolution: String

init(cxGraphicsBackend: String = "d3dmetal", wineMSync: Bool = true, mtlHudEnabled: Bool = false, x87PatchEnabled: Bool = false, dx9PatchEnabled: Bool = false, dxvk: String? = nil, wineEsync: String? = nil, d3dMEnableMetalFX: String? = nil, d3dSupportDXR: String? = nil, gameArguments: String = "", dxmtPreferredMaxFrameRate: Double = 0, dxmtMetalFXSpatial: Bool = false, dxmtMetalSpatialUpscaleFactor: Double = 1.0, advertiseAVX: Bool = true, envVariables: String = "", sdlEnabled: Bool = true, hidrawDisabled: Bool = false, virtualDesktopEnabled: Bool = false, virtualDesktopResolution: String = "1920x1080") {
self.cxGraphicsBackend = cxGraphicsBackend
self.wineMSync = wineMSync
self.mtlHudEnabled = mtlHudEnabled
Expand All @@ -91,6 +97,8 @@ class GameOptions: ObservableObject { // this is used as form state
self.envVariables = envVariables
self.enableSDL = sdlEnabled
self.disableHidraw = hidrawDisabled
self.virtualDesktopEnabled = virtualDesktopEnabled
self.virtualDesktopResolution = virtualDesktopResolution
}
func set(data: GameOptionsData) {
self.cxGraphicsBackend = data.cxGraphicsBackend
Expand All @@ -106,6 +114,8 @@ class GameOptions: ObservableObject { // this is used as form state
self.envVariables = data.envVariables
self.enableSDL = data.sdlEnabled
self.disableHidraw = data.hidrawDisabled
self.virtualDesktopEnabled = data.virtualDesktopEnabled
self.virtualDesktopResolution = data.virtualDesktopResolution
}
}

Expand Down
17 changes: 17 additions & 0 deletions Procyon/Util/Launcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,23 @@ func launchWindowsGame(id: String, cxAppPath: String, selectedBottle: String, op
console.error("Missing game options for game with id \(id) - cannot launch (options = nil)")
return
}

// Apply per-game resolution to user.reg (read on next Wine boot)
if let bottleURL = URL(string: selectedBottle) {
let resManager = BottleResolutionManager(bottleURL: bottleURL)
let currentState = try resManager.loadCurrentState()
let wantEnabled = options!.virtualDesktopEnabled
let wantResolution = options!.virtualDesktopResolution

let needsChange = currentState.isVirtualDesktopEnabled != wantEnabled ||
(wantEnabled && currentState.resolution != wantResolution)

if needsChange {
let resolution = wantEnabled ? wantResolution : nil
try resManager.applyResolution(resolution)
}
}

let f = FileManager.default

var command = ""
Expand Down
112 changes: 112 additions & 0 deletions Procyon/Util/Resolution.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//
// Resolution.swift
// Procyon
//
// Resolution configuration for CrossOver bottles using Wine registry.
// Based on CXRes logic.
//

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably won't need the resolution configuration, there's an incoming fix in crossover preview that will be ready on the next minor release
3Shain/dxmt#140 (comment)

import AppKit

struct ResolutionProfile: Codable, Identifiable, Equatable, Hashable {
var id = UUID()
var name: String
var resolution: String
}

struct ResolutionState {
var isVirtualDesktopEnabled: Bool
var resolution: String // e.g. "1920x1080"
var dpi: UInt32 // e.g. 96
}

class BottleResolutionManager {
private let bottleURL: URL
private let registryFile: WineRegistryFile

private let explorerPath = "Software\\\\Wine\\\\Explorer"
private let desktopsPath = "Software\\\\Wine\\\\Explorer\\\\Desktops"
private let fontsPath = "Software\\\\Wine\\\\Fonts"

init(bottleURL: URL) {
self.bottleURL = bottleURL
let regURL = bottleURL.appendingPathComponent("user.reg")
self.registryFile = WineRegistryFile(fileURL: regURL)
}

// MARK: - Read

func loadCurrentState() throws -> ResolutionState {
try registryFile.load()

let explorerSection = registryFile.section(forPath: explorerPath)
let desktopsSection = registryFile.section(forPath: desktopsPath)
let fontsSection = registryFile.section(forPath: fontsPath)

let desktopValue = explorerSection?.getValue(forKey: "Desktop") ?? ""
let isEnabled = !desktopValue.isEmpty
let resolution = desktopsSection?.getValue(forKey: "Default") ?? "1920x1080"
let dpiStr = fontsSection?.getValue(forKey: "LogPixels") ?? "96"
let dpi = UInt32(dpiStr) ?? 96

return ResolutionState(
isVirtualDesktopEnabled: isEnabled,
resolution: resolution,
dpi: dpi
)
}

// MARK: - Write

func applyResolution(_ resolution: String?, dpi: UInt32? = nil) throws {
try registryFile.load()

let explorerSection = registryFile.section(forPath: explorerPath)
let desktopsSection = registryFile.section(forPath: desktopsPath)

if let resolution = resolution {
// Enable virtual desktop with given resolution
explorerSection?.addOrSetValue(forKey: "Desktop", stringValue: "Default")
desktopsSection?.addOrSetValue(forKey: "Default", stringValue: resolution)
console.log("Setting resolution to \(resolution)")
} else {
// Disable virtual desktop
explorerSection?.addOrSetValue(forKey: "Desktop", stringValue: "")
console.log("Disabling virtual desktop")
}

if let dpi = dpi {
let fontsSection = registryFile.section(forPath: fontsPath)
fontsSection?.setDword(forKey: "LogPixels", value: dpi)
console.log("Setting DPI to \(dpi)")
}

try registryFile.save()
}

func disableVirtualDesktop() throws {
try applyResolution(nil)
}

// MARK: - Display detection

static func detectDisplayProfiles() -> [ResolutionProfile] {
let screens = NSScreen.screens
guard !screens.isEmpty else {
return [ResolutionProfile(name: "Default", resolution: "1920x1080")]
}
var profiles: [ResolutionProfile] = []
var seen = Set<String>()
for screen in screens {
let backing = screen.convertRectToBacking(screen.frame)
let w = Int(backing.width)
let h = Int(backing.height)
let res = "\(w)x\(h)"
guard !seen.contains(res) else { continue }
seen.insert(res)
let name = screen == screens.first ? "Built-in" : "External"
profiles.append(ResolutionProfile(name: name, resolution: res))
}
return profiles
}
}
Loading