Skip to main content

librashader_presets/
context.rs

1// pub use librashader_presets_context::*;
2
3//! Shader preset wildcard replacement context handling.
4//!
5//! Implements wildcard replacement of shader paths specified in
6//! [RetroArch#15023](https://github.com/libretro/RetroArch/pull/15023).
7use librashader_common::map::FastHashMap;
8use once_cell::sync::Lazy;
9use regex::bytes::Regex;
10use std::collections::VecDeque;
11use std::fmt::{Debug, Display, Formatter};
12use std::ops::Add;
13use std::path::{Component, Path, PathBuf};
14
15/// Valid video driver or runtime. This list is non-exhaustive.
16#[repr(u32)]
17#[non_exhaustive]
18#[derive(Debug, Copy, Clone)]
19pub enum VideoDriver {
20    /// None  (`null`)
21    None = 0,
22    /// OpenGL Core (`glcore`)
23    GlCore,
24    /// Legacy OpenGL (`gl`)
25    Gl,
26    /// Vulkan (`vulkan`)
27    Vulkan,
28    /// Direct3D 9 (`d3d9_hlsl`)
29    Direct3D9Hlsl,
30    /// Direct3D 11 (`d3d11`)
31    Direct3D11,
32    /// Direct3D12 (`d3d12`)
33    Direct3D12,
34    /// Metal (`metal`)
35    Metal,
36}
37
38impl Display for VideoDriver {
39    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
40        match self {
41            VideoDriver::None => f.write_str("null"),
42            VideoDriver::GlCore => f.write_str("glcore"),
43            VideoDriver::Gl => f.write_str("gl"),
44            VideoDriver::Vulkan => f.write_str("vulkan"),
45            VideoDriver::Direct3D11 => f.write_str("d3d11"),
46            VideoDriver::Direct3D9Hlsl => f.write_str("d3d9_hlsl"),
47            VideoDriver::Direct3D12 => f.write_str("d3d12"),
48            VideoDriver::Metal => f.write_str("metal"),
49        }
50    }
51}
52
53/// Valid extensions for shader extensions.
54#[repr(u32)]
55#[derive(Debug, Copy, Clone)]
56pub enum ShaderExtension {
57    /// `.slang`
58    Slang = 0,
59    /// `.glsl`
60    Glsl,
61    /// `.cg`
62    Cg,
63}
64
65impl Display for ShaderExtension {
66    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
67        match self {
68            ShaderExtension::Slang => f.write_str("slang"),
69            ShaderExtension::Glsl => f.write_str("glsl"),
70            ShaderExtension::Cg => f.write_str("cg"),
71        }
72    }
73}
74
75/// Valid extensions for shader presets
76#[repr(u32)]
77#[derive(Debug, Copy, Clone)]
78pub enum PresetExtension {
79    /// `.slangp`
80    Slangp = 0,
81    /// `.glslp`
82    Glslp,
83    /// `.cgp`
84    Cgp,
85}
86
87impl Display for PresetExtension {
88    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
89        match self {
90            PresetExtension::Slangp => f.write_str("slangp"),
91            PresetExtension::Glslp => f.write_str("glslp"),
92            PresetExtension::Cgp => f.write_str("cgp"),
93        }
94    }
95}
96
97/// Rotation of the viewport.
98#[repr(u32)]
99#[derive(Debug, Copy, Clone)]
100pub enum Rotation {
101    /// Zero
102    Zero = 0,
103    /// 90 degrees
104    Right = 1,
105    /// 180 degrees
106    Straight = 2,
107    /// 270 degrees
108    Reflex = 3,
109}
110
111impl From<u32> for Rotation {
112    fn from(value: u32) -> Self {
113        let value = value % 4;
114        match value {
115            0 => Rotation::Zero,
116            1 => Rotation::Right,
117            2 => Rotation::Straight,
118            3 => Rotation::Reflex,
119            _ => unreachable!(),
120        }
121    }
122}
123
124impl Add for Rotation {
125    type Output = Rotation;
126
127    fn add(self, rhs: Self) -> Self::Output {
128        let lhs = self as u32;
129        let out = lhs + rhs as u32;
130        Rotation::from(out)
131    }
132}
133
134impl Display for Rotation {
135    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
136        match self {
137            Rotation::Zero => f.write_str("0"),
138            Rotation::Right => f.write_str("90"),
139            Rotation::Straight => f.write_str("180"),
140            Rotation::Reflex => f.write_str("270"),
141        }
142    }
143}
144
145/// Orientation of  aspect ratios
146#[repr(u32)]
147#[derive(Debug, Copy, Clone)]
148pub enum Orientation {
149    /// Vertical orientation.
150    Vertical = 0,
151    /// Horizontal orientation.
152    Horizontal,
153}
154
155impl Display for Orientation {
156    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
157        match self {
158            Orientation::Vertical => f.write_str("VERT"),
159            Orientation::Horizontal => f.write_str("HORZ"),
160        }
161    }
162}
163
164/// An item representing a variable that can be replaced in a path preset.
165#[derive(Debug, Clone)]
166pub enum ContextItem {
167    /// The content directory of the game (`CONTENT-DIR`)
168    ContentDirectory(String),
169    /// The name of the libretro core (`CORE`)
170    CoreName(String),
171    /// The filename of the game (`GAME`)
172    GameName(String),
173    /// The name of the preset (`PRESET`)
174    Preset(String),
175    /// The name of the preset directory (`PRESET_DIR`)
176    PresetDirectory(String),
177    /// The video driver (runtime) (`VID-DRV`)
178    VideoDriver(VideoDriver),
179    /// The extension of shader types supported by the driver (`VID-DRV-SHADER-EXT`)
180    VideoDriverShaderExtension(ShaderExtension),
181    /// The extension of shader presets supported by the driver (`VID-DRV-PRESET-EXT`)
182    VideoDriverPresetExtension(PresetExtension),
183    /// The rotation that the core is requesting (`CORE-REQ-ROT`)
184    CoreRequestedRotation(Rotation),
185    /// Whether or not to allow core-requested rotation (`VID-ALLOW-CORE-ROT`)
186    AllowCoreRotation(bool),
187    /// The rotation the user is requesting (`VID-USER-ROT`)
188    UserRotation(Rotation),
189    /// The final rotation (`VID-FINAL-ROT`) calculated by the sum of `VID-USER-ROT` and `CORE-REQ-ROT`
190    FinalRotation(Rotation),
191    /// The user-adjusted screen orientation (`SCREEN-ORIENT`)
192    ScreenOrientation(Rotation),
193    /// The orientation of the viewport aspect ratio (`VIEW-ASPECT-ORIENT`)
194    ViewAspectOrientation(Orientation),
195    /// The orientation of the aspect ratio requested by the core (`CORE-ASPECT-ORIENT`)
196    CoreAspectOrientation(Orientation),
197    /// An external, arbitrary context variable.
198    ExternContext(String, String),
199}
200
201impl ContextItem {
202    fn toggle_str(v: bool) -> &'static str {
203        if v {
204            "ON"
205        } else {
206            "OFF"
207        }
208    }
209
210    /// The wildcard key associated with the context item.
211    pub fn key(&self) -> &str {
212        match self {
213            ContextItem::ContentDirectory(_) => "CONTENT-DIR",
214            ContextItem::CoreName(_) => "CORE",
215            ContextItem::GameName(_) => "GAME",
216            ContextItem::Preset(_) => "PRESET",
217            ContextItem::PresetDirectory(_) => "PRESET_DIR",
218            ContextItem::VideoDriver(_) => "VID-DRV",
219            ContextItem::CoreRequestedRotation(_) => "CORE-REQ-ROT",
220            ContextItem::AllowCoreRotation(_) => "VID-ALLOW-CORE-ROT",
221            ContextItem::UserRotation(_) => "VID-USER-ROT",
222            ContextItem::FinalRotation(_) => "VID-FINAL-ROT",
223            ContextItem::ScreenOrientation(_) => "SCREEN-ORIENT",
224            ContextItem::ViewAspectOrientation(_) => "VIEW-ASPECT-ORIENT",
225            ContextItem::CoreAspectOrientation(_) => "CORE-ASPECT-ORIENT",
226            ContextItem::VideoDriverShaderExtension(_) => "VID-DRV-SHADER-EXT",
227            ContextItem::VideoDriverPresetExtension(_) => "VID-DRV-PRESET-EXT",
228            ContextItem::ExternContext(key, _) => key,
229        }
230    }
231}
232
233impl Display for ContextItem {
234    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
235        match self {
236            ContextItem::ContentDirectory(v) => f.write_str(v),
237            ContextItem::CoreName(v) => f.write_str(v),
238            ContextItem::GameName(v) => f.write_str(v),
239            ContextItem::Preset(v) => f.write_str(v),
240            ContextItem::PresetDirectory(v) => f.write_str(v),
241            ContextItem::VideoDriver(v) => f.write_fmt(format_args!("{}", v)),
242            ContextItem::CoreRequestedRotation(v) => {
243                f.write_fmt(format_args!("{}-{}", self.key(), v))
244            }
245            ContextItem::AllowCoreRotation(v) => f.write_fmt(format_args!(
246                "{}-{}",
247                self.key(),
248                ContextItem::toggle_str(*v)
249            )),
250            ContextItem::UserRotation(v) => f.write_fmt(format_args!("{}-{}", self.key(), v)),
251            ContextItem::FinalRotation(v) => f.write_fmt(format_args!("{}-{}", self.key(), v)),
252            ContextItem::ScreenOrientation(v) => f.write_fmt(format_args!("{}-{}", self.key(), v)),
253            ContextItem::ViewAspectOrientation(v) => {
254                f.write_fmt(format_args!("{}-{}", self.key(), v))
255            }
256            ContextItem::CoreAspectOrientation(v) => {
257                f.write_fmt(format_args!("{}-{}", self.key(), v))
258            }
259            ContextItem::VideoDriverShaderExtension(v) => f.write_fmt(format_args!("{}", v)),
260            ContextItem::VideoDriverPresetExtension(v) => f.write_fmt(format_args!("{}", v)),
261            ContextItem::ExternContext(_, v) => f.write_fmt(format_args!("{}", v)),
262        }
263    }
264}
265
266/// A preset wildcard context.
267///
268/// Any items added after will have higher priority
269/// when passed to the shader preset parser.
270///
271/// When passed to the preset parser, the preset parser
272/// will automatically add inferred items at lowest priority.
273///
274/// Any items added by the user will override the automatically
275/// inferred items.
276#[derive(Debug, Clone)]
277pub struct WildcardContext(VecDeque<ContextItem>);
278
279impl WildcardContext {
280    /// Create a new wildcard context.
281    pub fn new() -> Self {
282        Self(VecDeque::new())
283    }
284
285    /// Prepend an item to the context builder.
286    pub fn prepend_item(&mut self, item: ContextItem) {
287        self.0.push_front(item);
288    }
289
290    /// Append an item to the context builder.
291    /// The new item will take precedence over all items added before it.
292    pub fn append_item(&mut self, item: ContextItem) {
293        self.0.push_back(item);
294    }
295
296    /// Prepend sensible defaults for the given video driver.
297    ///
298    /// Any values added, either previously or afterwards will not be overridden.
299    pub fn add_video_driver_defaults(&mut self, video_driver: VideoDriver) {
300        self.prepend_item(ContextItem::VideoDriverPresetExtension(
301            PresetExtension::Slangp,
302        ));
303        self.prepend_item(ContextItem::VideoDriverShaderExtension(
304            ShaderExtension::Slang,
305        ));
306        self.prepend_item(ContextItem::VideoDriver(video_driver));
307    }
308
309    /// Prepend default entries from the path of the preset.
310    ///
311    /// Any values added, either previously or afterwards will not be overridden.
312    pub fn add_path_defaults(&mut self, path: impl AsRef<Path>) {
313        let path = path.as_ref();
314        if let Some(preset_name) = path.file_stem() {
315            let preset_name = preset_name.to_string_lossy();
316            self.prepend_item(ContextItem::Preset(preset_name.into()))
317        }
318
319        if let Some(preset_dir_name) = path.parent().and_then(|p| {
320            if !p.is_dir() {
321                return None;
322            };
323            p.file_name()
324        }) {
325            let preset_dir_name = preset_dir_name.to_string_lossy();
326            self.prepend_item(ContextItem::PresetDirectory(preset_dir_name.into()))
327        }
328    }
329
330    /// Convert the context into a string hashmap.
331    ///
332    /// This is a one way conversion, and will normalize rotation context items
333    /// into `VID-FINAL-ROT`.
334    pub fn into_hashmap(mut self) -> FastHashMap<String, String> {
335        let mut map = FastHashMap::default();
336        let last_user_rot = self
337            .0
338            .iter()
339            .rfind(|i| matches!(i, ContextItem::UserRotation(_)));
340        let last_core_rot = self
341            .0
342            .iter()
343            .rfind(|i| matches!(i, ContextItem::CoreRequestedRotation(_)));
344
345        let final_rot = match (last_core_rot, last_user_rot) {
346            (Some(ContextItem::UserRotation(u)), None) => Some(ContextItem::FinalRotation(*u)),
347            (None, Some(ContextItem::CoreRequestedRotation(c))) => {
348                Some(ContextItem::FinalRotation(*c))
349            }
350            (Some(ContextItem::UserRotation(u)), Some(ContextItem::CoreRequestedRotation(c))) => {
351                Some(ContextItem::FinalRotation(*u + *c))
352            }
353            _ => None,
354        };
355
356        if let Some(final_rot) = final_rot {
357            self.prepend_item(final_rot);
358        }
359
360        for item in self.0 {
361            map.insert(String::from(item.key()), item.to_string());
362        }
363
364        map
365    }
366}
367
368pub(crate) fn apply_context(path: &mut PathBuf, context: &FastHashMap<String, String>) {
369    use std::ffi::{OsStr, OsString};
370
371    static WILDCARD_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("\\$([A-Z-_]+)\\$").unwrap());
372    if context.is_empty() {
373        return;
374    }
375    // Don't want to do any extra work if there's no match.
376    if !WILDCARD_REGEX.is_match(path.as_os_str().as_encoded_bytes()) {
377        return;
378    }
379
380    let mut new_path = PathBuf::with_capacity(path.capacity());
381    for component in path.components() {
382        match component {
383            Component::Normal(path) => {
384                let haystack = path.as_encoded_bytes();
385
386                let replaced =
387                    WILDCARD_REGEX.replace_all(haystack, |caps: &regex::bytes::Captures| {
388                        let Some(name) = caps.get(1) else {
389                            return caps[0].to_vec();
390                        };
391
392                        let Ok(key) = std::str::from_utf8(name.as_bytes()) else {
393                            return caps[0].to_vec();
394                        };
395                        if let Some(replacement) = context.get(key) {
396                            return OsString::from(replacement.to_string()).into_encoded_bytes();
397                        }
398                        return caps[0].to_vec();
399                    });
400
401                // SAFETY: The original source is valid encoded bytes, and our replacement is
402                // valid encoded bytes. This upholds the safety requirements of `from_encoded_bytes_unchecked`.
403                new_path.push(unsafe { OsStr::from_encoded_bytes_unchecked(&replaced.as_ref()) })
404            }
405            _ => new_path.push(component),
406        }
407    }
408
409    // If no wildcards are found within the path, or the path after replacing the wildcards does not exist on disk, the path returned will be unaffected.
410    if let Ok(true) = new_path.try_exists() {
411        *path = new_path;
412    }
413}