diff --git a/docs/TCP_COMMANDS.md b/docs/TCP_COMMANDS.md index 7c724721..ed6a0c72 100644 --- a/docs/TCP_COMMANDS.md +++ b/docs/TCP_COMMANDS.md @@ -55,6 +55,7 @@ Columns: **N** = native, **D** = DuckStation oracle. | `screenshot_wide_hires` | ✓ | | `path`, `base_x` | The displayed band of the native-wide surface at internal resolution (`wide_w×S` by `height×S`). `present_shot` is capped at the window; this checks a widescreen + internal-resolution combination at full size | | `present_shot` | ✓ | | `path` | PNG of the **composed present surface** — the frame after the backend fits the display buffer to the window, so it carries the presented aspect. ⚠ every other capture resolves the display buffer *before* that fit: on a 508×256 display in a 4:3 window they answer 508×256 while the player sees 640×480. Use this one for anything aspect-shaped (widescreen, letterbox), where a pre-fit buffer would hide the very stage the change touches. Staged and fulfilled on the next present, so the ack means *queued* — poll `present_shot_seq`. Unavailable headless and on the Vulkan backend (its swapchain has no readback hook) | | `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) | | `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, and queued dump count. 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) | @@ -480,9 +481,9 @@ between Play and TCP availability. ## Complete command index (generated) -**362 commands registered** — 349 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`). +**363 commands registered** — 350 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`). -74 of 362 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. +75 of 363 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. @@ -585,6 +586,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc | `event_ring_tail` | ✓ | | | | `exc_ring` | | ✓ | | | `first_failure` | ✓ | ✓ | ✓ | +| `fmv_chroma` | ✓ | | ✓ | | `fmv_state` | ✓ | | | | `fn_clear` | ✓ | | | | `fn_disable` | ✓ | | | diff --git a/docs/config_schema.md b/docs/config_schema.md index 187dd648..1c93be65 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -656,6 +656,26 @@ is a renderer redesign, not an option. SMAA needs its area/search lookup textures and three passes; on an image that is already area-resolved from a higher internal resolution it buys little over FXAA, so it is left for later. +### FMV chroma smoothing (`fmv_chroma_smoothing`) + +```toml +[video] +fmv_chroma_smoothing = false # default +fmv_filter = "bicubic" # optional upscale filter (needs antialiasing on) +``` + +MDEC movies are 4:2:0: one colour (Cb/Cr) sample per 2x2 pixels, which the +decoder replicates, so saturated edges in a 24-bit movie frame show 2-pixel +colour blocks around sharp luma. With `fmv_chroma_smoothing = true` the +OpenGL present of a 24-bit frame keeps each sample's luma (from whatever +`fmv_filter` reconstructs) and takes its chroma from a [1 2 1] tent over the +3x3 source pixels around it (BT.601), which removes the colour blocks without +softening luma detail. Guest VRAM and MDEC output are untouched; 15-bit frames +are never affected. It pairs with the existing `fmv_filter` upscale choice +(`nearest`, `bilinear`, `sharp`, `bicubic`). OpenGL only. +`PSX_FMV_CHROMA=0|1` overrides it for one run and the `fmv_chroma` TCP +command switches it live. + The runtime clamps N per backend: - Software and Vulkan stop at 4. diff --git a/generated/OpenBIOS.emitter.sha b/generated/OpenBIOS.emitter.sha index a7100871..3bb5ba02 100644 --- a/generated/OpenBIOS.emitter.sha +++ b/generated/OpenBIOS.emitter.sha @@ -1 +1 @@ -1a049b61e3b599c223bf00aa56dc55ddb9d5b6e2a894b9c34f6abaddf0cec844 +e5321c2360a49803d1595745f7076fb43655ec019c702167df2df9309cac67f7 diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 294e3a11..d86e54f0 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -813,6 +813,8 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& rt.video_pgxp_preserve_projection = toml::find(video, "pgxp_preserve_projection"); } + if (video.contains("fmv_chroma_smoothing")) + rt.video_fmv_chroma_smoothing = toml::find(video, "fmv_chroma_smoothing"); if (video.contains("pgxp_depth_buffer")) rt.video_pgxp_depth_buffer = toml::find(video, "pgxp_depth_buffer"); if (video.contains("pgxp_color_correction")) diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 86d9810b..82cf7d37 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -563,6 +563,9 @@ struct RuntimeConfig { // pgxp_depth_buffer) to close T-junction cracks above 1x ("off", // "fine" = 1 output px, "wide" = half a native px). bool video_pgxp_depth_buffer = false; + // fmv_chroma_smoothing: on 24-bit (MDEC) presents keep luma and smooth + // chroma over 3x3 source pixels (hides 4:2:0 colour blocks). OpenGL only. + bool video_fmv_chroma_smoothing = false; bool video_pgxp_color_correction = false; int video_pgxp_seam = 0; double video_pgxp_depth_threshold = 4096.0; diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index e954602b..baa041a5 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -285,6 +285,18 @@ if(BUILD_TESTING) --sdl-library "$" --output "${CMAKE_CURRENT_BINARY_DIR}/gl-hd-texture-results") set_tests_properties(gl_hd_texture_pack_test PROPERTIES LABELS "gpu;opengl;hardware" TIMEOUT 180) + # [video] fmv_chroma_smoothing: 24-bit presents lose their 2x2 chroma + # blocks, luma kept; 15-bit presents and off are byte-identical. + add_test(NAME gl_fmv_chroma_test + COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_texture_filter.py" + --fixture "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_fmv_chroma.c" + --cc "${CMAKE_C_COMPILER}" + --sdl-include "$" + --sdl-library "$" + --output "${CMAKE_CURRENT_BINARY_DIR}/gl-fmv-chroma-results") + set_tests_properties(gl_fmv_chroma_test PROPERTIES + LABELS "gpu;opengl;hardware" TIMEOUT 180 SKIP_RETURN_CODE 77) endif() add_executable(launcher_device_roundtrip_test tests/test_launcher_device_roundtrip.cpp) diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index 1d3187d0..169a3fb4 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -226,6 +226,14 @@ void gl_renderer_set_cpu_auth_dual(int on); * 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); +/* [video] fmv_chroma_smoothing: on 24-bit (MDEC) presents, keep each sample's + * luma and smooth its chroma over the 3x3 source pixels around it, hiding the + * 2x2 colour blocks of 4:2:0 video. Off by default. main.cpp notes whether + * the next gl_renderer_present is a 24-bit scanout. */ +void gl_renderer_set_fmv_chroma_smoothing(int on); +int gl_renderer_fmv_chroma_smoothing(void); +uint64_t gl_renderer_fmv_chroma_frames(void); +void gl_renderer_note_present_depth24(int d24); int gl_renderer_cpu_auth_dual(void); /* Post-savestate freeze probe: skip/swap/dirty-mark counters (GL present path). diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index dad3298a..7f3e83c2 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -8573,6 +8573,17 @@ static void handle_post_aa(int id, const char *json) gl_renderer_post_aa_gpu_us()); } +/* fmv_chroma: {"cmd":"fmv_chroma"} reads, {"cmd":"fmv_chroma","on":0|1} sets + * [video] fmv_chroma_smoothing live. Replies the smoothed-present count. */ +static void handle_fmv_chroma(int id, const char *json) +{ + int on = json_get_int(json, "on", -1); + if (on >= 0) gl_renderer_set_fmv_chroma_smoothing(on); + send_fmt("{\"id\":%d,\"ok\":true,\"on\":%d,\"frames\":%llu}", id, + gl_renderer_fmv_chroma_smoothing(), + (unsigned long long)gl_renderer_fmv_chroma_frames()); +} + static void handle_frame_perf(int id, const char *json) { (void)json; @@ -15665,6 +15676,7 @@ static const CmdEntry s_commands[] = { { "nclip_stats", handle_nclip_stats }, { "frame_perf", handle_frame_perf }, { "post_aa", handle_post_aa }, + { "fmv_chroma", handle_fmv_chroma }, { "gl_ws_ablate", handle_gl_ws_ablate }, { "gl_interp", handle_gl_interp }, { "render_pass_stats", handle_render_pass_stats }, diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 5201f60e..bcc8ea76 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -1649,6 +1649,7 @@ static const char *PRESENT_FS = "uniform vec2 u_sharp_scale;\n" "uniform int u_sharp;\n" "uniform float u_gamma;\n" + "uniform int u_chroma; /* [video] fmv_chroma_smoothing (24-bit frames) */\n" PSX_SCANLINE_UNIFORMS /* Catmull-Rom bicubic via 9 bilinear taps. Sharper than plain bilinear at * the same smoothness, with mild overshoot that reads as edge definition. @@ -1721,6 +1722,26 @@ static const char *PRESENT_FS = " }\n" " c = texture(u_tex, uv);\n" " }\n" + /* MDEC decodes 4:2:0 video: one Cb/Cr pair per 2x2 pixels, upsampled by + * replication, so saturated edges in a 24-bit movie frame show 2-pixel + * colour blocks around sharp luma. Chroma smoothing keeps the luma of the + * reconstructed sample and takes its chroma from a [1 2 1] tent over the + * 3x3 source pixels around it (BT.601), which spreads each block's colour + * into its neighbours without softening the luma detail. */ + " if (u_chroma != 0) {\n" + " vec2 ts = 1.0 / u_tex_size;\n" + " vec2 lo = min(u_uv_rect.xy, u_uv_rect.zw), hi = max(u_uv_rect.xy, u_uv_rect.zw);\n" + " vec2 base = (floor(v_uv * u_tex_size) + 0.5) * ts;\n" + " vec2 cc = vec2(0.0);\n" + " for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) {\n" + " vec3 s = texture(u_tex, clamp(base + vec2(i, j) * ts, lo, hi)).rgb;\n" + " float w = float((2 - abs(i)) * (2 - abs(j)));\n" + " cc += w * vec2(dot(s, vec3(-0.168736, -0.331264, 0.5)), dot(s, vec3(0.5, -0.418688, -0.081312)));\n" + " }\n" + " cc /= 16.0;\n" + " float y = dot(c.rgb, vec3(0.299, 0.587, 0.114));\n" + " c.rgb = clamp(vec3(y + 1.402 * cc.y, y - 0.344136 * cc.x - 0.714136 * cc.y, y + 1.772 * cc.x), 0.0, 1.0);\n" + " }\n" " c.rgb = psx_scanline(c.rgb, v_uv.y);\n" " if (u_gamma > 0.0 && u_gamma != 1.0) c.rgb = pow(max(c.rgb, vec3(0.0)), vec3(1.0 / u_gamma));\n" " frag = c;\n" @@ -5317,6 +5338,18 @@ static void upload_present_tex(const uint32_t *pixels, int w, int h, int linear) } static int s_fmv_filter_cfg = 0; /* VIDEO_FMV_FILTER_NEAREST */ +/* [video] fmv_chroma_smoothing, and whether the frame being presented is a + * 24-bit (MDEC) scanout (main.cpp notes it before gl_renderer_present). */ +static int s_fmv_chroma = 0, s_present_d24 = 0; +static GLint s_present_uChroma = -1; +static uint64_t s_fmv_chroma_frames = 0; +void gl_renderer_set_fmv_chroma_smoothing(int on) { + GL_RT_SYNC("fmv_chroma"); + s_fmv_chroma = on ? 1 : 0; +} +int gl_renderer_fmv_chroma_smoothing(void) { return s_fmv_chroma; } +uint64_t gl_renderer_fmv_chroma_frames(void) { GL_RT_SYNC("fmv_chroma_stats"); return s_fmv_chroma_frames; } +void gl_renderer_note_present_depth24(int d24) { GL_RT_SYNC("present_d24"); s_present_d24 = d24 ? 1 : 0; } void gl_renderer_set_fmv_filter(int cfg_value) { GL_RT_SYNC("set_fmv_filter"); @@ -6046,6 +6079,8 @@ int gl_renderer_init_context(SDL_Window *win) { p_glGetUniformLocation(s_present_prog, "u_sharp"); s_present_uGamma = p_glGetUniformLocation(s_present_prog, "u_gamma"); + s_present_uChroma = + p_glGetUniformLocation(s_present_prog, "u_chroma"); s_present_uScanline = p_glGetUniformLocation(s_present_prog, "u_scanline"); s_present_uScanStrength = @@ -6245,7 +6280,16 @@ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linea } else { p_glUniform4f(s_present_uUvRect, 0.f, 0.f, 1.f, 1.f); } + const int chroma = s_fmv_chroma && s_present_d24; + if (chroma) { + /* present_set_sharp only sets the source size for filtered modes; the + * chroma tent needs it in every mode. */ + p_glUniform1i(s_present_uChroma, 1); + if (s_present_uTexSize >= 0) p_glUniform2f(s_present_uTexSize, (float)src_w, (float)src_h); + s_fmv_chroma_frames++; + } p_glBindVertexArray(s_present_vao); glDrawArrays(GL_TRIANGLES, 0, 3); + if (chroma) p_glUniform1i(s_present_uChroma, 0); p_glBindVertexArray(0); p_glUseProgram(0); pres_record(GL_PRES_CPU, 0, 0, src_w, src_h, lx, ly, lw, lh); hold_capture_drawable(); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index c94e99de..39ad873f 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1280,6 +1280,7 @@ static int g_video_pgxp_position_fallback = 1; static int g_video_pgxp_preserve_projection = 0; /* PGXP renderer features (G1.14); PSX_PGXP_DEPTH / _COLOR / _SEAM override. */ static int g_video_pgxp_depth_buffer = 0; +static int g_video_fmv_chroma = 0; /* [video] fmv_chroma_smoothing / PSX_FMV_CHROMA */ static int g_video_pgxp_color_correction = 0; static int g_video_pgxp_seam = 0; static float g_video_pgxp_depth_threshold = 4096.0f; @@ -9192,6 +9193,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { * 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_note_present_depth24(depth24_frame ? 1 : 0); gl_renderer_present(sdl_pixel_buf, src_w, src_h, g_video_aa ? 1 : 0, pin_43 ? 1 : 0, 0 /* full width */); @@ -16069,6 +16071,7 @@ int main(int argc, char** argv) { gc.runtime.video_pgxp_preserve_projection ? 1 : 0; g_video_pgxp_mod_only = gc.runtime.video_pgxp_mod_only ? 1 : 0; g_video_pgxp_depth_buffer = gc.runtime.video_pgxp_depth_buffer ? 1 : 0; + g_video_fmv_chroma = gc.runtime.video_fmv_chroma_smoothing ? 1 : 0; g_video_pgxp_color_correction = gc.runtime.video_pgxp_color_correction ? 1 : 0; g_video_pgxp_seam = gc.runtime.video_pgxp_seam; g_video_pgxp_depth_threshold = (float)gc.runtime.video_pgxp_depth_threshold; @@ -18455,6 +18458,8 @@ session_reboot: if (const char* e = std::getenv("PSX_PGXP_COLOR")) g_video_pgxp_color_correction = (*e && *e != '0'); if (const char* e = std::getenv("PSX_PGXP_SEAM")) g_video_pgxp_seam = std::atoi(e); gl_renderer_set_pgxp_depth(g_video_pgxp_depth_buffer); + if (const char* e = std::getenv("PSX_FMV_CHROMA")) g_video_fmv_chroma = (*e && *e != '0'); + gl_renderer_set_fmv_chroma_smoothing(g_video_fmv_chroma); gl_renderer_set_pgxp_color_perspective(g_video_pgxp_color_correction); gl_renderer_set_pgxp_seam(g_video_pgxp_seam); if (const char* e = std::getenv("PSX_PGXP_DEPTH_THRESHOLD")) g_video_pgxp_depth_threshold = (float)std::atof(e); diff --git a/runtime/tests/test_gl_fmv_chroma.c b/runtime/tests/test_gl_fmv_chroma.c new file mode 100644 index 00000000..66a42448 --- /dev/null +++ b/runtime/tests/test_gl_fmv_chroma.c @@ -0,0 +1,180 @@ +/* [video] fmv_chroma_smoothing on a real, hidden OpenGL context. + * + * A synthetic 4:2:0-style 24-bit frame (constant luma, chroma constant per + * 2x2 block and flipping between saturated hues block to block, plus a sharp + * luma edge on the right) is presented through gl_renderer_present, the + * CPU/24-bit present path: + * - off, and on for a frame that is not a 24-bit scanout, are byte-identical; + * - on, chroma steps between neighbouring output pixels shrink a lot while + * luma stays (the flat-luma area keeps its luma within a few levels, and + * the sharp luma edge stays as sharp); + * - the smoothed-present counter counts only 24-bit presents. + * Original source-owned scene; no retail payload. */ +#define GL_PRESENT_TEST_HOOK(gen) cap_hook(gen) +static void cap_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); } +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 cap_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++; +} + +#define SW 160 +#define SH 120 +static uint32_t src[SW * SH]; +static uint32_t rgb(double y, double cb, double cr) { + double r = y + 1.402 * cr, g = y - 0.344136 * cb - 0.714136 * cr, b = y + 1.772 * cb; + int R = (int)(r < 0 ? 0 : r > 1 ? 255 : r * 255 + 0.5); + int G = (int)(g < 0 ? 0 : g > 1 ? 255 : g * 255 + 0.5); + int B = (int)(b < 0 ? 0 : b > 1 ? 255 : b * 255 + 0.5); + return 0xff000000u | ((uint32_t)R << 16) | ((uint32_t)G << 8) | (uint32_t)B; /* BGRA bytes */ +} +static void frame_src(void) { + for (int y = 0; y < SH; y++) + for (int x = 0; x < SW; x++) { + int blk = ((x >> 1) + (y >> 1)) & 1; + if (x < 120) src[y * SW + x] = rgb(0.5, blk ? 0.15 : -0.15, blk ? -0.15 : 0.15); + else src[y * SW + x] = rgb(x < 140 ? 0.15 : 0.85, 0.0, 0.0); /* sharp grey luma edge */ + } +} +static uint8_t *present(int d24) { + const int n0 = s_cap_n; + gl_renderer_note_present_depth24(d24); + gl_renderer_present(src, SW, SH, 0, 1, 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 double luma(const uint8_t *p) { return 0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2]; } +static double cb(const uint8_t *p) { return -0.168736 * p[0] - 0.331264 * p[1] + 0.5 * p[2]; } +/* Over the chroma-block area of the letterboxed image (middle rows, left + * 70%): mean |Cb step| between horizontal neighbours, and max luma error + * against the flat luma. Then the luma edge's width in output pixels. */ +static double s_mean_sat; +static void stats(const uint8_t *c, double *step, double *lerr, int *edge_w) { + double s = 0, sat = 0; int n = 0; *lerr = 0; + int y0 = s_cap_h / 3, y1 = 2 * s_cap_h / 3; + for (int y = y0; y < y1; y += 3) + for (int x = s_cap_w / 8 + 4; x < s_cap_w * 55 / 100; x++) { + const uint8_t *p = c + ((size_t)y * s_cap_w + x) * 3; + s += fabs(cb(p + 3) - cb(p)); n++; + int mx = p[0] > p[1] ? (p[0] > p[2] ? p[0] : p[2]) : (p[1] > p[2] ? p[1] : p[2]); + int mn = p[0] < p[1] ? (p[0] < p[2] ? p[0] : p[2]) : (p[1] < p[2] ? p[1] : p[2]); + sat += mx - mn; + double e = fabs(luma(p) - 127.5); + if (e > *lerr) *lerr = e; + } + *step = s / n; + s_mean_sat = sat / n; + int w = 0; + const int y = s_cap_h / 2; + for (int x = s_cap_w * 125 / 160; x < s_cap_w * 155 / 160; x++) { + double l = luma(c + ((size_t)y * s_cap_w + x) * 3); + w += l > 60 && l < 195; + } + *edge_w = w; +} + +int main(void) { + 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("FMV chroma 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; } + frame_src(); + check(gl_renderer_fmv_chroma_smoothing() == 0, "off by default"); + uint8_t *off = present(1); + gl_renderer_set_fmv_chroma_smoothing(1); + uint8_t *not24 = present(0); + const size_t bytes = (size_t)s_cap_w * s_cap_h * 3; + check(memcmp(off, not24, bytes) == 0, "a 15-bit present is untouched"); + check(gl_renderer_fmv_chroma_frames() == 0, "only 24-bit presents are smoothed"); + uint8_t *on = present(1); + check(gl_renderer_fmv_chroma_frames() == 1, "24-bit present smoothed"); + double s_off, s_on, l_off, l_on; int e_off, e_on; + stats(off, &s_off, &l_off, &e_off); + const double sat_off = s_mean_sat; + stats(on, &s_on, &l_on, &e_on); + const double sat_on = s_mean_sat; + printf("mean saturation off=%.1f on=%.1f\n", sat_off, sat_on); + printf("drawable=%dx%d cb_step off=%.2f on=%.2f luma_err off=%.1f on=%.1f edge_w off=%d on=%d\n", + s_cap_w, s_cap_h, s_off, s_on, l_off, l_on, e_off, e_on); + check(s_on < s_off / 3.0, "chroma block steps shrink"); + /* The blocks alternate between two hues, so the tent's average keeps a + * known fraction of the colour: never grey. A flat-chroma frame keeps + * its saturation exactly (below). */ + check(sat_on > 2.0, "colour is not lost"); + check(l_on < 6.0, "luma kept"); + check(e_on <= e_off + 1, "luma edge stays as sharp"); + /* Uniform chroma inside letterbox bars: smoothing must keep the colour + * exactly (within rounding). A tent that lost the source size would + * sample the black corner and turn the movie grey. */ + /* A letterboxed movie: black bars around a uniform colour. */ + for (int y = 0; y < SH; y++) + for (int x = 0; x < SW; x++) + src[y * SW + x] = (y < 12 || y >= SH - 12) ? rgb(0.0, 0.0, 0.0) : rgb(0.45, 0.12, -0.10); + uint8_t *flat_on = present(1); + gl_renderer_set_fmv_chroma_smoothing(0); + uint8_t *flat_off = present(1); + int worst = 0; + /* Away from the bars and the image's outer columns (the tent + * legitimately mixes at an edge). */ + for (int y = 3 * s_cap_h / 8; y < 5 * s_cap_h / 8; y++) + for (int x = 3 * 8; x < (s_cap_w - 8) * 3; x++) { + size_t i = (size_t)y * s_cap_w * 3 + x; + int d = abs((int)flat_on[i] - (int)flat_off[i]); + if (d > worst) worst = d; + } + printf("flat chroma worst channel delta=%d\n", worst); + check(worst <= 2, "uniform chroma kept exactly"); + free(flat_on); free(flat_off); + frame_src(); + uint8_t *off2 = present(1); + check(memcmp(off, off2, bytes) == 0, "off again is byte-identical"); + free(off); free(not24); free(on); free(off2); + 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; +}