use std::cell::Cell;
use std::ffi::{CStr, CString, c_char};
use ash::vk;
use crate::vulkan::owned::VkDevice;
use crate::components::UpscalerBackend;
use crate::vulkan::allocator::DeviceAllocator;
use crate::vulkan::context::{HDR_FORMAT, VkContext};
use crate::vulkan::graph_exec::GraphFrameParams;
use crate::vulkan::texture::{GpuImage, create_image, create_image_view, one_shot_submit};
#[cfg(ngx_sdk_bundled)]
mod dlss;
mod fsr;
mod xess;
#[derive(Clone, Copy)]
pub(in crate::vulkan) struct UpscaleImage {
pub(in crate::vulkan) image: vk::Image,
pub(in crate::vulkan) view: vk::ImageView,
pub(in crate::vulkan) format: vk::Format,
pub(in crate::vulkan) width: u32,
pub(in crate::vulkan) height: u32,
pub(in crate::vulkan) aspect: vk::ImageAspectFlags,
}
pub(in crate::vulkan) struct UpscaleInputs<'a> {
pub(in crate::vulkan) color: &'a UpscaleImage,
pub(in crate::vulkan) depth: &'a UpscaleImage,
pub(in crate::vulkan) motion: &'a UpscaleImage,
}
#[derive(Clone, Copy)]
pub(in crate::vulkan) struct UpscaleCamera {
pub(in crate::vulkan) jitter_offset: [f32; 2],
pub(in crate::vulkan) elapsed: f32,
pub(in crate::vulkan) near: f32,
pub(in crate::vulkan) far: f32,
pub(in crate::vulkan) fov_y_radians: f32,
}
pub(in crate::vulkan) trait VkUpscaleBackend: Send {
fn render_dims(&self) -> (u32, u32);
fn output_dims(&self) -> (u32, u32);
fn scale(&self) -> f32;
fn output_image(&self) -> &GpuImage;
fn output_layout(&self) -> vk::ImageLayout;
fn set_output_layout(&self, layout: vk::ImageLayout);
fn jitter_offset(&self, frame_index: u32) -> [f32; 2];
fn set_jitter(&self, offset: [f32; 2]);
fn jitter(&self) -> [f32; 2];
fn dispatch(
&self,
cmd: vk::CommandBuffer,
inputs: UpscaleInputs<'_>,
camera: UpscaleCamera,
) -> Result<(), String>;
fn destroy(&mut self, device: &VkDevice);
}
pub(super) fn resolve_render_dims(
output_width: u32,
output_height: u32,
upscale_scale: f32,
) -> (u32, u32, f32) {
let scale = if upscale_scale > 0.0 {
upscale_scale.clamp(1.0 / 3.0, 1.0)
} else {
1.0
};
let render_width = (((output_width as f32) * scale).round() as u32).max(1);
let render_height = (((output_height as f32) * scale).round() as u32).max(1);
(render_width, render_height, scale)
}
pub(super) fn frame_delta_ms(prev: &Cell<f32>, now: f32) -> f32 {
let last = prev.replace(now);
((now - last) * 1000.0).clamp(1.0, 100.0)
}
pub(super) fn halton_jitter_offset(frame_index: u32) -> [f32; 2] {
let idx = (frame_index % 16) + 1;
[radical_inverse(idx, 2) - 0.5, radical_inverse(idx, 3) - 0.5]
}
fn radical_inverse(mut i: u32, base: u32) -> f32 {
let inv_base = 1.0 / base as f32;
let mut f = 1.0_f32;
let mut r = 0.0_f32;
while i > 0 {
f *= inv_base;
r += f * (i % base) as f32;
i /= base;
}
r
}
pub(super) fn create_output_image(
alloc: &DeviceAllocator,
device: &VkDevice,
command_pool: vk::CommandPool,
queue: vk::Queue,
width: u32,
height: u32,
) -> Result<GpuImage, String> {
let pooled = create_image(
alloc,
&crate::vulkan::texture::ImageSpec {
width: width.max(1),
height: height.max(1),
format: HDR_FORMAT,
tiling: vk::ImageTiling::OPTIMAL,
usage: vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::SAMPLED,
mem_props: vk::MemoryPropertyFlags::DEVICE_LOCAL,
samples: vk::SampleCountFlags::TYPE_1,
},
)?;
let image = pooled.image();
let view = create_image_view(device, image, HDR_FORMAT, vk::ImageAspectFlags::COLOR)?;
one_shot_submit(device, command_pool, queue, |cmd| {
let barrier = vk::ImageMemoryBarrier::default()
.old_layout(vk::ImageLayout::UNDEFINED)
.new_layout(vk::ImageLayout::GENERAL)
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.image(image)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
})
.src_access_mask(vk::AccessFlags::empty())
.dst_access_mask(vk::AccessFlags::SHADER_WRITE);
unsafe {
device.cmd_pipeline_barrier(
cmd,
vk::PipelineStageFlags::TOP_OF_PIPE,
vk::PipelineStageFlags::COMPUTE_SHADER,
vk::DependencyFlags::empty(),
&[],
&[],
std::slice::from_ref(&barrier),
);
}
})?;
Ok(GpuImage::from_pooled(pooled, view))
}
#[derive(Clone, Copy)]
pub(super) struct LayoutTransition {
pub(super) from: vk::ImageLayout,
pub(super) to: vk::ImageLayout,
}
#[derive(Clone, Copy)]
pub(super) struct BarrierSync {
pub(super) src_stage: vk::PipelineStageFlags,
pub(super) src_access: vk::AccessFlags,
pub(super) dst_stage: vk::PipelineStageFlags,
pub(super) dst_access: vk::AccessFlags,
}
pub(super) fn image_barrier(
device: &VkDevice,
cmd: vk::CommandBuffer,
image: vk::Image,
aspect: vk::ImageAspectFlags,
transition: LayoutTransition,
sync: BarrierSync,
) {
let LayoutTransition {
from: old_layout,
to: new_layout,
} = transition;
let BarrierSync {
src_stage,
src_access,
dst_stage,
dst_access,
} = sync;
let barrier = vk::ImageMemoryBarrier::default()
.old_layout(old_layout)
.new_layout(new_layout)
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.image(image)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: aspect,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
})
.src_access_mask(src_access)
.dst_access_mask(dst_access);
unsafe {
device.cmd_pipeline_barrier(
cmd,
src_stage,
dst_stage,
vk::DependencyFlags::empty(),
&[],
&[],
std::slice::from_ref(&barrier),
);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(in crate::vulkan) enum ResolvedBackend {
Fsr,
Dlss,
Xess,
Native,
}
fn backend_order(
requested: UpscalerBackend,
dlss_avail: bool,
xess_avail: bool,
fsr_avail: bool,
) -> Vec<ResolvedBackend> {
let mut order: Vec<ResolvedBackend> = Vec::new();
match requested {
UpscalerBackend::Dlss if dlss_avail => order.push(ResolvedBackend::Dlss),
UpscalerBackend::Xess if xess_avail => order.push(ResolvedBackend::Xess),
UpscalerBackend::Fsr3 if fsr_avail => order.push(ResolvedBackend::Fsr),
_ => {}
}
for (cand, avail) in [
(ResolvedBackend::Dlss, dlss_avail),
(ResolvedBackend::Xess, xess_avail),
(ResolvedBackend::Fsr, fsr_avail),
] {
if avail && !order.contains(&cand) {
order.push(cand);
}
}
order.push(ResolvedBackend::Native);
order
}
fn dlss_available() -> bool {
cfg!(ngx_sdk_bundled)
}
fn xess_available() -> bool {
cfg!(xess_sdk_bundled)
}
fn fsr_available() -> bool {
cfg!(ffx_sdk_bundled)
}
#[derive(Clone, Copy)]
pub(in crate::vulkan) struct UpscalerGpu<'a> {
pub(in crate::vulkan) alloc: &'a DeviceAllocator,
pub(in crate::vulkan) instance: &'a ash::Instance,
pub(in crate::vulkan) device: &'a VkDevice,
pub(in crate::vulkan) physical_device: vk::PhysicalDevice,
pub(in crate::vulkan) command_pool: vk::CommandPool,
pub(in crate::vulkan) queue: vk::Queue,
}
pub(in crate::vulkan) fn build_upscaler(
gpu: UpscalerGpu<'_>,
output_width: u32,
output_height: u32,
upscale_scale: f32,
requested: UpscalerBackend,
) -> Result<(Option<Box<dyn VkUpscaleBackend>>, ResolvedBackend), String> {
for cand in backend_order(
requested,
dlss_available(),
xess_available(),
fsr_available(),
) {
let built: Option<Box<dyn VkUpscaleBackend>> = match cand {
ResolvedBackend::Fsr => {
fsr::FsrUpscaler::try_new(gpu, output_width, output_height, upscale_scale)?
.map(|u| Box::new(u) as Box<dyn VkUpscaleBackend>)
}
ResolvedBackend::Xess => {
xess::XessUpscaler::try_new(gpu, output_width, output_height, upscale_scale)?
.map(|u| Box::new(u) as Box<dyn VkUpscaleBackend>)
}
ResolvedBackend::Dlss => {
#[cfg(ngx_sdk_bundled)]
{
dlss::DlssUpscaler::try_new(gpu, output_width, output_height, upscale_scale)?
.map(|u| Box::new(u) as Box<dyn VkUpscaleBackend>)
}
#[cfg(not(ngx_sdk_bundled))]
{
None
}
}
ResolvedBackend::Native => None,
};
if let Some(b) = built {
tracing::info!(
"temporal upscaling: using {cand:?} backend (output {output_width}x{output_height})"
);
return Ok((Some(b), cand));
}
if cand != ResolvedBackend::Native {
tracing::warn!("temporal upscaling: {cand:?} unavailable, trying next backend");
}
}
tracing::info!("temporal upscaling: no backend available, rendering at native resolution");
Ok((None, ResolvedBackend::Native))
}
pub(in crate::vulkan) struct UpscaleSdk {
pub(in crate::vulkan) choice: ResolvedBackend,
xess: Option<xess::XessExtQuery>,
instance_exts: Vec<CString>,
dlss_device_exts: Vec<CString>,
min_api_version: u32,
}
impl UpscaleSdk {
pub(in crate::vulkan) fn prepare(temporal_upscaling: bool, requested: UpscalerBackend) -> Self {
let mut sdk = UpscaleSdk {
choice: ResolvedBackend::Native,
xess: None,
instance_exts: Vec::new(),
dlss_device_exts: Vec::new(),
min_api_version: 0,
};
if !temporal_upscaling {
return sdk;
}
let first = backend_order(
requested,
dlss_available(),
xess_available(),
fsr_available(),
)[0];
sdk.choice = first;
match first {
ResolvedBackend::Dlss =>
{
#[cfg(ngx_sdk_bundled)]
match dlss::required_extensions() {
Some((inst, dev)) => {
sdk.instance_exts = inst;
sdk.dlss_device_exts = dev;
}
None => {
tracing::warn!(
"temporal upscaling: DLSS required-extensions query failed; \
device creation will skip DLSS extensions (build_upscaler will \
fall back to FSR / native)"
);
sdk.choice = ResolvedBackend::Fsr;
}
}
}
ResolvedBackend::Xess => match xess::XessExtQuery::load() {
Some(q) => {
let (exts, min_api) = q.instance_extensions();
sdk.instance_exts = exts;
sdk.min_api_version = min_api;
sdk.xess = Some(q);
}
None => {
tracing::warn!(
"temporal upscaling: XeSS DLL / extension query unavailable; device \
creation will skip XeSS extensions (build_upscaler will fall back to \
FSR / native)"
);
sdk.choice = ResolvedBackend::Fsr;
}
},
_ => {}
}
sdk
}
pub(in crate::vulkan) fn instance_extension_ptrs(&self) -> Vec<*const std::os::raw::c_char> {
self.instance_exts.iter().map(|c| c.as_ptr()).collect()
}
pub(in crate::vulkan) fn min_api_version(&self) -> u32 {
self.min_api_version
}
pub(in crate::vulkan) fn device_extensions(
&self,
instance: &ash::Instance,
physical_device: vk::PhysicalDevice,
already: &[CString],
) -> Vec<CString> {
let supported = supported_device_extensions(instance, physical_device);
let raw: Vec<CString> = match self.choice {
ResolvedBackend::Dlss => self.dlss_device_exts.clone(),
ResolvedBackend::Xess => self
.xess
.as_ref()
.map(|q| q.device_extensions(instance, physical_device))
.unwrap_or_default(),
_ => Vec::new(),
};
raw.into_iter()
.filter(|name| supported.iter().any(|s| s == name))
.filter(|name| !already.iter().any(|a| a == name))
.collect()
}
pub(in crate::vulkan) fn xess_device_features(
&self,
instance: &ash::Instance,
physical_device: vk::PhysicalDevice,
head: *mut std::ffi::c_void,
) -> *mut std::ffi::c_void {
match (self.choice, self.xess.as_ref()) {
(ResolvedBackend::Xess, Some(q)) => q.device_features(instance, physical_device, head),
_ => head,
}
}
}
pub(super) unsafe fn copy_ext_names(count: u32, exts: *const *const c_char) -> Vec<CString> {
if exts.is_null() {
return Vec::new();
}
let mut out = Vec::with_capacity(count as usize);
for i in 0..count as usize {
let p = unsafe { *exts.add(i) };
if !p.is_null() {
out.push(unsafe { CStr::from_ptr(p) }.to_owned());
}
}
out
}
fn supported_device_extensions(
instance: &ash::Instance,
physical_device: vk::PhysicalDevice,
) -> Vec<CString> {
let props = unsafe { instance.enumerate_device_extension_properties(physical_device) }
.unwrap_or_default();
props
.iter()
.map(|e| {
let name = unsafe { std::ffi::CStr::from_ptr(e.extension_name.as_ptr()) };
CString::from(name)
})
.collect()
}
impl VkContext {
pub(in crate::vulkan) fn encode_upscale(
&self,
cmd: vk::CommandBuffer,
params: &GraphFrameParams<'_>,
) -> Result<(), String> {
let upscaler = match &self.upscale {
Some(u) => u,
None => return Ok(()),
};
let frame = params.frame_idx;
static LOGGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if !LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
let (rw, rh) = upscaler.render_dims();
let (ow, oh) = upscaler.output_dims();
tracing::info!(
"temporal upscaling: first encode_upscale firing (render {rw}x{rh} -> upscale {ow}x{oh})"
);
}
let gb = self.gbuffer.as_ref().ok_or(
"Upscale enabled but the unified G-buffer pre-pass is absent; upscaling needs its motion + depth",
)?;
let velocity = gb
.velocity_images
.get(frame)
.ok_or("upscale: gbuffer velocity slot out of range")?;
let depth = gb
.depth_images
.get(frame)
.ok_or("upscale: gbuffer depth slot out of range")?;
let scene = match self.ssr.as_ref().filter(|_| self.ssr_resolve_active) {
Some(s) => &s.output,
None => self
.hdr_resolve_images
.get(frame)
.ok_or("upscale: hdr resolve slot out of range")?,
};
let (rw, rh) = upscaler.render_dims();
image_barrier(
&self.device,
cmd,
scene.image,
vk::ImageAspectFlags::COLOR,
LayoutTransition {
from: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
to: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
},
BarrierSync {
src_stage: vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
src_access: vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
dst_stage: vk::PipelineStageFlags::COMPUTE_SHADER,
dst_access: vk::AccessFlags::SHADER_READ,
},
);
image_barrier(
&self.device,
cmd,
velocity.image,
vk::ImageAspectFlags::COLOR,
LayoutTransition {
from: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
to: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
},
BarrierSync {
src_stage: vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
src_access: vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
dst_stage: vk::PipelineStageFlags::COMPUTE_SHADER,
dst_access: vk::AccessFlags::SHADER_READ,
},
);
image_barrier(
&self.device,
cmd,
depth.image,
vk::ImageAspectFlags::DEPTH,
LayoutTransition {
from: vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
to: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
},
BarrierSync {
src_stage: vk::PipelineStageFlags::LATE_FRAGMENT_TESTS,
src_access: vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE,
dst_stage: vk::PipelineStageFlags::COMPUTE_SHADER,
dst_access: vk::AccessFlags::SHADER_READ,
},
);
if upscaler.output_layout() != vk::ImageLayout::GENERAL {
image_barrier(
&self.device,
cmd,
upscaler.output_image().image,
vk::ImageAspectFlags::COLOR,
LayoutTransition {
from: upscaler.output_layout(),
to: vk::ImageLayout::GENERAL,
},
BarrierSync {
src_stage: vk::PipelineStageFlags::FRAGMENT_SHADER,
src_access: vk::AccessFlags::SHADER_READ,
dst_stage: vk::PipelineStageFlags::COMPUTE_SHADER,
dst_access: vk::AccessFlags::SHADER_WRITE,
},
);
upscaler.set_output_layout(vk::ImageLayout::GENERAL);
}
let color = UpscaleImage {
image: scene.image,
view: scene.view,
format: HDR_FORMAT,
width: rw,
height: rh,
aspect: vk::ImageAspectFlags::COLOR,
};
let motion = UpscaleImage {
image: velocity.image,
view: velocity.view,
format: vk::Format::R16G16_SFLOAT,
width: rw,
height: rh,
aspect: vk::ImageAspectFlags::COLOR,
};
let depth_in = UpscaleImage {
image: depth.image,
view: depth.view,
format: vk::Format::D32_SFLOAT,
width: rw,
height: rh,
aspect: vk::ImageAspectFlags::DEPTH,
};
let near = params.near.max(1e-3);
let far = params.far.max(near + 1.0);
upscaler.dispatch(
cmd,
UpscaleInputs {
color: &color,
depth: &depth_in,
motion: &motion,
},
UpscaleCamera {
jitter_offset: upscaler.jitter(),
elapsed: params.elapsed,
near,
far,
fov_y_radians: params.fov_y_radians,
},
)?;
image_barrier(
&self.device,
cmd,
upscaler.output_image().image,
vk::ImageAspectFlags::COLOR,
LayoutTransition {
from: vk::ImageLayout::GENERAL,
to: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
},
BarrierSync {
src_stage: vk::PipelineStageFlags::COMPUTE_SHADER,
src_access: vk::AccessFlags::SHADER_WRITE,
dst_stage: vk::PipelineStageFlags::FRAGMENT_SHADER,
dst_access: vk::AccessFlags::SHADER_READ,
},
);
upscaler.set_output_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::UpscalerBackend as B;
fn resolved(req: B, dlss: bool, xess: bool, fsr: bool) -> ResolvedBackend {
backend_order(req, dlss, xess, fsr)[0]
}
#[test]
fn auto_prefers_dlss_then_xess_then_fsr_then_native() {
assert_eq!(resolved(B::Auto, true, true, true), ResolvedBackend::Dlss);
assert_eq!(resolved(B::Auto, false, true, true), ResolvedBackend::Xess);
assert_eq!(resolved(B::Auto, false, false, true), ResolvedBackend::Fsr);
assert_eq!(
resolved(B::Auto, false, false, false),
ResolvedBackend::Native
);
}
#[test]
fn explicit_choice_used_when_available() {
assert_eq!(resolved(B::Dlss, true, true, true), ResolvedBackend::Dlss);
assert_eq!(resolved(B::Xess, true, true, true), ResolvedBackend::Xess);
assert_eq!(resolved(B::Fsr3, true, true, true), ResolvedBackend::Fsr);
}
#[test]
fn explicit_choice_falls_through_when_unavailable() {
assert_eq!(resolved(B::Dlss, false, true, true), ResolvedBackend::Xess);
assert_eq!(resolved(B::Xess, false, false, true), ResolvedBackend::Fsr);
assert_eq!(
resolved(B::Fsr3, false, false, false),
ResolvedBackend::Native
);
}
#[test]
fn halton_jitter_is_centered_and_bounded() {
for f in 0..64u32 {
let [x, y] = halton_jitter_offset(f);
assert!((-0.5..0.5).contains(&x), "x={x} out of range");
assert!((-0.5..0.5).contains(&y), "y={y} out of range");
}
let [x, y] = halton_jitter_offset(0);
assert!((x - 0.0).abs() < 1e-6);
assert!((y - (1.0 / 3.0 - 0.5)).abs() < 1e-6);
}
#[test]
fn render_dims_apply_quality_scale() {
let (w, h, s) = resolve_render_dims(1920, 1080, 2.0 / 3.0);
assert_eq!((w, h), (1280, 720));
assert!((s - 2.0 / 3.0).abs() < 1e-6);
assert_eq!(resolve_render_dims(1920, 1080, 0.5).0, 960);
assert_eq!(resolve_render_dims(1920, 1080, 0.5).1, 540);
}
#[test]
fn render_dims_clamp_out_of_range_scale() {
let (w, h, s) = resolve_render_dims(800, 600, 2.0);
assert_eq!((w, h), (800, 600));
assert!((s - 1.0).abs() < 1e-6);
assert_eq!(resolve_render_dims(800, 600, 0.0), (800, 600, 1.0));
let (_, _, s2) = resolve_render_dims(900, 900, 0.1);
assert!((s2 - 1.0 / 3.0).abs() < 1e-6);
}
#[test]
fn frame_delta_is_clamped() {
let prev = Cell::new(0.0);
assert!((frame_delta_ms(&prev, 10.0) - 100.0).abs() < 1e-3);
assert!((frame_delta_ms(&prev, 10.016) - 16.0).abs() < 1e-2);
assert!((frame_delta_ms(&prev, 10.016) - 1.0).abs() < 1e-3);
}
}