Files
psxrecomp/docs/POST_EFFECTS.md
T
Shokunin 74dadc6004 GL: one post-effect pipeline; bloom and FXAA as built-in chains, user chains via [video] post_shader
Post effects now run as chains of full-screen fragment passes on one
executor (gl_postfx.inc), as asked in review. A chain is a small .pfx preset
(postfx_chain.c, GL-free): passes with a shader (a file next to the preset or
builtin:<name>), inputs (source, the optional primitive-derived mask,
previous or a named earlier pass), a scale, format, filter and uniforms.
The executor owns target allocation and lifetime, program build/cache,
uniform binding (u_rcp, u_size, u_org) and GL state save/restore.

Stages, in order, each from a fresh copy of the image: the built-in "bloom"
chain ([video] bloom), a user chain ([video] post_shader / PSX_POST_SHADER,
elective, off by default), and the built-in "fxaa"/"fxaa_hq" chain
([video] antialiasing_mode). FXAA folds in as a one-pass chain and its
output is byte-identical to the former fixed pass; it still runs on real
frames only. Generated (Smooth motion) frames run bloom and the user chain.

The "may glow" mask is an optional pipeline input: draws keep stencil bit 1
only while an active chain reads `mask`, and the window-sized mask texture is
built once per present for the chains that use it.

Tests: postfx_chain_test (format, built-ins, errors, file import, no GL);
gl_bloom_test gains an imported two-pass chain (byte-exact invert), the mask
input (HUD unlit, 3D lit), a bad preset, a pass that does not compile, and GL
state restored with bloom + a mask chain + FXAA on, all on the caller thread
and the render thread. docs/POST_EFFECTS.md documents the format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 02:15:46 -07:00

3.6 KiB

Post effects

Elective effects on the composed game image (OpenGL). All of them are off by default. With none active, nothing below runs and the frame is the stock one.

Every effect is a chain of full-screen fragment passes run by one executor (runtime/src/gl_postfx.inc). Three stages run in order, each from a fresh copy of the image:

stage switch chain
bloom [video] bloom = 0..2 (PSX_BLOOM) built-in bloom
user [video] post_shader = "<file>.pfx" (PSX_POST_SHADER) your preset
AA [video] antialiasing_mode = "fxaa" | "fxaa_hq" built-in fxaa / fxaa_hq

They run on whichever thread presents (the render thread included), before hold-last capture, screenshots and the OSD. Smooth motion's generated frames run bloom and the user chain; AA stays on real frames, as before. The executor owns targets, programs, uniforms and GL state, and puts back everything it touches.

Preset format (.pfx)

pfx 1                              # required first line
pass <name>                        # the last pass writes the window
  shader <file.glsl|builtin:name>  # file paths resolve next to the preset
  input <sampler> <source|mask|previous|<earlier pass>>
  scale <f>                        # target size as a fraction of the game rect (default 1)
  format rgba8|rgba16f             # default rgba8
  filter linear|nearest            # how later passes sample this target
  uniform <name> <f> [f [f [f]]]   # float .. vec4

A shader file is a GLSL 330 fragment body. It is appended to this header:

in vec2 v_uv; out vec4 o;      // v_uv: 0..1 over the game rect, bottom-up
uniform vec2 u_rcp, u_size;    // this pass's target: 1/size and size in px
uniform ivec2 u_org;           // window origin of the game rect
uniform sampler2D <sampler>;   // one per input

Built-in shaders: blur (9-tap Gaussian along u_dir, in px), fxaa, bloom_bright and bloom_composite. The built-in presets are listed in runtime/src/postfx_chain.c. A preset that fails to parse, or a pass that fails to compile, is logged and its stage skipped.

Example: invert the image.

pfx 1
pass invert
  shader invert.glsl
  input u_tex source

with invert.glsl containing:

void main(){ o = vec4(1.0 - texelFetch(u_tex, ivec2(gl_FragCoord.xy) - u_org, 0).rgb, 1.0); }

The mask input

mask is 1 where the last thing drawn was a 3D primitive (proven from GTE data, so it needs PGXP geometry) or an additive semi-transparent primitive (blend modes 1 and 3). It is 0 where an opaque 2D primitive drew last, such as a sprite, a fill or the HUD. Only while an active chain reads mask does every draw keep that mark in stencil bit 1 (bit 0 is the PS1 mask-bit mirror). The mark follows the picture through native-wide centre copies and Smooth motion surfaces. At the present it becomes a window-sized R8 texture. A chain that does not read mask costs none of this.

Bloom uses the mask at both ends: only lit pixels feed its bright pass, and only lit pixels take the glow back. HUD pixels are therefore never changed, even next to a lamp.

Tests

  • postfx_chain_test: the format, built-in presets, errors and file import.
  • gl_bloom_test: bloom on 2D, additive and 3D primitives, plus a HUD beside and over a lamp. Also an imported chain (byte-exact invert), the mask input (the HUD unlit, the 3D scene lit), a bad preset, a pass that does not compile, and GL state restored with every stage on. It runs on the caller thread and on the render thread.
  • gl_post_aa_test: FXAA on the pipeline. Its output is byte-identical to the former fixed pass.