From e730e03a8af01072b06bab63b64bf20697418f4e Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:43:43 -0700 Subject: [PATCH] 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 --- docs/TCP_COMMANDS.md | 7 +- docs/config_schema.md | 26 +++ generated/OpenBIOS.emitter.sha | 2 +- recompiler/src/config_loader.cpp | 9 + recompiler/src/config_loader.h | 3 + runtime/CMakeLists.txt | 10 + runtime/include/gpu_gl_renderer.h | 15 ++ runtime/src/debug_server.c | 20 ++ runtime/src/gl_bloom.inc | 309 ++++++++++++++++++++++++++++++ runtime/src/gpu_gl_renderer.c | 256 ++++++++++++++++++++----- runtime/src/main.cpp | 4 + runtime/tests/test_gl_bloom.c | 229 ++++++++++++++++++++++ 12 files changed, 842 insertions(+), 48 deletions(-) create mode 100644 runtime/src/gl_bloom.inc create mode 100644 runtime/tests/test_gl_bloom.c diff --git a/docs/TCP_COMMANDS.md b/docs/TCP_COMMANDS.md index 05c20cb9..fb7bd001 100644 --- a/docs/TCP_COMMANDS.md +++ b/docs/TCP_COMMANDS.md @@ -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":""}` 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":""}` 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` | ✓ | | | diff --git a/docs/config_schema.md b/docs/config_schema.md index 6950f926..5a803ca5 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -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. diff --git a/generated/OpenBIOS.emitter.sha b/generated/OpenBIOS.emitter.sha index 2e51216e..9da80817 100644 --- a/generated/OpenBIOS.emitter.sha +++ b/generated/OpenBIOS.emitter.sha @@ -1 +1 @@ -d0e31158eda04d03cc0da52b06f7dd9c8d142f24b6e5c4aeb1442683eeb90379 +c85714906d3fdc85c7dca4d04daa574a029e4d5c7b0f273e19c25e5633889916 diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index e874b9a9..82527140 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -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(v.as_integer()) + : toml::get(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(f); + } if (video.contains("supersample")) { const auto& v = toml::find(video, "supersample"); const double f = v.is_integer() ? static_cast(v.as_integer()) diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 1cefbd03..52b9459e 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -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; diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index ac311475..d4fce360 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -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 "$" + --sdl-library "$" + --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" diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index a506078f..13e15440 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -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); diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 6796c5cb..70facc77 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -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 }, diff --git a/runtime/src/gl_bloom.inc b/runtime/src/gl_bloom.inc new file mode 100644 index 00000000..7bfc974b --- /dev/null +++ b/runtime/src/gl_bloom.inc @@ -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; + } +} diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index c375e9ec..73235e3e 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -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); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index de26182c..72598b71 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -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) { diff --git a/runtime/tests/test_gl_bloom.c b/runtime/tests/test_gl_bloom.c new file mode 100644 index 00000000..277cc89f --- /dev/null +++ b/runtime/tests/test_gl_bloom.c @@ -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; +}