use std::cell::Cell;
use std::ffi::{CStr, CString, c_char, c_void};
use ash::vk;
use concinnity_core::components::UpscalerBackend;
use concinnity_core::render::dlss::DlssPreset;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::reactive_mask::ReactiveReader;
use crate::upscale_sdk::{
Availability, SdkLibrary, UpscaleCamera, UpscaleExtent, UpscaleRequest, preferred,
};
use crate::vulkan::allocator::DeviceAllocator;
use crate::vulkan::context::{HDR_FORMAT, VkContext};
use crate::vulkan::graph_exec::GraphFrameParams;
use crate::vulkan::owned::VkDevice;
use crate::vulkan::texture::{
GpuImage, ImageSpec, create_image, create_image_view, one_shot_submit,
};
pub(in crate::vulkan) use crate::upscale_sdk::ResolvedBackend;
#[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,
pub(in crate::vulkan) reactive: Option<&'a UpscaleImage>,
}
pub(in crate::vulkan) trait VkUpscaleBackend: Send {
fn extent(&self) -> UpscaleExtent;
fn output(&self) -> &UpscaleOutput;
fn jitter_offset(&self, frame_index: u32) -> [f32; 2];
fn jitter(&self) -> &Cell<[f32; 2]>;
fn dispatch(
&self,
cmd: vk::CommandBuffer,
inputs: UpscaleInputs<'_>,
camera: UpscaleCamera,
) -> RenderResult<()>;
fn request_history_reset(&self);
fn dlss_preset(&self) -> Option<DlssPreset> {
None
}
fn reactive_reader(&self) -> ReactiveReader;
fn destroy(&mut self);
}
#[repr(C)]
#[derive(Clone, Copy)]
struct ImageViewInfo {
image_view: vk::ImageView,
image: vk::Image,
subresource_range: vk::ImageSubresourceRange,
format: vk::Format,
width: u32,
height: u32,
}
impl ImageViewInfo {
fn empty() -> Self {
Self {
image_view: vk::ImageView::null(),
image: vk::Image::null(),
subresource_range: vk::ImageSubresourceRange::default(),
format: vk::Format::UNDEFINED,
width: 0,
height: 0,
}
}
fn of(img: &UpscaleImage) -> Self {
Self {
image_view: img.view,
image: img.image,
subresource_range: vk::ImageSubresourceRange {
aspect_mask: img.aspect,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
},
format: img.format,
width: img.width,
height: img.height,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(in crate::vulkan) struct OutputWrites {
stage: vk::PipelineStageFlags,
access: vk::AccessFlags,
usage: vk::ImageUsageFlags,
}
impl OutputWrites {
fn storage() -> Self {
Self {
stage: vk::PipelineStageFlags::COMPUTE_SHADER,
access: vk::AccessFlags::SHADER_WRITE,
usage: vk::ImageUsageFlags::STORAGE,
}
}
#[cfg(any(ngx_sdk_bundled, test))]
fn storage_and_clear() -> Self {
Self {
stage: vk::PipelineStageFlags::COMPUTE_SHADER | vk::PipelineStageFlags::TRANSFER,
access: vk::AccessFlags::SHADER_WRITE | vk::AccessFlags::TRANSFER_WRITE,
usage: vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::TRANSFER_DST,
}
}
fn image_usage(self) -> vk::ImageUsageFlags {
self.usage | vk::ImageUsageFlags::SAMPLED
}
fn acquire(
self,
src_stage: vk::PipelineStageFlags,
src_access: vk::AccessFlags,
) -> BarrierSync {
BarrierSync {
src_stage,
src_access,
dst_stage: self.stage,
dst_access: self.access,
}
}
fn release_to_sampling(self) -> BarrierSync {
BarrierSync {
src_stage: self.stage,
src_access: self.access,
dst_stage: vk::PipelineStageFlags::FRAGMENT_SHADER,
dst_access: vk::AccessFlags::SHADER_READ,
}
}
}
pub(in crate::vulkan) struct UpscaleOutput {
image: GpuImage,
size: (u32, u32),
writes: OutputWrites,
layout: Cell<vk::ImageLayout>,
}
impl UpscaleOutput {
fn create(gpu: UpscalerGpu<'_>, size: (u32, u32), writes: OutputWrites) -> RenderResult<Self> {
let pooled = create_image(
gpu.alloc,
&ImageSpec {
width: size.0.max(1),
height: size.1.max(1),
format: HDR_FORMAT,
tiling: vk::ImageTiling::OPTIMAL,
usage: writes.image_usage(),
mem_props: vk::MemoryPropertyFlags::DEVICE_LOCAL,
samples: vk::SampleCountFlags::TYPE_1,
},
)?;
let image = pooled.image();
let view = create_image_view(gpu.device, image, HDR_FORMAT, vk::ImageAspectFlags::COLOR)?;
one_shot_submit(gpu.device, gpu.command_pool, gpu.queue, |cmd| {
image_barrier(
gpu.device,
cmd,
image,
vk::ImageAspectFlags::COLOR,
LayoutTransition {
from: vk::ImageLayout::UNDEFINED,
to: vk::ImageLayout::GENERAL,
},
writes.acquire(
vk::PipelineStageFlags::TOP_OF_PIPE,
vk::AccessFlags::empty(),
),
);
})?;
Ok(Self {
image: GpuImage::from_pooled(pooled, view),
size,
writes,
layout: Cell::new(vk::ImageLayout::GENERAL),
})
}
pub(in crate::vulkan) fn image(&self) -> &GpuImage {
&self.image
}
fn as_upscale_image(&self) -> UpscaleImage {
UpscaleImage {
image: self.image.image,
view: self.image.view,
format: HDR_FORMAT,
width: self.size.0,
height: self.size.1,
aspect: vk::ImageAspectFlags::COLOR,
}
}
fn release(&mut self) {
self.image = GpuImage::null();
}
}
#[derive(Clone, Copy)]
struct LayoutTransition {
from: vk::ImageLayout,
to: vk::ImageLayout,
}
#[derive(Clone, Copy)]
struct BarrierSync {
src_stage: vk::PipelineStageFlags,
src_access: vk::AccessFlags,
dst_stage: vk::PipelineStageFlags,
dst_access: vk::AccessFlags,
}
fn image_barrier(
device: &VkDevice,
cmd: vk::CommandBuffer,
image: vk::Image,
aspect: vk::ImageAspectFlags,
transition: LayoutTransition,
sync: BarrierSync,
) {
let barrier = vk::ImageMemoryBarrier::default()
.old_layout(transition.from)
.new_layout(transition.to)
.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(sync.src_access)
.dst_access_mask(sync.dst_access);
unsafe {
device.cmd_pipeline_barrier(
cmd,
sync.src_stage,
sync.dst_stage,
vk::DependencyFlags::empty(),
&[],
&[],
std::slice::from_ref(&barrier),
);
}
}
fn availability() -> Availability {
Availability {
dlss: cfg!(ngx_sdk_bundled),
xess: cfg!(xess_sdk_bundled),
fsr: cfg!(ffx_sdk_bundled),
}
}
fn open_library(name: &str) -> Option<libloading::Library> {
unsafe { libloading::Library::new(name) }.ok()
}
impl SdkLibrary for libloading::Library {
fn symbol(&self, name: &CStr) -> Option<*const c_void> {
unsafe { self.get::<*const c_void>(name.to_bytes_with_nul()) }
.ok()
.map(|address| *address)
}
}
#[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: (u32, u32),
upscale_scale: f32,
requested: &mut UpscaleRequest,
) -> RenderResult<(Option<Box<dyn VkUpscaleBackend>>, ResolvedBackend)> {
let extent = UpscaleExtent::resolve(output, upscale_scale);
let (built, resolved) = requested.build_first_available(
availability(),
output,
|candidate,
#[cfg_attr(
not(ngx_sdk_bundled),
expect(unused_variables, reason = "only DLSS has render presets")
)]
preset| {
Ok(match candidate {
ResolvedBackend::Fsr => fsr::FsrUpscaler::try_new(gpu, extent)?.map(boxed),
ResolvedBackend::Xess => xess::XessUpscaler::try_new(gpu, extent)?.map(boxed),
#[cfg(ngx_sdk_bundled)]
ResolvedBackend::Dlss => {
dlss::DlssUpscaler::try_new(gpu, extent, preset)?.map(boxed)
}
_ => None,
})
},
)?;
requested.adopt_running_preset(built.as_deref().and_then(|u| u.dlss_preset()));
Ok((built, resolved))
}
fn boxed(backend: impl VkUpscaleBackend + 'static) -> Box<dyn VkUpscaleBackend> {
Box::new(backend)
}
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;
}
sdk.choice = preferred(requested, availability());
match sdk.choice {
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 library / 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 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 c_void,
) -> *mut c_void {
match (self.choice, self.xess.as_ref()) {
(ResolvedBackend::Xess, Some(q)) => q.device_features(instance, physical_device, head),
_ => head,
}
}
}
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 { 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<'_>,
) -> RenderResult<()> {
let Some(upscaler) = &self.upscale else {
return Ok(());
};
let frame = params.frame_idx;
let extent = upscaler.extent();
static LOGGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if !LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
tracing::info!("temporal upscaling: first encode_upscale firing ({extent})");
}
let gb = self.gbuffer_targets().ok_or_else(|| {
RenderError::Other(
"upscale: enabled but the unified G-buffer pre-pass is absent".into(),
)
})?;
let velocity = gb.velocity_images.get(frame).ok_or_else(|| {
RenderError::Other("upscale: gbuffer velocity slot out of range".into())
})?;
let depth = gb
.depth_images
.get(frame)
.ok_or_else(|| RenderError::Other("upscale: gbuffer depth slot out of range".into()))?;
let scene = self.post_scene_image(frame);
let (rw, rh) = extent.render;
let render_image = |image, view, format, aspect| UpscaleImage {
image,
view,
format,
width: rw,
height: rh,
aspect,
};
let color = render_image(
scene.image,
scene.view,
HDR_FORMAT,
vk::ImageAspectFlags::COLOR,
);
let motion = render_image(
velocity.image,
velocity.view,
vk::Format::R16G16_SFLOAT,
vk::ImageAspectFlags::COLOR,
);
let depth_in = render_image(
depth.image,
depth.view,
vk::Format::D32_SFLOAT,
vk::ImageAspectFlags::DEPTH,
);
let color_read = 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,
};
let stay_read_only = LayoutTransition {
from: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
to: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
};
let mask = self.reactive_mask(frame);
let reactive = match mask {
Some(m) if params.reactive.readable => Some(m),
Some(m) if upscaler.reactive_reader().requires() => {
crate::vulkan::reactive_mask::clear_outside_pass(&self.hw.device, cmd, m.image);
Some(m)
}
_ => None,
}
.map(|m| {
render_image(
m.image,
m.view,
crate::vulkan::reactive_mask::REACTIVE_MASK_FORMAT,
vk::ImageAspectFlags::COLOR,
)
});
let written_mask = reactive
.as_ref()
.filter(|_| params.reactive.readable)
.map(|m| m.image);
for image in [color.image, motion.image].into_iter().chain(written_mask) {
image_barrier(
&self.hw.device,
cmd,
image,
vk::ImageAspectFlags::COLOR,
stay_read_only,
color_read,
);
}
image_barrier(
&self.hw.device,
cmd,
depth_in.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,
},
);
let output = upscaler.output();
if output.layout.get() != vk::ImageLayout::GENERAL {
image_barrier(
&self.hw.device,
cmd,
output.image.image,
vk::ImageAspectFlags::COLOR,
LayoutTransition {
from: output.layout.get(),
to: vk::ImageLayout::GENERAL,
},
output.writes.acquire(
vk::PipelineStageFlags::FRAGMENT_SHADER,
vk::AccessFlags::SHADER_READ,
),
);
output.layout.set(vk::ImageLayout::GENERAL);
}
upscaler.dispatch(
cmd,
UpscaleInputs {
color: &color,
depth: &depth_in,
motion: &motion,
reactive: reactive.as_ref(),
},
UpscaleCamera::new(
upscaler.jitter().get(),
params.elapsed,
params.near,
params.fov_y_radians,
),
)?;
image_barrier(
&self.hw.device,
cmd,
output.image.image,
vk::ImageAspectFlags::COLOR,
LayoutTransition {
from: vk::ImageLayout::GENERAL,
to: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
},
output.writes.release_to_sampling(),
);
output.layout.set(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::mem::{offset_of, size_of};
#[test]
fn image_view_info_layout_matches_xess_v301_and_ngx_v150() {
assert_eq!(size_of::<ImageViewInfo>(), 48);
assert_eq!(offset_of!(ImageViewInfo, image_view), 0);
assert_eq!(offset_of!(ImageViewInfo, image), 8);
assert_eq!(offset_of!(ImageViewInfo, subresource_range), 16);
assert_eq!(offset_of!(ImageViewInfo, format), 36);
assert_eq!(offset_of!(ImageViewInfo, width), 40);
assert_eq!(offset_of!(ImageViewInfo, height), 44);
}
#[test]
fn storage_writes_need_no_transfer_usage() {
let writes = OutputWrites::storage();
assert_eq!(
writes.image_usage(),
vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::SAMPLED
);
let sync = writes.acquire(
vk::PipelineStageFlags::FRAGMENT_SHADER,
vk::AccessFlags::SHADER_READ,
);
assert_eq!(sync.dst_stage, vk::PipelineStageFlags::COMPUTE_SHADER);
assert_eq!(sync.dst_access, vk::AccessFlags::SHADER_WRITE);
}
#[test]
fn clearing_writes_declare_the_transfer_write() {
let writes = OutputWrites::storage_and_clear();
assert!(
writes
.image_usage()
.contains(vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::STORAGE)
);
let sync = writes.acquire(
vk::PipelineStageFlags::FRAGMENT_SHADER,
vk::AccessFlags::SHADER_READ,
);
assert!(sync.dst_stage.contains(vk::PipelineStageFlags::TRANSFER));
assert!(sync.dst_access.contains(vk::AccessFlags::TRANSFER_WRITE));
}
#[test]
fn release_covers_every_write_the_acquire_allowed() {
for writes in [OutputWrites::storage(), OutputWrites::storage_and_clear()] {
let acquire = writes.acquire(
vk::PipelineStageFlags::FRAGMENT_SHADER,
vk::AccessFlags::SHADER_READ,
);
let release = writes.release_to_sampling();
assert_eq!(release.src_stage, acquire.dst_stage);
assert_eq!(release.src_access, acquire.dst_access);
assert_eq!(release.dst_stage, vk::PipelineStageFlags::FRAGMENT_SHADER);
assert_eq!(release.dst_access, vk::AccessFlags::SHADER_READ);
}
}
#[test]
fn image_view_info_covers_one_mip_and_layer_of_the_aspect() {
let img = UpscaleImage {
image: vk::Image::null(),
view: vk::ImageView::null(),
format: vk::Format::D32_SFLOAT,
width: 64,
height: 32,
aspect: vk::ImageAspectFlags::DEPTH,
};
let info = ImageViewInfo::of(&img);
assert_eq!(
info.subresource_range.aspect_mask,
vk::ImageAspectFlags::DEPTH
);
assert_eq!(
(
info.subresource_range.level_count,
info.subresource_range.layer_count
),
(1, 1)
);
assert_eq!(
(info.width, info.height, info.format),
(64, 32, vk::Format::D32_SFLOAT)
);
assert_eq!(ImageViewInfo::empty().subresource_range.layer_count, 0);
}
}