GL: opt-in bloom ([video] bloom), sourced from 3D and additive primitives only

[video] bloom = 0.0 (off, default) .. 2.0 adds a glow around lights on the
OpenGL backend. PSX_BLOOM overrides it for one run; {"cmd":"bloom"} reads or
changes strength, threshold and a debug view live. It is a plain [video] key,
so [quality.*] presets can switch it off on low-end machines.

The HUD never glows: with bloom on every textured and untextured draw is
followed by two stencil-only passes of the same vertices that keep a
"may glow" mark in stencil bit 1 (bit 0 stays the mask-bit mirror). Opaque
primitives clear it under their coverage, then PGXP-proven 3D primitives and
additive semi-transparent texels/primitives (modes 1 and 3) set it, painter
ordered like the colour; 3D and non-3D primitives never share a batch while
bloom is on. Semi-transparent primitives leave the mark alone and the PGXP
depth-clear pass (colour writes off) marks nothing. Without PGXP geometry only
additive primitives glow.

The pass runs on the composed game image before AA and the OSD: the presented
surface's marks are blitted into a quarter-resolution stencil, a soft-knee
bright pass runs under it, three separable Gaussian iterations spread it and a
screen blend adds it back. The marks follow the picture: native-wide centre
copies and Smooth motion surfaces (redraw and reprojection) copy stencil with
colour, and generated frames get the same pass. Off: nothing runs, no state
changes, and the composed frame is byte-identical to master.

test_gl_bloom (gl_bloom_test): off is byte-identical before/after bloom and
with --dump against master; on, a bright 2D panel and a panel over a lit 3D
polygon are byte-identical to off, an additive and a 3D polygon glow; guest
VRAM is unchanged; on this thread and on the render thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-11 22:43:43 -07:00
parent 7b3b1ea286
commit e730e03a8a
12 changed files with 842 additions and 48 deletions
+5 -2
View File
@@ -62,6 +62,7 @@ Columns: **N** = native, **D** = DuckStation oracle.
| `present_shot_seq` | ✓ | | — | Completion counter for `present_shot`, plus `wrote` (1 = that completion produced a PNG). Sample before staging, poll until `seq` moves. Advances on success *and* failure, so the poll always terminates |
| `fmv_chroma` | ✓ | | `on` (optional: 0/1) | Read or set `[video] fmv_chroma_smoothing` live. Replies `on` and `frames` (24-bit presents smoothed so far) |
| `texture_lod` | ✓ | | `mode` (optional: 0 off, 1 mipmap), `aniso` (optional: 1..16) | Read or set `[video] texture_lod` / `anisotropic_filtering` live. Replies `mode`, `aniso` |
| `bloom` | ✓ | | `strength` (0..2), `threshold`, `view` (0 normal, 1 bloom only, 2 split, 3 sources), `time` (0/1), all optional | Read or set bloom live (`[video] bloom`). Replies the settings, `passes` and, while `time` is on, `us_mean`: the mean `glFinish`-fenced pass time over `timed` passes since the last query |
| `post_aa` | ✓ | | `mode` (optional: 0 off, 1 fxaa, 2 fxaa_hq) | Read or set post-process anti-aliasing live (`[video] antialiasing_mode`). Replies `mode`, `passes` (filtered presents so far) and `gpu_us`: the mean pass time since the last query when the run has `PSX_POST_AA_TIME=N` (N passes per present between `glFinish` fences, a measurement mode), else 0 |
| `hd_textures` | ✓ | | optional `replacements`, `dump` (0 or 1), `reload` (1) | HD pack root, switches, backend support, replacement count, matched/ready/applied draw counts, queued/pending dumps, composition hits/builds/pixels, GL uploads/evictions, decoded images/evictions/bytes. Optional controls update the configured host pack on the emulation thread; no configured root is an error. Reload retains unchanged upload identities. OpenGL displays replacements; software/Vulkan retain original artwork. Pair with `present_shot` for visual evidence and native VRAM probes for architectural data |
| `gl_interp` | ✓ | | — | OpenGL frame-rate presenter ([FRAME_RATE.md](FRAME_RATE.md)): enabled/suspended, host and target Hz, swaps, `source` (`vblank`/`flip`), `flip_period`, `captures` (new source frames) and `duplicates` (VBlanks that re-presented the same frame) |
@@ -491,9 +492,9 @@ between Play and TCP availability.
## Complete command index (generated)
**369 commands registered** — 356 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
**371 commands registered** — 358 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
82 of 369 have prose above; **287 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
83 of 371 have prose above; **288 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
@@ -509,6 +510,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `batch_sites` | ✓ | | ✓ |
| `bios_info` | ✓ | | ✓ |
| `bioscall_dump` | ✓ | | |
| `bloom` | ✓ | | ✓ |
| `c0_history` | ✓ | | |
| `call_focus_dump` | ✓ | | |
| `call_focus_reset` | ✓ | | |
@@ -654,6 +656,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `history` | ✓ | ✓ | ✓ |
| `hle_dump` | ✓ | | ✓ |
| `host_launch_timings` | ✓ | | ✓ |
| `host_osd_shot` | ✓ | | |
| `host_profile` | ✓ | | |
| `idle_skip` | ✓ | | |
| `imask_trace` | ✓ | | |
+26
View File
@@ -604,6 +604,32 @@ the pixels to show it. Native keeps the window exactly as before.
downsamples to the window. It accepts 1 to 32 in both `game.toml` and the
player's `settings.toml`; 1 (the default) is native and unchanged.
### Bloom (`bloom`)
Opt-in glow around lights, OpenGL only. Off (0) changes nothing.
```toml
[video]
bloom = 1.0 # 0.0 (off, default) .. 2.0 strength
```
Only what 3D primitives (proven from GTE data, so it needs PGXP geometry) and
additive semi-transparent primitives (blend modes 1 and 3) drew can glow: each
draw marks its pixels in a spare stencil bit, and a sprite, fill or HUD element
drawn over them clears the mark. The marks gate both ends of the pass: only
marked pixels feed the bright pass, and only marked pixels take the glow back,
so a HUD, sprite or fill pixel is never changed, even right next to a lamp
(the halo spreads over 3D surroundings only). The pass runs on the composed
game image like `antialiasing_mode` (before it, sharing its copy texture and
full-screen pass): a quarter-resolution bright pass, three Gaussian iterations
and a screen blend.
It follows 4:3, native-wide, supersample, dynamic resolution, the render
thread and Smooth motion (generated frames carry the marks of the frame they
are drawn from). Without PGXP geometry only additive primitives glow.
`PSX_BLOOM=<0..2>` overrides it for one run and the `bloom` TCP command reads
or changes it live. It is a plain `[video]` key, so a `[quality.*]` preset can
turn it off on low-end machines.
### Anti-aliasing (`antialiasing_mode`, `supersample`)
Both are opt-in and change nothing unless set.
+1 -1
View File
@@ -1 +1 @@
d0e31158eda04d03cc0da52b06f7dd9c8d142f24b6e5c4aeb1442683eeb90379
c85714906d3fdc85c7dca4d04daa574a029e4d5c7b0f273e19c25e5633889916
+9
View File
@@ -762,6 +762,15 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
else throw std::runtime_error(fmt::format(
"[video] antialiasing_mode must be \"off\", \"fxaa\" or \"fxaa_hq\": {}", mode));
}
// bloom: opt-in glow (OpenGL), 0.0 (off, default) .. 2.0.
if (video.contains("bloom")) {
const auto& v = toml::find(video, "bloom");
const double f = v.is_integer() ? static_cast<double>(v.as_integer())
: toml::get<double>(v);
if (!(f >= 0.0 && f <= 2.0))
throw std::runtime_error(fmt::format("[video] bloom out of range (0.0..2.0): {}", f));
rt.video_bloom = static_cast<float>(f);
}
if (video.contains("supersample")) {
const auto& v = toml::find(video, "supersample");
const double f = v.is_integer() ? static_cast<double>(v.as_integer())
+3
View File
@@ -455,6 +455,9 @@ struct RuntimeConfig {
// "fxaa_hq". Separate from the legacy antialiasing bool above, which the
// launcher and settings.toml already persist as the linear present filter.
int video_antialiasing_mode = 0;
// bloom: opt-in glow strength, 0.0 (off, the default) .. 2.0. A plain
// [video] key, so [quality.*] presets can set it.
float video_bloom = 0.0f;
// supersample: factor on the internal-resolution target (Match display or
// a line preset), 1.0..4.0, kept in thousandths (internal_resolution.h).
int video_supersample_milli = 1000;
+10
View File
@@ -388,6 +388,16 @@ if(BUILD_TESTING)
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-post-aa-results")
set_tests_properties(gl_post_aa_test PROPERTIES
LABELS "gpu;opengl;hardware" TIMEOUT 180 SKIP_RETURN_CODE 77)
add_test(NAME gl_bloom_test
COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_texture_filter.py"
--fixture "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_bloom.c"
--cc "${CMAKE_C_COMPILER}"
--sdl-include "$<TARGET_PROPERTY:SDL3::Headers,INTERFACE_INCLUDE_DIRECTORIES>"
--sdl-library "$<TARGET_FILE:SDL3::SDL3-static>"
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-bloom-results")
set_tests_properties(gl_bloom_test PROPERTIES
LABELS "gpu;opengl;hardware" TIMEOUT 180 SKIP_RETURN_CODE 77)
add_test(NAME gl_hd_texture_pack_test
COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_texture_filter.py"
+15
View File
@@ -184,6 +184,21 @@ int gl_renderer_has_bezel(void);
* filtered and guest VRAM is never touched. Takes effect at the next present;
* safe to call from any thread. */
enum { GL_POST_AA_OFF = 0, GL_POST_AA_FXAA = 1, GL_POST_AA_FXAA_HQ = 2 };
/* Opt-in bloom ([video] bloom, 0 = off .. 2; PSX_BLOOM overrides): glow from
* 3D and additive primitives only, never the HUD (gl_bloom.inc). Safe from
* any thread; takes effect at the next draw. */
void gl_renderer_set_bloom(float strength);
float gl_renderer_get_bloom(void);
typedef struct {
float strength, threshold;
int view;
uint64_t passes, us_n;
double us_mean; /* glFinish-bracketed pass cost while timing, then reset */
} GlBloomInfo;
/* Debug: negative arguments keep the current value. view 0 normal, 1 bloom
* only, 2 split (stock | bloom), 3 the masked bright pass. */
void gl_renderer_bloom_debug(float strength, float threshold, int view, int time,
GlBloomInfo *out);
int gl_renderer_set_post_aa(int mode);
int gl_renderer_post_aa(void);
uint64_t gl_renderer_post_aa_passes(void);
+20
View File
@@ -8598,6 +8598,25 @@ static void handle_synth_recurse(int id, const char *json)
/* post_aa: post-process anti-aliasing, live. {"cmd":"post_aa"} reads it,
* {"cmd":"post_aa","mode":0|1|2} sets it (off, fxaa, fxaa_hq). */
/* bloom: {"cmd":"bloom"} reads; optional "strength" (0..2), "threshold",
* "view" (0 normal, 1 bloom only, 2 split, 3 sources) and "time" (1: time
* each pass; the mean resets on read) set. */
static void handle_bloom(int id, const char *json)
{
const char *p = strstr(json, "\"strength\"");
float strength = -1.0f, threshold = -1.0f;
if (p && (p = strchr(p, ':'))) strength = (float)strtod(p + 1, NULL);
p = strstr(json, "\"threshold\"");
if (p && (p = strchr(p, ':'))) threshold = (float)strtod(p + 1, NULL);
GlBloomInfo bi;
gl_renderer_bloom_debug(strength, threshold, json_get_int(json, "view", -1),
json_get_int(json, "time", -1), &bi);
send_fmt("{\"id\":%d,\"ok\":true,\"strength\":%.3f,\"threshold\":%.3f,\"view\":%d,"
"\"passes\":%llu,\"timed\":%llu,\"us_mean\":%.1f}",
id, bi.strength, bi.threshold, bi.view, (unsigned long long)bi.passes,
(unsigned long long)bi.us_n, bi.us_mean);
}
static void handle_post_aa(int id, const char *json)
{
int mode = json_get_int(json, "mode", -1);
@@ -15922,6 +15941,7 @@ static const CmdEntry s_commands[] = {
{ "accurate_blending", handle_accurate_blending },
{ "texture_filter", handle_texture_filter },
{ "bloom", handle_bloom },
{ "gl_ws_ablate", handle_gl_ws_ablate },
{ "gl_interp", handle_gl_interp },
{ "render_pass_stats", handle_render_pass_stats },
+309
View File
@@ -0,0 +1,309 @@
/* ---- Bloom ([video] bloom) -------------------------------------------------
* Opt-in, off by default: with strength 0 nothing here runs, no draw is marked
* and the present path is the stock one.
*
* Marks. Only pixels a 3D primitive (PGXP-proven: needs PGXP geometry) or an
* additive semi-transparent primitive (modes 1 and 3) drew last are "lit":
* every draw keeps that in stencil bit 1 of the surface it draws into
* (BCLS_DRAW in gpu_gl_renderer.c), so a 2D sprite, a fill or the HUD drawn
* over a lamp clears it again. The mark travels with the picture: native-wide
* centre copies and frame-generation surfaces copy it.
*
* Pass. Like post-process AA (and with its plumbing: post_clip_rect, the
* shared copy texture, post_fullscreen) this works on the composed game image
* in the window, before AA and the OSD, so it composes with 4:3, native-wide,
* supersample and dynamic resolution. The presented surface's marks are
* blitted (NEAREST, flipped to window orientation) into a window-sized stencil
* and turned into a mask texture. The marks gate both ends: only lit pixels
* feed the soft-knee bright pass (quarter resolution, three separable Gaussian
* iterations), and only lit pixels take the screen blend back. A HUD, sprite
* or fill pixel is therefore never a source and never changed, even right
* next to a lamp. A temporal-blending in-between present reuses the last
* marks. Guest VRAM and the surfaces are never written. */
static float s_bloom_thr = 0.55f; /* bright-pass threshold (max channel) */
static int s_bloom_view = 0; /* debug: 1 bloom only, 2 split, 3 sources */
static int s_bloom_failed = 0;
static GLuint s_bloom_src_fbo = 0;
static int s_bloom_src[4]; /* x0,y0,x1,y1 in source FBO px (y0 = top) */
static int s_bloom_src_valid = 0, s_bloom_mask_ok = 0;
static GLuint s_bloom_mask_tex = 0, s_bloom_rb = 0, s_bloom_mfbo = 0; /* window-sized marks */
static GLuint s_bloom_tex[2] = {0, 0}, s_bloom_fbo[2] = {0, 0}; /* quarter-res bloom */
static int s_bloom_w = 0, s_bloom_h = 0;
static GLuint s_bloom_mask_prog = 0, s_bloom_bright_prog = 0, s_bloom_blur_prog = 0,
s_bloom_comp_prog = 0;
static uint64_t s_bloom_passes = 0;
static int s_bloom_time = 0; /* debug: glFinish-bracketed cost */
static double s_bloom_us_sum = 0.0; static uint64_t s_bloom_us_n = 0;
static const char *BLOOM_COMMON =
"#version 330\n"
"in vec2 v_uv; out vec4 o;\n";
static const char *BLOOM_MASK_FS_BODY =
"void main(){ o = vec4(1.0); }\n";
static const char *BLOOM_BRIGHT_FS_BODY =
"uniform sampler2D u_tex, u_mask; uniform vec2 u_rcp; uniform float u_thr;\n"
"vec3 S(vec2 p){ return textureLod(u_tex, p, 0.0).rgb * textureLod(u_mask, p, 0.0).r; }\n"
"void main(){\n"
/* 4 taps of the full-resolution image, lit pixels only */
" vec3 c = 0.25*(S(v_uv+u_rcp*vec2(-1,-1))+S(v_uv+u_rcp*vec2(1,-1))+S(v_uv+u_rcp*vec2(-1,1))+S(v_uv+u_rcp*vec2(1,1)));\n"
" float l = max(c.r, max(c.g, c.b));\n"
" float k = 0.15; float s = clamp(l - u_thr + k, 0.0, 2.0*k); s = s*s/(4.0*k+1e-5);\n"
" float w = max(s, l - u_thr) / max(l, 1e-4);\n"
" o = vec4(c * w, 1.0);\n"
"}\n";
static const char *BLOOM_BLUR_FS_BODY =
"uniform sampler2D u_tex; uniform vec2 u_dir;\n"
"void main(){\n"
" vec3 c = textureLod(u_tex, v_uv, 0.0).rgb * 0.2270270;\n"
" c += (textureLod(u_tex, v_uv + u_dir*1.3846154, 0.0).rgb + textureLod(u_tex, v_uv - u_dir*1.3846154, 0.0).rgb) * 0.3162162;\n"
" c += (textureLod(u_tex, v_uv + u_dir*3.2307692, 0.0).rgb + textureLod(u_tex, v_uv - u_dir*3.2307692, 0.0).rgb) * 0.0702703;\n"
" o = vec4(c, 1.0);\n"
"}\n";
/* Exact per-pixel: an unlit pixel is the copy's texel, unchanged. */
static const char *BLOOM_COMP_FS_BODY =
"uniform sampler2D u_tex, u_bloom, u_mask; uniform float u_k; uniform int u_view; uniform ivec2 u_org;\n"
"void main(){\n"
" ivec2 p = ivec2(gl_FragCoord.xy) - u_org;\n"
" vec4 c4 = texelFetch(u_tex, p, 0); vec3 c = c4.rgb;\n"
" bool lit = texelFetch(u_mask, p, 0).r > 0.5;\n"
" vec3 b = clamp(textureLod(u_bloom, v_uv, 0.0).rgb * u_k, 0.0, 1.0);\n"
" vec3 r = lit ? 1.0 - (1.0 - c) * (1.0 - b) : c;\n"
" if (u_view == 1 || u_view == 3) r = b;\n"
/* 2: split view, stock left / bloom right */
" if (u_view == 2) { if (v_uv.x < 0.5) r = c; if (abs(v_uv.x - 0.5) < 0.0012) r = vec3(1.0); }\n"
" o = vec4(r, c4.a);\n"
"}\n";
static GLuint bloom_build(const char *body) {
static char buf[4096];
snprintf(buf, sizeof(buf), "%s%s", BLOOM_COMMON, body);
return build_program(PRESENT_VS, buf);
}
static void bloom_tex(GLuint *t, int w, int h, GLenum ifmt, GLenum fmt, GLenum type, GLenum filt) {
if (!*t) glGenTextures(1, t);
glBindTexture(GL_TEXTURE_2D, *t);
glTexImage2D(GL_TEXTURE_2D, 0, ifmt, w, h, 0, fmt, type, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filt);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filt);
}
static void bloom_fbo(GLuint *f, GLuint tex) {
if (!*f) p_glGenFramebuffers(1, f);
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, *f);
p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
}
/* The present path's source: its FBO and the displayed rect, in native units
* of a texture tex_w x tex_h native units across (as present_target_quad). */
static void bloom_note_source(GLuint fbo, GLuint tex, float tex_w, float tex_h,
int x, int y, int w, int h) {
s_bloom_src_valid = 0;
if (!(s_bloom > 0.0f) || !fbo || !tex || tex_w <= 0.0f || tex_h <= 0.0f) return;
GLint tw = 0, th = 0;
glBindTexture(GL_TEXTURE_2D, tex);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &tw);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &th);
glBindTexture(GL_TEXTURE_2D, 0);
const float sx = (float)tw / tex_w, sy = (float)th / tex_h;
s_bloom_src_fbo = fbo;
s_bloom_src[0] = (int)lrintf((float)x * sx);
s_bloom_src[1] = (int)lrintf((float)y * sy);
s_bloom_src[2] = (int)lrintf((float)(x + w) * sx);
s_bloom_src[3] = (int)lrintf((float)(y + h) * sy);
s_bloom_src_valid = 1;
}
/* The GL state the pass changes, put back afterwards so whatever runs next
* (post-AA, the OSD, the next frame's draws) sees what it left. */
typedef struct {
GLboolean blend, depth, scissor, stencil;
GLfloat clear[4];
GLint viewport[4], fbo, rfbo;
} BloomGlState;
static void bloom_state_save(BloomGlState *st) {
st->blend = glIsEnabled(GL_BLEND); st->depth = glIsEnabled(GL_DEPTH_TEST);
st->scissor = glIsEnabled(GL_SCISSOR_TEST); st->stencil = glIsEnabled(GL_STENCIL_TEST);
glGetFloatv(GL_COLOR_CLEAR_VALUE, st->clear);
glGetIntegerv(GL_VIEWPORT, st->viewport);
glGetIntegerv(0x8CA6 /*DRAW_FRAMEBUFFER_BINDING*/, &st->fbo);
glGetIntegerv(0x8CAA /*READ_FRAMEBUFFER_BINDING*/, &st->rfbo);
}
static void bloom_state_restore(const BloomGlState *st) {
if (st->blend) glEnable(GL_BLEND); else glDisable(GL_BLEND);
if (st->depth) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST);
if (st->scissor) glEnable(GL_SCISSOR_TEST); else glDisable(GL_SCISSOR_TEST);
if (st->stencil) glEnable(GL_STENCIL_TEST); else glDisable(GL_STENCIL_TEST);
glClearColor(st->clear[0], st->clear[1], st->clear[2], st->clear[3]);
glViewport(st->viewport[0], st->viewport[1], st->viewport[2], st->viewport[3]);
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, (GLuint)st->fbo);
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, (GLuint)st->rfbo);
}
static void bloom_apply_pass(int src_ok, int lx, int ly, int lw, int lh);
/* fresh: the noted source is this present's (0: reuse the last marks). */
static void bloom_apply(int fresh, int lx, int ly, int lw, int lh) {
const int src_ok = fresh && s_bloom_src_valid;
s_bloom_src_valid = 0;
if (!(s_bloom > 0.0f) || !s_ctx || s_bloom_failed) return;
if (!post_clip_rect(&lx, &ly, &lw, &lh, 8)) return;
BloomGlState st;
bloom_state_save(&st);
bloom_apply_pass(src_ok, lx, ly, lw, lh);
bloom_state_restore(&st);
}
static void bloom_apply_pass(int src_ok, int lx, int ly, int lw, int lh) {
if (!s_bloom_comp_prog) {
s_bloom_mask_prog = bloom_build(BLOOM_MASK_FS_BODY);
s_bloom_bright_prog = bloom_build(BLOOM_BRIGHT_FS_BODY);
s_bloom_blur_prog = bloom_build(BLOOM_BLUR_FS_BODY);
s_bloom_comp_prog = bloom_build(BLOOM_COMP_FS_BODY);
if (!s_bloom_mask_prog || !s_bloom_bright_prog || !s_bloom_blur_prog || !s_bloom_comp_prog) {
fprintf(stdout, "psxrecomp: bloom shader build failed; bloom off\n");
s_bloom_failed = 1; return;
}
}
#ifndef PSX_NO_DEBUG_TOOLS
uint64_t t0 = 0;
if (s_bloom_time) { glFinish(); t0 = SDL_GetPerformanceCounter(); }
#endif
const int bw = lw / 4 > 0 ? lw / 4 : 1, bh = lh / 4 > 0 ? lh / 4 : 1;
p_glActiveTexture(PSXGL_TEXTURE0);
if (s_bloom_w != lw || s_bloom_h != lh) {
for (int i = 0; i < 2; i++) {
bloom_tex(&s_bloom_tex[i], bw, bh, 0x881A /*RGBA16F*/, GL_RGBA, GL_FLOAT, GL_LINEAR);
bloom_fbo(&s_bloom_fbo[i], s_bloom_tex[i]);
}
bloom_tex(&s_bloom_mask_tex, lw, lh, 0x8229 /*R8*/, 0x1903 /*RED*/, GL_UNSIGNED_BYTE, GL_NEAREST);
bloom_fbo(&s_bloom_mfbo, s_bloom_mask_tex);
if (!s_bloom_rb) p_glGenRenderbuffers(1, &s_bloom_rb);
p_glBindRenderbuffer(PSXGL_RENDERBUFFER, s_bloom_rb);
p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, lw, lh);
p_glFramebufferRenderbuffer(PSXGL_FRAMEBUFFER, PSXGL_DEPTH_STENCIL_ATTACHMENT,
PSXGL_RENDERBUFFER, s_bloom_rb);
s_bloom_w = lw; s_bloom_h = lh; s_bloom_mask_ok = 0;
}
glDisable(GL_BLEND); glDisable(GL_DEPTH_TEST); glDisable(GL_SCISSOR_TEST);
glClearColor(0.f, 0.f, 0.f, 0.f);
if (src_ok) {
/* marks: source rect (y0 = top row) -> window orientation (row 0 = bottom) */
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_bloom_src_fbo);
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_bloom_mfbo);
p_glBlitFramebuffer(s_bloom_src[0], s_bloom_src[1], s_bloom_src[2], s_bloom_src[3],
0, lh, lw, 0, GL_STENCIL_BUFFER_BIT, GL_NEAREST);
/* marks -> mask texture (1 = lit) */
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_bloom_mfbo);
glViewport(0, 0, lw, lh);
glClear(GL_COLOR_BUFFER_BIT);
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_EQUAL, 0x02, 0x02);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
post_fullscreen(s_bloom_mask_prog, p_glGetUniformLocation(s_bloom_mask_prog, "u_uv_rect"));
glDisable(GL_STENCIL_TEST);
s_bloom_mask_ok = 1;
}
if (!s_bloom_mask_ok) return;
post_copy_bind(lw, lh);
post_copy_window(lx, ly, lw, lh);
/* bright pass from lit pixels */
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_bloom_fbo[0]);
glViewport(0, 0, bw, bh);
p_glActiveTexture(PSXGL_TEXTURE0 + 1); glBindTexture(GL_TEXTURE_2D, s_bloom_mask_tex);
p_glActiveTexture(PSXGL_TEXTURE0);
p_glUseProgram(s_bloom_bright_prog);
p_glUniform1i(p_glGetUniformLocation(s_bloom_bright_prog, "u_tex"), 0);
p_glUniform1i(p_glGetUniformLocation(s_bloom_bright_prog, "u_mask"), 1);
p_glUniform2f(p_glGetUniformLocation(s_bloom_bright_prog, "u_rcp"), 1.0f / (float)lw, 1.0f / (float)lh);
p_glUniform1f(p_glGetUniformLocation(s_bloom_bright_prog, "u_thr"), s_bloom_thr);
post_fullscreen(s_bloom_bright_prog, p_glGetUniformLocation(s_bloom_bright_prog, "u_uv_rect"));
if (s_bloom_view != 3) {
p_glUseProgram(s_bloom_blur_prog);
p_glUniform1i(p_glGetUniformLocation(s_bloom_blur_prog, "u_tex"), 0);
const GLint ud = p_glGetUniformLocation(s_bloom_blur_prog, "u_dir");
const GLint uv = p_glGetUniformLocation(s_bloom_blur_prog, "u_uv_rect");
for (int it = 0; it < 3; it++) {
const float sp = (float)(1 + it);
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_bloom_fbo[1]);
glBindTexture(GL_TEXTURE_2D, s_bloom_tex[0]);
p_glUniform2f(ud, sp / (float)bw, 0.0f);
post_fullscreen(s_bloom_blur_prog, uv);
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_bloom_fbo[0]);
glBindTexture(GL_TEXTURE_2D, s_bloom_tex[1]);
p_glUniform2f(ud, 0.0f, sp / (float)bh);
post_fullscreen(s_bloom_blur_prog, uv);
}
}
/* composite into the window rect, lit pixels only */
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
glViewport(lx, ly, lw, lh);
p_glUseProgram(s_bloom_comp_prog);
p_glActiveTexture(PSXGL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, s_post_copy_tex);
p_glActiveTexture(PSXGL_TEXTURE0 + 1); glBindTexture(GL_TEXTURE_2D, s_bloom_tex[0]);
p_glActiveTexture(PSXGL_TEXTURE0 + 2); glBindTexture(GL_TEXTURE_2D, s_bloom_mask_tex);
p_glUniform1i(p_glGetUniformLocation(s_bloom_comp_prog, "u_tex"), 0);
p_glUniform1i(p_glGetUniformLocation(s_bloom_comp_prog, "u_bloom"), 1);
p_glUniform1i(p_glGetUniformLocation(s_bloom_comp_prog, "u_mask"), 2);
p_glUniform2i(p_glGetUniformLocation(s_bloom_comp_prog, "u_org"), lx, ly);
p_glUniform1f(p_glGetUniformLocation(s_bloom_comp_prog, "u_k"), s_bloom_view == 3 ? 1.0f : s_bloom);
p_glUniform1i(p_glGetUniformLocation(s_bloom_comp_prog, "u_view"), s_bloom_view);
post_fullscreen(s_bloom_comp_prog, p_glGetUniformLocation(s_bloom_comp_prog, "u_uv_rect"));
for (int u = 2; u >= 0; u--) { p_glActiveTexture(PSXGL_TEXTURE0 + u); glBindTexture(GL_TEXTURE_2D, 0); }
p_glBindVertexArray(0);
p_glUseProgram(0);
#ifndef PSX_NO_DEBUG_TOOLS
if (s_bloom_time) {
glFinish();
s_bloom_us_sum += (double)(SDL_GetPerformanceCounter() - t0) * 1e6 /
(double)SDL_GetPerformanceFrequency();
s_bloom_us_n++;
}
#endif
s_bloom_passes++;
}
/* Objects died with the context. */
static void bloom_release(void) {
s_bloom_mask_tex = s_bloom_rb = s_bloom_mfbo = 0;
s_bloom_tex[0] = s_bloom_tex[1] = s_bloom_fbo[0] = s_bloom_fbo[1] = 0;
s_bloom_mask_prog = s_bloom_bright_prog = s_bloom_blur_prog = s_bloom_comp_prog = 0;
s_bloom_w = s_bloom_h = 0; s_bloom_failed = 0; s_bloom_mask_ok = 0; s_bloom_src_valid = 0;
}
/* PSX_BLOOM=<0..2> overrides [video] bloom. */
void gl_renderer_set_bloom(float strength) {
GL_RT_SYNC("set_bloom");
const char *e = getenv("PSX_BLOOM");
if (e && *e) strength = (float)atof(e);
if (!(strength >= 0.0f)) strength = 0.0f;
if (strength > 2.0f) strength = 2.0f;
s_bloom = strength;
s_bcls_on = strength > 0.0f;
pgxp_render_wanted_update();
}
float gl_renderer_get_bloom(void) { return s_bloom; }
void gl_renderer_bloom_debug(float strength, float threshold, int view, int time,
GlBloomInfo *out) {
GL_RT_SYNC("bloom_debug");
if (strength >= 0.0f && strength <= 2.0f) {
s_bloom = strength; s_bcls_on = strength > 0.0f;
pgxp_render_wanted_update();
}
if (threshold > 0.0f) s_bloom_thr = threshold;
if (view >= 0) s_bloom_view = view;
if (time >= 0) s_bloom_time = time ? 1 : 0;
if (out) {
out->strength = s_bloom; out->threshold = s_bloom_thr; out->view = s_bloom_view;
out->passes = s_bloom_passes;
out->us_mean = s_bloom_us_n ? s_bloom_us_sum / (double)s_bloom_us_n : 0.0;
out->us_n = s_bloom_us_n;
s_bloom_us_sum = 0.0; s_bloom_us_n = 0;
}
}
+211 -45
View File
@@ -1373,9 +1373,13 @@ static GlPresEvent s_pres_ring[GL_PRES_RING_CAP];
static uint64_t s_pres_seq = 0;
static void post_aa_apply(int lx, int ly, int lw, int lh);
static float s_bloom = 0.0f; /* [video] bloom strength (gl_bloom.inc) */
static void bloom_apply(int fresh, int lx, int ly, int lw, int lh);
static void pres_record(int path, int dx, int dy, int w, int h,
int lx, int ly, int lw, int lh) {
/* The composed game image, before hold-last capture and the OSD. */
if (s_bloom > 0.0f && (path == GL_PRES_VRAM || path == GL_PRES_WIDE || path == GL_PRES_INTERP))
bloom_apply(path != GL_PRES_INTERP, lx, ly, lw, lh);
if (s_post_aa && (path == GL_PRES_VRAM || path == GL_PRES_WIDE || path == GL_PRES_INTERP))
post_aa_apply(lx, ly, lw, lh);
s_native_surface_pending=s_native_surface_enabled &&
@@ -1461,8 +1465,8 @@ static const char *PRESENT_VS =
* (luma contrast test, edge orientation, bounded end-of-edge search,
* sub-pixel blend); it is one copy plus one full-screen pass. */
static GLuint build_program(const char *vs, const char *fs);
static GLuint s_paa_prog = 0, s_paa_tex = 0;
static int s_paa_tw = 0, s_paa_th = 0, s_paa_failed = 0;
static GLuint s_paa_prog = 0;
static int s_paa_failed = 0;
static GLint s_paa_uTex = -1, s_paa_uRcp = -1, s_paa_uUvRect = -1, s_paa_uQuality = -1;
static uint64_t s_paa_passes = 0;
#ifndef PSX_NO_DEBUG_TOOLS
@@ -1546,20 +1550,62 @@ uint64_t gl_renderer_post_aa_passes(void) {
return s_paa_passes;
}
/* ---- shared post-pass plumbing (post-process AA, bloom) -------------------
* Both passes work in place on the composed game image in the window: clip
* the rect to the drawable, snapshot it into one shared copy texture (the
* passes run one after the other, each from a fresh copy), and draw a
* full-window triangle over it with PRESENT_VS. */
static GLuint s_post_copy_tex = 0;
static int s_post_copy_w = 0, s_post_copy_h = 0;
/* Clip [lx,ly,lw,lh] to the drawable; 0 when less than min px remain. */
static int post_clip_rect(int *lx, int *ly, int *lw, int *lh, int min) {
int ww = 0, wh = 0;
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
if (*lx < 0) { *lw += *lx; *lx = 0; }
if (*ly < 0) { *lh += *ly; *ly = 0; }
if (*lx + *lw > ww) *lw = ww - *lx;
if (*ly + *lh > wh) *lh = wh - *ly;
return *lw >= min && *lh >= min;
}
/* Bind (and size) the shared copy texture on unit 0. */
static void post_copy_bind(int lw, int lh) {
p_glActiveTexture(PSXGL_TEXTURE0);
if (!s_post_copy_tex || s_post_copy_w != lw || s_post_copy_h != lh) {
if (!s_post_copy_tex) glGenTextures(1, &s_post_copy_tex);
glBindTexture(GL_TEXTURE_2D, s_post_copy_tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, lw, lh, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
s_post_copy_w = lw; s_post_copy_h = lh;
} else {
glBindTexture(GL_TEXTURE_2D, s_post_copy_tex);
}
}
/* Window rect -> the shared copy (bottom-up, as the window). */
static void post_copy_window(int lx, int ly, int lw, int lh) {
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, lx, ly, lw, lh);
}
/* One full-target triangle with prog (PRESENT_VS). PRESENT_VS flips v;
* (0,1,1,0) cancels it for a bottom-up copy. */
static void post_fullscreen(GLuint prog, GLint uv_rect) {
p_glUseProgram(prog);
p_glUniform4f(uv_rect, 0.0f, 1.0f, 1.0f, 0.0f);
p_glBindVertexArray(s_present_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
static void post_aa_release(void) {
s_paa_prog = 0; s_paa_tex = 0; s_paa_tw = s_paa_th = 0; s_paa_failed = 0;
s_paa_prog = 0; s_paa_failed = 0;
s_post_copy_tex = 0; s_post_copy_w = s_post_copy_h = 0;
}
/* The composed image in [lx,ly,lw,lh] of the bound window target, in place. */
static void post_aa_apply(int lx, int ly, int lw, int lh) {
if (!s_post_aa || !s_ctx || s_paa_failed) return;
int ww = 0, wh = 0;
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
if (lx < 0) { lw += lx; lx = 0; }
if (ly < 0) { lh += ly; ly = 0; }
if (lx + lw > ww) lw = ww - lx;
if (ly + lh > wh) lh = wh - ly;
if (lw < 3 || lh < 3) return;
if (!post_clip_rect(&lx, &ly, &lw, &lh, 3)) return;
if (!s_paa_prog) {
s_paa_prog = build_program(PRESENT_VS, POST_AA_FS);
if (!s_paa_prog) { s_paa_failed = 1; return; }
@@ -1568,19 +1614,7 @@ static void post_aa_apply(int lx, int ly, int lw, int lh) {
s_paa_uUvRect = p_glGetUniformLocation(s_paa_prog, "u_uv_rect");
s_paa_uQuality = p_glGetUniformLocation(s_paa_prog, "u_quality");
}
p_glActiveTexture(PSXGL_TEXTURE0);
if (!s_paa_tex || s_paa_tw != lw || s_paa_th != lh) {
if (!s_paa_tex) glGenTextures(1, &s_paa_tex);
glBindTexture(GL_TEXTURE_2D, s_paa_tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, lw, lh, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
s_paa_tw = lw; s_paa_th = lh;
} else {
glBindTexture(GL_TEXTURE_2D, s_paa_tex);
}
post_copy_bind(lw, lh);
#ifndef PSX_NO_DEBUG_TOOLS
if (s_paa_time < 0) {
const char *e = getenv("PSX_POST_AA_TIME");
@@ -1594,18 +1628,14 @@ static void post_aa_apply(int lx, int ly, int lw, int lh) {
const int reps = 1;
#endif
for (int r = 0; r < reps; r++) {
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, lx, ly, lw, lh);
post_copy_window(lx, ly, lw, lh);
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
glViewport(lx, ly, lw, lh);
p_glUseProgram(s_paa_prog);
p_glUniform1i(s_paa_uTex, 0);
p_glUniform2f(s_paa_uRcp, 1.0f / (float)lw, 1.0f / (float)lh);
p_glUniform1i(s_paa_uQuality, s_post_aa == GL_POST_AA_FXAA_HQ ? 1 : 0);
/* PRESENT_VS flips v; (0,1,1,0) cancels it for a bottom-up copy. */
p_glUniform4f(s_paa_uUvRect, 0.0f, 1.0f, 1.0f, 0.0f);
p_glBindVertexArray(s_present_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
post_fullscreen(s_paa_prog, s_paa_uUvRect);
p_glBindVertexArray(0);
p_glUseProgram(0);
}
@@ -1826,6 +1856,7 @@ static const char *GEO_VS =
"noperspective out vec4 v_col;\n"
"smooth out vec3 v_col_p; /* perspective-correct colour (PGXP, G1.14) */\n"
"flat out int v_cp;\n"
"noperspective out float v_b3d; /* bloom source: 1 on a PGXP 3D vertex */\n"
"void main(){\n"
" /* a_col.a carries the mask bit (0/1) and, for a PGXP 3D vertex, its\n"
" * GTE SZ: a = mask + 2*(sz + 65536*cp), cp = perspective colour. A\n"
@@ -1836,6 +1867,7 @@ static const char *GEO_VS =
" if (a < 0.0) { m = 0.0; zn = 0.9999; sz = 0.0; cp = 0.0; } /* inside the far plane: never clipped */\n"
" else if (sz > 0.5) { zn = 1.0 - 512.0 / (max(sz * (1.0 - u_zbias) - (u_zbias > 0.0 ? 48.0 : 0.0), 1.0) + 256.0); if (cp > 0.5) w = sz / 1024.0; }\n"
" v_col = vec4(a_col.rgb, m); v_col_p = a_col.rgb; v_cp = int(cp);\n"
" v_b3d = (a >= 0.0 && sz > 0.5) ? 1.0 : 0.0;\n"
" float xb = a_pos.x;\n"
" if (u_xscale < 0.0) {\n"
" float s = -u_xscale; float h = u_xhalf / s;\n"
@@ -1846,7 +1878,12 @@ static const char *GEO_VS =
static const char *GEO_FS =
"#version 330\n"
"noperspective in vec4 v_col; smooth in vec3 v_col_p; flat in int v_cp; out vec4 frag;\n"
"void main(){ frag = v_cp != 0 ? vec4(v_col_p, v_col.a) : v_col; }\n";
"noperspective in float v_b3d;\n"
"uniform int u_bcls; /* bloom source pass (gl_bloom.inc): 0 off, 2 clear, 1 mark */\n"
"uniform int u_badd; /* this batch blends additively */\n"
"void main(){\n"
" if (u_bcls != 0) { if (u_bcls == 1 && !(v_b3d > 0.999 || u_badd != 0)) discard; frag = vec4(0.0); return; }\n"
" frag = v_cp != 0 ? vec4(v_col_p, v_col.a) : v_col; }\n";
/* Textured prims: sample raw 1555 VRAM (integer), CLUT decode per depth,
* texture window, optional bilinear, texel-0 discard, STP-split discard,
@@ -1892,6 +1929,7 @@ static const char *TEX_VS =
"flat out vec4 v_hd_source; flat out int v_hd_mode;\n"
"flat out int v_filter;\n"
"smooth out vec3 v_col_p; flat out int v_cp;\n"
"noperspective out float v_b3d; /* bloom source: 1 on a PGXP 3D vertex */\n"
"void main(){ v_uv = a_uv; v_uv_p = a_uv; v_col = a_col; v_col_p = a_col.rgb;\n"
" v_persp = (a_q > 0.0) ? 1 : 0;\n"
" v_tpage = ivec2(a_tpage + 0.5); v_clut = ivec2(a_clut + 0.5);\n"
@@ -1920,7 +1958,7 @@ static const char *TEX_VS =
" float pz = a_col.a < 0.0 ? -a_col.a : 0.0;\n"
" float cp = pz >= 65536.0 ? 1.0 : 0.0, sz = pz - 65536.0 * cp, zn = 0.0;\n"
" if (sz > 0.5) { zn = 1.0 - 512.0 / (max(sz * (1.0 - u_zbias) - (u_zbias > 0.0 ? 48.0 : 0.0), 1.0) + 256.0); if (a_q <= 0.0 && cp > 0.5) w = sz / 1024.0; }\n"
" v_cp = int(cp);\n"
" v_cp = int(cp); v_b3d = sz > 0.5 ? 1.0 : 0.0;\n"
" vec2 ndc = vec2((xb+u_shift+u_xoff)/u_xhalf - 1.0, (a_pos.y+u_shift)/256.0 - 1.0);\n"
" gl_Position = vec4(ndc * w, zn * w, w); }\n";
static const char *TEX_FS =
@@ -1929,6 +1967,8 @@ static const char *TEX_FS =
"smooth in vec2 v_uv_p; flat in int v_persp;\n"
"smooth in vec3 v_col_p; flat in int v_cp;\n"
"out vec4 frag; out vec4 blend_factor;\n"
"noperspective in float v_b3d;\n"
"uniform int u_bcls; /* bloom source pass (gl_bloom.inc): 0 off, 2 clear, 1 mark */\n"
"flat in ivec2 v_tpage; /* texture page base, VRAM px */\n"
"flat in ivec2 v_clut; /* CLUT base, VRAM px */\n"
"flat in int v_depth; /* 0=4bit 1=8bit 2=15bit */\n"
@@ -2157,6 +2197,11 @@ static const char *TEX_FS =
" stp = stpf >= 0.5 ? 1 : 0;\n"
" }\n"
" if ((v_filter & 16) != 0 && v_hd_mode == 0) rgb = lod_sample(uv0, dx0, dy0, stp, rgb);\n"
" if (u_bcls != 0) { /* 3D, or additive (modes 1 and 3) semi texels */\n"
" if (u_bcls == 1 && !(v_b3d > 0.999 || ((v_semi == 2 || v_semi == 4) && stp == 1))) discard;\n"
" if (u_bcls == 2 && v_semi != 0 && stp == 1) discard; /* see-through: keeps what is under */\n"
" frag = vec4(0.0); blend_factor = vec4(0.0); return;\n"
" }\n"
" if (u_semipass == 1 && stp == 1) discard;\n"
" if (u_semipass == 2 && stp == 0) discard;\n"
" if (v_raw == 0) rgb = clamp(rgb * (v_cp != 0 ? v_col_p : v_col.rgb) * 2.0, 0.0, 1.0);\n"
@@ -3308,7 +3353,11 @@ int gl_renderer_get_wide_fast(void) { return s_wide_fast; }
* the middle of a replayed frame; they are safe at session_reboot. */
static int s_pgxp_render_wanted = 0; /* any PGXP renderer feature on */
static void pgxp_render_wanted_update(void) {
__atomic_store_n(&s_pgxp_render_wanted, (s_pgxp_depth || s_pgxp_cpersp || s_pgxp_seam) ? 1 : 0, __ATOMIC_RELEASE);
/* Bloom marks PGXP-proven 3D triangles as its sources, so it wants the
* per-triangle depth too (it never enables the depth test). */
__atomic_store_n(&s_pgxp_render_wanted,
(s_pgxp_depth || s_pgxp_cpersp || s_pgxp_seam || s_bloom > 0.0f) ? 1 : 0,
__ATOMIC_RELEASE);
}
int gl_renderer_pgxp_render_wanted(void) {
return __atomic_load_n(&s_pgxp_render_wanted, __ATOMIC_ACQUIRE);
@@ -3432,6 +3481,7 @@ static float s_tb[TEXBATCH_MAXV * TEXV];
static int s_tb_n = 0; /* verts queued */
static int s_tb_semi = -2;
static int s_tb_depth = 0; /* PGXP depth mode (batch key) */
static int s_tb_b3d = 0; /* bloom on: 3D (bloom source) batch (key) */
static int s_tb_mask = 0, s_tb_filter = 0;
static GLuint s_tb_bank_tex;
static GLuint s_tb_hd_tex;
@@ -3580,6 +3630,75 @@ void gl_renderer_batch_diag(uint64_t out[9]) {
* blended per the PSX mode. Cross-prim order is kept by isolating semi prims to
* one per batch (see gpu_textured_triangle), so this batch holds a single prim
* whose two passes do not self-overlap. */
/* ---- bloom source marking ([video] bloom; gl_bloom.inc) -------------------
* With bloom on, every textured and untextured draw is followed by two
* stencil-only passes of the same vertices that set stencil bit 1 (bit 0 is
* the mask-bit mirror) to "this pixel may feed the bloom": first every covered
* pixel clears it, then the fragments of 3D primitives (PGXP-proven, so this
* needs PGXP geometry) and of additive semi-transparent primitives set it.
* Opaque 2D, sprites and the HUD therefore clear it wherever they draw,
* painter ordered like the colour; semi-transparent texels and primitives
* leave it as it was. Bloom off: none of this runs and no state changes. */
static int s_bcls_on = 0;
static GLint s_tex_uBcls = -1, s_geo_uBcls = -1, s_geo_uBadd = -1;
typedef struct {
GLboolean st, bl, dm, cm[4];
GLint func, ref, vmask, wmask, sf, zf, zp;
} BclsSave;
static void bcls_begin(BclsSave *v, int tex, int additive) {
v->st = glIsEnabled(GL_STENCIL_TEST); v->bl = glIsEnabled(GL_BLEND);
glGetBooleanv(GL_DEPTH_WRITEMASK, &v->dm);
glGetBooleanv(GL_COLOR_WRITEMASK, v->cm);
glGetIntegerv(GL_STENCIL_FUNC, &v->func); glGetIntegerv(GL_STENCIL_REF, &v->ref);
glGetIntegerv(GL_STENCIL_VALUE_MASK, &v->vmask); glGetIntegerv(GL_STENCIL_WRITEMASK, &v->wmask);
glGetIntegerv(GL_STENCIL_FAIL, &v->sf); glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &v->zf);
glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &v->zp);
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDisable(GL_BLEND);
glDepthMask(GL_FALSE);
glEnable(GL_STENCIL_TEST);
glStencilMask(0x02);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
if (!tex) p_glUniform1i(s_geo_uBadd, additive);
}
/* pass 2: clear bit 1 under every covered pixel; pass 1: set it on sources. */
static void bcls_pass(int tex, int pass) {
glStencilFunc(GL_ALWAYS, pass == 1 ? 0x02 : 0x00, 0xFF);
p_glUniform1i(tex ? s_tex_uBcls : s_geo_uBcls, pass);
}
static void bcls_end(const BclsSave *v, int tex) {
p_glUniform1i(tex ? s_tex_uBcls : s_geo_uBcls, 0);
if (!tex) p_glUniform1i(s_geo_uBadd, 0);
glColorMask(v->cm[0], v->cm[1], v->cm[2], v->cm[3]);
if (v->bl) glEnable(GL_BLEND);
glDepthMask(v->dm);
glStencilFunc((GLenum)v->func, v->ref, (GLuint)v->vmask);
glStencilMask((GLuint)v->wmask);
glStencilOp((GLenum)v->sf, (GLenum)v->zf, (GLenum)v->zp);
if (!v->st) glDisable(GL_STENCIL_TEST);
}
/* A draw with colour writes off (the PGXP depth-clear pass) draws nothing
* one sees, so it marks nothing either. */
static int bcls_colour_draw(void) {
GLboolean cm[4]; glGetBooleanv(GL_COLOR_WRITEMASK, cm);
return cm[0] || cm[1] || cm[2];
}
/* DRAW, then (bloom on) its two marking passes. GEO semi modes 1 and 3 add.
* A semi-transparent primitive does not clear: what shows through it (a lamp
* under a fog or shade quad) keeps glowing. */
#define BCLS_DRAW(tex, semi, DRAW) do { \
DRAW; \
if (s_bcls_on && bcls_colour_draw()) { \
BclsSave bcls_sv_; \
bcls_begin(&bcls_sv_, (tex), (semi) == 1 || (semi) == 3); \
if ((semi) < 0) { bcls_pass((tex), 2); DRAW; } \
bcls_pass((tex), 1); DRAW; \
bcls_end(&bcls_sv_, (tex)); \
} \
} while (0)
#include "gl_bloom.inc"
/* tb_mask / check are the batch's set-mask and the mask-check state it was
* drawn under; track_stencil lets the canonical pass record deferred stencil
* (a replay into the high-resolution window must not touch that flag). */
@@ -3624,6 +3743,13 @@ static void tex_draw_passes_ex(int nverts, int semi, int tb_mask, int check,
p_glUniform1i(s_uSemipass, 2);
glDrawArrays(GL_TRIANGLES, 0, nverts);
}
if (s_bcls_on) {
BclsSave sv;
bcls_begin(&sv, 1, 0);
bcls_pass(1, 2); glDrawArrays(GL_TRIANGLES, 0, nverts);
bcls_pass(1, 1); glDrawArrays(GL_TRIANGLES, 0, nverts);
bcls_end(&sv, 1);
}
}
static void tex_batch_draw_passes(int nverts, int semi) {
tex_draw_passes_ex(nverts, semi, s_tb_mask, s_mask_check, 1);
@@ -3869,7 +3995,7 @@ static void hiw_replay_wide(void) {
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)((size_t)c->vcount * 6 * sizeof(float)),
s_hq_v + c->vfirst, PSXGL_STREAM_DRAW);
depth_apply_ex(c->depth, 0);
glDrawArrays(GL_TRIANGLES, 0, c->vcount);
BCLS_DRAW(0, c->semi, glDrawArrays(GL_TRIANGLES, 0, c->vcount));
depth_restore_ex(c->depth, 0, c->vcount);
}
}
@@ -3950,7 +4076,7 @@ static void hiw_flush_queue(void) {
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)((size_t)c->vcount * 6 * sizeof(float)),
s_hq_v + c->vfirst, PSXGL_STREAM_DRAW);
depth_apply_ex(c->depth, 0);
glDrawArrays(GL_TRIANGLES, 0, c->vcount);
BCLS_DRAW(0, c->semi, glDrawArrays(GL_TRIANGLES, 0, c->vcount));
depth_restore_ex(c->depth, 0, c->vcount);
}
}
@@ -4047,6 +4173,7 @@ static float s_fb[(FLATBATCH_MAXV + 2 * FLATBATCH_MAXL) * 6];
static int s_fb_n = 0;
static int s_fb_semi = -2;
static int s_fb_depth = 0; /* PGXP depth mode (batch key) */
static int s_fb_b3d = 0; /* bloom on: 3D (bloom source) batch (key) */
static int s_fb_mask = -1;
/* GL_TRIANGLES, or GL_LINES for a batch of native-wide lines (gpu_geometry). */
static GLenum s_fb_mode = GL_TRIANGLES;
@@ -4135,8 +4262,8 @@ static void flush_flat_batch_impl(void) {
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)((nverts + 2 * nl) * 6 * sizeof(float)),
s_fb, PSXGL_STREAM_DRAW);
depth_apply_ex(dmode, 0);
if (nl) flat_batch_draw_hr_lines(nverts, nl);
else glDrawArrays(fmode, 0, nverts);
BCLS_DRAW(0, semi, if (nl) flat_batch_draw_hr_lines(nverts, nl);
else glDrawArrays(fmode, 0, nverts));
depth_restore_ex(dmode, 0, nverts);
int mirror = g_wide_cur && !s_wide_suppress && s_ws_ablate != 1 &&
!(!g_ws_bd_stretch_on && mirror_flat_batch_center_only(nverts));
@@ -4157,7 +4284,7 @@ static void flush_flat_batch_impl(void) {
wide_target_begin(dx, s_geo_uXoff, s_geo_uXhalf);
wide_set_bd_scale(s_geo_uXscale, s_geo_uXcenter);
depth_apply_ex(dmode, 0);
if (s_ws_ablate != 2) glDrawArrays(fmode, 0, nverts);
if (s_ws_ablate != 2) BCLS_DRAW(0, semi, glDrawArrays(fmode, 0, nverts));
depth_restore_ex(dmode, 0, nverts);
wide_clear_bd_scale(s_geo_uXscale, s_geo_uXcenter);
wide_target_end(s_geo_uXoff, s_geo_uXhalf);
@@ -4514,7 +4641,7 @@ static void gpu_geometry(GLenum mode, const int *xs, const int *ys,
p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo);
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(draw_n * 6 * sizeof(float)),
draw_verts, PSXGL_STREAM_DRAW);
glDrawArrays(draw_mode, 0, draw_n);
BCLS_DRAW(0, semi, glDrawArrays(draw_mode, 0, draw_n));
if (is_line && s_out_scale > 1 && draw_mode == GL_LINES) {
/* windowed: hr drew a 1x line; the S surfaces take the quad */
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(6 * 6 * sizeof(float)),
@@ -4538,7 +4665,7 @@ static void gpu_geometry(GLenum mode, const int *xs, const int *ys,
gl_perf_mirror_begin();
wide_target_begin(dx, s_geo_uXoff, s_geo_uXhalf);
wide_set_bd_scale(s_geo_uXscale, s_geo_uXcenter);
if (s_ws_ablate != 2) glDrawArrays(draw_mode, 0, draw_n);
if (s_ws_ablate != 2) BCLS_DRAW(0, semi, glDrawArrays(draw_mode, 0, draw_n));
wide_clear_bd_scale(s_geo_uXscale, s_geo_uXcenter);
wide_target_end(s_geo_uXoff, s_geo_uXhalf);
gl_perf_mirror_end();
@@ -4550,9 +4677,11 @@ static void gpu_geometry(GLenum mode, const int *xs, const int *ys,
int gate = bd_prim_gate(xs, n, 0);
const int dmode = n == 3 ? pgxp_tri_depth_mode(semi) : 0;
if (dmode) depth_before_tri();
/* Bloom on: 3D sources and the rest never share a batch (BCLS_DRAW). */
const int b3d = s_bcls_on && n == 3 && pgxp_tri_is_3d();
if (s_fb_n > 0 && (s_fb_mode != GL_TRIANGLES || s_fb_semi != semi ||
s_fb_mask != (int)s_mask_set || s_fb_gate != gate ||
s_fb_depth != dmode))
s_fb_depth != dmode || s_fb_b3d != b3d))
flush_flat_batch();
if (s_fb_n + n > FLATBATCH_MAXV)
flush_flat_batch();
@@ -4561,6 +4690,7 @@ static void gpu_geometry(GLenum mode, const int *xs, const int *ys,
s_fb_mask = (int)s_mask_set;
s_fb_gate = gate;
s_fb_depth = dmode;
s_fb_b3d = b3d;
float mask_a = s_mask_set ? 1.0f : 0.0f;
float *v0 = &s_fb[s_fb_n * 6];
@@ -4708,6 +4838,9 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
(twx != s_tb_twin[0] || twy != s_tb_twin[1] ||
tox != s_tb_twin[2] || toy != s_tb_twin[3])) reason = 5;
else if (tdmode != s_tb_depth) reason = 7;
/* Bloom marks a batch in two passes (clear, then sources), so
* one batch holds either 3D sources or not: painter order. */
else if (s_bcls_on && pgxp_tri_is_3d() != s_tb_b3d) reason = 7;
}
if (reason >= 0) {
s_batch_reason[reason]++;
@@ -4717,6 +4850,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
if (s_tb_n == 0) { /* opening a batch: capture its keyed state */
s_tb_semi = batch_semi; s_tb_mask = s_mask_set; s_tb_filter = filter; s_tb_gate = gate;
s_tb_depth = tdmode;
s_tb_b3d = s_bcls_on && pgxp_tri_is_3d();
s_tb_bank_tex = s_selected_bank_tex;
s_tb_bank_live_clut = s_selected_bank_live_clut;
s_tb_hd_tex = hd_tex;
@@ -4808,7 +4942,7 @@ static void wide_flat_rect_direct(int wx, int y, int ww, int h, uint16_t c, int
p_glBindVertexArray(s_geo_vao);
p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo);
p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW);
glDrawArrays(GL_TRIANGLES, 0, 6);
BCLS_DRAW(0, semi, glDrawArrays(GL_TRIANGLES, 0, 6));
if (s_hiw) p_glUniform1f(s_geo_uShift, s_shift_hr);
p_glUniform1f(s_geo_uXoff, 0.0f);
p_glUniform1f(s_geo_uXhalf, 512.0f);
@@ -5920,6 +6054,9 @@ static int init_gpu_raster(void) {
accurate_apply_uniforms();
s_uLimits = p_glGetUniformLocation(s_tex_prog, "u_limits");
s_uHdTexture = p_glGetUniformLocation(s_tex_prog, "u_hd_texture");
s_tex_uBcls = p_glGetUniformLocation(s_tex_prog, "u_bcls");
s_geo_uBcls = p_glGetUniformLocation(s_geo_prog, "u_bcls");
s_geo_uBadd = p_glGetUniformLocation(s_geo_prog, "u_badd");
s_uBlitSrc = p_glGetUniformLocation(s_blit_prog, "u_src");
s_uBlitPass = p_glGetUniformLocation(s_blit_prog, "u_stp_pass");
s_uBlitMaskset = p_glGetUniformLocation(s_blit_prog, "u_maskset");
@@ -6395,6 +6532,7 @@ void gl_renderer_shutdown(void) {
s_hold_th = 0;
s_interp_fbo = 0; /* died with the context */
post_aa_release();
bloom_release();
}
/* CPU-readout present (24-bit FMV frames and the PSX_GL_FORCE_CPU_PRESENT
@@ -9895,6 +10033,7 @@ static void present_vram_impl(int disp_x, int disp_y, int w, int h, int linear,
}
present_target_quad(src_tex, tw, th,
src_x, disp_y, w, h, linear, lx, ly, lw, lh, 1, 1, src_scale);
bloom_note_source(src_fbo, src_tex, tw, th, src_x, disp_y, w, h);
}
pres_record(GL_PRES_VRAM, disp_x, disp_y, w, h, lx, ly, lw, lh);
gpu_timeline_note(GTL_PRESENT, GTL_PATH_VRAM_FBO,
@@ -9925,6 +10064,9 @@ static void wide_blit_center(GLuint wide_fbo, int base_x, int disp_y, int disp_h
if (native_w <= 0) return;
int S = s_out_scale;
(void)disp_y; (void)disp_h;
/* Bloom on: the centre's bloom-source bits (stencil bit 1) come with it;
* bit 0 is rebuilt from alpha anyway (the wst_add below). */
const GLbitfield bits = GL_COLOR_BUFFER_BIT | (s_bcls_on ? GL_STENCIL_BUFFER_BIT : 0);
/* Colour only: the centre's stencil is left behind (see s_wst_*). */
wst_add(wide_index(wide_fbo), g_wide_off, 0, g_wide_off + native_w, VRAM_H);
/* Copy the canonical framebuffer column into the wide surface CENTRE over the
@@ -9948,7 +10090,7 @@ static void wide_blit_center(GLuint wide_fbo, int base_x, int disp_y, int disp_h
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo);
p_glBlitFramebuffer(cx0 * R, 0, cx1 * R, VRAM_H * R,
g_wide_off * S, 0, (g_wide_off + native_w) * S, VRAM_H * S,
GL_COLOR_BUFFER_BIT, GL_NEAREST);
bits, GL_NEAREST);
}
for (int t = 0; t < s_hiw_n; t++) {
const HiwTile *T = in ? in : &s_hiw_t[t];
@@ -9958,7 +10100,7 @@ static void wide_blit_center(GLuint wide_fbo, int base_x, int disp_y, int disp_h
p_glBlitFramebuffer((h0 - T->x0) * S, 0, (h1 - T->x0) * S, VRAM_H * S,
(g_wide_off + h0 - cx0) * S, 0,
(g_wide_off + h1 - cx0) * S, VRAM_H * S,
GL_COLOR_BUFFER_BIT, GL_NEAREST);
bits, GL_NEAREST);
}
if (in) break;
}
@@ -9973,7 +10115,7 @@ static void wide_blit_center(GLuint wide_fbo, int base_x, int disp_y, int disp_h
(base_x + native_w) * S, VRAM_H * S,
g_wide_off * S, 0,
(g_wide_off + native_w) * S, VRAM_H * S,
GL_COLOR_BUFFER_BIT, GL_NEAREST);
bits, GL_NEAREST);
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
}
@@ -10062,6 +10204,8 @@ static int present_wide_fbo_impl(int disp_x, int disp_y, int disp_h, int linear)
? (float)wide_as / (float)s_out_scale : 1.0f;
present_target_quad(tex, (float)g_wide_w * wext, (float)VRAM_H * wext,
0, disp_y, g_wide_w, disp_h, linear, lx, ly, lw, lh, 1, 1, s_out_scale);
bloom_note_source(fbo, tex, (float)g_wide_w * wext, (float)VRAM_H * wext,
0, disp_y, g_wide_w, disp_h);
pres_record(GL_PRES_WIDE, disp_x, disp_y, g_wide_w, disp_h, lx, ly, lw, lh);
gpu_timeline_note(GTL_PRESENT, GTL_PATH_WIDE_FBO,
(uint32_t)disp_x | ((uint32_t)disp_y << 16));
@@ -11624,7 +11768,18 @@ static void fg_blit2(GLuint src, GLuint dst, int sx, int sy, int dx, int dy, int
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, src);
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, dst);
glDisable(GL_SCISSOR_TEST);
p_glBlitFramebuffer(sx, sy, sx + w, sy + h, dx, dy, dx + w, dy + h, GL_COLOR_BUFFER_BIT, GL_NEAREST);
/* Bloom on: the bloom-source bits come along (stencil bit 1; bit 0 is
* marked stale by the caller and rebuilt from alpha). */
p_glBlitFramebuffer(sx, sy, sx + w, sy + h, dx, dy, dx + w, dy + h,
GL_COLOR_BUFFER_BIT | (s_bcls_on ? GL_STENCIL_BUFFER_BIT : 0), GL_NEAREST);
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
}
static void fg_blit_stencil(GLuint src, GLuint dst, int sx, int sy, int dx, int dy, int w, int h) {
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, src);
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, dst);
glDisable(GL_SCISSOR_TEST);
p_glBlitFramebuffer(sx, sy, sx + w, sy + h, dx, dy, dx + w, dy + h, GL_STENCIL_BUFFER_BIT, GL_NEAREST);
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
}
@@ -11928,11 +12083,17 @@ static void fg_compose(const FgList *b, GLuint wide_fbo, GLuint wide_tex, int li
if (b->wide) {
present_target_quad(wide_tex, (float)g_wide_w, (float)VRAM_H, 0, b->disp[1], g_wide_w,
b->disp[3], linear, lx, ly, lw, lh, 1, 1, s_out_scale);
bloom_note_source(wide_fbo, wide_tex, (float)g_wide_w, (float)VRAM_H, 0, b->disp[1],
g_wide_w, b->disp[3]);
} else {
present_bezel(ww, wh, lx, ly, lw, lh);
present_target_quad(s_hr_tex, (float)VRAM_W, (float)VRAM_H, b->disp[0], b->disp[1],
b->disp[2], b->disp[3], linear, lx, ly, lw, lh, 1, 1, s_out_scale);
bloom_note_source(s_hr_fbo, s_hr_tex, (float)VRAM_W, (float)VRAM_H, b->disp[0], b->disp[1],
b->disp[2], b->disp[3]);
}
/* A generated frame is the same picture: the same bloom. */
if (s_bloom > 0.0f) bloom_apply(1, lx, ly, lw, lh);
}
/* Draw and present the frame at phase t between the two lists. Returns 1
@@ -12506,6 +12667,11 @@ static int fg_generate(double t, int swap) {
if (L->disp[0] != b->disp[0] || lh != dh || L->disp[2] != b->disp[2]) return 0;
/* Reprojected frames start from the list's snapshot instead (below). */
const int will_rp = s_fg_reproject && L == b && s_fg_fit.ok && rp_snap_slot((int)(L - s_fg_l)) >= 0;
if (will_rp && s_bcls_on) {
/* The warp writes colour only: the newer real frame's bloom sources. */
fg_blit_stencil(s_hr_fbo, s_fg_hr_fbo, b->disp[0] * S, dy * S, L->disp[0] * S, ly * S, b->disp[2] * S, dh * S);
if (iw >= 0) fg_blit_stencil(s_wide_fbo[iw], s_fg_w_fbo, 0, dy * S, 0, ly * S, g_wide_w * S, dh * S);
}
if (!will_rp) {
fg_blit2(s_hr_fbo, s_fg_hr_fbo, b->disp[0] * S, dy * S, L->disp[0] * S, ly * S, b->disp[2] * S, dh * S);
if (iw >= 0) fg_blit2(s_wide_fbo[iw], s_fg_w_fbo, 0, dy * S, 0, ly * S, g_wide_w * S, dh * S);
+4
View File
@@ -1229,6 +1229,7 @@ extern "C" EMSCRIPTEN_KEEPALIVE void psx_web_set_smooth_60fps(int enabled) {
static int g_video_scale = 1; /* internal-resolution SSAA factor */
static bool g_video_aa = true; /* linear present filtering */
static int g_video_post_aa = 0; /* [video] antialiasing_mode / PSX_AA_MODE (GL_POST_AA_*) */
static float g_video_bloom = 0.0f; /* [video] bloom (PSX_BLOOM overrides) */
static int g_video_ss_milli = 1000; /* [video] supersample / PSX_SUPERSAMPLE */
/* FMV present reconstruction (VIDEO_FMV_FILTER_*), pushed to the GL renderer
* once the config is resolved. Only consulted while g_video_aa is on. */
@@ -16332,6 +16333,7 @@ int main(int argc, char** argv) {
}
g_video_aa = gc.runtime.video_antialiasing;
g_video_post_aa = gc.runtime.video_antialiasing_mode;
g_video_bloom = gc.runtime.video_bloom;
g_video_ss_milli = gc.runtime.video_supersample_milli;
g_video_texfilter = gc.runtime.video_texture_filter;
g_video_fmv_filter = gc.runtime.video_fmv_filter;
@@ -19114,6 +19116,7 @@ session_reboot:
}
gl_renderer_set_swap_interval(0);
(void)gl_renderer_set_post_aa(g_video_post_aa);
gl_renderer_set_bloom(g_video_bloom);
g_gl_active = gl_renderer_init_context(s_headless_gl_window) != 0;
if (!g_gl_active || gr_backend() != GR_BACKEND_OPENGL ||
!(SDL_GetWindowFlags(s_headless_gl_window) & SDL_WINDOW_HIDDEN)) {
@@ -19326,6 +19329,7 @@ session_reboot:
gl_renderer_set_dynamic_resolution(
((dynres_requested() || dbg_live_scale) && g_video_scale_applies) ? 1 : 0);
(void)gl_renderer_set_post_aa(g_video_post_aa);
gl_renderer_set_bloom(g_video_bloom);
g_gl_active = (gl_renderer_init_context(sdl_window) != 0);
dynres_setup();
if (dbg_live_scale && g_gl_active) {
+229
View File
@@ -0,0 +1,229 @@
/* Bloom ([video] bloom) on a real, hidden OpenGL context.
*
* - off: the composed frame is the stock one (byte-identical before and
* after a round trip through bloom; the pass never runs);
* - on: a bright 2D primitive (sprite, HUD) and a 2D one drawn over a lit 3D
* one gain no glow at all (byte-identical to off), while an additive
* primitive and a PGXP-proven 3D one glow around their edges;
* - guest VRAM / readbacks are the same either way, with and without the
* render thread.
* With --dump PATH the off frame of every scene is written raw (RGB,
* bottom-up) so a run against another tree can be compared byte for byte
* (BLOOM_FIXTURE_NO_BLOOM builds it without the bloom API). */
#define GL_PRESENT_TEST_HOOK(gen) bloom_capture_hook(gen)
static void bloom_capture_hook(int gen);
#define main scale_fixture_main
#include "test_gl_scale_invariance.c"
#undef main
void gpu_hd_textures_begin_upload(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;}
void gpu_hd_textures_track_upload(int x,int y,int w,int h,const uint16_t* p){(void)x;(void)y;(void)w;(void)h;(void)p;}
void gpu_hd_textures_invalidate(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;}
void gpu_hd_textures_begin_copy(int sx,int sy,int x,int y,int w,int h){(void)sx;(void)sy;(void)x;(void)y;(void)w;(void)h;}
void gpu_hd_textures_end_copy(void){}
#ifndef PSX_NO_DEBUG_TOOLS
int present_image_ring_accepting(void){return 0;}
int present_image_ring_thumb_w(int w,int h){(void)w;(void)h;return 0;}
void present_image_ring_push(uint32_t f,const uint8_t* p,int w,int pitch,int flip){(void)f;(void)p;(void)w;(void)pitch;(void)flip;}
void present_image_ring_push_argb(uint32_t f,const uint32_t* p,int w,int h,int pitch){(void)f;(void)p;(void)w;(void)h;(void)pitch;}
#endif
void present_shot_done(int ok) { (void)ok; }
int host_osd_needs_present(void) { return 0; }
int psx_present_vsync_owns_cadence(void) { return 0; }
void latency_ring_mark(LatencyStage stage) { (void)stage; }
void psx_host_sleep_ms(unsigned ms) { SDL_Delay(ms); }
void psx_host_sleep_micros(unsigned us) { SDL_Delay((us + 999u) / 1000u); }
int present_shot_take(char* out,int n) { (void)out; (void)n; return 0; }
int host_osd_image(const uint32_t** p,int* w,int* h) { (void)p; (void)w; (void)h; return 0; }
int host_osd_volume_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
void host_osd_present_done(void) {}
int psx_rewind_overlay_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
float psx_rewind_slide(void) { return 0; }
int psx_savestate_menu_overlay_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
static uint8_t *s_cap; static int s_cap_w, s_cap_h, s_cap_n;
static void bloom_capture_hook(int gen) {
(void)gen;
SDL_GL_GetDrawableSize(s_win, &s_cap_w, &s_cap_h);
free(s_cap);
s_cap = (uint8_t *)malloc((size_t)s_cap_w * s_cap_h * 3);
glReadBuffer(GL_BACK);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, s_cap_w, s_cap_h, GL_RGB, GL_UNSIGNED_BYTE, s_cap);
glPixelStorei(GL_PACK_ALIGNMENT, 4);
s_cap_n++;
}
enum { SC_2D, SC_ADD, SC_3D, SC_HUD_OVER_3D, SC_PARTIAL, SC_N };
static const char *k_scene[SC_N] = { "2d", "additive", "3d", "hud-over-3d", "partial" };
/* SC_PARTIAL's grey HUD panels (native x, y, w, h): one beside the lamp, one
* covering part of it. */
static const int k_hud[2][4] = { { 204, 100, 40, 40 }, { 150, 110, 30, 20 } };
/* A PGXP-proven triangle: precise positions plus GTE SZ, as gpu.c sends. */
static void tri3d(int x0, int y0, int x1, int y1, int x2, int y2, uint16_t c) {
glb_set_precise_triangle(1, x0 * 65536, y0 * 65536, x1 * 65536, y1 * 65536,
x2 * 65536, y2 * 65536);
glb_set_depth_triangle(1, 2000.0f, 2000.0f, 2000.0f);
glb_draw_flat_triangle(x0, y0, x1, y1, x2, y2, c);
}
static uint8_t *frame(int scene, float bloom, uint64_t *vram_hash) {
#ifndef BLOOM_FIXTURE_NO_BLOOM
gl_renderer_set_bloom(bloom);
#else
(void)bloom;
#endif
glb_set_draw_area(0, 0, FRAME_W - 1, FRAME_H - 1); glb_set_draw_offset(0, 0);
glb_set_mask_bits(0, 0); glb_set_semi_transparency(0, 0);
glb_fill_rect(0, 0, FRAME_W, FRAME_H, 0x0000);
/* A dark 3D ground everywhere: lit (may take a halo), below the threshold. */
tri3d(0, 0, FRAME_W, 0, 0, FRAME_H, 0x0842);
tri3d(FRAME_W, 0, FRAME_W, FRAME_H, 0, FRAME_H, 0x0842);
switch (scene) {
case SC_2D: /* a white HUD panel */
glb_draw_flat_rect(120, 90, 80, 60, 0x7fff);
break;
case SC_ADD: /* an additive light halo over black */
glb_set_semi_transparency(1, 1);
glb_draw_flat_triangle(120, 150, 200, 150, 160, 80, 0x7fff);
glb_set_semi_transparency(0, 0);
break;
case SC_3D: /* a lit 3D polygon */
tri3d(120, 150, 200, 150, 160, 80, 0x7fff);
break;
case SC_PARTIAL: /* a visible lamp with grey HUD panels beside and over it */
tri3d(120, 150, 200, 150, 160, 80, 0x7fff);
for (int i = 0; i < 2; i++) glb_draw_flat_rect(k_hud[i][0], k_hud[i][1], k_hud[i][2], k_hud[i][3], 0x4210);
break;
case SC_HUD_OVER_3D: /* the same 3D polygon under an opaque panel */
tri3d(120, 150, 200, 150, 160, 80, 0x7fff);
glb_draw_flat_rect(100, 70, 120, 90, 0x7fff);
break;
}
gl_renderer_sync_cpu();
check(gl_renderer_fbo_peek(0, 0, FRAME_W, FRAME_H, peek), "vram readback");
*vram_hash = fnv(peek, FRAME_W * FRAME_H * 2, 0xcbf29ce484222325ull);
const int n0 = s_cap_n;
if (s_rth_on) gl_renderer_render_thread_frame_boundary();
gl_renderer_present_vram(0, 0, FRAME_W, FRAME_H, 0, 0);
if (s_rth_on) {
gl_renderer_render_thread_frame_boundary();
gl_renderer_render_thread_sync("bloom-capture");
}
check(s_cap_n == n0 + 1 && s_cap, "present captured");
uint8_t *c = (uint8_t *)malloc((size_t)s_cap_w * s_cap_h * 3);
memcpy(c, s_cap, (size_t)s_cap_w * s_cap_h * 3);
return c;
}
/* Window pixels of SC_PARTIAL's HUD panels that differ between a and b. */
static long hud_changed(const uint8_t *a, const uint8_t *b) {
const int sx = s_cap_w / FRAME_W, sy = s_cap_h / FRAME_H;
long n = 0;
for (int i = 0; i < 2; i++)
for (int y = k_hud[i][1] * sy; y < (k_hud[i][1] + k_hud[i][3]) * sy; y++)
for (int x = k_hud[i][0] * sx; x < (k_hud[i][0] + k_hud[i][2]) * sx; x++) {
const size_t o = ((size_t)(s_cap_h - 1 - y) * s_cap_w + x) * 3; /* bottom-up */
n += memcmp(a + o, b + o, 3) != 0;
}
return n;
}
/* Brightness the pass added on the dark ground around the shapes (the glow). */
static long glow(const uint8_t *off, const uint8_t *on) {
long sum = 0;
for (int i = 0; i < s_cap_w * s_cap_h; i++) {
if (off[i * 3] > 0x50 || off[i * 3 + 1] > 0x50 || off[i * 3 + 2] > 0x50) continue;
for (int c = 0; c < 3; c++) sum += abs((int)on[i * 3 + c] - (int)off[i * 3 + c]);
}
return sum;
}
int main(int argc, char **argv) {
const char *dump = argc > 2 && !strcmp(argv[1], "--dump") ? argv[2] : NULL;
if (SDL_Init(SDL_INIT_VIDEO) != 0) { fprintf(stderr, "SKIP no video\n"); return 77; }
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
#if defined(__APPLE__)
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
#endif
SDL_Window *win = SDL_CreateWindow("Bloom hidden test", 0, 0, 640, 480,
SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN);
if (!win) { fprintf(stderr, "SKIP no window\n"); return 77; }
glb_init(vram); glb_set_scale(2); gl_renderer_set_swap_interval(0);
if (!gl_renderer_init_context(win)) { fprintf(stderr, "SKIP no GL context\n"); return 77; }
uint8_t *off[SC_N]; uint64_t h_off[SC_N];
for (int s = 0; s < SC_N; s++) off[s] = frame(s, 0.0f, &h_off[s]);
const size_t bytes = (size_t)s_cap_w * s_cap_h * 3;
if (dump) {
FILE *f = fopen(dump, "wb");
for (int s = 0; s < SC_N; s++)
check(f && fwrite(off[s], 1, bytes, f) == bytes, "dump");
if (f) fclose(f);
}
#ifndef BLOOM_FIXTURE_NO_BLOOM
GlBloomInfo bi;
gl_renderer_bloom_debug(-1.0f, -1.0f, -1, -1, &bi);
check(bi.passes == 0, "off never runs the bloom pass");
for (int pass = 0; pass < 2; pass++) { /* 0: this thread, 1: render thread */
if (pass == 1) {
check(gl_renderer_render_thread_start(2) == 1, "render thread started");
gl_renderer_render_thread_frame_boundary();
}
long g[SC_N];
for (int s = 0; s < SC_N; s++) {
uint64_t h = 0;
uint8_t *on = frame(s, 1.5f, &h);
g[s] = glow(off[s], on);
check(h == h_off[s], "guest VRAM identical with bloom on");
if (s == SC_2D || s == SC_HUD_OVER_3D)
check(memcmp(off[s], on, bytes) == 0, "2D / HUD pixels never glow");
if (s == SC_PARTIAL) {
const long hc = hud_changed(off[s], on);
printf("partial: hud pixels changed=%ld\n", hc);
check(hc == 0, "HUD panels beside and over a lamp are byte-identical");
}
free(on);
uint8_t *back = frame(s, 0.0f, &h);
check(memcmp(off[s], back, bytes) == 0 && h == h_off[s], "off after bloom is byte-identical");
free(back);
}
printf("%s drawable=%dx%d glow 2d=%ld additive=%ld 3d=%ld hud-over-3d=%ld\n",
pass ? "rt" : "st", s_cap_w, s_cap_h, g[SC_2D], g[SC_ADD], g[SC_3D], g[SC_HUD_OVER_3D]);
check(g[SC_2D] == 0 && g[SC_HUD_OVER_3D] == 0, "no glow from 2D");
check(g[SC_ADD] > 20000, "additive primitive glows");
check(g[SC_3D] > 20000, "3D primitive glows");
check(g[SC_PARTIAL] > 20000, "a partly covered lamp still glows");
}
gl_renderer_render_thread_stop();
/* The pass leaves the GL state as it found it, both ways round. */
{
uint64_t h = 0;
free(frame(SC_3D, 1.5f, &h)); /* marks for a reused-marks pass */
int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh);
for (int on = 0; on < 2; on++) {
BloomGlState want, got;
if (on) { glEnable(GL_BLEND); glEnable(GL_DEPTH_TEST); glEnable(GL_SCISSOR_TEST); glEnable(GL_STENCIL_TEST); }
else { glDisable(GL_BLEND); glDisable(GL_DEPTH_TEST); glDisable(GL_SCISSOR_TEST); glDisable(GL_STENCIL_TEST); }
glClearColor(0.25f, 0.5f, 0.75f, on ? 1.0f : 0.0f);
glViewport(3, 5, 101 + on, 77);
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, on ? s_hr_fbo : 0);
bloom_state_save(&want);
const uint64_t n0 = s_bloom_passes;
bloom_apply(0, 0, 0, ww, wh);
bloom_state_save(&got);
check(s_bloom_passes == n0 + 1, "state test ran the pass");
check(memcmp(&want, &got, sizeof want) == 0, "bloom pass leaves GL state unchanged");
}
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
glDisable(GL_BLEND); glDisable(GL_DEPTH_TEST); glDisable(GL_SCISSOR_TEST); glDisable(GL_STENCIL_TEST);
}
(void)k_scene;
#endif
for (int s = 0; s < SC_N; s++) free(off[s]);
check(glGetError() == GL_NO_ERROR, "GL errors");
printf("checks=%d failures=%d\n", checks, failures);
gl_renderer_shutdown(); SDL_DestroyWindow(win); SDL_Quit();
return failures ? 1 : 0;
}