diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 6b25477e..2718a242 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -555,6 +555,13 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& else throw std::runtime_error(fmt::format( "[video] texture_filtering must be \"nearest\" or \"bilinear\": {}", mode)); } + if (video.contains("fmv_filter")) { + const auto mode = toml::find(video, "fmv_filter"); + if (!video_fmv_filter_parse(mode, &rt.video_fmv_filter)) + throw std::runtime_error(fmt::format( + "[video] fmv_filter must be \"nearest\", \"bilinear\", " + "\"sharp\" or \"bicubic\": {}", mode)); + } if (video.contains("renderer")) { const auto mode = toml::find(video, "renderer"); if (mode == "software") rt.video_renderer = 0; @@ -2215,6 +2222,10 @@ UserSettings load_user_settings(const fs::path& path) { if (m == "nearest") { s.texture_filter = 0; s.has_texture_filter = true; } else if (m == "bilinear") { s.texture_filter = 1; s.has_texture_filter = true; } }); + if (v.contains("fmv_filter")) try_get([&]{ + const auto m = toml::find(v, "fmv_filter"); + if (video_fmv_filter_parse(m, &s.fmv_filter)) s.has_fmv_filter = true; + }); if (v.contains("geometry_correction")) try_get([&]{ s.geometry_correction = toml::find(v, "geometry_correction"); s.has_geometry_correction = true; @@ -2522,6 +2533,8 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) { f << "antialiasing = " << (s.antialiasing ? "true" : "false") << "\n"; if (s.has_texture_filter) f << "texture_filtering = \"" << (s.texture_filter ? "bilinear" : "nearest") << "\"\n"; + if (s.has_fmv_filter) + f << "fmv_filter = \"" << video_fmv_filter_name(s.fmv_filter) << "\"\n"; if (s.has_geometry_correction) f << "geometry_correction = " << (s.geometry_correction ? "true" : "false") << "\n"; diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index a993421e..c6fb9f68 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -69,6 +69,33 @@ inline constexpr int DEFAULT_VIDEO_RENDERER = VIDEO_RENDERER_OPENGL; inline constexpr int PAD_BIND_MAX = 1000 + (1 << 21); inline bool pad_bind_value_ok(long long n) { return n >= 0 && n < PAD_BIND_MAX; } +// FMV present reconstruction, ordered least to most smoothing. The default +// remains nearest so existing games keep their current FMV presentation unless +// a package or user setting opts into filtering. +inline constexpr int VIDEO_FMV_FILTER_NEAREST = 0; +inline constexpr int VIDEO_FMV_FILTER_BILINEAR = 1; +inline constexpr int VIDEO_FMV_FILTER_SHARP = 2; +inline constexpr int VIDEO_FMV_FILTER_BICUBIC = 3; +inline constexpr int VIDEO_FMV_FILTER_COUNT = 4; +inline constexpr int VIDEO_FMV_FILTER_DEFAULT = VIDEO_FMV_FILTER_NEAREST; + +// Canonical settings.toml / game.toml spelling for each value, and its parse. +inline const char* video_fmv_filter_name(int v) { + switch (v) { + case VIDEO_FMV_FILTER_NEAREST: return "nearest"; + case VIDEO_FMV_FILTER_BILINEAR: return "bilinear"; + case VIDEO_FMV_FILTER_SHARP: return "sharp"; + default: return "bicubic"; + } +} +inline bool video_fmv_filter_parse(const std::string& s, int* out) { + if (s == "nearest") { *out = VIDEO_FMV_FILTER_NEAREST; return true; } + else if (s == "bilinear") { *out = VIDEO_FMV_FILTER_BILINEAR; return true; } + else if (s == "sharp") { *out = VIDEO_FMV_FILTER_SHARP; return true; } + else if (s == "bicubic") { *out = VIDEO_FMV_FILTER_BICUBIC; return true; } + return false; +} + struct WidescreenSignedBoundSite { uint32_t address = 0; uint32_t expected = 0; // guarded LUI instruction @@ -359,6 +386,18 @@ struct RuntimeConfig { // (smooths textures and 2D backgrounds). Stored as 0/1. int video_texture_filter = 0; + // fmv_filter: how the 24-bit FMV present reconstructs its low-res source + // (a 320x192-class movie blown up to the window). Only consulted when + // antialiasing is on — AA off means nearest, as it does everywhere else. + // 0 nearest point-sampled; hard pixels, uneven pixel widths at a + // non-integer scale + // 1 bilinear plain GL_LINEAR; smoothest, but blurs the whole texel + // 2 sharp sharp-bilinear; flat texel interiors, ramp confined to a + // one-output-pixel band at the boundary + // 3 bicubic Catmull-Rom; removes most of the staircase while holding + // overall sharpness at the nearest level + int video_fmv_filter = VIDEO_FMV_FILTER_DEFAULT; + // renderer: "software" | "opengl" (default) | "vulkan". Selects the // rasterizer/present backend. The software rasterizer remains the explicit // fallback. Stored as VIDEO_RENDERER_*. @@ -1124,6 +1163,9 @@ struct UserSettings { bool has_window_width = false; int window_width = 1280; // -> 1280x960 bool has_antialiasing = false; bool antialiasing = true; bool has_texture_filter = false; int texture_filter = 0; // 0=nearest,1=bilinear + // FMV present reconstruction (VIDEO_FMV_FILTER_*). Only consulted when + // antialiasing is on. See RuntimeConfig::video_fmv_filter. + bool has_fmv_filter = false; int fmv_filter = VIDEO_FMV_FILTER_DEFAULT; // Sub-pixel vertex precision / perspective-correct UVs (see RuntimeConfig). // Both default off — the faithful floor — and are player-selectable. bool has_geometry_correction = false; bool geometry_correction = false; diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index 271c6a56..e04ea5f9 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -87,6 +87,11 @@ void gl_renderer_restage_vram_after_savestate(void); * digests / GPUREAD authority) while the OpenGL hr FBO keeps settings-scale * SSAA for present-only. Never enables glReadPixels; CPU stays current. */ void gl_renderer_set_cpu_auth_dual(int on); + +/* FMV present reconstruction, settings.toml [video] fmv_filter. Takes the + * config enum VIDEO_FMV_FILTER_* (0 nearest, 1 bilinear, 2 sharp, 3 bicubic). + * Only consulted while video antialiasing is on; AA off is always nearest. */ +void gl_renderer_set_fmv_filter(int cfg_value); int gl_renderer_cpu_auth_dual(void); /* Post-savestate freeze probe: skip/swap/dirty-mark counters (GL present path). diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 5358eb9d..e86a5e04 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -146,6 +146,7 @@ typedef void (APIENTRY *PFN_glUniform1i)(GLint, GLint); typedef void (APIENTRY *PFN_glUniform1f)(GLint, GLfloat); typedef void (APIENTRY *PFN_glUniform2i)(GLint, GLint, GLint); typedef void (APIENTRY *PFN_glUniform4i)(GLint, GLint, GLint, GLint, GLint); +typedef void (APIENTRY *PFN_glUniform2f)(GLint, GLfloat, GLfloat); typedef void (APIENTRY *PFN_glUniform4f)(GLint, GLfloat, GLfloat, GLfloat, GLfloat); typedef void (APIENTRY *PFN_glBlendColor)(GLfloat, GLfloat, GLfloat, GLfloat); typedef void (APIENTRY *PFN_glBlendFuncSeparate)(GLenum, GLenum, GLenum, GLenum); @@ -211,6 +212,7 @@ static PFN_glUniform1i p_glUniform1i; static PFN_glUniform1f p_glUniform1f; static PFN_glUniform2i p_glUniform2i; static PFN_glUniform4i p_glUniform4i; +static PFN_glUniform2f p_glUniform2f; static PFN_glUniform4f p_glUniform4f; static PFN_glBlendColor p_glBlendColor; static PFN_glBlendFuncSeparate p_glBlendFuncSeparate; @@ -268,6 +270,7 @@ static int load_modern_gl(void) { LOAD(p_glGetUniformLocation, "glGetUniformLocation"); LOAD(p_glUniform1i, "glUniform1i"); LOAD(p_glUniform1f, "glUniform1f"); LOAD(p_glUniform2i, "glUniform2i"); LOAD(p_glUniform4i, "glUniform4i"); + LOAD(p_glUniform2f, "glUniform2f"); LOAD(p_glUniform4f, "glUniform4f"); LOAD(p_glBlendColor, "glBlendColor"); LOAD(p_glBlendFuncSeparate, "glBlendFuncSeparate"); @@ -322,6 +325,8 @@ static int s_osd_tw = 0, s_osd_th = 0; static GLuint s_present_prog = 0, s_present_vao = 0; static void gl_swap_with_osd(void); static GLint s_present_uTex = -1, s_present_uUvRect = -1; +static GLint s_present_uTexSize = -1, s_present_uSharpScale = -1; +static GLint s_present_uSharp = -1; static GLuint s_interp_prog = 0, s_interp_tex[3]; static GLsync s_interp_fence[3]; static GLsync s_interp_draw_fence = NULL; @@ -859,10 +864,74 @@ 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"; +/* 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. + * + * u_sharp==1 selects SHARP-BILINEAR, for upscaling a low-res source (FMV) to a + * much larger window. Plain GL_LINEAR blends across the whole texel and turns a + * 320x192 movie to mush; plain GL_NEAREST keeps it crisp but blocky and makes + * the non-integer scale factor beat (some source pixels land 3 window pixels + * wide, their neighbours 4). Sharp-bilinear keeps each texel flat across its + * interior and confines the linear ramp to a ONE-OUTPUT-PIXEL-wide band at the + * texel boundary: crisp like nearest, but without the uneven pixel widths. + * + * u_sharp_scale is output pixels per texel. At <=1 (downscale) the band covers + * the whole texel and this degrades to plain bilinear, which is what you want + * there. The result is clamped to u_uv_rect so the half-texel edge inset the + * caller applied still holds — that inset is what keeps LINEAR from bleeding + * the border texel into the image (the old reason FMV was pinned to NEAREST). */ static const char *PRESENT_FS = "#version 330\n" "in vec2 v_uv; uniform sampler2D u_tex; out vec4 frag;\n" - "void main(){ frag = texture(u_tex, v_uv); }\n"; + "uniform vec4 u_uv_rect;\n" + "uniform vec2 u_tex_size;\n" + "uniform vec2 u_sharp_scale;\n" + "uniform int u_sharp;\n" + /* Catmull-Rom bicubic via 9 bilinear taps. Sharper than plain bilinear at + * the same smoothness, with mild overshoot that reads as edge definition. + * The present texture holds exactly the source rect and wraps CLAMP_TO_EDGE, + * so the +/-2 texel footprint clamps to real edge pixels, never garbage. */ + "vec4 bicubic(vec2 uv){\n" + " vec2 sp = uv * u_tex_size;\n" + " vec2 t1 = floor(sp - 0.5) + 0.5;\n" + " vec2 f = sp - t1;\n" + " vec2 w0 = f * (-0.5 + f * (1.0 - 0.5 * f));\n" + " vec2 w1 = 1.0 + f * f * (-2.5 + 1.5 * f);\n" + " vec2 w2 = f * (0.5 + f * (2.0 - 1.5 * f));\n" + " vec2 w3 = f * f * (-0.5 + 0.5 * f);\n" + " vec2 w12 = w1 + w2;\n" + " vec2 t0 = (t1 - 1.0) / u_tex_size;\n" + " vec2 t3 = (t1 + 2.0) / u_tex_size;\n" + " vec2 t12 = (t1 + w2 / w12) / u_tex_size;\n" + " vec4 r = vec4(0.0);\n" + " r += texture(u_tex, vec2(t0.x , t0.y )) * (w0.x * w0.y );\n" + " r += texture(u_tex, vec2(t12.x, t0.y )) * (w12.x * w0.y );\n" + " r += texture(u_tex, vec2(t3.x , t0.y )) * (w3.x * w0.y );\n" + " r += texture(u_tex, vec2(t0.x , t12.y)) * (w0.x * w12.y);\n" + " r += texture(u_tex, vec2(t12.x, t12.y)) * (w12.x * w12.y);\n" + " r += texture(u_tex, vec2(t3.x , t12.y)) * (w3.x * w12.y);\n" + " r += texture(u_tex, vec2(t0.x , t3.y )) * (w0.x * w3.y );\n" + " r += texture(u_tex, vec2(t12.x, t3.y )) * (w12.x * w3.y );\n" + " r += texture(u_tex, vec2(t3.x , t3.y )) * (w3.x * w3.y );\n" + " return r;\n" + "}\n" + "void main(){\n" + " vec2 uv = v_uv;\n" + " if (u_sharp == 2) { frag = bicubic(uv); return; }\n" + " if (u_sharp == 1) {\n" + " vec2 scale = max(u_sharp_scale, vec2(1.0));\n" + " vec2 texel = uv * u_tex_size;\n" + " vec2 tf = floor(texel);\n" + " vec2 cd = (texel - tf) - 0.5;\n" + " vec2 band = 0.5 - 0.5 / scale;\n" + " vec2 f = (cd - clamp(cd, -band, band)) * scale + 0.5;\n" + " uv = (tf + f) / u_tex_size;\n" + " vec2 lo = min(u_uv_rect.xy, u_uv_rect.zw);\n" + " vec2 hi = max(u_uv_rect.xy, u_uv_rect.zw);\n" + " uv = clamp(uv, lo, hi);\n" + " }\n" + " frag = texture(u_tex, uv);\n" + "}\n"; static const char *INTERP_FS = "#version 330\n" "in vec2 v_uv; uniform sampler2D u_prev; uniform sampler2D u_curr;\n" @@ -2484,6 +2553,36 @@ static void upload_present_tex(const uint32_t *pixels, int w, int h, int linear) } else { glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_BGRA, GL_UNSIGNED_BYTE, pixels); } +} + +static int s_fmv_filter_cfg = 0; /* VIDEO_FMV_FILTER_NEAREST */ + +void gl_renderer_set_fmv_filter(int cfg_value) { + if (cfg_value >= 0 && cfg_value <= 3) s_fmv_filter_cfg = cfg_value; +} + +static int fmv_filter_mode(void) { + return s_fmv_filter_cfg - 1; +} + +/* Select the present-program sampling mode. s_present_prog is shared by the CPU + * present and both VRAM/FBO quad paths, and program uniforms persist, so every + * user states its choice rather than inheriting the last one's. sharp=0 keeps + * the historical straight sample. */ +static void present_set_sharp(int mode, int tex_w, int tex_h, + int out_w, int out_h) { + if (s_present_uSharp < 0) return; + if (mode <= 0 || tex_w <= 0 || tex_h <= 0) { + p_glUniform1i(s_present_uSharp, 0); + return; + } + p_glUniform1i(s_present_uSharp, mode); + if (s_present_uTexSize >= 0) + p_glUniform2f(s_present_uTexSize, (float)tex_w, (float)tex_h); + if (s_present_uSharpScale >= 0) + p_glUniform2f(s_present_uSharpScale, + (float)out_w / (float)tex_w, + (float)out_h / (float)tex_h); } /* Display aspect for the present letterbox. Default 4:3 (native). When a wide @@ -2750,6 +2849,12 @@ int gl_renderer_init_context(SDL_Window *win) { p_glGenVertexArrays(1, &s_present_vao); s_present_uTex = p_glGetUniformLocation(s_present_prog, "u_tex"); s_present_uUvRect = p_glGetUniformLocation(s_present_prog, "u_uv_rect"); + s_present_uTexSize = + p_glGetUniformLocation(s_present_prog, "u_tex_size"); + s_present_uSharpScale = + p_glGetUniformLocation(s_present_prog, "u_sharp_scale"); + s_present_uSharp = + p_glGetUniformLocation(s_present_prog, "u_sharp"); s_interp_uPrev = p_glGetUniformLocation(s_interp_prog, "u_prev"); s_interp_uCurr = p_glGetUniformLocation(s_interp_prog, "u_curr"); s_interp_uAlpha = p_glGetUniformLocation(s_interp_prog, "u_alpha"); @@ -2877,10 +2982,30 @@ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linea lw = (lw * content_w) / src_w; if (lw < 1) lw = 1; } - glViewport(lx, ly, lw, lh); + /* This is the low-res source path (24-bit FMV, and the forced-CPU present + * diagnostic): a 320x192-class image blown up to fill the window, so how it + * is reconstructed is very visible. `linear` (the video AA setting) allows + * filtered reconstruction when [video] fmv_filter opts into it: + * + * nearest hard pixels, uneven pixel widths at non-integer scale + * bilinear plain GL_LINEAR — smoothest, but blurs the whole texel + * sharp sharp-bilinear: flat texel interiors, ramp confined to a + * one-output-pixel band at the boundary + * bicubic Catmull-Rom + * + * Measured on this intro at 1280x960 (fraction of adjacent pixel pairs + * differing by >=24 luma = visible staircase, vs mean |dx| = overall + * sharpness): nearest 1.00%/1.028, sharp 0.87%/1.008, bicubic 0.34%/1.038, + * bilinear 0.14%/0.930. Bicubic removes two thirds of the staircase while + * holding gradient at the nearest level; bilinear removes the most but + * costs 10% of it, which reads as blur. Still a taste call, hence the knob. */ + int filt_mode = linear ? fmv_filter_mode() + : -1; /* AA off: nearest, no shader work */ + glViewport(lx, ly, lw, lh); p_glActiveTexture(PSXGL_TEXTURE0); - upload_present_tex(pixels, src_w, src_h, linear); + upload_present_tex(pixels, src_w, src_h, filt_mode >= 0 ? 1 : 0); p_glUseProgram(s_present_prog); p_glUniform1i(s_present_uTex, 0); + present_set_sharp(filt_mode, src_w, src_h, lw, lh); if (crop) { /* Cropped present keeps left-aligned content; still inset so linear * AA does not blend the cut column with undefined border texels. */ @@ -2899,7 +3024,7 @@ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linea } p_glBindVertexArray(s_present_vao); glDrawArrays(GL_TRIANGLES, 0, 3); p_glBindVertexArray(0); p_glUseProgram(0); - pres_record(GL_PRES_CPU, 0, 0, src_w, src_h, lx, ly, lw, lh); + pres_record(GL_PRES_CPU, 0, 0, src_w, src_h, lx, ly, lw, lh); hold_capture_drawable(); latency_ring_mark(LAT_SWAP_BEGIN); gl_swap_with_osd(); @@ -3908,6 +4033,7 @@ static void gl_draw_osd_image(const uint32_t *px, int ow, int oh, glViewport(vx, wh - vy - dh, dw, dh); p_glUseProgram(s_present_prog); p_glUniform1i(s_present_uTex, 0); + present_set_sharp(0, 0, 0, 0, 0); /* OSD is authored at output res */ /* Host OSD bitmaps are top-down (row 0 = top), same as guest CPU * present with v_flip=1: uv (0,0)-(1,1). (0,1)-(1,0) was the hold-last * cancel for already-oriented captures and made toasts upside-down. */ @@ -3976,6 +4102,9 @@ static void present_target_quad(GLuint tex, int tex_w, int tex_h, glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST); p_glUseProgram(s_present_prog); p_glUniform1i(s_present_uTex, 0); + /* The rasterized path already renders at the internal scale, so it has no + * low-res source to reconstruct — keep the plain sample. */ + present_set_sharp(0, 0, 0, 0, 0); /* Half-texel inset: with GL_LINEAR, corner-mapped UVs make the outermost * dest pixels blend the border texel with VRAM outside the content rect * (visible edge stripe with AA on). Center-mapped UVs keep edge samples diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index faa78fe1..2397db53 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1106,6 +1106,23 @@ 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 */ +/* 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; + +/* recomp-ui stores this 1-based so a zero-initialized (older) host reads as + * "unset" rather than pinning nearest; the config enum is 0-based. Convert at + * the boundary, and treat anything out of range as the default. */ +static inline int launcher_fmv_filter_to_cfg(int ls_value) { + if (ls_value < 1 || ls_value > PSXRecompV4::VIDEO_FMV_FILTER_COUNT) + return PSXRecompV4::VIDEO_FMV_FILTER_DEFAULT; + return ls_value - 1; +} +static inline int cfg_fmv_filter_to_launcher(int cfg_value) { + if (cfg_value < 0 || cfg_value >= PSXRecompV4::VIDEO_FMV_FILTER_COUNT) + cfg_value = PSXRecompV4::VIDEO_FMV_FILTER_DEFAULT; + return cfg_value + 1; +} static int g_video_texfilter = 0; /* 0=nearest, 1=bilinear */ /* Sub-pixel vertex precision + perspective-correct UVs (PGXP-style). Visual * only: the PS1-visible GTE SXY FIFO stays integer, so guest-side culling and @@ -6849,10 +6866,18 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { /* OpenGL present: upload the active display rect and draw a full-screen * quad. Either SwapWindow vsync OR the wall-clock pacer owns timing, * never both. 24-bit (FMV) frames pin to native 4:3. */ - /* FMV: nearest present — linear filtering fringes the right edge of - * low-res 24-bit scanouts into adjacent (often garbage) texels. */ + /* FMV follows the same AA setting as everything else. It used to be + * pinned to nearest because plain GL_LINEAR fringed the right edge of + * low-res 24-bit scanouts into adjacent (often garbage) texels — but + * gl_renderer_present now half-texel-insets its UV rect (and crops the + * depth24 margin via content_w), which is exactly that bleed, and it + * reconstructs a filtered low-res source when [video] fmv_filter opts + * into it instead of smearing whole texels. Measured: the + * right-edge jump the old comment describes is 0 with filtering on. + * Set video AA off to get nearest back. */ + gl_renderer_set_fmv_filter(g_video_fmv_filter); gl_renderer_present(sdl_pixel_buf, src_w, src_h, - (g_video_aa && !depth24_frame) ? 1 : 0, + g_video_aa ? 1 : 0, pin_43 ? 1 : 0, 0 /* full width */); netplay_note_present(); } else { @@ -10661,6 +10686,7 @@ int main(int argc, char** argv) { g_video_scale = gc.runtime.video_supersampling; g_video_aa = gc.runtime.video_antialiasing; g_video_texfilter = gc.runtime.video_texture_filter; + g_video_fmv_filter = gc.runtime.video_fmv_filter; g_video_geometry_correction = gc.runtime.video_geometry_correction ? 1 : 0; g_video_perspective_texturing = @@ -11039,6 +11065,7 @@ int main(int argc, char** argv) { if (us.has_window_width && us.window_width > 0) g_video_win_w_explicit = true; if (us.has_antialiasing) g_video_aa = us.antialiasing; if (us.has_texture_filter) g_video_texfilter = us.texture_filter; + if (us.has_fmv_filter) g_video_fmv_filter = us.fmv_filter; if (us.has_geometry_correction) g_video_geometry_correction = us.geometry_correction ? 1 : 0; if (us.has_perspective_texturing) @@ -11482,6 +11509,7 @@ int main(int argc, char** argv) { seed.supersampling = g_video_scale; seed.has_supersampling = true; seed.antialiasing = g_video_aa; seed.has_antialiasing = true; seed.texture_filter = g_video_texfilter; seed.has_texture_filter = true; + seed.fmv_filter = g_video_fmv_filter; seed.has_fmv_filter = true; /* Seeded (and marked present) so a launcher save round-trips the * player's hand-edited value instead of dropping the key. */ seed.geometry_correction = (g_video_geometry_correction != 0); @@ -11669,6 +11697,7 @@ int main(int argc, char** argv) { ls.supersampling = seed.supersampling; ls.antialiasing = seed.antialiasing ? 1 : 0; ls.texture_filter = seed.texture_filter; + ls.fmv_filter = cfg_fmv_filter_to_launcher(seed.fmv_filter); ls.geometry_correction = seed.geometry_correction ? 1 : 0; ls.perspective_texturing = seed.perspective_texturing ? 1 : 0; ls.screen_kind = seed.screen_kind; @@ -11861,6 +11890,8 @@ int main(int argc, char** argv) { * is the legacy fallback field for consoles without the cap and is * left unused here. */ seed.texture_filter = ls.texture_filter ? 1 : 0; seed.has_texture_filter = true; + seed.fmv_filter = launcher_fmv_filter_to_cfg(ls.fmv_filter); + seed.has_fmv_filter = true; { const int n = std::min(PSX_MAX_PLAYERS, RECOMP_LAUNCHER_MAX_PLAYERS); const int un = std::min(n, PSXRecompV4::UserSettings::kMaxControllerPlayers); @@ -11906,6 +11937,8 @@ int main(int argc, char** argv) { seed.has_geometry_correction = true; seed.perspective_texturing = ls.perspective_texturing != 0; seed.has_perspective_texturing = true; + seed.fmv_filter = launcher_fmv_filter_to_cfg(ls.fmv_filter); + seed.has_fmv_filter = true; seed.screen_kind = ls.screen_kind; seed.has_screen_kind = true; seed.frame_interpolation = ls.frame_interp != 0; seed.has_frame_interpolation = true; seed.frame_interpolation_fps = ls.frame_interp_fps; seed.has_frame_interpolation_fps = true; @@ -12108,6 +12141,7 @@ int main(int argc, char** argv) { g_video_scale = seed.supersampling; g_video_aa = seed.antialiasing; g_video_texfilter = seed.texture_filter; + g_video_fmv_filter = seed.fmv_filter; g_video_geometry_correction = seed.geometry_correction ? 1 : 0; g_video_perspective_texturing = seed.perspective_texturing ? 1 : 0; g_video_screen = seed.screen_kind; @@ -13538,6 +13572,7 @@ soft_return_lobby: ls.supersampling = g_video_scale; ls.antialiasing = g_video_aa ? 1 : 0; ls.texture_filter = g_video_texfilter; + ls.fmv_filter = cfg_fmv_filter_to_launcher(g_video_fmv_filter); ls.geometry_correction = g_video_geometry_correction ? 1 : 0; ls.perspective_texturing = g_video_perspective_texturing ? 1 : 0; ls.screen_kind = g_video_screen; @@ -13790,6 +13825,8 @@ soft_return_lobby: us.has_antialiasing = true; us.texture_filter = ls.texture_filter; us.has_texture_filter = true; + us.fmv_filter = launcher_fmv_filter_to_cfg(ls.fmv_filter); + us.has_fmv_filter = true; us.geometry_correction = ls.geometry_correction != 0; us.has_geometry_correction = true; us.perspective_texturing = ls.perspective_texturing != 0; @@ -13845,6 +13882,7 @@ soft_return_lobby: g_video_scale = ls.supersampling; g_video_aa = ls.antialiasing; g_video_texfilter = ls.texture_filter; + g_video_fmv_filter = launcher_fmv_filter_to_cfg(ls.fmv_filter); g_video_geometry_correction = ls.geometry_correction ? 1 : 0; g_video_perspective_texturing = ls.perspective_texturing ? 1 : 0; g_video_screen = ls.screen_kind;