Files
psxrecomp/runtime/include/postfx_chain.h
T
Shokunin 8dbe2165ff bloom: cost of real and generated frames
Smooth motion lost most of its generated frames with bloom on (R4 race,
120 Hz: ~30% of bloom-off). Measured and fixed in order of cost:

- Depth-stencil blits (CPU milliseconds each on macOS) are gone:
  generation surfaces no longer copy the marks and are not marked while
  drawn (generated frames reuse the newer real frame's mask); the
  native-wide centre copy is colour only again and the mask takes the
  centre's marks from the hr surface; the mask itself is resolved in the
  presented surface's own pixels through an R8 target that shares the
  surface's depth-stencil renderbuffer, then mapped to the window.
- Marking needs one pass per batch, not two: 3D batches only set, 2D
  opaque ones only clear, semi-transparent ones only set; only batches of
  unknown make (queued replays, lines) still do both.
- No full-size copy of the image: `source_down` (a blit straight to the
  pass's size) feeds the bright pass, and the composite screen-blends onto
  the window (`blend screen`; unlit pixels are discarded, so they keep their
  exact value). The debug split/bloom-only views go with the copy.

M4, R4 race, render thread + reprojection, PSX_FRAME_GEN_FORCE, a host
loaded by other work (absolute rates low): bloom pass on real / generated
frames 1.2 / 0.7 ms at 1080p, 1.3 / 0.9 ms at 1440p, 2.6 / 1.6 ms at 4K
(glFinish-bracketed); generated frames with bloom 79-80% of bloom-off at
all three (was ~30%). A 1/8-resolution blur chain was tried and kept no
more frames, so the 1/4 chain (the look) stays.

Tests: parser (blend, source_down, bloom preset reads no full copy), GL
fixture (source_down + blend screen chain exact; HUD and mask checks
unchanged), FXAA unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 04:03:35 -07:00

97 lines
4.4 KiB
C

/* postfx_chain.h — post-processing pass chains (docs/POST_EFFECTS.md).
*
* A chain is a list of full-screen fragment passes run on the composed game
* image by the OpenGL executor (gl_postfx.inc). This header is the GL-free
* part: the preset format and its parser, so it can be unit tested alone.
*
* Preset (.pfx) format, one directive per line, '#' comments:
*
* pfx 1 # format version (required first)
* pass <name> # starts a pass; the last pass writes the window
* shader <file.glsl|builtin:name> # fragment body (see below)
* input <sampler> <source|source_down|mask|previous|<pass name>>
* # source_down: the image scaled to this pass's
* # target (one bilinear blit, no full-size copy)
* scale <f> # target size, fraction of the game rect (default 1)
* format rgba8|rgba16f # target format (default rgba8)
* filter linear|nearest # how later passes sample this target (default linear)
* uniform <name> <f> [f [f [f]]] # float, vec2, vec3 or vec4
* blend replace|screen # how the window pass lands (default replace):
* # screen = o + dst * (1 - o); discard leaves dst
* generated reuse|run # Smooth motion's generated frames: reuse this
* # pass's target from the last real frame (not
* # for the last pass), or run it (default)
*
* A shader body is GLSL 330 appended to a header that declares
* in vec2 v_uv; out vec4 o;
* uniform vec2 u_rcp; // 1 / this pass's target size in px
* uniform vec2 u_size; // this pass's target size in px
* uniform ivec2 u_org; // window origin of the game rect (gl_FragCoord - u_org = px in it)
* plus one `uniform sampler2D <sampler>;` per input. v_uv is 0..1 over the
* game rect, bottom-up like the window. */
#ifndef PSX_POSTFX_CHAIN_H
#define PSX_POSTFX_CHAIN_H
#ifdef __cplusplus
extern "C" {
#endif
enum { PFX_MAX_PASSES = 16, PFX_MAX_INPUTS = 4, PFX_MAX_UNIFORMS = 8, PFX_NAME = 32 };
/* Input sources; a value >= 0 is an earlier pass's index. */
enum { PFX_IN_SOURCE = -1, PFX_IN_MASK = -2, PFX_IN_SOURCE_DOWN = -3 };
enum { PFX_BLEND_REPLACE = 0, PFX_BLEND_SCREEN = 1 };
enum { PFX_FMT_RGBA8 = 0, PFX_FMT_RGBA16F = 1 };
typedef struct {
char sampler[PFX_NAME];
int from; /* PFX_IN_* or an earlier pass index */
} PfxInput;
typedef struct {
char name[PFX_NAME];
int count; /* 1..4 */
float v[4];
} PfxUniform;
typedef struct {
char name[PFX_NAME];
char *shader; /* fragment body (owned) */
float scale;
int format, nearest;
int reuse_generated; /* generated frames reuse the last real frame's target */
int blend; /* PFX_BLEND_*: the window pass only */
int ninputs, nuniforms;
PfxInput in[PFX_MAX_INPUTS];
PfxUniform u[PFX_MAX_UNIFORMS];
} PfxPass;
typedef struct {
char name[PFX_NAME];
int npasses;
int uses_mask; /* some pass reads `mask` */
PfxPass pass[PFX_MAX_PASSES];
} PfxChain;
/* Resolves `shader` arguments: builtin:<name> and file paths (relative to the
* preset). Returns a malloc'd body or NULL (err filled). */
typedef char *(*PfxShaderLoader)(const char *arg, void *user, char *err, int errlen);
/* Parse preset text into *out (zeroed first). Returns 1, or 0 with a
* "line N: ..." message in err. */
int pfx_chain_parse(const char *text, const char *name, PfxShaderLoader load, void *user,
PfxChain *out, char *err, int errlen);
/* Load a preset file; shader files resolve relative to its directory. */
int pfx_chain_load_file(const char *path, PfxChain *out, char *err, int errlen);
/* A built-in preset ("bloom", "fxaa", "fxaa_hq"); 0 if unknown. */
int pfx_chain_builtin(const char *name, PfxChain *out, char *err, int errlen);
/* A built-in shader body by name, or NULL. */
const char *pfx_builtin_shader(const char *name);
/* Set every pass's uniform `name` (count values); returns how many were set. */
int pfx_chain_set_uniform(PfxChain *c, const char *name, int count, const float *v);
void pfx_chain_free(PfxChain *c);
#ifdef __cplusplus
}
#endif
#endif