#![expect(
non_camel_case_types,
reason = "inline FFX bindings keep the SDK's own C type names"
)]
use std::cell::Cell;
use std::ffi::c_void;
use std::ptr;
use ash::vk;
use ash::vk::Handle;
use crate::vulkan::owned::VkDevice;
use super::{UpscaleCamera, UpscaleInputs, UpscalerGpu, VkUpscaleBackend};
use crate::vulkan::texture::GpuImage;
type ffxContext = *mut c_void;
type ffxReturnCode_t = u32;
const FFX_API_RETURN_OK: u32 = 0;
#[repr(C)]
struct ffxApiHeader {
ty: u64,
p_next: *mut ffxApiHeader,
}
const FFX_API_CREATE_CONTEXT_DESC_TYPE_BACKEND_VK: u64 = 0x0000003;
const FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE: u64 = 0x00010000;
const FFX_API_DISPATCH_DESC_TYPE_UPSCALE: u64 = 0x00010001;
const FFX_API_QUERY_DESC_TYPE_UPSCALE_GETJITTERPHASECOUNT: u64 = 0x00010004;
const FFX_API_QUERY_DESC_TYPE_UPSCALE_GETJITTEROFFSET: u64 = 0x00010005;
const FFX_API_CONFIGURE_DESC_TYPE_GLOBALDEBUG1: u64 = 0x0000001;
const FFX_API_CONFIGURE_GLOBALDEBUG_LEVEL_VERBOSE: u32 = 0xfffffff;
#[repr(C)]
struct ffxConfigureDescGlobalDebug1 {
header: ffxApiHeader,
fp_message: FfxApiMessage,
debug_level: u32,
}
const FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE: u32 = 1 << 0;
const FFX_UPSCALE_ENABLE_AUTO_EXPOSURE: u32 = 1 << 5;
const FFX_API_RESOURCE_TYPE_TEXTURE2D: u32 = 2;
const FFX_API_RESOURCE_USAGE_READ_ONLY: u32 = 0;
const FFX_API_RESOURCE_USAGE_UAV: u32 = 1 << 1;
const FFX_API_RESOURCE_USAGE_DEPTHTARGET: u32 = 1 << 2;
const FFX_API_RESOURCE_STATE_UNORDERED_ACCESS: u32 = 1 << 1;
const FFX_API_RESOURCE_STATE_COMPUTE_READ: u32 = 1 << 2;
const FFX_API_SURFACE_FORMAT_R16G16B16A16_FLOAT: u32 = 4;
const FFX_API_SURFACE_FORMAT_R32_FLOAT: u32 = 28;
const FFX_API_SURFACE_FORMAT_R16G16_FLOAT: u32 = 18;
#[repr(C)]
#[derive(Clone, Copy)]
struct FfxApiDimensions2D {
width: u32,
height: u32,
}
#[repr(C)]
#[derive(Clone, Copy)]
struct FfxApiFloatCoords2D {
x: f32,
y: f32,
}
#[repr(C)]
struct FfxApiResourceDescription {
ty: u32, format: u32, width_or_size: u32,
height_or_stride: u32,
depth_or_alignment: u32,
mip_count: u32,
flags: u32,
usage: u32,
}
#[repr(C)]
struct FfxApiResource {
resource: *mut c_void, description: FfxApiResourceDescription,
state: u32, }
impl FfxApiResource {
fn empty() -> Self {
Self {
resource: ptr::null_mut(),
description: FfxApiResourceDescription {
ty: 0,
format: 0,
width_or_size: 0,
height_or_stride: 0,
depth_or_alignment: 0,
mip_count: 0,
flags: 0,
usage: 0,
},
state: 0,
}
}
}
#[repr(C)]
struct ffxCreateBackendVKDesc {
header: ffxApiHeader,
vk_device: *mut c_void,
vk_physical_device: *mut c_void,
vk_device_proc_addr: *mut c_void,
}
type FfxApiMessage = Option<extern "C" fn(ty: u32, message: *const u16)>;
#[cfg(windows)]
extern "C" fn ffx_message_sink(ty: u32, message: *const u16) {
if message.is_null() {
return;
}
let mut len = 0usize;
let mut p = message;
unsafe {
while *p != 0 {
len += 1;
p = p.add(1);
}
}
let slice = unsafe { std::slice::from_raw_parts(message, len) };
let text = String::from_utf16_lossy(slice);
match ty {
0 => tracing::error!("FFX: {text}"),
1 => tracing::warn!("FFX: {text}"),
other => tracing::info!("FFX[{other}]: {text}"),
}
}
fn message_callback() -> FfxApiMessage {
#[cfg(windows)]
{
Some(ffx_message_sink)
}
#[cfg(not(windows))]
{
None
}
}
#[repr(C)]
struct ffxCreateContextDescUpscale {
header: ffxApiHeader,
flags: u32,
max_render_size: FfxApiDimensions2D,
max_upscale_size: FfxApiDimensions2D,
fp_message: FfxApiMessage,
}
#[repr(C)]
struct ffxDispatchDescUpscale {
header: ffxApiHeader,
command_list: *mut c_void, color: FfxApiResource,
depth: FfxApiResource,
motion_vectors: FfxApiResource,
exposure: FfxApiResource,
reactive: FfxApiResource,
transparency_and_composition: FfxApiResource,
output: FfxApiResource,
jitter_offset: FfxApiFloatCoords2D,
motion_vector_scale: FfxApiFloatCoords2D,
render_size: FfxApiDimensions2D,
upscale_size: FfxApiDimensions2D,
enable_sharpening: bool,
sharpness: f32,
frame_time_delta: f32,
pre_exposure: f32,
reset: bool,
camera_near: f32,
camera_far: f32,
camera_fov_angle_vertical: f32,
view_space_to_meters_factor: f32,
flags: u32,
}
#[repr(C)]
struct ffxQueryDescUpscaleGetJitterPhaseCount {
header: ffxApiHeader,
render_width: u32,
display_width: u32,
out_phase_count: *mut i32,
}
#[repr(C)]
struct ffxQueryDescUpscaleGetJitterOffset {
header: ffxApiHeader,
index: i32,
phase_count: i32,
out_x: *mut f32,
out_y: *mut f32,
}
#[repr(C)]
struct ffxAllocationCallbacks {
user_data: *mut c_void,
alloc: *mut c_void,
dealloc: *mut c_void,
}
type PfnFfxCreateContext = unsafe extern "C" fn(
context: *mut ffxContext,
desc: *mut ffxApiHeader,
mem_cb: *const ffxAllocationCallbacks,
) -> ffxReturnCode_t;
type PfnFfxDestroyContext = unsafe extern "C" fn(
context: *mut ffxContext,
mem_cb: *const ffxAllocationCallbacks,
) -> ffxReturnCode_t;
type PfnFfxQuery =
unsafe extern "C" fn(context: *mut ffxContext, desc: *mut ffxApiHeader) -> ffxReturnCode_t;
type PfnFfxDispatch =
unsafe extern "C" fn(context: *mut ffxContext, desc: *const ffxApiHeader) -> ffxReturnCode_t;
type PfnFfxConfigure =
unsafe extern "C" fn(context: *mut ffxContext, desc: *const ffxApiHeader) -> ffxReturnCode_t;
struct FfxApi {
_lib: libloading::Library,
create_context: PfnFfxCreateContext,
destroy_context: PfnFfxDestroyContext,
configure: PfnFfxConfigure,
query: PfnFfxQuery,
dispatch: PfnFfxDispatch,
}
impl FfxApi {
fn load() -> Option<Self> {
let lib_name = if cfg!(windows) {
"amd_fidelityfx_vk.dll"
} else {
"libamd_fidelityfx_vk.so"
};
unsafe {
let lib = libloading::Library::new(lib_name).ok()?;
let create_context = *lib.get::<PfnFfxCreateContext>(b"ffxCreateContext\0").ok()?;
let destroy_context = *lib
.get::<PfnFfxDestroyContext>(b"ffxDestroyContext\0")
.ok()?;
let configure = *lib.get::<PfnFfxConfigure>(b"ffxConfigure\0").ok()?;
let query = *lib.get::<PfnFfxQuery>(b"ffxQuery\0").ok()?;
let dispatch = *lib.get::<PfnFfxDispatch>(b"ffxDispatch\0").ok()?;
Some(FfxApi {
_lib: lib,
create_context,
destroy_context,
configure,
query,
dispatch,
})
}
}
}
pub(in crate::vulkan) struct FsrUpscaler {
ffx: FfxApi,
ctx: ffxContext,
output: GpuImage,
output_layout: Cell<vk::ImageLayout>,
render_width: u32,
render_height: u32,
output_width: u32,
output_height: u32,
upscale_scale: f32,
jitter_phase_count: i32,
jitter: Cell<[f32; 2]>,
prev_elapsed: Cell<f32>,
reset_pending: Cell<bool>,
}
unsafe impl Send for FsrUpscaler {}
impl FsrUpscaler {
pub(super) fn try_new(
gpu: UpscalerGpu<'_>,
output_width: u32,
output_height: u32,
upscale_scale: f32,
) -> Result<Option<Self>, String> {
let UpscalerGpu {
alloc,
instance,
device,
physical_device,
command_pool,
queue,
} = gpu;
let ffx = match FfxApi::load() {
Some(api) => api,
None => {
if cfg!(ffx_sdk_bundled) {
tracing::warn!(
"FidelityFX FSR (Vulkan): amd_fidelityfx_vk.dll was bundled at build \
time but failed to load at runtime; trying the next backend"
);
} else {
tracing::warn!(
"FidelityFX FSR (Vulkan): amd_fidelityfx_vk.dll not found (build.rs did \
not bundle it; set FIDELITYFX_SDK_ROOT or put the DLL on PATH). \
Trying the next backend."
);
}
return Ok(None);
}
};
let (render_width, render_height, scale) =
super::resolve_render_dims(output_width, output_height, upscale_scale);
let mut backend = ffxCreateBackendVKDesc {
header: ffxApiHeader {
ty: FFX_API_CREATE_CONTEXT_DESC_TYPE_BACKEND_VK,
p_next: ptr::null_mut(),
},
vk_device: device.handle().as_raw() as usize as *mut c_void,
vk_physical_device: physical_device.as_raw() as usize as *mut c_void,
vk_device_proc_addr: instance.fp_v1_0().get_device_proc_addr as usize as *mut c_void,
};
let mut upscale = ffxCreateContextDescUpscale {
header: ffxApiHeader {
ty: FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE,
p_next: &mut backend.header as *mut ffxApiHeader,
},
flags: FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE | FFX_UPSCALE_ENABLE_AUTO_EXPOSURE,
max_render_size: FfxApiDimensions2D {
width: output_width,
height: output_height,
},
max_upscale_size: FfxApiDimensions2D {
width: output_width,
height: output_height,
},
fp_message: message_callback(),
};
let mut ctx: ffxContext = ptr::null_mut();
let rc = unsafe {
(ffx.create_context)(
&mut ctx,
&mut upscale.header as *mut ffxApiHeader,
ptr::null(),
)
};
if rc != FFX_API_RETURN_OK || ctx.is_null() {
tracing::warn!(
"FidelityFX FSR (Vulkan): ffxCreateContext returned {rc}; trying the next backend"
);
return Ok(None);
}
let mut global_debug = ffxConfigureDescGlobalDebug1 {
header: ffxApiHeader {
ty: FFX_API_CONFIGURE_DESC_TYPE_GLOBALDEBUG1,
p_next: ptr::null_mut(),
},
fp_message: message_callback(),
debug_level: FFX_API_CONFIGURE_GLOBALDEBUG_LEVEL_VERBOSE,
};
let rc_dbg =
unsafe { (ffx.configure)(&mut ctx, &global_debug.header as *const ffxApiHeader) };
let _ = &mut global_debug;
if rc_dbg != FFX_API_RETURN_OK {
tracing::warn!(
"FidelityFX FSR (Vulkan): global debug configure returned {rc_dbg} (non-fatal)"
);
}
tracing::info!(
"FidelityFX FSR (Vulkan): context created: render {}x{} -> upscale {}x{} (scale {:.3})",
render_width,
render_height,
output_width,
output_height,
scale
);
let mut phase_count: i32 = 0;
let mut jpc_desc = ffxQueryDescUpscaleGetJitterPhaseCount {
header: ffxApiHeader {
ty: FFX_API_QUERY_DESC_TYPE_UPSCALE_GETJITTERPHASECOUNT,
p_next: ptr::null_mut(),
},
render_width,
display_width: output_width,
out_phase_count: &mut phase_count,
};
let rc = unsafe { (ffx.query)(&mut ctx, &mut jpc_desc.header as *mut ffxApiHeader) };
if rc != FFX_API_RETURN_OK || phase_count <= 0 {
tracing::warn!(
"FidelityFX FSR (Vulkan): jitter-phase-count query returned {rc} (phase_count={phase_count})"
);
phase_count = 8;
}
let output = match super::create_output_image(
alloc,
device,
command_pool,
queue,
output_width,
output_height,
) {
Ok(img) => img,
Err(e) => {
unsafe {
let _ = (ffx.destroy_context)(&mut ctx, ptr::null());
}
return Err(e);
}
};
Ok(Some(FsrUpscaler {
ffx,
ctx,
output,
output_layout: Cell::new(vk::ImageLayout::GENERAL),
render_width,
render_height,
output_width,
output_height,
upscale_scale: scale,
jitter_phase_count: phase_count,
jitter: Cell::new([0.0, 0.0]),
prev_elapsed: Cell::new(0.0),
reset_pending: Cell::new(true),
}))
}
}
impl VkUpscaleBackend for FsrUpscaler {
fn render_dims(&self) -> (u32, u32) {
(self.render_width, self.render_height)
}
fn output_dims(&self) -> (u32, u32) {
(self.output_width, self.output_height)
}
fn scale(&self) -> f32 {
self.upscale_scale
}
fn output_image(&self) -> &GpuImage {
&self.output
}
fn output_layout(&self) -> vk::ImageLayout {
self.output_layout.get()
}
fn set_output_layout(&self, layout: vk::ImageLayout) {
self.output_layout.set(layout);
}
fn set_jitter(&self, offset: [f32; 2]) {
self.jitter.set(offset);
}
fn jitter(&self) -> [f32; 2] {
self.jitter.get()
}
fn jitter_offset(&self, frame_index: u32) -> [f32; 2] {
let mut jx = 0.0_f32;
let mut jy = 0.0_f32;
let index = (frame_index as i32).rem_euclid(self.jitter_phase_count.max(1));
let mut desc = ffxQueryDescUpscaleGetJitterOffset {
header: ffxApiHeader {
ty: FFX_API_QUERY_DESC_TYPE_UPSCALE_GETJITTEROFFSET,
p_next: ptr::null_mut(),
},
index,
phase_count: self.jitter_phase_count,
out_x: &mut jx,
out_y: &mut jy,
};
let rc = unsafe {
(self.ffx.query)(
&self.ctx as *const ffxContext as *mut ffxContext,
&mut desc.header as *mut ffxApiHeader,
)
};
if rc != FFX_API_RETURN_OK {
return [0.0, 0.0];
}
[jx, jy]
}
fn dispatch(
&self,
cmd: vk::CommandBuffer,
inputs: UpscaleInputs<'_>,
camera: UpscaleCamera,
) -> Result<(), String> {
let UpscaleInputs {
color,
depth,
motion,
} = inputs;
let UpscaleCamera {
jitter_offset,
elapsed,
near: camera_near,
far: camera_far,
fov_y_radians: camera_fov_y_radians,
} = camera;
let mk = |image: vk::Image, format: u32, usage: u32, state: u32, w: u32, h: u32| {
FfxApiResource {
resource: image.as_raw() as usize as *mut c_void,
description: FfxApiResourceDescription {
ty: FFX_API_RESOURCE_TYPE_TEXTURE2D,
format,
width_or_size: w,
height_or_stride: h,
depth_or_alignment: 1,
mip_count: 1,
flags: 0,
usage,
},
state,
}
};
let color_res = mk(
color.image,
FFX_API_SURFACE_FORMAT_R16G16B16A16_FLOAT,
FFX_API_RESOURCE_USAGE_READ_ONLY,
FFX_API_RESOURCE_STATE_COMPUTE_READ,
self.render_width,
self.render_height,
);
let depth_res = mk(
depth.image,
FFX_API_SURFACE_FORMAT_R32_FLOAT,
FFX_API_RESOURCE_USAGE_DEPTHTARGET,
FFX_API_RESOURCE_STATE_COMPUTE_READ,
self.render_width,
self.render_height,
);
let mv_res = mk(
motion.image,
FFX_API_SURFACE_FORMAT_R16G16_FLOAT,
FFX_API_RESOURCE_USAGE_READ_ONLY,
FFX_API_RESOURCE_STATE_COMPUTE_READ,
self.render_width,
self.render_height,
);
let output_res = mk(
self.output.image,
FFX_API_SURFACE_FORMAT_R16G16B16A16_FLOAT,
FFX_API_RESOURCE_USAGE_UAV,
FFX_API_RESOURCE_STATE_UNORDERED_ACCESS,
self.output_width,
self.output_height,
);
let reset = self.reset_pending.replace(false);
let dt_ms = super::frame_delta_ms(&self.prev_elapsed, elapsed);
let mut desc = ffxDispatchDescUpscale {
header: ffxApiHeader {
ty: FFX_API_DISPATCH_DESC_TYPE_UPSCALE,
p_next: ptr::null_mut(),
},
command_list: cmd.as_raw() as usize as *mut c_void,
color: color_res,
depth: depth_res,
motion_vectors: mv_res,
exposure: FfxApiResource::empty(),
reactive: FfxApiResource::empty(),
transparency_and_composition: FfxApiResource::empty(),
output: output_res,
jitter_offset: FfxApiFloatCoords2D {
x: jitter_offset[0],
y: jitter_offset[1],
},
motion_vector_scale: FfxApiFloatCoords2D {
x: self.render_width as f32,
y: self.render_height as f32,
},
render_size: FfxApiDimensions2D {
width: self.render_width,
height: self.render_height,
},
upscale_size: FfxApiDimensions2D {
width: self.output_width,
height: self.output_height,
},
enable_sharpening: false,
sharpness: 0.0,
frame_time_delta: dt_ms,
pre_exposure: 1.0,
reset,
camera_near,
camera_far,
camera_fov_angle_vertical: camera_fov_y_radians,
view_space_to_meters_factor: 1.0,
flags: 0,
};
let rc = unsafe {
(self.ffx.dispatch)(
&self.ctx as *const ffxContext as *mut ffxContext,
&desc.header as *const ffxApiHeader,
)
};
let _ = &mut desc.header;
if rc != FFX_API_RETURN_OK {
return Err(format!("ffxDispatch (upscale, vulkan) returned {rc}"));
}
Ok(())
}
fn destroy(&mut self, _device: &VkDevice) {
if !self.ctx.is_null() {
unsafe {
let _ = (self.ffx.destroy_context)(&mut self.ctx, ptr::null());
}
self.ctx = ptr::null_mut();
}
self.output = GpuImage::null();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::mem::size_of;
#[test]
fn ffx_struct_sizes_match_sdk_v114() {
assert_eq!(size_of::<ffxApiHeader>(), 16);
assert_eq!(size_of::<FfxApiDimensions2D>(), 8);
assert_eq!(size_of::<FfxApiFloatCoords2D>(), 8);
assert_eq!(size_of::<FfxApiResourceDescription>(), 32);
assert_eq!(size_of::<FfxApiResource>(), 48);
assert_eq!(size_of::<ffxCreateBackendVKDesc>(), 40);
}
}