Skip to main content

nvidia_streamline/
lib.rs

1use {
2    anyhow::{Result, ensure},
3    ash::vk,
4    std::{
5        path::{Path, PathBuf},
6        sync::Arc,
7    },
8};
9
10#[cfg(all(target_os = "windows", feature = "vk-graph"))]
11use {
12    ash::vk::Handle as _,
13    vk_graph::driver::{image::Image, instance::Instance},
14};
15
16#[cfg(nvidia_streamline_native)]
17mod ffi;
18#[cfg(nvidia_streamline_native)]
19mod native;
20#[cfg(not(nvidia_streamline_native))]
21mod stub;
22
23#[cfg(nvidia_streamline_native)]
24use native as backend;
25#[cfg(not(nvidia_streamline_native))]
26use stub as backend;
27
28/// Whether this build includes the native Streamline backend.
29pub const NATIVE_BACKEND_AVAILABLE: bool = cfg!(nvidia_streamline_native);
30
31#[derive(Clone, Copy, Debug, Default, PartialEq)]
32#[repr(C)]
33pub struct DlssRrFrameConstants {
34    pub camera_view_to_clip: [f32; 16],
35    pub clip_to_camera_view: [f32; 16],
36    pub clip_to_prev_clip: [f32; 16],
37    pub prev_clip_to_clip: [f32; 16],
38    pub world_to_camera_view: [f32; 16],
39    pub camera_view_to_world: [f32; 16],
40    pub jitter_offset: [f32; 2],
41    pub mvec_scale: [f32; 2],
42    pub camera_pos: [f32; 3],
43    pub camera_near: f32,
44    pub camera_up: [f32; 3],
45    pub camera_far: f32,
46    pub camera_right: [f32; 3],
47    pub camera_fov: f32,
48    pub camera_forward: [f32; 3],
49    pub camera_aspect_ratio: f32,
50    pub frame_index: u32,
51    pub reset: u32,
52}
53
54#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
55#[repr(u32)]
56pub enum DlssRrQuality {
57    #[default]
58    Balanced = 0,
59    Quality = 1,
60    Dlaa = 2,
61}
62
63#[derive(Clone, Copy, Debug)]
64#[repr(C)]
65pub struct DlssRrResources {
66    pub input_color: VulkanImage,
67    pub output_color: VulkanImage,
68    pub depth: VulkanImage,
69    pub motion: VulkanImage,
70    pub diffuse_albedo: VulkanImage,
71    pub specular_albedo: VulkanImage,
72    pub normal_roughness: VulkanImage,
73    pub reflection_guide: ReflectionGuide,
74}
75
76#[derive(Clone)]
77pub struct Evaluator {
78    context: Arc<backend::Context>,
79}
80
81impl Evaluator {
82    #[must_use]
83    pub fn is_active(&self) -> bool {
84        self.context.is_active()
85    }
86
87    /// # Safety
88    ///
89    /// Unless null (which returns false), `physical_device` must be a valid handle enumerated
90    /// from this context's interposer instance. The physical device, its parent instance, and
91    /// the Vulkan/interposer loaders and their function pointers must remain valid throughout
92    /// the call; do not destroy the instance or unload either loader concurrently.
93    #[must_use]
94    pub unsafe fn is_dlss_rr_supported(&self, physical_device: vk::PhysicalDevice) -> bool {
95        unsafe { self.context.is_dlss_rr_supported(physical_device) }
96    }
97
98    /// # Errors
99    ///
100    /// Returns an error if the backend is unavailable, the context is shut down,
101    /// the output extent is zero, or Streamline fails to return a nonzero render extent.
102    pub fn optimal_render_extent(
103        &self,
104        output_extent: [u32; 2],
105        quality: DlssRrQuality,
106    ) -> Result<[u32; 2]> {
107        self.context.optimal_render_extent(output_extent, quality)
108    }
109
110    /// # Safety
111    ///
112    /// This evaluator and its owning Streamline context must remain valid until execution
113    /// completes. The command buffer must be recording on the context's device; every image and
114    /// view must belong to that device, and each declared layout must match its actual state. All
115    /// handles must remain valid until GPU execution completes. If dimensions or quality change,
116    /// prior evaluations must have completed first.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if the backend is unavailable, the context is shut down,
121    /// the command buffer is null, or Streamline rejects the evaluation.
122    pub unsafe fn evaluate_dlss_rr(
123        &self,
124        command_buffer: vk::CommandBuffer,
125        frame: &DlssRrFrameConstants,
126        resources: &DlssRrResources,
127        quality: DlssRrQuality,
128    ) -> Result<()> {
129        unsafe {
130            self.context
131                .evaluate_dlss_rr(command_buffer, frame, resources, quality)
132        }
133    }
134
135    /// # Safety
136    ///
137    /// All Streamline GPU work must have completed before shutdown.
138    ///
139    /// # Errors
140    ///
141    /// Returns an error if native Streamline shutdown fails. An already shut-down
142    /// context or unavailable backend is a successful no-op.
143    pub unsafe fn shutdown(&self) -> Result<()> {
144        unsafe { self.context.shutdown() }
145    }
146}
147
148/// Owns the Vulkan instance created by the Streamline interposer.
149///
150/// Shut Streamline down after GPU completion but before destroying child devices, then drop the
151/// imported instance wrappers and all children before dropping this owner. The owner retains the
152/// interposer loader and ash instance, independently of the `vk-graph` feature. If Streamline is
153/// still active or the thread is panicking, dropping the owner intentionally leaks the native proxy
154/// and context. Child lifetimes are the caller's responsibility; they are not tracked by this owner.
155#[cfg(target_os = "windows")]
156pub struct ProxyInstanceOwner {
157    owner: Option<backend::ProxyOwner>,
158    evaluator: Option<Evaluator>,
159}
160
161#[cfg(target_os = "windows")]
162impl ProxyInstanceOwner {
163    /// # Safety
164    ///
165    /// This owner must outlive all uses and clones of the returned loader and its function pointers.
166    /// Do not create additional instances through this entry; use the owned interposer instance.
167    ///
168    /// # Panics
169    ///
170    /// Panics if the internal owner invariant is violated.
171    #[must_use]
172    pub unsafe fn entry(&self) -> &ash::Entry {
173        self.owner
174            .as_ref()
175            .expect("proxy vulkan instance owner invariant")
176            .entry()
177    }
178
179    /// # Panics
180    ///
181    /// Panics if the internal owner invariant is violated.
182    #[must_use]
183    pub fn evaluator(&self) -> Evaluator {
184        self.evaluator
185            .as_ref()
186            .expect("proxy vulkan instance owner invariant")
187            .clone()
188    }
189
190    /// # Safety
191    ///
192    /// Do not destroy this instance yourself. This owner must outlive all instance clones, function
193    /// pointers, and child objects. After GPU completion, shut the evaluator down before destroying
194    /// child devices, then destroy all children before dropping this owner.
195    ///
196    /// # Panics
197    ///
198    /// Panics if the internal owner invariant is violated.
199    #[must_use]
200    pub unsafe fn instance(&self) -> &ash::Instance {
201        self.owner
202            .as_ref()
203            .expect("proxy vulkan instance owner invariant")
204            .instance()
205    }
206}
207
208#[cfg(target_os = "windows")]
209impl Drop for ProxyInstanceOwner {
210    fn drop(&mut self) {
211        if std::thread::panicking() || self.evaluator.as_ref().is_some_and(Evaluator::is_active) {
212            if let Some(owner) = self.owner.take() {
213                std::mem::forget(owner);
214            }
215
216            if let Some(evaluator) = self.evaluator.take() {
217                std::mem::forget(evaluator);
218            }
219
220            return;
221        }
222
223        if let Some(mut owner) = self.owner.take() {
224            owner.destroy();
225        }
226
227        self.evaluator.take();
228    }
229}
230
231/// A vk-graph instance created through the Streamline Vulkan interposer.
232///
233/// Keep this owner alive while using the instance. After GPU work completes, shut its evaluator
234/// down before dropping child devices. The owner may be dropped only after every child and cloned
235/// instance has been dropped. Dropping while Streamline is active or the thread is panicking
236/// intentionally leaks the native proxy; child lifetimes are not tracked.
237#[cfg(all(target_os = "windows", feature = "vk-graph"))]
238pub struct ProxyVulkanInstance {
239    instance: Option<Instance>,
240    owner: Option<backend::ProxyOwner>,
241    evaluator: Option<Evaluator>,
242}
243
244#[cfg(all(target_os = "windows", feature = "vk-graph"))]
245impl ProxyVulkanInstance {
246    /// # Safety
247    ///
248    /// After GPU completion, the evaluator must be shut down before child devices are destroyed.
249    /// Every child object and cloned `vk_graph::Instance` created from the returned instance must
250    /// then be destroyed before this owner.
251    ///
252    /// # Panics
253    ///
254    /// Panics if the internal owner invariant is violated.
255    #[must_use]
256    pub unsafe fn instance(&self) -> &Instance {
257        self.instance
258            .as_ref()
259            .expect("proxy vulkan instance owner invariant")
260    }
261
262    /// # Panics
263    ///
264    /// Panics if the internal owner invariant is violated.
265    #[must_use]
266    pub fn evaluator(&self) -> Evaluator {
267        self.evaluator
268            .as_ref()
269            .expect("proxy vulkan instance owner invariant")
270            .clone()
271    }
272
273    /// # Safety
274    ///
275    /// The returned owner must outlive the instance and every device or resource created from it.
276    /// After GPU completion, shut the evaluator down before destroying child devices.
277    ///
278    /// # Panics
279    ///
280    /// Panics if the internal owner invariant is violated.
281    #[must_use]
282    pub unsafe fn into_parts(mut self) -> (Instance, ProxyInstanceOwner) {
283        let instance = self
284            .instance
285            .take()
286            .expect("proxy vulkan instance owner invariant");
287        let owner = ProxyInstanceOwner {
288            owner: self.owner.take(),
289            evaluator: self.evaluator.take(),
290        };
291        (instance, owner)
292    }
293}
294
295#[cfg(all(target_os = "windows", feature = "vk-graph"))]
296impl Drop for ProxyVulkanInstance {
297    fn drop(&mut self) {
298        if std::thread::panicking() || self.evaluator.as_ref().is_some_and(Evaluator::is_active) {
299            if let Some(instance) = self.instance.take() {
300                std::mem::forget(instance);
301            }
302
303            if let Some(owner) = self.owner.take() {
304                std::mem::forget(owner);
305            }
306
307            if let Some(evaluator) = self.evaluator.take() {
308                std::mem::forget(evaluator);
309            }
310
311            return;
312        }
313
314        drop(self.instance.take());
315
316        if let Some(mut owner) = self.owner.take() {
317            owner.destroy();
318        }
319
320        self.evaluator.take();
321    }
322}
323
324#[derive(Clone, Copy, Debug, Eq, PartialEq)]
325#[repr(C)]
326pub struct ReflectionGuide {
327    image: VulkanImage,
328    kind: ReflectionGuideKind,
329    reserved: u32,
330}
331
332impl ReflectionGuide {
333    #[must_use]
334    pub fn specular_motion_vectors(image: VulkanImage) -> Self {
335        Self {
336            image,
337            kind: ReflectionGuideKind::SpecularMotionVectors,
338            reserved: 0,
339        }
340    }
341
342    /// # Errors
343    ///
344    /// Returns an error unless the image format is `VK_FORMAT_R32_SFLOAT`.
345    pub fn specular_hit_distance(image: VulkanImage) -> Result<Self> {
346        ensure!(
347            image.format == vk::Format::R32_SFLOAT.as_raw().cast_unsigned(),
348            "a specular hit-distance guide must use vk_format_r32_sfloat"
349        );
350
351        Ok(Self {
352            image,
353            kind: ReflectionGuideKind::SpecularHitDistance,
354            reserved: 0,
355        })
356    }
357
358    #[must_use]
359    pub fn image(self) -> VulkanImage {
360        self.image
361    }
362
363    #[must_use]
364    pub fn kind(self) -> ReflectionGuideKind {
365        self.kind
366    }
367}
368
369#[derive(Clone, Copy, Debug, Eq, PartialEq)]
370#[repr(u32)]
371pub enum ReflectionGuideKind {
372    SpecularMotionVectors = 0,
373    SpecularHitDistance = 1,
374}
375
376pub struct Streamline {
377    evaluator: Evaluator,
378    runtime_dir: PathBuf,
379    #[cfg(target_os = "windows")]
380    interposer_path: PathBuf,
381    #[cfg(target_os = "windows")]
382    proxy_created: bool,
383}
384
385impl Streamline {
386    /// Returns the build-staged Streamline runtime directory on the supported target.
387    #[must_use]
388    pub fn staged_runtime_dir() -> Option<&'static Path> {
389        #[cfg(nvidia_streamline_native)]
390        {
391            Some(Path::new(env!("NVIDIA_STREAMLINE_RUNTIME_DIR")))
392        }
393        #[cfg(not(nvidia_streamline_native))]
394        {
395            None
396        }
397    }
398
399    fn validate_project_identity(project_id: &str, engine_version: &str) -> Result<()> {
400        ensure!(
401            !project_id.is_empty(),
402            "nvidia project id must not be empty"
403        );
404        ensure!(
405            !engine_version.is_empty(),
406            "nvidia engine version must not be empty"
407        );
408        ensure!(
409            !project_id.as_bytes().contains(&0),
410            "nvidia project id contains a null byte"
411        );
412        ensure!(
413            !engine_version.as_bytes().contains(&0),
414            "nvidia engine version contains a null byte"
415        );
416
417        Ok(())
418    }
419
420    /// # Errors
421    ///
422    /// Returns an error if either identity string is empty or contains a null byte,
423    /// the native backend is unavailable, the runtime directory is not absolute or
424    /// lacks the interposer, another context is active, or native initialization fails.
425    pub fn initialize(
426        project_id: &str,
427        engine_version: &str,
428        runtime_dir: impl AsRef<Path>,
429    ) -> Result<Self> {
430        Self::validate_project_identity(project_id, engine_version)?;
431
432        let runtime_dir = runtime_dir.as_ref().to_owned();
433        let context = Arc::new(backend::Context::initialize(
434            project_id,
435            engine_version,
436            &runtime_dir,
437        )?);
438        #[cfg(target_os = "windows")]
439        let interposer_path = runtime_dir.join("sl.interposer.dll");
440
441        Ok(Self {
442            evaluator: Evaluator { context },
443            runtime_dir,
444            #[cfg(target_os = "windows")]
445            interposer_path,
446            #[cfg(target_os = "windows")]
447            proxy_created: false,
448        })
449    }
450
451    #[must_use]
452    pub fn runtime_dir(&self) -> &Path {
453        &self.runtime_dir
454    }
455
456    #[must_use]
457    pub fn evaluator(&self) -> Evaluator {
458        self.evaluator.clone()
459    }
460
461    /// # Safety
462    ///
463    /// Unless null (which returns false), `physical_device` must be a valid handle enumerated
464    /// from this context's interposer instance. The physical device, its parent instance, and
465    /// the Vulkan/interposer loaders and their function pointers must remain valid throughout
466    /// the call; do not destroy the instance or unload either loader concurrently.
467    #[must_use]
468    pub unsafe fn is_dlss_rr_supported(&self, physical_device: vk::PhysicalDevice) -> bool {
469        unsafe { self.evaluator.is_dlss_rr_supported(physical_device) }
470    }
471
472    /// # Errors
473    ///
474    /// Returns the errors described by [`Evaluator::optimal_render_extent`].
475    pub fn optimal_render_extent(
476        &self,
477        output_extent: [u32; 2],
478        quality: DlssRrQuality,
479    ) -> Result<[u32; 2]> {
480        self.evaluator.optimal_render_extent(output_extent, quality)
481    }
482
483    /// # Safety
484    ///
485    /// This evaluator and its owning Streamline context must remain valid until execution
486    /// completes. The command buffer must be recording on the context's device; every image and
487    /// view must belong to that device, and each declared layout must match its actual state. All
488    /// handles must remain valid until GPU execution completes. If dimensions or quality change,
489    /// prior evaluations must have completed first.
490    ///
491    /// # Errors
492    ///
493    /// Returns the errors described by [`Evaluator::evaluate_dlss_rr`].
494    pub unsafe fn evaluate_dlss_rr(
495        &self,
496        command_buffer: vk::CommandBuffer,
497        frame: &DlssRrFrameConstants,
498        resources: &DlssRrResources,
499        quality: DlssRrQuality,
500    ) -> Result<()> {
501        unsafe {
502            self.evaluator
503                .evaluate_dlss_rr(command_buffer, frame, resources, quality)
504        }
505    }
506
507    /// Creates an ash instance through the Streamline interposer without requiring `vk-graph`.
508    ///
509    /// Only one proxy may be successfully created per context, across both creation APIs.
510    /// Devices must be created through this instance so the interposer observes their creation;
511    /// this does not register externally created devices.
512    ///
513    /// # Safety
514    ///
515    /// `create_info` must satisfy Vulkan's instance creation requirements. Every reachable
516    /// pointer (including application info, name arrays and strings, and the entire `p_next`
517    /// chain) must be correctly aligned, initialized, and valid for its declared type and
518    /// length throughout the call. Any callbacks and user data retained by Vulkan must remain
519    /// valid for all possible invocations, including instance destruction.
520    ///
521    /// The returned owner must outlive all imported wrappers, instance/loader clones, function
522    /// pointers, and child objects. Do not destroy its instance yourself. After GPU completion,
523    /// shut the evaluator down before destroying child devices, then destroy all children and
524    /// drop imported wrappers before dropping the owner and unloading its interposer.
525    ///
526    /// # Errors
527    ///
528    /// Returns an error if a proxy already exists, the backend is unavailable,
529    /// the context is shut down, or loading the interposer or creating the instance fails.
530    #[cfg(target_os = "windows")]
531    pub unsafe fn create_proxy_ash_instance(
532        &mut self,
533        create_info: &vk::InstanceCreateInfo<'_>,
534    ) -> Result<ProxyInstanceOwner> {
535        ensure!(
536            !self.proxy_created,
537            "a streamline proxy vulkan instance has already been created"
538        );
539
540        let owner = unsafe {
541            self.evaluator
542                .context
543                .create_proxy_ash_instance(&self.interposer_path, create_info)?
544        };
545
546        self.proxy_created = true;
547        Ok(ProxyInstanceOwner {
548            owner: Some(owner),
549            evaluator: Some(self.evaluator.clone()),
550        })
551    }
552
553    /// Creates a vk-graph instance through the Streamline interposer.
554    ///
555    /// Only one proxy may be successfully created per context, across both creation APIs.
556    /// Devices must be created through this instance so the interposer observes their creation;
557    /// this does not register externally created devices.
558    ///
559    /// # Safety
560    ///
561    /// `create_info` must satisfy Vulkan's instance creation requirements. Every reachable
562    /// pointer (including application info, name arrays and strings, and the entire `p_next`
563    /// chain) must be correctly aligned, initialized, and valid for its declared type and
564    /// length throughout the call. Any callbacks and user data retained by Vulkan must remain
565    /// valid for all possible invocations, including instance destruction.
566    ///
567    /// The returned owner must outlive all imported wrappers, instance/loader clones, function
568    /// pointers, and child objects. Do not destroy its instance yourself. After GPU completion,
569    /// shut the evaluator down before destroying child devices, then destroy all children and
570    /// drop imported wrappers before dropping the owner and unloading its interposer.
571    ///
572    /// # Errors
573    ///
574    /// Returns an error if a proxy already exists, the backend is unavailable,
575    /// the context is shut down, or loading, creating, or importing the instance fails.
576    #[cfg(all(target_os = "windows", feature = "vk-graph"))]
577    pub unsafe fn create_proxy_vulkan_instance(
578        &mut self,
579        create_info: &vk::InstanceCreateInfo<'_>,
580    ) -> Result<ProxyVulkanInstance> {
581        ensure!(
582            !self.proxy_created,
583            "a streamline proxy vulkan instance has already been created"
584        );
585
586        let (instance, owner) = unsafe {
587            self.evaluator
588                .context
589                .create_proxy_vulkan_instance(&self.interposer_path, create_info)?
590        };
591
592        self.proxy_created = true;
593        Ok(ProxyVulkanInstance {
594            instance: Some(instance),
595            owner: Some(owner),
596            evaluator: Some(self.evaluator.clone()),
597        })
598    }
599
600    /// # Safety
601    ///
602    /// All Streamline GPU work must have completed before shutdown.
603    ///
604    /// # Errors
605    ///
606    /// Returns the errors described by [`Evaluator::shutdown`].
607    pub unsafe fn shutdown(&self) -> Result<()> {
608        unsafe { self.evaluator.shutdown() }
609    }
610}
611
612#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
613#[repr(C)]
614pub struct VulkanImage {
615    pub image: u64,
616    pub view: u64,
617    pub state: u32,
618    pub width: u32,
619    pub height: u32,
620    pub format: u32,
621    pub mip_levels: u32,
622    pub array_layers: u32,
623    pub flags: u32,
624    pub usage: u32,
625}
626
627impl VulkanImage {
628    #[cfg(all(target_os = "windows", feature = "vk-graph"))]
629    #[must_use]
630    pub fn with_layout(image: &Image, view: vk::ImageView, layout: vk::ImageLayout) -> Self {
631        Self {
632            image: image.handle.as_raw(),
633            view: view.as_raw(),
634            state: layout.as_raw().cast_unsigned(),
635            width: image.info.width,
636            height: image.info.height,
637            format: image.info.format.as_raw().cast_unsigned(),
638            mip_levels: image.info.mip_level_count,
639            array_layers: image.info.array_layer_count,
640            flags: image.info.flags.as_raw(),
641            usage: image.info.usage.as_raw(),
642        }
643    }
644}
645
646#[cfg(test)]
647mod tests {
648    use {
649        super::*,
650        std::mem::{align_of, offset_of, size_of},
651    };
652
653    #[test]
654    fn native_ffi_layouts_are_stable() {
655        assert_eq!(size_of::<DlssRrQuality>(), 4);
656        assert_eq!(DlssRrQuality::default(), DlssRrQuality::Balanced);
657        assert_eq!(DlssRrQuality::Balanced as u32, 0);
658        assert_eq!(DlssRrQuality::Quality as u32, 1);
659        assert_eq!(DlssRrQuality::Dlaa as u32, 2);
660
661        assert_eq!(size_of::<VulkanImage>(), 48);
662        assert_eq!(align_of::<VulkanImage>(), 8);
663        assert_eq!(offset_of!(VulkanImage, image), 0);
664        assert_eq!(offset_of!(VulkanImage, view), 8);
665        assert_eq!(offset_of!(VulkanImage, state), 16);
666        assert_eq!(offset_of!(VulkanImage, usage), 44);
667
668        assert_eq!(size_of::<ReflectionGuideKind>(), 4);
669        assert_eq!(size_of::<ReflectionGuide>(), 56);
670        assert_eq!(align_of::<ReflectionGuide>(), 8);
671        assert_eq!(offset_of!(ReflectionGuide, image), 0);
672        assert_eq!(offset_of!(ReflectionGuide, kind), 48);
673        assert_eq!(offset_of!(ReflectionGuide, reserved), 52);
674
675        assert_eq!(size_of::<DlssRrResources>(), 392);
676        assert_eq!(align_of::<DlssRrResources>(), 8);
677        assert_eq!(offset_of!(DlssRrResources, reflection_guide), 336);
678
679        assert_eq!(size_of::<DlssRrFrameConstants>(), 472);
680        assert_eq!(align_of::<DlssRrFrameConstants>(), 4);
681        assert_eq!(offset_of!(DlssRrFrameConstants, jitter_offset), 384);
682        assert_eq!(offset_of!(DlssRrFrameConstants, frame_index), 464);
683        assert_eq!(offset_of!(DlssRrFrameConstants, reset), 468);
684    }
685
686    #[test]
687    fn specular_hit_distance_requires_r32_sfloat() {
688        let invalid = VulkanImage {
689            format: vk::Format::R16_SFLOAT.as_raw().cast_unsigned(),
690            ..Default::default()
691        };
692        assert!(ReflectionGuide::specular_hit_distance(invalid).is_err());
693
694        let valid = VulkanImage {
695            format: vk::Format::R32_SFLOAT.as_raw().cast_unsigned(),
696            ..Default::default()
697        };
698        let guide = ReflectionGuide::specular_hit_distance(valid).unwrap();
699        assert_eq!(guide.kind(), ReflectionGuideKind::SpecularHitDistance);
700    }
701
702    #[test]
703    fn project_identity_requires_non_empty_c_strings() {
704        assert!(Streamline::validate_project_identity("", "1.0.0").is_err());
705        assert!(Streamline::validate_project_identity("project", "").is_err());
706        assert!(Streamline::validate_project_identity("project\0id", "1.0.0").is_err());
707        assert!(Streamline::validate_project_identity("project", "1\0.0").is_err());
708        Streamline::validate_project_identity("8e3b4a2d-44cc-4d62-9725-86e7412f4eb0", "1.0.0")
709            .unwrap();
710    }
711
712    #[cfg(not(nvidia_streamline_native))]
713    #[test]
714    fn unsupported_target_keeps_stub_backend() {
715        const { assert!(!NATIVE_BACKEND_AVAILABLE) };
716        assert!(Streamline::staged_runtime_dir().is_none());
717        assert!(Streamline::initialize("project", "1.0.0", ".").is_err());
718        let streamline = Streamline {
719            evaluator: Evaluator {
720                context: Arc::new(backend::Context),
721            },
722            runtime_dir: PathBuf::new(),
723            #[cfg(target_os = "windows")]
724            interposer_path: PathBuf::new(),
725            #[cfg(target_os = "windows")]
726            proxy_created: false,
727        };
728
729        // Null is explicitly allowed and does not require an instance or loader.
730        unsafe {
731            assert!(!streamline.is_dlss_rr_supported(vk::PhysicalDevice::null()));
732            assert!(
733                !streamline
734                    .evaluator()
735                    .is_dlss_rr_supported(vk::PhysicalDevice::null())
736            );
737        }
738    }
739
740    #[cfg(all(target_os = "windows", not(nvidia_streamline_native)))]
741    #[test]
742    fn proxy_creation_failures_preserve_single_instance_guard() {
743        let mut streamline = Streamline {
744            evaluator: Evaluator {
745                context: Arc::new(backend::Context),
746            },
747            runtime_dir: PathBuf::new(),
748            interposer_path: PathBuf::new(),
749            proxy_created: false,
750        };
751        let create_info = vk::InstanceCreateInfo::default();
752
753        // No nested pointers or callbacks; the stub cannot create an owner.
754        assert!(unsafe { streamline.create_proxy_ash_instance(&create_info) }.is_err());
755
756        assert!(!streamline.proxy_created);
757
758        #[cfg(feature = "vk-graph")]
759        {
760            // No nested pointers or callbacks; the stub cannot create an owner.
761            assert!(unsafe { streamline.create_proxy_vulkan_instance(&create_info) }.is_err());
762
763            assert!(!streamline.proxy_created);
764        }
765
766        streamline.proxy_created = true;
767
768        // The guard rejects creation before accessing the pointer-free create info.
769        let error = unsafe { streamline.create_proxy_ash_instance(&create_info) }
770            .err()
771            .unwrap();
772
773        assert_eq!(
774            error.to_string(),
775            "a streamline proxy vulkan instance has already been created"
776        );
777
778        #[cfg(feature = "vk-graph")]
779        {
780            // The guard rejects creation before accessing the pointer-free create info.
781            let error = unsafe { streamline.create_proxy_vulkan_instance(&create_info) }
782                .err()
783                .unwrap();
784
785            assert_eq!(
786                error.to_string(),
787                "a streamline proxy vulkan instance has already been created"
788            );
789        }
790    }
791}