Files
Swordpdf--ps5-framegen/docs/algorithm.md
T
swordpdfandClaude Opus 5.5 3cdea16128 Multi-frame generation: 2 or 3 frames between two of the game's (AI-assisted)
set_generated(n) and interpolate_next(cmd, i): the interpolation at any
moment t (the previous frame at x + 2t v, the current one at x - 2(1 - t) v,
the blend weighted to the nearer frame), the first frame of a group keeping
the still history and the quads' motions; fg-test --gen N; the example and
the docs (the multiplier, its input lag, the swapchain's images).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaNprJCmpBhrkpzRHVPLXM
2026-10-07 07:57:06 +03:00

132 lines
8.6 KiB
Markdown

# The algorithm
A generated frame is the picture at the moment halfway between two frames of the game: the previous one (A) and the current one (B).
For a pixel `x` of it and a motion `v` (half of the way the content moves from A to B), the content at `x` is where A shows it at
`x + v` and where B shows it at `x - v`. If `v` is right, those two samples agree, and their blend is the pixel. Everything below is
about finding a `v` that is right, and about what to show where there is none.
All of it runs in three groups of compute passes a frame, recorded by `interpolator::record()` (`src/ps5_framegen.cpp`).
## 1. Optical flow (FSR3's, unchanged)
The passes `of_*`: the luma of each frame, a 7-level pyramid of it, a scene change detection from luma histograms, and a block search
from the coarsest level down with a filter and an upscale at each level (FSR3's "v5" optical flow). The result is one motion per 8x8
block of B, in whole pixels, pointing from B to A.
Two changes, both in the callback headers:
- **Scene changes:** FSR3 counted the first 5 frames after a reset, and the 4 frames after a detected scene change, as scene
changes, with no flow and no interpolation. Now only the frame of the change itself (and the first frame after a reset) is.
The motion comes back a frame after a cut instead of four, so there is no judder after every cut.
- **Portability:** the search's reductions run in shared memory (`FFX_FG_PORTABLE_WAVE`), not in wave operations that assume
32 or 64 lanes. Software devices have 4.
## 2. The vector field (FSR3's)
`fi_setup` and `fi_vector_field` push each block's motion, halved, to where the block is halfway. Where two blocks land on the same
place, the faster one wins. FSR3 keeps this field to sample a motion at any pixel. It is one candidate here among several.
## 3. The interpolation (`computeFrameinterpolationQuad`, one 2x2 quad a thread)
### Candidates
| Index | Candidate | Bias | Why |
|---|---|---|---|
| (zero) | no motion | 0.008 | the HUD, text, still scenery, a character the camera follows |
| 0 | the vector field, blended | 0 | FSR3's own answer |
| 1-4 | the field's 4 nearest cells, one by one | 0.004 | a blend of two motions at an edge is neither |
| 5 | the optical flow of the quad's block | 0.004 | the field may have put another block's motion here |
| 6-9 | the optical flow of the 4 blocks around it | 0.006 | at the edge of something moving, its own motion and the background's are both nearby |
| 10 | the quad's motion in the last generated frame | 0.004 | motion goes on; the choice stays the same from frame to frame |
| 11 | the quad's best one, refined | as its source | sub-pixel precision, see below |
A motion found twice, within a quarter of a pixel, is tried once. A motion under half a pixel counts as no motion.
### Score
For each candidate and pixel, the error is the sum of the three channels' differences between the two samples, from 0 to 3. The
score adds half of the quad's mean error, so that the quad decides together where its pixels cannot tell, and the candidate's bias.
Ties go to motion: a moving pattern can match "no motion" for a frame or two.
Penalties added to the bias:
- **Consistency (`FgUnlike`, 0.12):** what the candidate takes from B at `x - v`, and from A at `x + v`, should have moved by
`2v` according to the optical flow there. The most alike of the 2x2 nearest blocks counts, which is lenient where two motions
meet. Tolerance: 3 px plus 15% of the motion.
- The reason: two dark samples agree just as well as the right ones. In GTA IV a pixel of Niko's ear took the background's
motion from the block beside it, because A's dark wall and B's dark hair agreed better than the ear under the head's motion
(the ear's shading changes as the head moves). The ear came out with a black speck.
- **Off screen (`FG_BORDER`, 0.06):** when one of the two samples falls off the screen, there is nothing to compare, so the
sample on the screen is used alone at a middling score.
- This is the strip that comes in or goes out at the edge of the screen as the camera turns. Before, that strip filled with
whatever else happened to agree.
### Sub-pixel refinement
The flow is in whole pixels, so half vectors come in steps of half a pixel. Along a sharp edge moving a fraction of a pixel, the two
samples miss each other by up to a quarter of a pixel each, and the graffiti on GTA IV's walls shimmered along every line.
The quad's best candidate is tried half a pixel to each side on each axis. A parabola through the three errors gives the best
fraction, and that motion becomes one more candidate. Quads that already agree within 0.03 are left as they are.
### Choosing, per pixel
- **The best candidate:** each pixel takes its own best (the lowest score). If it agrees (error under `FG_MATCH`, 0.12), the pixel
is the blend of its two samples.
- **Own match (m73f):** the quad's choice may not agree at one of its pixels while another motion does, at that pixel alone. That
pixel belongs to what that motion moves, for example an ear in a quad that is mostly background, so it takes that motion. It is
not an edge.
- **Edges:** where nothing agrees, something in front covers or uncovers what moves behind it. Of the best moving candidate's two
samples, the one that another motion explains belongs to what is in front, so the other one is shown. "Another motion" means no
motion, or the best moving candidate that differs by more than 2 px. When neither or both are explained, the pixel is the blend
(FSR3's own fallback). A soft ramp between `FG_MATCH` and `FG_MATCH + FG_MATCH_SOFT` keeps this from flickering.
- **Still history:** a pixel that has not changed for 8 frames in a row (`rw_static_history`) shows no motion, whatever the
candidates say.
- The reason: inside a thin letter of a subtitle over a moving background, a moving candidate's two samples both land on the
background and agree just as well. A single pair of frames cannot tell that from a pattern moving by its own period (a fence,
bricks). The history can, because that coincidence does not last.
- The history is cleared when the interpolator starts. Before m74 it started with whatever the memory held, and random pixels
of moving things were held still.
### Other moments than halfway
With several frames generated (`set_generated`), the frame at moment `t` (0 is the previous frame, 1 the current one) takes A at
`x + 2t v` and B at `x - 2(1 - t) v`. At `t = 0.5` that is exactly `x + v` and `x - v`. The blend weighs the nearer frame more. The
vector field stays FSR3's halfway one for every moment: its candidates are a little off away from the middle, and the other
candidates are not tied to a moment. The still history and the quads' motions are updated by the first frame of the group only.
### Debug view
With `dispatchFlags` bit 2 (`set_debug_view(true)`), the output shows the decisions instead of the picture:
| Colour | Decision |
|---|---|
| grey | no motion |
| white | still by the history |
| green | a motion agreed |
| red / blue | an edge where A's / B's sample was kept |
| magenta | an edge, blended |
Built with `-DFG_DEBUG_VECTORS`, it shows the chosen motion in red and green and the candidate index in blue, for scripts.
## What was tried and dropped
| Tried | Result |
|---|---|
| A median of the edge pixels (m73f's cleanup pass) | The specks it was for were not edge pixels; it changed little else and cost a pass |
| A local search for quads where nothing agrees (for hands swinging as a character walks) | Spurious matches elsewhere made the whole frame worse, and the hands, which also rotate, were not recovered |
| Skipping the candidates for quads that did not change | A pattern moving by its own period looks unchanged too: the moving checkerboard of the "all" scene came out three times worse |
| Stopping after the vector field when it agrees | Cost precision, saved little |
They are still in the source behind test defines (`FG_QUIET_SKIP`, `FG_FIELD_EARLY`), or described in the history of RPCS3-PS5.
## Known limits
- **Small fast things that turn as they move** (hands, a thrown object): the flow's 8x8 blocks follow what is around them, and no
single motion fits a rotating hand. They come out fragmented or faded in the generated frame. The real frames before and after
are untouched, so at 60 fps or more it is hard to see.
- **What a camera turn uncovers at the screen's edge:** the flow has nothing there (the content was off screen in A). The on-screen
sample is used when the right motion is among the candidates.
- **Without depth there is no order between two motions:** where two layers move differently, the edge test guesses which one is in
front from which sample another motion explains.