Opt-in post-process anti-aliasing (FXAA) and a supersample factor

[video] antialiasing_mode = "off" | "fxaa" | "fxaa_hq" (PSX_AA_MODE)
filters the composed game image after the game quad and before hold-last
capture, OSD and swap; guest VRAM, readbacks and the Smooth motion /
frame generation sources are untouched. Off (default) never runs the pass
and is byte-identical to master.

[video] supersample = 1.0..4.0 (PSX_SUPERSAMPLE) multiplies the internal-
resolution target (Match display or a line preset), so the existing area
resolve does ordered-grid SSAA; dynamic resolution takes it as its ceiling.

Debug server: post_aa reads/sets the mode live. Real-GL fixture
gl_post_aa_test; supersample checks in test_internal_resolution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-07 21:09:44 -07:00
parent cbfd24fda5
commit 976da1a812
10 changed files with 419 additions and 4 deletions
+18
View File
@@ -748,6 +748,24 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
if (video.contains("antialiasing")) {
rt.video_antialiasing = toml::find<bool>(video, "antialiasing");
}
if (video.contains("antialiasing_mode")) {
const auto mode = toml::find<std::string>(video, "antialiasing_mode");
if (mode == "off") rt.video_antialiasing_mode = 0;
else if (mode == "fxaa") rt.video_antialiasing_mode = 1;
else if (mode == "fxaa_hq") rt.video_antialiasing_mode = 2;
else throw std::runtime_error(fmt::format(
"[video] antialiasing_mode must be \"off\", \"fxaa\" or \"fxaa_hq\": {}", mode));
}
if (video.contains("supersample")) {
const auto& v = toml::find(video, "supersample");
const double f = v.is_integer() ? static_cast<double>(v.as_integer())
: toml::get<double>(v);
if (!(f >= 1.0 && f <= 4.0)) {
throw std::runtime_error(fmt::format(
"[video] supersample out of range (1.0..4.0): {}", f));
}
rt.video_supersample_milli = static_cast<int>(f * 1000.0 + 0.5);
}
if (video.contains("texture_filtering")) {
const auto mode = toml::find<std::string>(video, "texture_filtering");
if (mode == "nearest") rt.video_texture_filter = 0;
+9
View File
@@ -450,6 +450,15 @@ struct RuntimeConfig {
// Defaults to true.
bool video_antialiasing = true;
// antialiasing_mode: post-process anti-aliasing of the composed game image
// (OpenGL; GL_POST_AA_* in gpu_gl_renderer.h): "off" (default) | "fxaa" |
// "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;
// 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;
// texture_filtering: "nearest" (default, native PSX look) | "bilinear"
// (smooths textures and 2D backgrounds). Stored as 0/1.
int video_texture_filter = 0;
+10
View File
@@ -264,6 +264,16 @@ if(BUILD_TESTING)
--sdl-library "$<TARGET_FILE:SDL3::SDL3-static>"
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-filter-results")
set_tests_properties(gl_texture_filter_test PROPERTIES LABELS "gpu;opengl;hardware" TIMEOUT 180)
add_test(NAME gl_post_aa_test
COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_texture_filter.py"
--fixture "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_post_aa.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-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_hd_texture_pack_test
COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_texture_filter.py"
+8
View File
@@ -176,6 +176,14 @@ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linea
* Returns 0 only if a texture could not be created. */
int gl_renderer_set_bezel(const void *rgba, int w, int h);
int gl_renderer_has_bezel(void);
/* Post-process anti-aliasing of the composed game image ([video]
* antialiasing_mode, PSX_AA_MODE). Off by default; OSD and bezel are not
* 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 };
int gl_renderer_set_post_aa(int mode);
int gl_renderer_post_aa(void);
uint64_t gl_renderer_post_aa_passes(void);
/* Clear to black + swap (display-disabled frame). */
void gl_renderer_present_blank(void);
+48
View File
@@ -112,6 +112,54 @@ static inline int psx_resolve_internal_scale(int value, int ref_lines,
return s < 1 ? 1 : s;
}
/* ---- Supersample ([video] supersample, PSX_SUPERSAMPLE) ----------------------
* A factor on the TARGET of a line-count preset or Match display, in
* thousandths (1000 = 1.0, the historical behaviour; range 1000..4000). The
* renderer already area-resolves an internal image taller than the output, so
* asking for 2x the display's lines is ordered-grid SSAA of the whole frame:
* Match display at 1080 px with 2.0 renders 2160 lines (S = 9) and resolves
* to 1080. Native and the legacy integer factor are left alone. Dynamic
* resolution takes the result as its ceiling and steps under it as usual. */
#define PSX_SS_MILLI_MIN 1000
#define PSX_SS_MILLI_MAX 4000
static inline int psx_ss_milli_clamp(int m) {
return m < PSX_SS_MILLI_MIN ? PSX_SS_MILLI_MIN
: m > PSX_SS_MILLI_MAX ? PSX_SS_MILLI_MAX : m;
}
/* "1.5", "2", "2x" -> 1500, 2000, 2000. 0 for anything else. */
static inline int psx_ss_parse_milli(const char *s) {
if (!s || !*s) return 0;
long whole = 0, frac = 0, div = 1;
const char *p = s;
while (*p >= '0' && *p <= '9') { whole = whole * 10 + (*p - '0'); p++; if (whole > 100) return 0; }
if (p == s) return 0;
if (*p == '.') { p++; while (*p >= '0' && *p <= '9') { if (div < 1000) { frac = frac * 10 + (*p - '0'); div *= 10; } p++; } }
if (*p == 'x' || *p == 'X') p++;
if (*p) return 0;
long m = whole * 1000 + frac * (1000 / div);
if (m < PSX_SS_MILLI_MIN || m > PSX_SS_MILLI_MAX) return 0;
return (int)m;
}
/* psx_resolve_internal_scale with the supersample factor on the target. */
static inline int psx_resolve_internal_scale_ss(int value, int ref_lines,
int display_px_h, int s_max,
int ss_milli) {
ss_milli = psx_ss_milli_clamp(ss_milli);
if (ss_milli != PSX_SS_MILLI_MIN) {
if (value == PSX_IR_DISPLAY) {
if (display_px_h > 0)
display_px_h = (int)(((long long)display_px_h * ss_milli + 999) / 1000);
} else if (value >= PSX_IR_MIN_LINES) {
long long t = ((long long)value * ss_milli + 999) / 1000;
value = t > 4LL * PSX_IR_MAX_LINES ? 4 * PSX_IR_MAX_LINES : (int)t;
}
}
return psx_resolve_internal_scale(value, ref_lines, display_px_h, s_max);
}
/* A legacy [video] supersampling factor as a value the launcher can show:
* the preset that resolves to the same scale (1 -> Native, 3 -> 720p,
* 5 -> 1080p, 6 -> 1440p, 9 -> 4K, 12 -> 5K, 18 -> 8K at 240 lines),
+11
View File
@@ -8557,6 +8557,16 @@ static void handle_synth_recurse(int id, const char *json)
#endif
}
/* 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). */
static void handle_post_aa(int id, const char *json)
{
int mode = json_get_int(json, "mode", -1);
if (mode >= 0) (void)gl_renderer_set_post_aa(mode);
send_fmt("{\"id\":%d,\"ok\":true,\"mode\":%d,\"passes\":%llu}", id,
gl_renderer_post_aa(), (unsigned long long)gl_renderer_post_aa_passes());
}
static void handle_frame_perf(int id, const char *json)
{
(void)json;
@@ -15632,6 +15642,7 @@ static const CmdEntry s_commands[] = {
{ "gpu_timeline", handle_gpu_timeline },
{ "nclip_stats", handle_nclip_stats },
{ "frame_perf", handle_frame_perf },
{ "post_aa", handle_post_aa },
{ "gl_ws_ablate", handle_gl_ws_ablate },
{ "gl_interp", handle_gl_interp },
{ "render_pass_stats", handle_render_pass_stats },
+141
View File
@@ -574,6 +574,7 @@ static int s_osd_tw = 0, s_osd_th = 0;
static GLuint s_present_prog = 0, s_present_vao = 0;
static GLuint s_xr_color_prog = 0, s_xr_native_tex = 0;
static void gl_swap_with_osd(void);
static int s_post_aa = 0; /* GL_POST_AA_*; post-process AA (post_aa_apply) */
static int s_native_surface_enabled, s_native_surface_pending;
static int s_native_surface_rect[4]; /* Fresh native backbuffer content, GL coordinates. */
static double s_native_surface_distance, s_native_surface_width, s_native_surface_units;
@@ -1298,8 +1299,12 @@ int gl_renderer_coh_get(uint64_t seq, GlCohEvent *out) {
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 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_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 &&
(path==GL_PRES_VRAM || path==GL_PRES_WIDE || path==GL_PRES_CPU || path==GL_PRES_BLANK);
s_native_surface_rect[0]=lx;s_native_surface_rect[1]=ly;
@@ -1367,6 +1372,141 @@ static const char *PRESENT_VS =
" v_uv = vec2(mix(u_uv_rect.x,u_uv_rect.z,p.x),\n"
" mix(u_uv_rect.y,u_uv_rect.w,1.0-p.y));\n"
" gl_Position = vec4(p*2.0-1.0,0.0,1.0); }\n";
/* ---- Post-process anti-aliasing ([video] antialiasing_mode) ----------------
* Opt-in, off by default: with the mode off nothing below runs and the present
* is byte-identical to a build without it. When on, the composed game image
* (the letterbox rect pres_record is told about, after the game quad and
* before hold-last capture, OSD, screenshots and the swap) is copied out of
* the drawable and redrawn through an edge filter. It works on the output
* pixels only: guest VRAM, the hr/wide surfaces, readbacks, render passes and
* the Smooth motion / frame generation sources are never touched, so it
* composes with every present path (4:3, native-wide, interpolated and
* generated frames) and with internal resolution / dynamic resolution, whose
* area resolve has already happened by then. OSD and bezel stay sharp.
*
* FXAA here is an independent implementation of the published technique
* (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 GLint s_paa_uTex = -1, s_paa_uRcp = -1, s_paa_uUvRect = -1, s_paa_uQuality = -1;
static uint64_t s_paa_passes = 0;
static const char *POST_AA_FS =
"#version 330\n"
"in vec2 v_uv; out vec4 o;\n"
"uniform sampler2D u_tex; uniform vec2 u_rcp; uniform int u_quality;\n"
"float L(vec3 c){ return dot(c, vec3(0.299,0.587,0.114)); }\n"
"float LS(vec2 p){ return L(textureLod(u_tex,p,0.0).rgb); }\n"
"void main(){\n"
" vec3 cM = textureLod(u_tex, v_uv, 0.0).rgb; float lM = L(cM);\n"
" float lN = LS(v_uv+vec2(0.0, u_rcp.y)), lS = LS(v_uv-vec2(0.0, u_rcp.y));\n"
" float lE = LS(v_uv+vec2(u_rcp.x,0.0)), lW = LS(v_uv-vec2(u_rcp.x,0.0));\n"
" float mx = max(lM,max(max(lN,lS),max(lE,lW)));\n"
" float mn = min(lM,min(min(lN,lS),min(lE,lW)));\n"
" float range = mx - mn;\n"
/* thresholds: absolute 1/16 (1/24 at high quality), relative 1/8 */
" float thr = u_quality > 0 ? 0.0417 : 0.0625;\n"
" if (range < max(thr, mx*0.125)) { o = vec4(cM,1.0); return; }\n"
" float lNE = LS(v_uv+u_rcp), lSW = LS(v_uv-u_rcp);\n"
" float lNW = LS(v_uv+vec2(-u_rcp.x,u_rcp.y)), lSE = LS(v_uv+vec2(u_rcp.x,-u_rcp.y));\n"
" float eH = abs(lNW+lNE-2.0*lN) + 2.0*abs(lW+lE-2.0*lM) + abs(lSW+lSE-2.0*lS);\n"
" float eV = abs(lNW+lSW-2.0*lW) + 2.0*abs(lN+lS-2.0*lM) + abs(lNE+lSE-2.0*lE);\n"
" bool horz = eH >= eV;\n"
" float l1 = horz ? lS : lW, l2 = horz ? lN : lE;\n"
" float g1 = abs(l1-lM), g2 = abs(l2-lM);\n"
" bool neg = g1 >= g2;\n"
" float grad = 0.25*max(g1,g2);\n"
" float step = horz ? u_rcp.y : u_rcp.x;\n"
" float lLocal; if (neg) { step = -step; lLocal = 0.5*(l1+lM); } else lLocal = 0.5*(l2+lM);\n"
" vec2 p = v_uv; if (horz) p.y += 0.5*step; else p.x += 0.5*step;\n"
" vec2 off = horz ? vec2(u_rcp.x,0.0) : vec2(0.0,u_rcp.y);\n"
" vec2 p1 = p - off, p2 = p + off;\n"
" float e1 = LS(p1)-lLocal, e2 = LS(p2)-lLocal;\n"
" bool d1 = abs(e1) >= grad, d2 = abs(e2) >= grad;\n"
" int steps = u_quality > 0 ? 12 : 8;\n"
" for (int i = 1; i < steps && !(d1 && d2); i++) {\n"
" float k = i < 2 ? 1.0 : (i < 5 ? 1.5 : (i < 8 ? 2.0 : 4.0));\n"
" if (!d1) { p1 -= off*k; e1 = LS(p1)-lLocal; d1 = abs(e1) >= grad; }\n"
" if (!d2) { p2 += off*k; e2 = LS(p2)-lLocal; d2 = abs(e2) >= grad; }\n"
" }\n"
" float dist1 = horz ? v_uv.x-p1.x : v_uv.y-p1.y;\n"
" float dist2 = horz ? p2.x-v_uv.x : p2.y-v_uv.y;\n"
" bool near1 = dist1 < dist2; float dmin = min(dist1,dist2);\n"
" float len = dist1 + dist2;\n"
" bool mLess = (lM - lLocal) < 0.0;\n"
" bool good = ((near1 ? e1 : e2) < 0.0) != mLess;\n"
" float eo = good ? (0.5 - dmin/len) : 0.0;\n"
" float avg = (2.0*(lN+lS+lE+lW) + lNE+lNW+lSE+lSW) / 12.0;\n"
" float sp = clamp(abs(avg-lM)/range, 0.0, 1.0);\n"
" sp = (-2.0*sp + 3.0)*sp*sp; sp = sp*sp*0.75;\n"
" float f = max(eo, sp);\n"
" vec2 q = v_uv; if (horz) q.y += f*step; else q.x += f*step;\n"
" o = vec4(textureLod(u_tex, q, 0.0).rgb, 1.0);\n"
"}\n";
int gl_renderer_set_post_aa(int mode) {
if (mode < GL_POST_AA_OFF || mode > GL_POST_AA_FXAA_HQ) mode = GL_POST_AA_OFF;
s_post_aa = mode; /* read by the GL thread at the next present */
return 1;
}
int gl_renderer_post_aa(void) { return s_post_aa; }
uint64_t gl_renderer_post_aa_passes(void) { return s_paa_passes; }
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;
}
/* 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 (!s_paa_prog) {
s_paa_prog = build_program(PRESENT_VS, POST_AA_FS);
if (!s_paa_prog) { s_paa_failed = 1; return; }
s_paa_uTex = p_glGetUniformLocation(s_paa_prog, "u_tex");
s_paa_uRcp = p_glGetUniformLocation(s_paa_prog, "u_rcp");
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);
}
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 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);
p_glBindVertexArray(0);
p_glUseProgram(0);
glBindTexture(GL_TEXTURE_2D, 0);
s_paa_passes++;
}
/* Present sampling. u_sharp==0 is the historical behaviour: sample straight at
* v_uv, so the texture's own filter (NEAREST or LINEAR) decides everything.
*
@@ -5526,6 +5666,7 @@ void gl_renderer_shutdown(void) {
s_hold_tw = 0;
s_hold_th = 0;
s_interp_fbo = 0; /* died with the context */
post_aa_release();
}
/* CPU-readout present (24-bit FMV frames and the PSX_GL_FORCE_CPU_PRESENT
+28 -4
View File
@@ -1220,6 +1220,8 @@ extern "C" EMSCRIPTEN_KEEPALIVE void psx_web_set_smooth_60fps(int enabled) {
/* [video] options, resolved from the game config (defaults: native + AA). */
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 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. */
static int g_video_fmv_filter = PSXRecompV4::VIDEO_FMV_FILTER_DEFAULT;
@@ -1365,8 +1367,9 @@ static int effective_internal_resolution(void) {
static void apply_internal_resolution(int display_px_h) {
const int preset = effective_internal_resolution();
if (preset == PSX_IR_UNSET) return;
g_video_scale = psx_resolve_internal_scale(preset, g_video_ref_lines,
display_px_h, video_scale_ceiling());
g_video_scale = psx_resolve_internal_scale_ss(preset, g_video_ref_lines,
display_px_h, video_scale_ceiling(),
g_video_ss_milli);
}
/* The value the launcher row starts on: the preset, or the legacy factor shown
@@ -15773,6 +15776,8 @@ int main(int argc, char** argv) {
g_video_win_w = gc.runtime.video_window_width;
}
g_video_aa = gc.runtime.video_antialiasing;
g_video_post_aa = gc.runtime.video_antialiasing_mode;
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;
g_video_geometry_correction =
@@ -18062,6 +18067,22 @@ session_reboot:
else std::fprintf(stdout, "psxrecomp: PSX_INTERNAL_RESOLUTION=%s not understood "
"(native, 720p, 1080p, 1440p, 4k, 5k, 8k, display, or lines)\n", e);
}
/* PSX_SUPERSAMPLE (1.0..4.0) and PSX_AA_MODE (off, fxaa, fxaa_hq): one-run
* overrides of [video] supersample / antialiasing_mode. */
if (const char* e = std::getenv("PSX_SUPERSAMPLE")) {
const int m = psx_ss_parse_milli(e);
if (m) g_video_ss_milli = m;
else std::fprintf(stdout, "psxrecomp: PSX_SUPERSAMPLE=%s not understood (1.0..4.0)\n", e);
}
if (const char* e = std::getenv("PSX_AA_MODE")) {
if (!std::strcmp(e, "off")) g_video_post_aa = GL_POST_AA_OFF;
else if (!std::strcmp(e, "fxaa")) g_video_post_aa = GL_POST_AA_FXAA;
else if (!std::strcmp(e, "fxaa_hq")) g_video_post_aa = GL_POST_AA_FXAA_HQ;
else std::fprintf(stdout, "psxrecomp: PSX_AA_MODE=%s not understood (off, fxaa, fxaa_hq)\n", e);
}
if (g_video_ss_milli != 1000)
std::fprintf(stdout, "psxrecomp: supersample %.3gx on the internal-resolution target\n",
g_video_ss_milli / 1000.0);
apply_internal_resolution(psx_sdl_display_pixel_height(nullptr));
if (g_video_scale < 1) g_video_scale = 1;
if (const char* e = std::getenv("PSX_DYNRES"))
@@ -18459,6 +18480,7 @@ session_reboot:
return 1;
}
gl_renderer_set_swap_interval(0);
(void)gl_renderer_set_post_aa(g_video_post_aa);
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)) {
@@ -18640,8 +18662,9 @@ session_reboot:
* surface is allocated at context init below. */
if (effective_internal_resolution() == PSX_IR_DISPLAY && g_video_scale_applies) {
const int dh = psx_sdl_display_pixel_height(sdl_window);
const int s = psx_resolve_internal_scale(PSX_IR_DISPLAY, g_video_ref_lines,
dh, GL_MAX_INTERNAL_SCALE);
const int s = psx_resolve_internal_scale_ss(PSX_IR_DISPLAY, g_video_ref_lines,
dh, GL_MAX_INTERNAL_SCALE,
g_video_ss_milli);
gr_set_scale(s);
g_video_requested_scale = s;
std::fprintf(stdout, "psxrecomp: internal resolution Match display: "
@@ -18652,6 +18675,7 @@ session_reboot:
* (the ceiling) and the level steps under it (dynres_setup). */
gl_renderer_set_dynamic_resolution(
(dynres_requested() && g_video_scale_applies) ? 1 : 0);
(void)gl_renderer_set_post_aa(g_video_post_aa);
g_gl_active = (gl_renderer_init_context(sdl_window) != 0);
dynres_setup();
+131
View File
@@ -0,0 +1,131 @@
/* Post-process anti-aliasing ([video] antialiasing_mode) on a real, hidden
* OpenGL context.
*
* - off: the composed frame is untouched (byte-identical before and after a
* round trip through the AA modes; the pass never runs);
* - fxaa / fxaa_hq: a hard polygon edge gains intermediate shades (less
* stair-stepping) while flat regions away from the edge stay identical;
* - guest VRAM / readbacks are the same whatever the mode.
* With --dump PATH the off frame is written raw (RGB, bottom-up) so a run
* against another tree can be compared byte for byte. */
#define GL_PRESENT_TEST_HOOK(gen) aa_capture_hook(gen)
static void aa_capture_hook(int gen);
#define main scale_fixture_main
#include "test_gl_scale_invariance.c"
#undef main
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); }
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 aa_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++;
}
/* A white polygon with a shallow top edge (long stair steps) on black. */
static uint8_t *frame(int mode, uint64_t *vram_hash) {
#ifndef AA_FIXTURE_NO_POST_AA
gl_renderer_set_post_aa(mode);
#else
(void)mode;
#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);
glb_draw_flat_triangle(20, 150, 300, 110, 300, 200, 0x7fff);
glb_draw_flat_triangle(20, 150, 300, 200, 20, 200, 0x7fff);
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;
gl_renderer_present_vram(0, 0, FRAME_W, FRAME_H, 0, 0);
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;
}
static int mid_tones(const uint8_t *c) {
int n = 0;
for (int i = 0; i < s_cap_w * s_cap_h; i++) { int g = c[i * 3 + 1]; n += g > 24 && g < 231; }
return n;
}
/* Pixels whose 5x5 neighbourhood in ref is flat must be identical in c. */
static int flat_changed(const uint8_t *ref, const uint8_t *c) {
int bad = 0;
for (int y = 2; y < s_cap_h - 2; y++)
for (int x = 2; x < s_cap_w - 2; x++) {
const uint8_t *r = ref + ((size_t)y * s_cap_w + x) * 3;
int flat = 1;
for (int dy = -2; dy <= 2 && flat; dy++)
for (int dx = -2; dx <= 2; dx++)
if (memcmp(ref + ((size_t)(y + dy) * s_cap_w + x + dx) * 3, r, 3)) { flat = 0; break; }
if (flat && memcmp(c + ((size_t)y * s_cap_w + x) * 3, r, 3)) bad++;
}
return bad;
}
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("Post AA 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(1); gl_renderer_set_swap_interval(0);
if (!gl_renderer_init_context(win)) { fprintf(stderr, "SKIP no GL context\n"); return 77; }
uint64_t h_off = 0, h_fx = 0, h_hq = 0, h_off2 = 0;
uint8_t *off = frame(GL_POST_AA_OFF, &h_off);
if (dump) {
FILE *f = fopen(dump, "wb");
check(f && fwrite(off, 1, (size_t)s_cap_w * s_cap_h * 3, f) == (size_t)s_cap_w * s_cap_h * 3, "dump");
if (f) fclose(f);
}
#ifndef AA_FIXTURE_NO_POST_AA
check(gl_renderer_post_aa_passes() == 0, "off never runs the AA pass");
uint8_t *fx = frame(GL_POST_AA_FXAA, &h_fx);
uint8_t *hq = frame(GL_POST_AA_FXAA_HQ, &h_hq);
uint8_t *off2 = frame(GL_POST_AA_OFF, &h_off2);
const size_t bytes = (size_t)s_cap_w * s_cap_h * 3;
const int m_off = mid_tones(off), m_fx = mid_tones(fx), m_hq = mid_tones(hq);
printf("drawable=%dx%d mid_tones off=%d fxaa=%d fxaa_hq=%d flat_changed fxaa=%d hq=%d passes=%llu\n",
s_cap_w, s_cap_h, m_off, m_fx, m_hq, flat_changed(off, fx), flat_changed(off, hq),
(unsigned long long)gl_renderer_post_aa_passes());
check(gl_renderer_post_aa_passes() == 2, "one pass per AA present");
check(m_fx > m_off + 40, "fxaa softens the polygon edge");
check(m_hq > m_off + 40, "fxaa_hq softens the polygon edge");
check(memcmp(off, fx, bytes) != 0, "fxaa changes the frame");
check(flat_changed(off, fx) == 0 && flat_changed(off, hq) == 0, "flat regions untouched");
check(memcmp(off, off2, bytes) == 0, "off after AA is byte-identical");
check(h_off == h_fx && h_off == h_hq && h_off == h_off2, "guest VRAM identical in every mode");
free(fx); free(hq); free(off2);
#endif
free(off);
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;
}
@@ -200,6 +200,21 @@ int main() {
expect("legacy round trip",
psx_resolve_internal_scale(psx_ir_from_supersampling(n, 240), 240, 0, 32), n);
// Supersample: a factor on the target, not on native or the legacy factor.
expect("ss parse 1.5", psx_ss_parse_milli("1.5"), 1500);
expect("ss parse 2x", psx_ss_parse_milli("2x"), 2000);
expect("ss parse 4", psx_ss_parse_milli("4"), 4000);
expect("ss reject 0.5", psx_ss_parse_milli("0.5"), 0);
expect("ss reject 5", psx_ss_parse_milli("5"), 0);
expect("ss reject junk", psx_ss_parse_milli("2y"), 0);
expect("ss 1.0 is the plain resolve",
psx_resolve_internal_scale_ss(PSX_IR_DISPLAY, 240, 1080, 32, 1000), 5);
expect("ss 2.0 match display 1080", psx_resolve_internal_scale_ss(PSX_IR_DISPLAY, 240, 1080, 32, 2000), 9);
expect("ss 1.5 at 1440p", psx_resolve_internal_scale_ss(1440, 240, 0, 32, 1500), 9);
expect("ss leaves native", psx_resolve_internal_scale_ss(PSX_IR_NATIVE, 240, 1080, 32, 4000), 1);
expect("ss clamps to the ceiling", psx_resolve_internal_scale_ss(4320, 240, 0, 32, 4000), 32);
expect("ss unknown display stays 1", psx_resolve_internal_scale_ss(PSX_IR_DISPLAY, 240, 0, 32, 2000), 1);
launcher_round_trips();
if (g_failures) return 1;