rres/
lib.rs

1// Copyright (c) 2022 Namkhai B.
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// (at your option) any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program.  If not, see <http://www.gnu.org/licenses/>.
15//
16// SPDX-License-Identifier: GPL-3.0-only
17
18use std::env;
19use std::fs;
20use std::os;
21use std::path;
22
23use anyhow::Context;
24use drm::control::{Device as ControlDevice, Mode};
25use drm::Device;
26
27mod fsr;
28
29// Card handle
30// Really just to get a file descriptor for `drm`
31struct Card(std::fs::File);
32
33impl os::fd::AsFd for Card {
34    fn as_fd(&self) -> os::fd::BorrowedFd<'_> {
35        self.0.as_fd()
36    }
37}
38
39impl Card {
40    pub fn open<P: AsRef<path::Path>>(path: P) -> Self {
41        let mut options = std::fs::OpenOptions::new();
42        options.read(true);
43        options.write(true);
44        Card(options.open(path).unwrap())
45    }
46}
47
48// Implement `drm` types
49impl Device for Card {}
50impl ControlDevice for Card {}
51
52/// Build FSR arguments for gamescope
53pub fn gamescope(res: (u16, u16), fsr_mode: &str) -> anyhow::Result<Vec<String>> {
54    let gamescope_bin: String = env::var("RRES_GAMESCOPE").unwrap_or("gamescope".to_string());
55    let mut gamescope_runner: Vec<String> = vec![gamescope_bin];
56
57    let args = if !fsr_mode.is_empty() && fsr_mode.to_lowercase() != "native" {
58        let Ok(fsr) = fsr::Fsr::try_from(fsr_mode) else {
59            return Err(anyhow::anyhow!("invalid FSR mode: {}", fsr_mode));
60        };
61
62        let fsr_res = fsr.generate(res);
63        format!(
64            "-W {} -H {} -U -w {} -h {}",
65            res.0, res.1, fsr_res.0, fsr_res.1
66        )
67    } else {
68        format!("-W {} -H {}", res.0, res.1)
69    };
70
71    gamescope_runner.extend(args.split(' ').map(|s| s.to_owned()));
72
73    Ok(gamescope_runner)
74}
75
76/// Get all the displays from the system or selected card
77pub fn get_displays(card: Option<String>) -> anyhow::Result<Vec<Mode>> {
78    // Store found displays
79    let mut displays: Vec<Mode> = vec![];
80    // Store the checked cards
81    let mut cards: Vec<path::PathBuf> = vec![];
82
83    if let Some(c) = card {
84        // Open single card
85        let mut file = path::PathBuf::from("/dev/dri/");
86        file.push(&c);
87        if !file.exists() || !c.starts_with("card") {
88            return Err(anyhow::anyhow!("invalid card ({c})"));
89        }
90        cards.push(file);
91    } else {
92        // Open every card on the system
93        for entry in fs::read_dir("/dev/dri/")? {
94            let file = entry?;
95            if let Some(name) = file.file_name().to_str() {
96                if name.starts_with("card") {
97                    cards.push(file.path());
98                }
99            }
100        }
101    }
102
103    // Sort cards (card0, card1, card2...)
104    cards.sort();
105
106    // Read card list
107    for file in cards {
108        let gpu = Card::open(file);
109        let info = gpu.get_driver()?;
110        log::debug!("Found GPU: {}", info.name().to_string_lossy());
111        // Find displays
112        match get_card_modes(&gpu) {
113            Ok(modes) => displays.extend_from_slice(&modes),
114            Err(e) => log::error!("failed to read modes: {e}"),
115        }
116    }
117
118    Ok(displays)
119}
120
121/// Get the resolution from first display
122pub fn get_res() -> anyhow::Result<(u16, u16)> {
123    get_res_card(None)
124}
125
126/// Get the resolution from the first display of the selected card
127pub fn get_res_card(card: Option<String>) -> anyhow::Result<(u16, u16)> {
128    let res;
129
130    if let Ok(forced) = env::var("RRES_FORCE_RES") {
131        if let Some((x, y)) = forced.split_once('x') {
132            res = (x.parse()?, y.parse()?);
133        } else {
134            return Err(anyhow::anyhow!("failed to parse RRES_FORCE_RES"));
135        }
136    } else {
137        let displays = get_displays(card)?;
138
139        let selection: usize = env::var("RRES_DISPLAY")
140            .unwrap_or_else(|_| "0".to_string())
141            .parse()
142            .context("Failed to parse RRES_DISPLAY")?;
143
144        if selection > displays.len() - 1 {
145            return Err(anyhow::anyhow!("invalid display: {}", selection));
146        }
147
148        res = displays[selection].size();
149    }
150
151    Ok(res)
152}
153
154/// Get all the connected display's modes from a libdrm card.
155pub fn get_card_modes<G: ControlDevice>(gpu: &G) -> anyhow::Result<Vec<Mode>> {
156    let mut modes: Vec<Mode> = vec![];
157
158    let resources = gpu
159        .resource_handles()
160        .context("failed to get resource handles")?;
161    let connectors = resources.connectors();
162    for handle in connectors {
163        let connector = gpu
164            .get_connector(*handle, false)
165            .context("failed to get connector handle")?;
166        if connector.state() == drm::control::connector::State::Connected {
167            // Connected, get mode
168            modes.push(get_connector_mode(gpu, &connector)?);
169        }
170    }
171    Ok(modes)
172}
173
174/// Get current display mode from connector
175///
176/// Note: nVidia GPUs don't share the current encoder+crtc, so this function will report the
177/// native display's resolution instead of the current resolution.
178fn get_connector_mode<G: ControlDevice>(
179    gpu: &G,
180    connector: &drm::control::connector::Info,
181) -> anyhow::Result<Mode> {
182    if connector.state() != drm::control::connector::State::Connected {
183        return Err(anyhow::anyhow!("Connector is disconnected"));
184    }
185    if let Some(encoder_handle) = connector.current_encoder() {
186        // Get the encoder then crtc
187        let encoder = gpu.get_encoder(encoder_handle)?;
188        if let Some(crtc_handle) = encoder.crtc() {
189            let crtc = gpu.get_crtc(crtc_handle).context("failed to get crtc")?;
190            // Get current mode, and store it
191            if let Some(current_mode) = crtc.mode() {
192                log::debug!(
193                    "Found display: {:?}, {}x{}",
194                    connector.interface(),
195                    current_mode.size().0,
196                    current_mode.size().1
197                );
198                return Ok(current_mode);
199            }
200        }
201    }
202    // nVidia GPUs don't expose the encoder (and thus neither the crtc)
203    log::warn!(
204        "Could not detect current mode for display {:?},",
205        connector.interface()
206    );
207    log::warn!("reading native resolution");
208    return Ok(connector.modes()[0]);
209}