29 minute read

FSR은 하나의 기술이 아닙니다. 2021년의 FSR 1과 2026년의 FSR Upscaling 4.1은 이름만 같고 원리도, 요구 조건도, 지원 하드웨어도 완전히 다릅니다. 그런데 자료를 찾아보면 “FSR”이라는 하나의 단어로 뭉쳐 설명하는 경우가 많아서, 실제로 통합하려고 하면 어느 문서를 봐야 하는지부터 헷갈립니다.

이 글에서는 FSR을 세대별로 정리합니다. 다만 연혁 나열이 목적은 아닙니다. 각 세대는 이전 세대가 갖고 있던 무언가를 내주고 새 능력을 얻었고, 그 거래를 이해하면 지금 무엇을 붙여야 하는지가 자연스럽게 따라옵니다. 특히 겉보기에 가장 지루한 세대(FSR 3.1)가 실은 가장 중요한 변화를 만들었다는 점이 이 글의 핵심입니다.

템포럴 업스케일링 자체의 원리(지터가 왜 필요한가, 모션 벡터가 무엇을 하는가)는 MetalFX 업스케일링 글에서 자세히 다뤘습니다. 여기서는 그 원리는 전제로 두고 FSR의 규약과 선택에 집중합니다.

1. 세대별 거래 내역

먼저 전체 그림입니다.

세대 핵심 기법 새로 요구한 것 대신 얻은 것
FSR 1 (2021) 공간 필터 (EASU + RCAS) 이미 AA된 깨끗한 입력 어떤 GPU·API에서도 셰이더 두 개로 끝
FSR 2 (2022) 템포럴 리프로젝션 모션 벡터·뎁스·지터 없던 디테일 복원, TAA 대체
FSR 3 (2023) 프레임 생성 프레젠트 경로(스왑체인) 통제권 표시 프레임레이트 배증
FSR 3.1 (2024) 안정 ABI + 서명 DLL “소스를 받아 컴파일”하는 자유 드라이버가 구현을 갈아끼울 수 있게 됨
FSR 4 (2025) 신경망 업스케일러 RDNA 3/4 하드웨어, DirectX 12 화질 도약, 마스크 튜닝 불필요
Redstone (2025~) ML 디노이저·GI 캐시·ML 프레임 생성 위와 같음 업스케일링 밖으로 확장

한 줄로 요약하면 이렇습니다.

FSR의 역사는 “아무 데서나 돌아가는 것”을 조금씩 팔아서 “화질”을 사 온 역사다.

FSR 1은 GPU도 API도 가리지 않았습니다. FSR 2는 하드웨어는 안 가렸지만 렌더러에 조건을 걸었습니다. FSR 3은 프레임을 어떻게 화면에 내보내는지까지 관여하기 시작했습니다. 그리고 FSR 4는 마침내 AMD GPU를 요구합니다. 이 흐름을 알고 보면 각 세대의 API 설계가 왜 그렇게 생겼는지가 설명됩니다.

용어 하나를 정리해 둡니다. AMD는 최근 브랜딩을 FidelityFX에서 FSR로 통합했습니다. 예전 FidelityFX SDK는 지금 AMD FSR SDK이고, 신경망 기반 새 기능들은 AMD FSR Upscaling, AMD FSR Frame Generation 같은 이름을 씁니다. 반면 옛 세대는 AMD FidelityFX Super Resolution 3.1.5처럼 원래 이름을 유지합니다. 그래서 지금 SDK 안에는 두 계열의 이름이 섞여 있습니다.

2. FSR 1 — 시간을 쓰지 않는다는 선택

2021년의 FSR 1은 한 장의 이미지만 보고 크기를 키웁니다. 모션 벡터도, 뎁스도, 이전 프레임도 쓰지 않습니다. 지금 기준으로는 원시적으로 보이지만, 그 제약이 곧 장점이었습니다.

2-1. EASU — 에지를 따라 늘린 커널

첫 번째 패스인 EASU(Edge-Adaptive Spatial Upsampling)가 실제 확대를 담당합니다. 원리는 이렇습니다.

출력 픽셀 하나를 만들 때 입력에서 12탭 이웃을 읽습니다. 그리고 그 이웃의 기울기(gradient)를 보고 에지의 방향과 세기를 추정합니다. 문서가 쓰는 표현은 “기울기 반전(gradient reversal)”, 즉 이웃한 기울기들이 서로 얼마나 어긋나는지입니다. 이 값이 재구성 가중치를 결정합니다.

가중치를 적용하는 커널은 Lanczos(2)를 늘린 것입니다. 등방(isotropic) 커널을 쓰면 에지도 함께 흐려지니, 추정한 에지 방향으로 커널을 늘려 에지를 따라서는 넓게, 에지를 가로질러서는 좁게 샘플을 섞습니다. 얼마나 늘릴지는 기울기에서 나옵니다.

Lanczos에는 함정이 하나 있습니다. 음의 로브(negative lobe)가 있어서 대비가 큰 경계에 링잉(에지 옆의 밝고 어두운 띠)이 생깁니다. EASU는 가장 가까운 2×2 입력 텍셀의 최소·최대값으로 출력을 클램프해서 이걸 막습니다. 커널이 만들어 낸 값이 원래 이웃에 없던 밝기라면 잘라 버리는 것입니다.

2-2. RCAS — 링잉 없는 샤프닝

두 번째 패스 RCAS(Robust Contrast-Adaptive Sharpening)는 출력 해상도에서 디테일을 살립니다. AMD의 기존 CAS에서 파생되었는데, 흔히 “CAS에서 적응 샤프닝을 떼어낸 것”으로 설명되곤 합니다. 그건 정확하지 않습니다. AMD 자신의 설명은 이렇습니다.

“CAS는 지역 대비를 가변 샤프닝 양으로 변환하는 데 단순화된 방식을 쓴다. RCAS는 더 정확한 방식을 써서, 클리핑되기 전에 가능한 최대 지역 샤프닝을 풀어낸다. RCAS에는 노이즈로 판단되는 것의 샤프닝을 제한하는 과정도 들어 있다.”

즉 적응성을 뺀 것이 아니라 어림짐작을 방정식 풀이로 바꾼 것입니다. 필터 모양은 CAS와 똑같은 5탭 십자입니다.

    w
  w 1 w      출력 = (w*(n+e+s+w) + m) / (4*w + 1)
    w

w는 음의 로브 가중치이고, 이 값이 클수록 선명해지면서 링잉 위험도 커집니다. CAS는 지역 대비에서 휴리스틱으로 w를 뽑고 sqrt로 모양을 다듬습니다. RCAS는 대신 출력이 [0, 1] 범위를 벗어나는 지점을 직접 계산합니다. 위 식을 0과 1에 대해 각각 w로 풀면 두 개의 한계값이 나오고, 그중 클리핑이 일어나지 않는 쪽을 골라 상한(0.25 - 1/16)으로 자릅니다. 그래서 “그 픽셀에서 낼 수 있는 가장 강한 샤프닝”이 자동으로 정해집니다.

이름의 Robust는 MSAA 입력에 대한 것입니다. MSAA는 에지에 계단형 그라디언트를 만들어 개별 탭 기반 판정을 흔드는데, RCAS는 개별 탭 대신 이웃의 최소·최대에 4를 곱한 값을 써서 이걸 안정화합니다.

CAS에서 실제로 없어진 것은 스케일링, 리니어 공간 동작, 그리고 출력 saturate 입니다. 마지막 항목이 문서에 굵게 적힌 경고의 이유입니다.

“RCAS에 음수를 입력하면 NaN이 출력된다.”

입력 컬러가 [0, 1] 범위로 정규화되어 있어야 한다는 뜻입니다. 그리고 AMD는 반대 방향의 오용도 막아 둡니다. EASU 없이 RCAS만 쓰지 말라는 것이고, 업스케일링 없이 샤프닝만 원하면 CAS를 쓰라는 것입니다.

2-3. 대신 요구하는 것

FSR 1은 입력에 조건을 겁니다.

  • 이미 잘 안티에일리어싱되어 있어야 합니다. 문서의 표현은 “TAA, MSAA 같은 기법으로 잘 AA된” 입력입니다. FSR 1은 AA를 하지 않습니다.
  • 노이즈가 없어야 합니다. 노이즈는 기울기 판정을 망칩니다.
  • 퍼셉추얼 공간이어야 합니다. 리니어가 아니라 sRGB이고, [0, 1] 범위입니다.
  • 음의 밉 바이어스를 걸어 텍스처 디테일을 확보해야 합니다.

파이프라인 위치는 여기서 따라 나옵니다. AA와 톤매핑이 끝난 뒤, 노이즈를 넣는 후처리(필름 그레인, 색수차)와 UI보다는 앞입니다.

품질 프리셋은 네 가지입니다. AMD는 “일관성을 위해 게임에 FSR을 노출할 때 이 프리셋을 쓰라”고 권합니다.

모드 차원별 배율 면적 배율 렌더 해상도 밉 바이어스
Ultra Quality 1.3× 1.69× 77% −0.38
Quality 1.5× 2.25× 67% −0.58
Balanced 1.7× 2.89× 59% −0.77
Performance 2.0× 4.0× 50% −1.00

밉 바이어스 공식은 이렇습니다. AMD 자료마다 표에 적힌 값이 조금씩 달라서, 공식으로 계산하는 편이 낫습니다.

mipBias = -log2(출력 해상도 / 렌더 해상도)

3절에서 볼 FSR 2의 공식과 비교하면 -1.0 항이 없습니다. 이 차이가 두 기술의 성격을 요약합니다. 템포럴 누적은 여러 프레임에 걸쳐 고주파를 복원할 수 있으니 밉을 한 단계 더 당겨도 되지만, 한 장만 보는 FSR 1은 그만큼 당기면 그냥 지글거립니다.

Performance(2.0배)가 상한인 데도 이유가 있습니다. 문서가 밝히는 EASU의 설계 범위는 “1배에서 4배 면적“이고, 면적 4배가 곧 차원별 2배입니다. 그 이상은 알고리즘이 상정한 범위 밖입니다. FSR 2가 Ultra Performance(3.0배)를 새로 넣을 수 있었던 것은 원리가 달라졌기 때문입니다.

사소하지만 헷갈리는 비대칭도 하나 있습니다. FSR 1에는 Ultra Quality가 있고 Ultra Performance가 없습니다. FSR 2는 정확히 그 반대입니다.

2-4. 그래서 아무 데서나 돌았다

FSR 1의 진짜 특징은 화질이 아니라 요구사항이 거의 없다는 점입니다.

  • 셰이더 두 개면 끝입니다. DirectX 12는 HLSL(셰이더 모델 6.2), Vulkan은 GLSL 4.50으로 돌아갑니다.
  • 상태가 없습니다. 프레임 간 히스토리가 없으니 리셋도, 고스팅도, 지터도 없습니다.
  • 렌더러를 고칠 필요가 없습니다. 모션 벡터를 안 쓰기 때문입니다.

그래서 FSR 1은 벤더를 가리지 않고 돌았고, 게임 엔진 밖(드라이버 스케일링, 에뮬레이터, 심지어 동영상 재생기)에서도 널리 이식되었습니다. 업스케일링 기술 중에 이 정도로 널리 퍼진 것은 없습니다.

한계도 같은 이유에서 나옵니다. 없는 정보를 만들어 낼 수 없습니다. 입력에 남은 에일리어싱은 지워지지 않고 더 크고 선명한 에일리어싱이 됩니다. 서브픽셀 디테일은 복원되지 않습니다. 반대로 정지 화면과 움직이는 화면의 품질이 완전히 같다는 점은 장점입니다. 템포럴 기법의 고질병인 고스팅과 떨림이 원리적으로 없습니다.

그리고 지금도 FSR 1이 맞는 상황이 있습니다. 모션 벡터를 뽑을 수 없는 렌더러라면 선택지가 이것뿐입니다.

3. FSR 2 — 시간을 정보로 쓰기

2022년의 FSR 2는 완전히 다른 기술입니다. TAA와 업스케일링을 하나로 합친 템포럴 업스케일러이고, 여기서부터 “없던 디테일을 복원한다”는 말이 성립합니다.

원리 자체는 템포럴 업스케일링 일반론과 같습니다. 매 프레임 카메라를 서브픽셀만큼 흔들어 그리고, 모션 벡터로 이전 프레임을 현재 위치에 되돌리고, 뎁스로 되돌릴 수 없는 곳을 가려낸 뒤 누적합니다. 이 부분은 MetalFX 글에서 자세히 다뤘으니, 여기서는 FSR의 규약과 FSR만의 장치를 봅니다.

3-1. 입력과 규약

리소스 해상도 포맷 비고
컬러 렌더 앱 지정 HDR이면 FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE
뎁스 렌더 R32_FLOAT Reversed-Z / 무한 원거리 플래그
모션 벡터 렌더 2×float motionVectorScale로 단위 변환
리액티브 마스크 렌더 R8_UNORM 옵션
투명도·합성 마스크 렌더 R8_UNORM 옵션
노출 1×1 R32_FLOAT 자동 노출을 켜면 옵션

플래그 이름은 현행 통합 API 기준으로 적었습니다. 2022년 당시 FSR 2 전용 API는 FFX_FSR2_ENABLE_DEPTH_INVERTED처럼 FFX_FSR2_ 접두어를 썼는데, 5절에서 볼 API 개편 이후 FFX_UPSCALE_로 통일되었습니다. 의미는 같습니다.

모션 벡터는 현재 프레임에서 이전 프레임 위치를 향하고, 스크린 공간 [-width, -height] ~ [width, height] 범위입니다. 내부적으로는 16비트 정밀도로 다룹니다. 엔진의 단위나 부호가 다르면 motionVectorScale 하나로 흡수합니다.

지터는 Halton(2,3)이고, 위상 개수는 스케일 비율에서 나옵니다.

지터 위상 수 = (int)(8 * n²)      // n = 차원별 스케일 비율

2배 업스케일이면 32개입니다. 출력 픽셀 하나가 입력 픽셀 4분의 1에 해당하니, 출력 픽셀당 8개 샘플을 모으려면 입력 픽셀당 32개가 필요하다는 계산입니다.

한 가지 짚어 둘 것이 있습니다. AMD 문서는 이 공식을 ceil(8 * n²)로 적어 두었는데, 구현은 올림이 아니라 버림입니다. 실제 코드는 int32_t(basePhaseCount * pow(...)) 형태로 캐스팅합니다. Balanced(1.7배)에서 차이가 드러납니다. 8 × 1.7²은 23.12이고, 올림이면 24, 버림이면 23입니다. AMD 문서의 표에 적힌 값은 23이라서 코드와 표가 일치하고 공식 설명만 틀렸습니다. 직접 계산해서 넣을 생각이면 버림으로 맞춰야 합니다.

값은 직접 계산하지 말고 ffxQueryDescUpscaleGetJitterPhaseCount와 ffxQueryDescUpscaleGetJitterOffset으로 받는 편이 안전합니다. 그리고 오프셋으로 (0,0)을 넘기면 안 됩니다. 그 프레임은 지터가 없는 것과 같아서 정보가 낭비됩니다.

밉 바이어스 공식은 이후 세대까지 그대로 유지됩니다.

mipBias = log2(렌더 해상도 / 출력 해상도) - 1.0

노출은 두 값으로 나뉩니다. 프리 노출은 입력에 이미 곱해져 있는 상수를 되돌리는 용도이고, 노출은 앱의 톤매핑과 일치해야 하는 값입니다. 자동 노출을 켜면 다음을 계산합니다.

exposureIso100 = log2((평균 휘도 * 100) / 12.5)

ISO 100 필름의 반응을 기준으로 삼는다는 뜻이고, 12.5는 반사광식 노출계의 표준 상수입니다. 카메라 노출 계산에서 쓰는 그 식입니다.

3-2. 내부 구조

FSR 2는 여섯 단계로 돌아갑니다.

패스 하는 일
Compute Luminance Pyramid 컬러에서 휘도 피라미드를 만든다 (이후 판정의 기준)
Reconstruct & Dilate 이전 프레임 뎁스를 복원하고 모션 벡터를 딜레이트한다
Depth Clip 뎁스 불연속으로 리프로젝션이 유효하지 않은 픽셀을 걸러낸다
Create Locks 보호할 픽셀에 락을 건다
Reproject & Accumulate 히스토리를 되돌려 현재 샘플과 누적한다
RCAS 출력 해상도에서 샤프닝

딜레이트가 왜 필요한지 짚어 둘 만합니다. 모션 벡터는 오브젝트의 중심에서는 정확하지만 에지에서는 배경과 전경 중 어느 것의 움직임인지 애매합니다. 그대로 쓰면 에지가 번집니다. 딜레이트는 이웃에서 가장 가까운(뎁스가 작은) 픽셀의 모션 벡터를 퍼뜨려 전경의 움직임이 에지를 지배하게 만듭니다.

3-3. 락 — 얇은 것을 지키는 장치

FSR 2에서 가장 FSR답고 흥미로운 부분입니다.

먼저 문제를 정확히 짚어야 합니다. 범인은 “평균”이 아니라 컬러 렉티피케이션(color rectification) 입니다.

템포럴 누적은 히스토리를 그냥 믿을 수 없습니다. 리프로젝션이 조금이라도 틀리면 엉뚱한 색이 끌려와 고스팅이 되니까요. 그래서 FSR 2는 현재 프레임 이웃의 색으로 YCoCg 바운딩 박스를 만들고, 되돌린 히스토리 색을 그 박스 안으로 잘라냅니다. 이것이 렉티피케이션이고, 템포럴 업스케일러의 안정성을 지탱하는 장치입니다.

그런데 폭이 1픽셀인 특징에는 이 장치가 독으로 작용합니다. 전선이나 철망은 지터 시퀀스 중 몇 개의 위상에서만 샘플에 걸립니다. 걸리지 않은 프레임에서는 그 색이 현재 이웃 박스 안에 없고, 그러면 렉티피케이션이 히스토리에 살아 있던 전선 색을 잘라 버립니다. AMD의 표현이 정확합니다.

“얇은 특징은 전체 시퀀스 중 몇 개의 지터 샘플에서만 나타날 수 있다. 컬러 렉티피케이션은 그것이 입력 이미지의 일부가 아닌 순간 곧바로 깎아낸다. 그 결과는 불안정한 이미지, 또는 아주 흐려진 특징이다.”

락(lock) 은 이 깎아냄을 막는 장치입니다. AMD의 정의도 그대로입니다.

“직관적으로 픽셀 락은 그 픽셀에 컬러 렉티피케이션이 적용되지 않게 막는 메커니즘이다.”

동작은 네 단계입니다.

  • 생성 — 3×3 휘도 이웃을 임계값과 비교합니다. 이웃과 뚜렷하게 다른 능선(ridge) 형태의 픽셀은 얇은 특징일 가능성이 높으므로 락을 겁니다. 문서가 직접 드는 예가 “전선이나 사슬 울타리”입니다.
  • 기록 — 락 상태 텍스처의 빨강 채널에 남은 수명, 초록 채널에 락을 건 시점의 휘도를 저장합니다.
  • 만료 — 수명은 매 프레임 줄어듭니다. 감소량은 지터 시퀀스 길이에 묶여 있어서, 시퀀스가 짧을수록 잘못 걸린 락이 빨리 죽습니다. 지터 위상 수를 필요 이상으로 늘리면 안 되는 또 하나의 이유입니다.
  • 해제 — 만료를 기다리면 늦습니다. 그래서 Reproject & Accumulate 단계에서 초록 채널의 휘도를 현재 값과 비교합니다(셰이딩 변화 감지). 픽셀의 외형이 불연속적으로 바뀌었으면 즉시 락을 풉니다. 지키려던 특징이 이미 다른 것으로 바뀐 상황이니까요. 디스오클루전도 같은 역할을 합니다.

정리하면 락은 “이 픽셀에는 렉티피케이션을 걸지 마라” 라는 표시이고, 만료·셰이딩 변화·디스오클루전 세 가지가 그 표시를 언제 놓을지 결정합니다. 안정성을 위한 장치(렉티피케이션)와 디테일을 위한 장치(락)가 서로를 견제하는 구조입니다.

3-4. 두 개의 마스크

FSR 2 통합에서 가장 손이 많이 가는 부분이 마스크 두 개입니다. 이름이 비슷해서 헷갈리는데, 서로 다른 손잡이입니다.

마스크 무엇을 조절하나 값의 의미 주 용도
리액티브 히스토리 대비 현재 프레임의 기여도 0 = 반응 없음, 클수록 현재 프레임 비중 증가 (권장 상한 약 0.9) 알파 블렌딩 오브젝트, 파티클
투명도·합성 히스토리 보호 장치(락) 0 = 변경 없음, 1 = 락 완전 제거 레이트레이싱 반사, 애니메이션 텍스처

리액티브 마스크는 “히스토리를 덜 믿어라”이고, 투명도·합성 마스크는 “락을 풀어라, 그리고 휘도 불안정성 판정을 빼라”입니다. 알파 블렌딩된 파티클은 모션 벡터에 기록되지 않으니 리액티브 쪽이 맞고, 레이트레이싱 반사처럼 내용이 정당하게 계속 변하는 픽셀은 락에 묶여 있으면 오히려 잘못되므로 투명도·합성 쪽이 맞습니다.

둘 다 옵션이고, 넘기지 않으면 클리어된 1×1 텍스처로 취급됩니다. 직접 만들기 어려우면 자동 생성 헬퍼가 있습니다. ffxDispatchDescUpscaleGenerateReactiveMask는 불투명 오브젝트만 그린 컬러 버퍼와 전체 컬러 버퍼를 비교하는 휘도 휴리스틱으로 마스크를 만듭니다. 두 버퍼가 다른 곳이 곧 반투명이 얹힌 곳이라는 발상입니다.

다만 AMD는 자동 생성에 분명한 선을 그어 둡니다.

“이 기능은 FSR을 새 엔진이나 타이틀에 통합할 때 도움을 주기 위한 것이다. 그러나 최상의 품질을 위해서는 리액티브 마스크와 투명도·합성 마스크를 직접 렌더링하는 것을 여전히 권장한다. 머티리얼에 기반해 값을 만드는 것이, 최종 이미지에서 자동 생성하는 것보다 더 신뢰할 만할 것으로 기대되기 때문이다.”

“최종 이미지에서 추론하는 것보다 머티리얼에서 만드는 것이 낫다”는 이 판단을 기억해 두면 좋습니다. 6절에서 정확히 뒤집힙니다.

이 마스크 튜닝이 게임마다 다르고 정답이 없다는 점이 FSR 2 통합의 실질적인 비용이었습니다.

3-5. 비용과 위치

AMD가 공개한 수치로 감각을 잡으면, RX 7900 XTX에서 4K 출력 기준으로 Quality(1.5배)는 약 0.7ms에 438MB, Performance(2.0배)는 약 0.6ms에 363MB입니다. 실제 사용량은 ffxQueryDescUpscaleGetGPUMemoryUsage로 질의할 수 있습니다.

위치는 후처리 앞입니다. 스크린스페이스 반사, AO, 디노이저는 렌더 해상도에서 업스케일 전에 돌리고, 톤매핑·DOF·필름 그레인·색수차·비네트는 출력 해상도에서 업스케일 뒤에 돌립니다.

여기서 FSR 1과의 가장 구조적인 차이가 드러납니다. FSR 1은 톤매핑이 끝난 퍼셉추얼 컬러를 받고, FSR 2는 톤매핑 전의 리니어 HDR을 받습니다. 프레임에서 앉는 자리가 아예 다릅니다. FSR 2가 TAA를 대체하니 당연한 결과입니다. AA된 입력이 필요한 후처리는 모두 업스케일 뒤로 가고, 뎁스가 필요한 후처리는 모두 앞에 남습니다.

한 가지 덧붙이면, FSR 2도 내부에서는 톤매핑을 합니다. 히스토리 누적과 색 판정을 하려면 값의 범위를 눌러야 하니까요. 다만 되돌릴 수 있는(reversible) 톤매퍼를 쓰고 출력 직전에 되돌립니다. 그래서 넣은 신호와 같은 영역으로 결과가 나옵니다. 재미있는 것은 이때 쓰는 연산자가 FSR 1이 함께 배포한 SRTM(Simple Reversible Tone-Mapper)이라는 점입니다. 원리는 버렸지만 도구는 물려받았습니다.

4. FSR 3 — 없는 프레임을 만들어내기

FSR 3은 업스케일링 위에 프레임 생성을 얹었습니다. 2023년 8월에 발표되고 9월 말 게임 패치로 먼저 출시된 뒤, 12월에 소스가 공개되었습니다. 초기 공개 버전은 DirectX 12 전용이었고 릴리스 노트에 “FSR3의 Vulkan 지원은 개발 중”이라고 적혀 있었습니다.

구성이 네 덩어리로 늘어납니다.

구성 역할
업스케일러 렌더 해상도 → 출력 해상도 (FSR 2 계열)
옵티컬 플로우 이미지에서 직접 움직임을 추정
프레임 보간 두 프레임 사이의 중간 프레임 합성
프레임 페이싱 스왑체인 생성한 프레임을 균일한 간격으로 제시

4-1. 모션 벡터가 있는데 옵티컬 플로우가 왜 또 필요한가

이게 FSR 3에서 가장 먼저 생기는 질문입니다. 게임이 이미 정확한 모션 벡터를 주는데 왜 이미지를 비교해서 움직임을 다시 추정하느냐는 것이죠.

답은 모션 벡터가 지오메트리의 변환만 안다는 데 있습니다. 정점을 이전 프레임 행렬로 한 번 더 변환해서 얻는 값이니, 정점이 움직이지 않았는데 화면에서 움직이는 것들은 전부 놓칩니다.

  • 그림자와 반사 — 그림자를 드리우는 오브젝트가 움직이면 그림자도 움직이지만, 그림자를 받는 바닥의 모션 벡터는 0입니다.
  • 파티클과 알파 효과 — 대부분 모션 벡터를 쓰지 않습니다.
  • 애니메이션 텍스처와 셰이더로 움직이는 것 — UV 스크롤, 물결, 아지랑이.
  • 조명 변화 — 깜빡이는 광원 아래에서 색이 바뀌는 것도 “움직임”으로 보입니다.

업스케일링에서는 이런 픽셀이 조금 번지는 정도로 끝나지만, 프레임 보간에서는 곧바로 눈에 보이는 아티팩트가 됩니다. 중간 프레임을 만들려면 그 픽셀이 어디로 갔는지 알아야 하니까요.

FidelityFX Optical Flow는 AMD Fluid Motion Frames 기술을 기반으로 하고, 8×8 블록 단위로 움직임을 계산합니다. 구조는 두 단계입니다.

준비 단계 — 입력을 휘도로 변환하고 Single Pass Downsampler로 여섯 단계 피라미드를 만듭니다. 동시에 이미지를 9개 영역으로 나눠 히스토그램을 비교해 씬 전환을 감지합니다.

메인 단계 — 피라미드의 각 레벨에서 일곱 번 반복합니다. SAD(절대차 합)로 가장 잘 맞는 블록을 찾고(검색), 3×3 미디언 필터로 튀는 값을 제거하고(필터링), 다음 레벨을 위해 2배로 키웁니다(업스케일). 거친 레벨에서 대략 잡고 세밀한 레벨로 내려오는 전형적인 계층적 블록 매칭입니다.

출력은 블록 해상도의 R16G16_SINT 모션 벡터와 씬 전환 플래그(3×1 R32_UINT)입니다.

4-2. 프레임 보간 — 두 개의 움직임 중 고르기

프레임 보간은 아홉 개 패스로 구성되는데, 설계의 핵심은 하나입니다. 게임 모션 벡터와 옵티컬 플로우로 각각 모션 벡터 필드를 만들고, 색이 더 잘 맞는 쪽을 고른다.

흐름을 따라가면 이렇습니다.

  1. Setup — 원자적 연산을 쓰는 내부 서피스(카운터, 게임 모션 벡터 필드, 옵티컬 플로우 벡터 필드, 디스오클루전 마스크)를 클리어합니다.
  2. ReconstructPreviousDepth — 모션 벡터를 길이의 절반만큼 리프로젝트·딜레이트해서 보간 프레임의 뎁스를 만듭니다. 중간 시점의 기준계를 세우는 단계입니다. 절반이라는 숫자가 곧 “두 프레임 사이 정확히 중간”이라는 뜻입니다.
  3. Game Motion Vector Field — 딜레이트된 모션 벡터를 우선순위 정보(카메라 거리, 색 유사도)와 함께 패킹합니다.
  4. Game Motion Vector Field Inpainting Pyramid — 3번은 원자적 쓰기라서 아무도 쓰지 않은 구멍이 남습니다. SPD로 밉 체인을 만들어 채우는데, 2D에서 가장 가깝고 뎁스에서 가장 먼 벡터를 고릅니다.
  5. Optical Flow Vector Field — 같은 일을 옵티컬 플로우로 합니다. 우선순위 기준은 크기와 색 일치도입니다.
  6. Disocclusion Mask — 보간 프레임과 이전 프레임 사이, 보간 프레임과 현재 프레임 사이의 두 개의 가림 마스크를 만듭니다.
  7. Interpolation — 두 마스크와 두 모션 필드로 색을 섞습니다. 옵티컬 플로우 쪽이 색이 더 잘 맞으면 그쪽을 씁니다.
  8. Inpainting Pyramid — 보간된 색의 밉 체인을 만듭니다. 구멍은 무시되므로, 밉에서 샘플링하면 2D로 가장 가까운 유효한 색들의 평균이 나옵니다.
  9. Inpainting — 남은 구멍을 채우고, 필요하면 디버그 시각화를 얹습니다.

인페인팅이 두 번 나온다는 점이 눈에 띕니다. 프레임 보간은 본질적으로 양쪽에서 아무 정보도 오지 않는 픽셀이 생기는 연산이고(중간 시점에서 처음 드러나는 영역), 그 구멍을 메우는 것이 작업량의 큰 부분입니다.

4-3. 페이싱 — 스왑체인을 내줘야 한다

프레임 보간의 진짜 난제는 화질이 아니라 타이밍입니다.

렌더 프레임 사이에 보간 프레임을 끼워 넣어 60fps를 120fps로 만들려면, 표시 간격이 8.3ms로 균일해야 합니다. 그런데 렌더 프레임은 씬 복잡도에 따라 도착 시간이 들쭉날쭉합니다. 앱이 평소처럼 Present를 부르면 “보간-실제-보간-실제”의 간격이 제멋대로가 되고, 프레임레이트 숫자는 두 배인데 체감은 오히려 나빠집니다.

그래서 FSR 3은 스왑체인을 대체합니다. Vulkan에서는 ffxQueryDescSwapchainReplacementFunctionsVK로 대체 함수 포인터를 받아 꽂습니다.

pOutGetSwapchainImagesKHR    → vkGetSwapchainImagesKHR 대체
pOutAcquireNextImageKHR      → vkAcquireNextImageKHR 대체
pOutQueuePresentKHR          → vkQueuePresentKHR 대체
pOutCreateSwapchainFFXAPI    → 스왑체인 생성
pOutDestroySwapchainFFXAPI   → 스왑체인 파괴

DirectX 12에서는 같은 역할을 스왑체인 컨텍스트 생성이 담당합니다. 결과적으로 프레임을 화면에 내보내는 경로를 FSR에 넘기는 것이고, 이것이 앞에서 말한 “FSR 3이 요구한 대가”입니다. 오버레이나 캡처 도구가 DXGI를 후킹하고 있으면 충돌이 생기는 이유도 여기 있습니다.

비동기 실행도 옵션입니다. 컨텍스트 생성 시 FFX_FRAMEGENERATION_ENABLE_ASYNC_WORKLOAD_SUPPORT를 켜고 스왑체인 설정에서 allowAsyncWorkloads와 별도의 컴퓨트·프레젠트 큐를 지정하면, 보간 작업을 렌더링과 겹쳐 돌릴 수 있습니다.

4-4. UI를 어떻게 할 것인가

보간은 톤매핑이 끝난 최종 이미지에서 일어납니다. 그런데 UI를 그 이미지에 이미 그려 놓았다면, UI까지 보간 대상이 되어 글자가 흐물거립니다. 문서가 제시하는 방법은 세 가지입니다.

방법 내용 대가
콜백 frameGenerationCallback이 제시되는 프레임마다 호출되고, 거기서 UI를 다시 그린다 코드 변경이 가장 크지만 UI도 부드러워진다
별도 텍스처 UI를 별도 텍스처에 그려 넘기면 보간 프레임 위에 합성해 준다 가장 무난하다
HUD 없는 컬러 HUDLessColor 리소스를 함께 넘기면 UI 영역을 자동으로 검출해 합성한다 렌더 타깃 하나가 더 든다

4-5. 지연시간이라는 함정

프레임 생성에서 반드시 짚어야 하는 점입니다. 프레임 생성은 표시되는 프레임 수를 늘리지만 입력 지연을 줄이지 않습니다.

보간 프레임은 앞뒤 두 렌더 프레임이 모두 있어야 만들 수 있습니다. 즉 렌더가 끝난 프레임을 곧바로 내보내지 못하고 붙잡아 둬야 하므로, 지연은 오히려 늘어납니다. 화면은 120fps처럼 부드러운데 조작 반응은 60fps보다 못한 상태가 될 수 있습니다.

그래서 프레임 생성은 지연 감소 기술과 함께 쓰는 것이 전제입니다. 프레임 생성만 켜서 벤치마크 숫자를 올리는 것은 의미가 없고, 입력 지연을 함께 관리해야 실제 체감이 개선됩니다.

입력 프레임레이트에도 하한이 있습니다. 문서의 표현은 이렇습니다.

애플리케이션의 프레임레이트가 60fps(해석적 프레임 생성 3.1.6) 또는 30fps(ML 프레임 생성 4.0.1) 아래로 떨어지면, 생성된 프레임에 사용자 경험을 해칠 정도로 심한 아티팩트가 생길 수 있다.

두 프레임 사이 간격이 벌어지면 그 사이에서 일어난 일이 너무 많아 보간이 성립하지 않습니다. 30fps에서 60fps를 만드는 용도가 아니라, 이미 충분히 빠른 것을 더 부드럽게 만드는 용도라는 뜻입니다.

4-6. 스왑체인을 내준 대가 — Anti-Lag 2와의 악수

지연 감소를 실제로 붙이려고 하면 4-3절에서 스왑체인을 내준 대가가 구체적인 형태로 드러납니다. 이 부분이 FSR 3 설계에서 가장 재미있는 지점이라 따로 떼어 둡니다.

AMD Anti-Lag 2는 드라이버가 아니라 게임 코드 안에서 동작합니다. 입력을 폴링하기 직전에 지연을 삽입해서 CPU가 GPU보다 너무 앞서 달리지 않게 맞추는 방식입니다. 이전 세대인 Anti-Lag+는 같은 삽입 지점을 드라이버가 게임 함수를 후킹해서 확보했는데, 그게 안티치트에 부정 조작으로 잡혀 2023년 10월에 기능이 철회되었습니다. 그래서 Anti-Lag 2는 게임이 직접 통합하는 SDK가 되었습니다.

문제는 프레임 생성과의 조합입니다. Anti-Lag 2는 지금 제시되는 프레임이 실제 프레임인지 보간 프레임인지를 알아야 합니다. 그런데 그 판단을 아는 쪽은 FSR의 대체 스왑체인이고, 그것은 AMD가 서명한 DLL입니다. 서명된 DLL이 게임 코드를 직접 부를 수는 없습니다.

해법은 DXGI의 프라이빗 데이터를 통한 간접 전달입니다. 게임이 자기 Anti-Lag 2 컨텍스트 포인터를 정해진 GUID로 스왑체인에 꽂아 둡니다.

// {5083ae5b-8070-4fca-8ee5-3582dd367d13}
static const GUID IID_IFfxAntiLag2Data =
    {0x5083ae5b, 0x8070, 0x4fca, {0x8e,0xe5,0x35,0x82,0xdd,0x36,0x7d,0x13}};

struct AntiLag2Data {
    AMD::AntiLag2DX12::Context* context;
    bool                        enabled;
} data;

data.context = &m_AntiLag2Context;
data.enabled = m_AntiLag2Enabled;
pSwapChain->SetPrivateData(IID_IFfxAntiLag2Data, sizeof(data), &data);

그러면 FSR의 스왑체인이 Present 직전에 같은 GUID로 데이터를 꺼내 프레임 종류를 통보합니다.

bool isInterpolated = frameType != PacingData::Real;
if (SUCCEEDED(swapChain->GetPrivateData(IID_IFfxAntiLag2Data, &size, &data))) {
    if (data.enabled)
        AMD::AntiLag2DX12::SetFrameGenFrameType(data.context, isInterpolated);
}
swapChain->Present(uSyncInterval, uFlags);

구조체 정의와 GUID는 두 SDK에 똑같이 중복 선언되어 있습니다. 헤더를 공유하면 서로 의존성이 생기니 일부러 복사해 둔 것입니다. 그래서 게임은 Update()(입력 타이밍)와 MarkEndOfFrameRendering()(렌더 제출 경계)만 직접 부르고, 프레임 종류 통보는 FSR 3.1.1부터 스왑체인이 대신 해 줍니다.

두 컴포넌트가 하나의 GUID와 구조체 레이아웃만으로 악수하는 이 설계는, 프레젠트 경로를 남에게 넘기면 그 뒤로는 이런 우회로가 필요해진다는 사실을 잘 보여줍니다.

5. FSR 3.1 — 가장 지루해 보이는, 가장 중요한 세대

2024년 3월 GDC에서 발표되고 7월에 공개된 FSR 3.1의 릴리스 노트는 네 줄로 요약됩니다.

  • “FSR 3.1 업스케일러는 고스팅을 줄이고, 수렴 속도를 개선하고, 깜빡임을 줄인다.”
  • “FSR 프레임 보간의 프레임 페이싱이 더 나은 경험을 위해 개선되었다.”
  • “FSR 3.1은 프레임 보간 과정을 업스케일링과 분리하는 기능을 추가해, 사용자가 원하는 어떤 업스케일러와도, 또는 네이티브 해상도에서도 쓸 수 있게 한다.”
  • “FSR 3.1은 FSR API를 도입한다. 이 인터페이스를 쓰는 애플리케이션이 앞으로 나올 FSR 버전으로 더 쉽게 업데이트될 수 있도록, FSR 통합을 미래 대비(future proof)로 만든다.”

앞의 두 줄은 흔한 화질 개선입니다. 뒤의 두 줄이 이 세대의 정체입니다. API가 바뀌었습니다.

5-1. 그 전까지의 문제

FSR 1·2·3.0은 소스로 배포되었습니다. 셰이더와 C 코드를 받아 게임 빌드에 넣고 ffxFsr2ContextCreate / ffxFsr2ContextDispatch 같은 함수를 직접 호출했습니다. 오픈소스의 이상적인 모습이고, 이식성도 이 방식 덕분이었습니다.

문제는 출시 시점에 버전이 얼어붙는다는 것입니다. 업스케일러가 개선되어도 게임 바이너리에 컴파일되어 들어간 코드는 그대로입니다. 개선을 전달하려면 스튜디오가 SDK를 갱신하고 패치를 내보내야 합니다. 이미 출시된 수백 개 타이틀에 대해 그 일이 일어나기를 기대하는 것은 현실적이지 않습니다.

5-2. FSR API의 설계

FSR 3.1은 안정 ABI를 도입했습니다. ffx_api.h에 선언된 함수는 다섯 개뿐입니다.

ffxCreateContext(...)    // 컨텍스트 생성
ffxDestroyContext(...)   // 파괴
ffxDispatch(...)         // 매 프레임 실행
ffxQuery(...)            // 질의 (지원 버전, 필요 리소스, 지터 값 ...)
ffxConfigure(...)        // 런타임 설정 변경

AMD이 스스로 정리한 설계 원칙 세 줄이 그대로 이 절의 요약입니다.

  • “함수가 다섯 개뿐이고, 대부분의 정보는 확장을 추가할 수 있는 구조체를 통해 교환된다.”
  • “AMD가 사전 빌드하고 서명한 단일 FidelityFX DLL에 링크해야 한다.”
  • “커스텀 백엔드는 더 이상 지원되지 않는다. 이 API를 통해서는 네이티브 DirectX 12와 Vulkan 백엔드만 사용할 수 있다.”

세 번째 줄은 짚어 둘 만합니다. 여기서 Vulkan은 빠지지 않았습니다. 뒤에서 볼 신경망 기능들이 DirectX 12 전용인 것과 별개로, 해석적 FSR 3.1은 두 API를 모두 지원합니다. 대신 직접 만든 백엔드를 꽂는 자유가 없어졌습니다.

기능이 늘어도 함수가 늘지 않습니다. 대신 모든 인자를 디스크립터 구조체의 연결 리스트로 전달합니다. 각 디스크립터는 헤더에 type(구조체 종류를 식별하는 값)과 pNext(다음 디스크립터 포인터)를 갖습니다. Vulkan을 써 본 사람에게는 익숙한 패턴입니다.

ffxCreateContextDescUpscale upscaleDesc = {};
upscaleDesc.header.type    = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE;
upscaleDesc.maxRenderSize  = { renderW,  renderH  };
upscaleDesc.maxUpscaleSize = { displayW, displayH };
upscaleDesc.flags = FFX_UPSCALE_ENABLE_DEPTH_INVERTED
                  | FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE;

// 백엔드는 pNext 체인으로 붙인다
ffxCreateBackendDX12Desc backendDesc = {};
backendDesc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_BACKEND_DX12;
backendDesc.device      = d3d12Device;
upscaleDesc.header.pNext = &backendDesc.header;

ffxContext context = nullptr;
ffxCreateContext(&context, &upscaleDesc.header, nullptr);

이 구조가 중요한 이유는 파라미터를 추가해도 ABI가 깨지지 않기 때문입니다. 새 기능은 새 type을 가진 구조체로 체인에 추가됩니다. 오래된 구현은 모르는 type을 무시하고, 새 구현은 없는 구조체를 기본값으로 취급합니다. 함수 시그니처가 다섯 개로 고정이니 링크 호환성도 유지됩니다.

여기에 서명된 사전 빌드 DLL 배포가 결합됩니다. FSR 3.1 시절에는 amd_fidelityfx_dx12.dll 하나였는데, SDK 2.0에서 로더 하나와 기능별 DLL로 쪼개졌습니다.

DLL 담당
amd_fidelityfx_loader_dx12.dll 기능 DLL을 찾아 로드
amd_fidelityfx_upscaler_dx12.dll 업스케일링
amd_fidelityfx_framegeneration_dx12.dll 프레임 생성
amd_fidelityfx_denoiser_dx12.dll Ray Regeneration
amd_fidelityfx_radiancecache_dx12.dll Radiance Caching

게임은 쓰는 기능의 DLL만 배포하면 됩니다. 그리고 로더가 중간에 있으므로 드라이버가 다른 구현을 제공할 수 있습니다. 버전은 ffxQueryDescGetVersions로 질의하고, ffxOverrideVersion 구조체를 체인에 붙여 특정 버전을 강제할 수 있습니다.

5-3. 그래서 드라이버 업그레이드가 가능해졌다

이 설계의 대가는 몇 년 뒤에 현금화되었습니다. 게임을 패치하지 않고 업스케일러를 갈아끼우는 것이 가능해졌습니다.

Adrenalin 드라이버에서 FSR 4 토글을 켜고 게임 안에서는 FSR 3.1을 선택하면, 드라이버가 게임의 FSR 3.1 구현을 FSR 4로 대체합니다. AMD가 명시한 조건은 정확히 5-2절의 설계에서 따라 나옵니다.

  • DirectX 12여야 합니다.
  • 게임이 가이드라인에 맞게 서명된 FSR 3.1 DLL을 통합했어야 합니다.
  • Vulkan 타이틀이나 서드파티 플러그인 같은 비표준 경로는 대상이 아닙니다.

게임이 SDK 소스를 자기 바이너리에 컴파일해 넣었다면 드라이버가 끼어들 지점이 없습니다. 서명된 DLL을 로더를 통해 부르고 있어야 교체가 성립합니다. “소스 배포”를 포기한 대가로 “사후 업그레이드 가능성”을 얻은 것이고, 이것이 FSR 3.1이 중요한 이유입니다.

같은 설계가 반대 방향으로도 작동합니다. 개발자는 FSR Upscaling 4를 붙이기만 하면 됩니다.

“AMD FSR Upscaling 4는 AMD Radeon RX 9000 또는 RX 7000 시리즈 이상의 외장 GPU를 요구한다. 다른 하드웨어에서 실행될 때 API는 자동으로 AMD FSR 3.1.5를 선택한다.”

폴백 분기를 직접 쓸 필요가 없습니다. 하나 통합하면 신경망 경로와 해석적 경로가 하드웨어에 따라 갈립니다.

같은 일이 프레임 생성에서도 반복됩니다. FSR 3.1.4는 ffxDispatchDescFrameGenerationPrepareCameraInfo라는 카메라 정보 구조체를 체인에 붙일 수 있게 했고, “3.1.4부터는 최상의 품질을 위해 필수 입력”이라고 못을 박았습니다. 당시에는 왜 갑자기 카메라 파라미터를 요구하는지 알기 어려웠는데, SDK 2.3의 설명이 답입니다.

“이전에 AMD FSR 3.1.4를 통합한 게임은 향후 AMD Software: Adrenalin Edition 릴리스를 통해 ML 기반 AMD FSR Frame Generation 4.0.1로 자동 버전 업그레이드 대상이 된다(RX 9000 시리즈, DX12 전용).”

즉 3.1.4의 그 구조체는 몇 세대 뒤에 올 신경망 프레임 생성이 필요할 입력을 미리 받아 두는 장치였습니다. 앞으로 올 구현이 요구할 데이터를 지금 규약에 넣어 두면, 그 구현이 도착했을 때 게임을 고치지 않아도 됩니다.

이 관점에서 보면 이해가 안 되던 릴리스도 설명됩니다. 해석적 프레임 생성 3.1.6은 화질 변경이 하나도 없고 내용이 전부 API 배관 작업입니다. 컨텍스트 생성 시 버전을 못 박는 구조체(ffxCreateContextDescFrameGenerationVersion)를 추가하고, “향후 ABI 호환성을 보장하기 위해 내부 구현을 수정”한 것이 전부입니다. 기능을 넣은 릴리스가 아니라 교체 가능성을 유지하는 릴리스입니다.

FSR 3.1은 이와 함께 업스케일러와 프레임 생성을 분리했습니다. 그 전까지 둘은 한 덩어리였는데, 분리 이후 FSR 업스케일링에 다른 프레임 생성을 붙이거나 그 반대 조합이 가능해졌습니다.

6. FSR 4 — 신경망으로 갈아타기

FSR 4는 손으로 쓴 누적 규칙을 학습된 모델로 대체했습니다. AMD Instinct GPU 클러스터에서 게임 캡처 데이터로 훈련했고, RDNA 4의 ML 가속 하드웨어에서 추론합니다.

2025년 8월 FidelityFX SDK 2.0과 함께 4.0.2로 공개되었고, 처음에는 RX 9000 시리즈 전용이었습니다. 그러다 FSR Upscaling 4.1.1(FSR SDK 2.3, 2026년 6월)에서 RDNA 3 기반 RX 7000 시리즈로 확장되었습니다. 지금 AMD의 정리는 이렇습니다.

“AMD FSR Upscaling(이전 명칭 AMD FSR 4)은 AMD FSR 2/3/4를 통합하고 필요한 경우 FSR 3.1.5로 폴백하므로, 사실상 AMD FSR 3을 대체한다.”

업스케일러 계보는 3.1.5에서 끝나고 그 뒤는 4.x라는 뜻입니다.

6-1. 입력은 거의 그대로다

좋은 소식은 통합 표면이 FSR 2와 거의 같다는 것입니다.

입력 해상도 포맷 비고
컬러 렌더 앱 지정 리니어 컬러 공간. 불가피하면 FFX_UPSCALE_ENABLE_NON_LINEAR_COLORSPACE
뎁스 렌더 1×float Reversed-Z 권장, 플래그로 알림
모션 벡터 렌더 2×float 아래 규약 참고
노출 1×1 R32_FLOAT FFX_UPSCALE_ENABLE_AUTO_EXPOSURE를 켜면 생략

생성 시 플래그로 렌더러의 규약을 알려줍니다.

플래그 의미
FFX_UPSCALE_ENABLE_DEPTH_INVERTED Reversed-Z (가까울수록 큰 값)
FFX_UPSCALE_ENABLE_DEPTH_INFINITE 무한 원거리 평면
FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE HDR 입력
FFX_UPSCALE_ENABLE_AUTO_EXPOSURE 노출을 스스로 계산
FFX_UPSCALE_ENABLE_DISPLAY_RESOLUTION_MOTION_VECTORS 모션 벡터가 출력 해상도
FFX_UPSCALE_ENABLE_MOTION_VECTORS_JITTER_CANCELLATION 모션 벡터에 지터가 섞여 있으니 빼 달라
FFX_UPSCALE_ENABLE_DEBUG_CHECKING 디스패치마다 입력 검증

모션 벡터 규약은 이렇습니다. 방향은 현재 프레임에서 이전 프레임 위치를 향하고, 단위는 스크린 공간 픽셀로 [-width, -height]에서 [width, height] 범위입니다. <width, height>가 화면 대각선만큼 이동한 경우에 해당합니다. 엔진이 NDC나 UV 단위로 저장한다면 디스패치의 motionVectorScale로 흡수합니다. 부호 반전도 이 스케일에 담을 수 있어서 셰이더를 고칠 필요가 없습니다.

그리고 모션 벡터에는 지터를 적용하지 않습니다. 나머지 렌더 해상도 입력은 모두 지터를 적용한 상태여야 합니다. 엔진이 이미 지터가 섞인 모션 벡터를 만들고 있다면 위의 지터 캔슬레이션 플래그를 켜면 됩니다.

지터는 Halton(2,3)이고 API가 값을 계산해 줍니다. ffxQueryDescUpscaleGetJitterPhaseCount로 시퀀스 길이를 먼저 얻고, ffxQueryDescUpscaleGetJitterOffset으로 프레임별 오프셋을 받습니다.

품질 모드 스케일 지터 위상 수 밉 바이어스
Native AA 1.0× — —
Quality 1.5× 18 −1.58
Balanced 1.7× 23 −1.76
Performance 2.0× 32 −2.00
Ultra Performance 3.0× 72 −2.58

위상 수와 밉 바이어스는 FSR 2와 같은 공식입니다(3-1절). 표의 23이 24가 아닌 이유도 거기서 설명한 버림 때문입니다.

mipBias = log2(렌더 해상도 / 출력 해상도) - 1.0

frameTimeDelta는 밀리초 단위로 넘깁니다(60fps면 약 16.6). 템포럴 자동 노출에 쓰입니다. 노출은 두 값으로 갈립니다. 프리 노출은 입력을 나눠 원래 신호를 복원하는 데 쓰이고, 노출은 그 결과에 곱해집니다. 후자는 앱의 톤매핑과 일치해야 하고, 자동 노출을 켜면 ISO 100 필름 반응에 맞춘 값을 계산합니다.

파이프라인 위치는 다음과 같습니다.

렌더 해상도: 지오메트리/라이팅 → SSR, AO, 디노이저
  → [FSR Upscaling]            ← 리니어 컬러, 뎁스, 모션 벡터, 노출
출력 해상도: 톤매핑, DOF, 모션 블러, 필름 그레인, 색수차, 비네트
  → UI

그리고 별도의 TAA 패스는 없습니다. FSR Upscaling이 TAA를 대체합니다.

6-2. 사라진 마스크

FSR 4에서 가장 흥미로운 변화는 추가된 것이 아니라 없어진 것입니다.

FSR 4는 더 이상 리액티브 마스크나 투명도·합성 마스크를 요구하지 않습니다. 넘길 수는 있습니다.

앞 절에서 봤듯 이 마스크들은 해석적 누적기에게 “이 픽셀은 히스토리를 믿지 마라”를 손으로 알려주는 장치였습니다. 파티클과 알파 블렌딩이 문제가 되는 이유가 모션 벡터에 기록되지 않기 때문이니, 그 사실을 사람이 별도 렌더 타깃에 적어 준 것입니다.

학습된 모델은 그 판단을 입력에서 유추합니다. 여기서 3-4절의 문장이 뒤집힙니다. AMD는 자동 생성 마스크를 두고 “머티리얼에 기반해 만드는 것이 최종 이미지에서 추론하는 것보다 신뢰할 만하다”고 했는데, 신경망은 정확히 그 신뢰하지 못하겠다던 일을 해내면서 마스크를 아예 필요 없게 만들었습니다. 휴리스틱으로는 안 되던 것이 학습으로는 되는 전형적인 사례입니다.

실무적으로는 이게 화질만큼 중요합니다. 마스크 튜닝은 업스케일러 통합에서 가장 지루하고 가장 게임별인 작업이었고, 정답이 없어서 끝을 내기도 어려웠습니다. 그게 없어졌습니다.

버전마다 필요한 리소스가 다를 수 있으니, 무엇이 필수이고 무엇이 옵션인지는 ffxQueryDescUpscaleGetResourceRequirements로 질의하는 것이 맞습니다. 하드코딩하지 않는 편이 안전합니다.

6-3. 대가

FSR 4가 내준 것은 명확합니다.

  • 하드웨어 중립성. RX 7000 이상 AMD 외장 GPU가 필요합니다. FSR 1~3은 경쟁사 GPU에서도 돌았습니다.
  • 소스 공개. 사전 빌드된 서명 DLL로만 배포됩니다. 이전 세대(3.1.5 이하)는 여전히 오픈소스입니다.
  • API 다양성. ML 기능들은 DirectX 12 전용입니다. 셰이더 모델 6.6이 필요하고, 프레임 생성과 Ray Regeneration은 Windows 11을 요구합니다.

세 번째는 실무에서 꽤 아픕니다. Vulkan으로 만든 게임은 드라이버 업그레이드 대상도, 직접 통합 대상도 아닙니다.

7. Redstone — 업스케일링 밖으로

2025년 12월 FSR SDK 2.1과 함께 공개된 Redstone은 신경망을 업스케일링 밖으로 확장한 묶음입니다. FidelityFX에서 FSR로 이름이 통합된 것도 이때입니다. 구성은 네 가지입니다.

구성 하는 일 현재 버전
FSR Upscaling ML 업스케일링 + AA 4.1.1
FSR Frame Generation ML 프레임 보간 4.0.1
FSR Ray Regeneration ML 디노이저 1.2.0
FSR Radiance Caching 온라인 학습 GI 캐시 0.9.0 (기술 프리뷰)

옛 해석적 구현들도 같은 SDK에 남아 폴백을 담당합니다(FSR 2.3.4와 3.1.5, 프레임 생성 3.1.6, 프레임 생성 스왑체인 3.1.7).

7-1. Ray Regeneration — 분리된 디노이저

Ray Regeneration은 레이트레이싱 결과를 정리하는 ML 디노이저입니다. MetalFX의 디노이즈 스케일러나 NVIDIA의 Ray Reconstruction과 목적은 같은데 설계 선택이 다릅니다. 디노이징과 업스케일링을 합치지 않았습니다.

Ray Regeneration은 업스케일링과 독립적으로 디노이징을 수행하는 분리된(decoupled) 디노이저다.

합치면 정보를 더 쓸 수 있어 유리하지만, 디노이즈가 끝난 뒤에 렌더 콘텐츠를 더 얹을 수 없습니다. 분리하면 그 자유가 생깁니다. 권장 순서는 렌더 해상도에서 노이지한 신호를 그리고 → Ray Regeneration으로 디노이즈하고 → FSR Upscaling으로 확대하는 것입니다.

통합의 핵심은 신호를 쪼개서 넘기는 것입니다. 컨텍스트 생성 시 signalFlags로 어떤 신호를 디노이즈할지 고릅니다.

신호 플래그 내용
FFX_DENOISER_SIGNAL_DIRECT_DIFFUSE 직접광 디퓨즈 래디언스
FFX_DENOISER_SIGNAL_DIRECT_SPECULAR 직접광 스페큘러 래디언스
FFX_DENOISER_SIGNAL_INDIRECT_DIFFUSE 간접광 디퓨즈
FFX_DENOISER_SIGNAL_INDIRECT_SPECULAR 간접광 스페큘러
FFX_DENOISER_SIGNAL_DOMINANT_LIGHT_VISIBILITY 주 광원 가시성
FFX_DENOISER_SIGNAL_AMBIENT_OCCLUSION AO (1.2.0에서 추가)
FFX_DENOISER_SIGNAL_SPECULAR_OCCLUSION 스페큘러 오클루전 (1.2.0에서 추가)

앞의 다섯 개 중 최소 하나는 켜야 합니다. 신호를 쪼개는 이유는 디퓨즈와 스페큘러의 통계적 성질이 다르기 때문입니다. 스페큘러는 러프니스에 따라 로브가 좁아지고 히트 거리에 따라 필터 폭이 달라져야 하는데, 합쳐 놓으면 그 구분이 사라집니다. 그래서 래디언스 신호는 RGBA16_FLOAT로 넘기고 알파 채널에 레이 히트 거리를 담습니다.

여기에 노이즈 없는 가이드 버퍼들이 붙습니다.

리소스 포맷 내용
리니어 뎁스 R32_FLOAT 뷰 공간 z (부호 있음)
모션 벡터 RGBA16_FLOAT xy = UV 공간 스크린 모션, z = 뎁스 변화량
노멀 RGB10A2_UNORM RG = 팔면체 인코딩 노멀, B = 리니어 러프니스, A = 머티리얼 종류(0~3)
디퓨즈 알베도 RGBA8_UNORM 예: BaseColor × (1 − Metalness)
스페큘러 알베도 RGBA8_UNORM BRDF LUT 샘플링이나 런타임 근사로 생성

노멀 텍스처의 알파에 들어가는 머티리얼 종류 ID가 재미있는 부분입니다. 서로 다른 표면끼리 블렌딩되어 생기는 고스팅을 막는 용도입니다. 디노이저 입장에서 “이 두 픽셀은 섞어도 되는 같은 종류인가”를 판단할 근거를 주는 것입니다.

체커보드 모드도 있습니다. 프레임마다 픽셀을 번갈아 추적하고, 디노이저가 뎁스·노멀 기반 바이래터럴 필터로 전체 해상도를 복원합니다. 반쪽 레이 수로 돌리는 셈입니다.

옵션 값은 ffxConfigure로 조정합니다. 대표적으로 FFX_API_CONFIGURE_DENOISER_KEY_STABILITY_BIAS(안정성 대 반응성), ..._CROSS_BILATERAL_NORMAL_STRENGTH(노멀 기반 에지 보존), ..._MAX_RADIANCE(클리핑), ..._DISOCCLUSION_THRESHOLD가 있습니다. 카메라 컷에서는 FFX_DENOISER_DISPATCH_RESET으로 히스토리를 버려야 합니다. 업스케일러의 히스토리 리셋과 같은 이야기입니다.

문서가 못을 박아 두는 문장이 있습니다.

“최상의 결과를 위해 고품질 입력이 필수적이다. 이 알고리즘은 입력 데이터에 이미 존재하는 아티팩트를 ‘교정’하도록 설계되지 않았다.”

그래서 조언은 샘플링 쪽으로 갑니다. 좋은 해시 함수를 쓰고 블루 노이즈의 상관 아티팩트를 조심하라는 것입니다. 디노이저는 노이즈를 지우지만 상관된 패턴은 지우지 못합니다.

비용 감각을 위해 AMD가 공개한 수치를 옮기면, RX 9070 XT에서 1920×1080, 신호 4개 + 주 광원 기준으로 약 4.09ms이고 GPU 메모리는 362MB(상주 163MB, 에일리어싱 가능 199MB)입니다. 가벼운 패스가 아닙니다. 레이를 줄여 버는 시간과 비교해서 판단해야 합니다.

7-2. Radiance Caching — 실시간으로 학습하는 GI 캐시

Radiance Caching은 몬테카를로 패스 트레이서와 함께 도는 조명 캐시입니다. 핵심은 신경망을 미리 굽지 않는다는 점입니다.

패스 트레이서가 만들어 내는 데이터로 런타임에 GPU에서 계속 학습하는 온라인 모델이다. 사전 계산이나 베이킹이 필요 없다.

2차 이후 바운스를 실제 레이로 추적하는 대신 학습된 예측으로 대체합니다. 그러면 경로 길이가 짧아지고, 남는 몬테카를로 노이즈도 줄어들어 뒤따르는 디노이저와 업스케일러가 일하기 쉬워집니다. 글로시 표면이나 레이어드 BSDF처럼 복잡한 신호를 신경망으로 표현하기 쉽다는 점을 이용합니다.

아직 기술 프리뷰(0.9.0) 입니다. RDNA 4와 Adrenalin 25.12.1 이상이 필요하고 프로덕션에 넣을 단계는 아닙니다. Ray Regeneration 문서에도 “Radiance Caching은 완성도에 도움이 되지만 저주파 노이즈나 시간적 불안정성을 감시하라. 캐시의 아티팩트가 증폭될 수 있다”는 경고가 달려 있습니다.

7-3. ML 프레임 생성

프레임 생성도 신경망으로 교체되었습니다. 옵티컬 플로우 추정과 모션 벡터를 함께 쓰는 구조는 유지하면서, 픽셀별 움직임과 외형을 예측하는 단계가 신경망으로 바뀌었습니다. AMD의 설명은 해석적 보간에서 흔한 고스팅과 모션 불연속이 크게 줄었다는 것입니다.

통합에서 새로 주의할 점은 카메라 파라미터입니다. 위치와 up·right·forward 벡터를 정확히 넘겨야 하고, 문서는 “이 필드를 지정하지 않거나 부정확하거나 정밀도가 낮은 값을 넘기면 잘못된 렌더링이 발생할 수 있다”고 명시합니다. 스왑체인과 페이싱 요구사항은 그대로입니다. 최선의 페이싱을 위해 스왑체인에 대한 간섭 없는 접근이 필요하고, DXGI 호출을 후킹하는 서드파티 오버레이가 있으면 문제가 생길 수 있습니다.

8. 지금 통합한다면

정리 삼아 판단 기준을 적어 둡니다.

FSR Upscaling 4 (현행) — DirectX 12 렌더러이고 모션 벡터·뎁스·지터를 낼 수 있으면 이것입니다. 통합 하나로 AMD 하드웨어에서는 신경망 경로가, 나머지에서는 FSR 3.1.5가 자동으로 선택됩니다. 리액티브 마스크 작업이 필요 없어서 예전보다 훨씬 쌉니다.

FidelityFX Super Resolution 3.1.5 / 2.3.4 (해석적) — Vulkan을 써야 하거나, 소스가 필요하거나, 오래된 하드웨어까지 폭넓게 지원해야 하면 이쪽입니다. 오픈소스이고 셰이더 모델 6.2로 RX 500 시리즈까지 내려갑니다.

FSR 1 — 모션 벡터를 낼 수 없는 렌더러(2D, UI 위주, 비디오, 간단한 3D)이거나, 이미 만족스러운 AA가 있고 마지막에 크기만 키우고 싶을 때입니다. 여전히 유효한 선택입니다.

프레임 생성 — 업스케일링을 붙인 다음에 고민할 문제입니다. 스왑체인 통제권과 UI 처리 설계가 따라오므로 별도 작업으로 잡는 편이 정확합니다.

그리고 틀리기 쉬운 지점들입니다.

  • 모션 벡터를 안 쓰는 것들. 문서가 반복하는 조언은 “불투명, 알파 테스트, 알파 블렌딩 오브젝트 모두가 커버하는 모든 픽셀에 모션 벡터를 써라”입니다. UV 스크롤 같은 정점 셰이더 효과도 계산에 반영해야 합니다. 모션 벡터를 내는 오브젝트가 많을수록 품질이 올라갑니다.
  • 지터 적용 범위. 렌더 해상도로 그리는 모든 것(불투명, 반투명, 레이트레이싱)에 서브픽셀 지터를 적용해야 합니다. 모션 벡터만 예외입니다. 그리고 지터 오프셋으로 (0,0)을 넘기면 안 됩니다.
  • 밉 바이어스를 잊는 것. 이걸 빼면 업스케일링을 켰는데 텍스처가 흐려집니다. 업스케일러 탓으로 오해하기 쉬운 대표적인 증상입니다.
  • 파이프라인 위치. 톤매핑, DOF, 모션 블러, 필름 그레인, 색수차, 비네트는 업스케일 뒤입니다. 앞에 두면 히스토리 누적과 에지 판정을 망칩니다. 반대로 뎁스를 쓰는 후처리(SSR, AO, 디노이저)는 업스케일 앞에 남아야 합니다.
  • 버전 하드코딩. 필요한 리소스와 지터 값은 질의로 받는 게 맞습니다. 드라이버가 구현을 바꿀 수 있는 구조라는 점을 기억해야 합니다.
  • Vulkan 계획. ML 기능이 DirectX 12 전용이라는 제약은 지금도 유효합니다. 크로스 플랫폼 계획이 있으면 초기에 정해야 합니다.

마치며

FSR을 처음 볼 때 헷갈리는 이유는 이름 하나가 서로 다른 여섯 개의 결정을 덮고 있기 때문입니다. 세대별로 무엇을 내주고 무엇을 얻었는지로 보면 정리가 됩니다.

기술적으로 가장 배울 만한 지점은 화질이 아니라 FSR 3.1의 API 설계라고 생각합니다. 함수를 다섯 개로 고정하고 인자를 pNext 체인으로 옮기고, 소스 배포를 서명 DLL과 로더로 바꾼 결정이 몇 년 뒤 “출시된 게임을 패치하지 않고 업스케일러를 교체한다”는 결과를 만들었습니다. 기능 하나를 추가한 변경이 아니라 앞으로 기능을 추가할 수 있는 여지를 만든 변경이었습니다.

업스케일러의 세대 교체는 알고리즘 문제이기 전에 배포 문제다. 게임 바이너리에 컴파일해 넣은 개선은 전달되지 않고, ABI 뒤에 숨긴 개선은 드라이버가 배달한다.

참고 자료

현행 SDK와 API

세대별 기술 문서

릴리스와 배경