mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 19:07:47 -04:00
Merge branch 'review/718-fmv-safety-20261024' into review/psx-prs-20261024
# Conflicts: # runtime/src/debug_server.c
This commit is contained in:
@@ -23,6 +23,13 @@ psxrecomp_add_runtime_target(psx-runtime
|
||||
# (docs/ENHANCEMENTS.md G1.10). Titles get their <exe>_pgxp the same way.
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_executable(gpu_fmv_hd_test tests/test_gpu_fmv_hd.cpp src/gpu_fmv_hd.c)
|
||||
target_include_directories(gpu_fmv_hd_test PRIVATE include ${PSX_SDL_INCLUDE_DIRS})
|
||||
target_compile_definitions(gpu_fmv_hd_test PRIVATE SDL_MAIN_HANDLED PSX_FMV_HD_TESTING=1
|
||||
$<$<BOOL:${PSX_SDL3}>:PSX_SDL3=1>)
|
||||
target_link_libraries(gpu_fmv_hd_test PRIVATE ${PSX_SDL_LIBRARIES})
|
||||
add_test(NAME gpu_fmv_hd_test COMMAND gpu_fmv_hd_test)
|
||||
set_tests_properties(gpu_fmv_hd_test PROPERTIES TIMEOUT 20)
|
||||
# Windows UTF-8 text boundary (issue #371): the helpers legacy Windows
|
||||
# relies on, and — with the shipped manifest — a UTF-8 process code page.
|
||||
if(WIN32)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
/* gpu_fmv_hd -- high-definition replacement of FMV movies (24-bit MDEC frames).
|
||||
*
|
||||
* A pack (<exe>/fmv_hd, or PSX_FMV_HD_DIR) holds one folder per movie:
|
||||
* index.bin a FmvHdIndexHeader, then for every source frame a sig_w x sig_h
|
||||
* luminance signature (u8, the mean of each cell of the original frame);
|
||||
* frames/NNNNNN.jpg HD frames, `sub` per source frame (interpolation), numbered
|
||||
* source_index * sub + k.
|
||||
* Every presented 24-bit frame's signature is matched against the pack (a local search
|
||||
* around the last recognised frame, then a global one); the matching HD frame, decoded
|
||||
* ahead by worker threads, is presented instead. Audio, timing and control (skipping the
|
||||
* movie) stay the game's own. No pack: no effect.
|
||||
*/
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct FmvHdIndexHeader {
|
||||
char magic[8]; /* "FMVHD002" */
|
||||
uint32_t frames; /* source frames */
|
||||
uint32_t sig_w, sig_h; /* signature size */
|
||||
uint32_t sub; /* HD frames per source frame (1 = no interpolation) */
|
||||
uint32_t fps_num, fps_den; /* source frame rate */
|
||||
uint32_t src_w, src_h; /* source frame size */
|
||||
uint32_t disp_w, disp_h; /* native PSX display the movie is shown in (e.g. 320x240) */
|
||||
int32_t off_x, off_y; /* movie position in that display; -1 = centred */
|
||||
} FmvHdIndexHeader;
|
||||
|
||||
typedef struct FmvHdImage {
|
||||
const uint8_t *rgba; /* 8-bit RGBA, valid until the next call */
|
||||
int w, h;
|
||||
uint64_t serial; /* changes with the image: otherwise no need to upload it again */
|
||||
} FmvHdImage;
|
||||
|
||||
/* A presented 24-bit frame (32-bit BGRA, src_w x src_h, content_w useful columns or 0).
|
||||
* Returns 1 and fills *out when an HD frame replaces it. Called by the presenter. */
|
||||
int gpu_fmv_hd_lookup(const uint32_t *bgra, int src_w, int src_h, int content_w, FmvHdImage *out);
|
||||
/* Presenter owner, after its render worker has drained and before SDL teardown.
|
||||
* Joins decoders and releases pack/held-image state; the next lookup can reload. */
|
||||
void gpu_fmv_hd_shutdown(void);
|
||||
|
||||
/* Presenter-owner statistics: frames in HD, signatures, tracking losses. */
|
||||
void gpu_fmv_hd_stats(uint64_t *hd_frames, uint64_t *queries, uint64_t *misses);
|
||||
|
||||
/* Presenter-owner state: JSON fields without braces. Cross-thread callers
|
||||
* use gl_renderer_fmv_hd_debug_json to synchronize with rendering. */
|
||||
int gpu_fmv_hd_debug_json(char *out, size_t cap);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -30,6 +30,8 @@ int gl_renderer_texture_banks_supported(void);
|
||||
* Both entry points drain pending draws before changing or deleting textures. */
|
||||
void gl_renderer_set_hd_texture_mode(int enabled);
|
||||
void gl_renderer_clear_hd_texture_cache(void);
|
||||
/* Drain the presenter before reading its HD movie diagnostics. */
|
||||
int gl_renderer_fmv_hd_debug_json(char *out, size_t capacity);
|
||||
/* Only on the renderer thread, before the immediately following HD draw. */
|
||||
int gl_renderer_hd_texture_cached(uint64_t key, uint64_t generation,
|
||||
uint32_t* width, uint32_t* height);
|
||||
|
||||
@@ -389,6 +389,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/present_thread.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/frame_gen.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_gl_renderer.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_fmv_hd.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gl_batch_policy_lle.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gl_batch_policy_hle.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gl_batch_policy_select.c
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
#include "card_data_writes.h"
|
||||
#include "crash_trace.h"
|
||||
#include "gpu_gl_renderer.h"
|
||||
#include "gpu_fmv_hd.h"
|
||||
#include "render_pass.h"
|
||||
#include "frame_trace.h"
|
||||
#include "mod_plugins.h" /* psx_mod_render_pass_status */
|
||||
@@ -9456,6 +9457,13 @@ static void handle_perf(int id, const char *json) {
|
||||
send_fmt("{\"id\":%d,\"ok\":true,%s}", id, buf);
|
||||
}
|
||||
|
||||
static void handle_fmv_hd(int id, const char *json) {
|
||||
(void)json;
|
||||
char buf[512];
|
||||
gl_renderer_fmv_hd_debug_json(buf, sizeof buf);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,%s}", id, buf);
|
||||
}
|
||||
|
||||
static void handle_display_aspect(int id, const char *json) {
|
||||
int num=json_get_int(json,"num",-1), den=json_get_int(json,"den",-1);
|
||||
int adaptive=json_get_int(json,"adaptive",0);
|
||||
@@ -16325,6 +16333,7 @@ static const CmdEntry s_commands[] = {
|
||||
{ "ws_aspect", handle_ws_aspect },
|
||||
{ "display_aspect", handle_display_aspect },
|
||||
{ "perf", handle_perf },
|
||||
{ "fmv_hd", handle_fmv_hd },
|
||||
{ "window_size", handle_window_size },
|
||||
{ "ws_nw", handle_ws_nw },
|
||||
{ "debug_key", handle_debug_key },
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
/* gpu_fmv_hd.c -- HD replacement of FMV movies (see gpu_fmv_hd.h).
|
||||
*
|
||||
* Recognition: a sig_w x sig_h luminance signature of the presented 24-bit frame,
|
||||
* compared (mean absolute difference) around the last recognised frame first, then over
|
||||
* the whole pack when tracking is lost. Decoding: worker threads read and decode the
|
||||
* JPEG/PNG of the upcoming frames ahead into a small ring of RGBA images.
|
||||
*/
|
||||
#include "gpu_fmv_hd.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <limits.h>
|
||||
|
||||
#include "psx_sdl.h"
|
||||
|
||||
#define STBI_NO_STDIO
|
||||
#include "../third_party/stb_image.h"
|
||||
|
||||
#ifdef PSX_FMV_HD_TESTING
|
||||
extern void *fmv_hd_test_malloc(size_t);
|
||||
extern void *fmv_hd_test_realloc(void *, size_t);
|
||||
extern SDL_Thread *fmv_hd_test_start_thread(SDL_ThreadFunction, void *);
|
||||
#define malloc fmv_hd_test_malloc
|
||||
#define realloc fmv_hd_test_realloc
|
||||
#endif
|
||||
|
||||
#define FMV_MAX_MOVIES 16
|
||||
#define FMV_RING 32
|
||||
#define FMV_WORKERS 6
|
||||
#define FMV_FILE_MAX (64u * 1024u * 1024u)
|
||||
#define FMV_DECODE_MAX (256u * 1024u * 1024u)
|
||||
#define FMV_HOLD_S 0.5 /* hold of the last HD frame (not recognised / not ready) */
|
||||
#define FMV_LOCAL_BACK 3
|
||||
#define FMV_LOCAL_AHEAD 16
|
||||
#define FMV_ACCEPT_LOCAL 14 /* mean difference accepted near the last frame */
|
||||
#define FMV_ACCEPT_GLOBAL 9 /* mean difference accepted in a global search */
|
||||
#define FMV_NEAR_GLOBAL 27 /* difference under which a vertical re-alignment is tried */
|
||||
|
||||
typedef struct Movie {
|
||||
char dir[1024];
|
||||
FmvHdIndexHeader h;
|
||||
uint8_t *sigs; /* frames * sig_w * sig_h */
|
||||
int dy; /* learnt vertical offset (native lines) */
|
||||
} Movie;
|
||||
|
||||
typedef struct Slot {
|
||||
int movie, frame; /* frame = HD index (source * sub + k); -1 free */
|
||||
int state; /* 0 free, 1 requested, 2 ready, 3 failed */
|
||||
uint8_t *rgba;
|
||||
int w, h;
|
||||
uint64_t used;
|
||||
size_t bytes;
|
||||
} Slot;
|
||||
|
||||
static int s_init = 0, s_enabled = 0;
|
||||
static Movie s_movies[FMV_MAX_MOVIES];
|
||||
static int s_nmovies = 0;
|
||||
static Slot s_ring[FMV_RING];
|
||||
static SDL_mutex *s_mutex;
|
||||
static SDL_cond *s_cond;
|
||||
static SDL_Thread *s_workers[FMV_WORKERS];
|
||||
static int s_stopping;
|
||||
static size_t s_decoded_bytes;
|
||||
static size_t s_index_bytes;
|
||||
static uint64_t s_clock = 0;
|
||||
static int s_cur_movie = -1, s_cur_src = -1;
|
||||
static uint64_t s_cur_since = 0; /* performance counter at the last frame change */
|
||||
static uint64_t s_hd_frames = 0, s_queries = 0, s_misses = 0;
|
||||
static uint8_t *s_shown = NULL; /* copy of the presented HD frame */
|
||||
static size_t s_shown_cap = 0;
|
||||
static int s_shown_w = 0, s_shown_h = 0;
|
||||
static uint64_t s_shown_at = 0, s_held = 0; /* time of the last HD frame; frames held */
|
||||
static int s_shown_movie = -1, s_shown_frame = -1;
|
||||
static uint64_t s_shown_serial = 0;
|
||||
|
||||
/* Mid-movie, falling back to the original frame (4:3 framing, another size) for a single
|
||||
* frame makes the screen jump: the last HD frame is held while it is recent. */
|
||||
static int hold_last(FmvHdImage *out) {
|
||||
if (!s_shown || !s_shown_w || !s_shown_at) return 0;
|
||||
const double age = (double)(SDL_GetPerformanceCounter() - s_shown_at) / (double)SDL_GetPerformanceFrequency();
|
||||
if (age > FMV_HOLD_S) return 0;
|
||||
out->rgba = s_shown; out->w = s_shown_w; out->h = s_shown_h; out->serial = s_shown_serial;
|
||||
++s_held;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int read_file(const char *path, uint8_t **out, size_t *len) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return 0;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long n = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (n <= 0 || (unsigned long)n > FMV_FILE_MAX) { fclose(f); return 0; }
|
||||
uint8_t *b = (uint8_t *)malloc((size_t)n);
|
||||
if (!b) { fclose(f); return 0; }
|
||||
if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return 0; }
|
||||
fclose(f);
|
||||
*out = b; *len = (size_t)n;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void frame_path(const Movie *m, int frame, char *out, size_t cap) {
|
||||
snprintf(out, cap, "%s/frames/%06d.jpg", m->dir, frame);
|
||||
FILE *f = fopen(out, "rb");
|
||||
if (f) { fclose(f); return; }
|
||||
snprintf(out, cap, "%s/frames/%06d.png", m->dir, frame);
|
||||
}
|
||||
|
||||
static int worker(void *arg) {
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
SDL_LockMutex(s_mutex);
|
||||
Slot *job = NULL;
|
||||
while (!job) {
|
||||
if (s_stopping) { SDL_UnlockMutex(s_mutex); return 0; }
|
||||
for (int i = 0; i < FMV_RING; ++i)
|
||||
if (s_ring[i].state == 1) { job = &s_ring[i]; break; }
|
||||
if (!job) SDL_CondWait(s_cond, s_mutex);
|
||||
}
|
||||
job->state = 4; /* in progress */
|
||||
const int movie = job->movie, frame = job->frame;
|
||||
SDL_UnlockMutex(s_mutex);
|
||||
|
||||
char path[1100];
|
||||
frame_path(&s_movies[movie], frame, path, sizeof path);
|
||||
uint8_t *file = NULL; size_t len = 0;
|
||||
int w = 0, h = 0, n = 0;
|
||||
uint8_t *rgba = NULL;
|
||||
size_t reserved = 0;
|
||||
if (read_file(path, &file, &len)) {
|
||||
if (stbi_info_from_memory(file, (int)len, &w, &h, &n) &&
|
||||
w > 0 && h > 0 && w <= 8192 && h <= 8192 &&
|
||||
(uint64_t)w * h * 4 <= FMV_DECODE_MAX) {
|
||||
const size_t need = (size_t)w * h * 4;
|
||||
SDL_LockMutex(s_mutex);
|
||||
if (!s_stopping && need <= FMV_DECODE_MAX - s_decoded_bytes) {
|
||||
s_decoded_bytes += need;
|
||||
reserved = need;
|
||||
}
|
||||
SDL_UnlockMutex(s_mutex);
|
||||
if (reserved) {
|
||||
int actual_w, actual_h;
|
||||
rgba = stbi_load_from_memory(file, (int)len, &actual_w, &actual_h, &n, 4);
|
||||
if (rgba && (actual_w != w || actual_h != h)) {
|
||||
stbi_image_free(rgba); rgba = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
free(file);
|
||||
}
|
||||
SDL_LockMutex(s_mutex);
|
||||
if (!s_stopping && job->movie == movie && job->frame == frame && job->state == 4) {
|
||||
job->rgba = rgba; job->w = w; job->h = h;
|
||||
job->bytes = rgba ? reserved : 0;
|
||||
job->state = rgba ? 2 : 3;
|
||||
} else if (rgba) {
|
||||
stbi_image_free(rgba);
|
||||
rgba = NULL;
|
||||
}
|
||||
if (!rgba) s_decoded_bytes -= reserved;
|
||||
SDL_UnlockMutex(s_mutex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void load_movie(const char *dir) {
|
||||
if (s_nmovies >= FMV_MAX_MOVIES) return;
|
||||
char path[1100];
|
||||
snprintf(path, sizeof path, "%s/index.bin", dir);
|
||||
uint8_t *b = NULL; size_t len = 0;
|
||||
if (!read_file(path, &b, &len)) return;
|
||||
FmvHdIndexHeader h;
|
||||
if (len < sizeof h) { free(b); return; }
|
||||
memcpy(&h, b, sizeof h);
|
||||
const uint64_t sig = (uint64_t)h.sig_w * h.sig_h;
|
||||
const uint64_t bytes = sig * h.frames;
|
||||
if (memcmp(h.magic, "FMVHD002", 8) != 0 || !h.frames || !h.sub ||
|
||||
!h.sig_w || h.sig_w > 64 || !h.sig_h || h.sig_h > 64 ||
|
||||
h.sub > INT_MAX / 2 || (uint64_t)h.frames * h.sub > INT_MAX - FMV_RING ||
|
||||
h.src_w > 8192 || h.src_h > 8192 || h.disp_w > 8192 || h.disp_h > 8192 ||
|
||||
h.off_x < -1 || h.off_x > 8192 || h.off_y < -1 || h.off_y > 8192 ||
|
||||
bytes > FMV_FILE_MAX - sizeof h || bytes > len - sizeof h ||
|
||||
bytes > FMV_FILE_MAX - s_index_bytes) { free(b); return; }
|
||||
Movie *m = &s_movies[s_nmovies];
|
||||
snprintf(m->dir, sizeof m->dir, "%s", dir);
|
||||
m->h = h;
|
||||
m->sigs = (uint8_t *)malloc((size_t)bytes);
|
||||
if (!m->sigs) { free(b); return; }
|
||||
memcpy(m->sigs, b + sizeof h, (size_t)bytes);
|
||||
s_index_bytes += (size_t)bytes;
|
||||
free(b);
|
||||
++s_nmovies;
|
||||
}
|
||||
|
||||
static void init_once(void) {
|
||||
if (s_init) return;
|
||||
s_init = 1;
|
||||
char root[1024];
|
||||
const char *env = getenv("PSX_FMV_HD_DIR");
|
||||
if (env && env[0] == '0' && env[1] == 0) return;
|
||||
if (env && env[0]) snprintf(root, sizeof root, "%s", env);
|
||||
else {
|
||||
const char *base = SDL_GetBasePath();
|
||||
snprintf(root, sizeof root, "%sfmv_hd", base ? base : "");
|
||||
#if !defined(PSX_SDL3)
|
||||
SDL_free((void *)base);
|
||||
#endif
|
||||
}
|
||||
/* Movie folders: liste.txt (one name per line), portable without dirent. */
|
||||
char list[1100];
|
||||
snprintf(list, sizeof list, "%s/liste.txt", root);
|
||||
uint8_t *b = NULL; size_t len = 0;
|
||||
if (!read_file(list, &b, &len)) {
|
||||
return;
|
||||
}
|
||||
char *txt = (char *)realloc(b, len + 1);
|
||||
if (!txt) { free(b); return; }
|
||||
txt[len] = 0;
|
||||
for (char *line = strtok(txt, "\r\n"); line; line = strtok(NULL, "\r\n")) {
|
||||
if (!line[0] || line[0] == '#') continue;
|
||||
if (strchr(line, '/') || strchr(line, '\\') || strchr(line, ':') ||
|
||||
!strcmp(line, ".") || !strcmp(line, "..")) continue;
|
||||
char dir[1100];
|
||||
snprintf(dir, sizeof dir, "%s/%s", root, line);
|
||||
load_movie(dir);
|
||||
}
|
||||
free(txt);
|
||||
if (!s_nmovies) return;
|
||||
s_mutex = SDL_CreateMutex();
|
||||
s_cond = SDL_CreateCond();
|
||||
if (!s_mutex || !s_cond) { gpu_fmv_hd_shutdown(); s_init = 1; return; }
|
||||
for (int i = 0; i < FMV_RING; ++i) { s_ring[i].movie = -1; s_ring[i].frame = -1; }
|
||||
for (int i = 0; i < FMV_WORKERS; ++i) {
|
||||
#ifdef PSX_FMV_HD_TESTING
|
||||
s_workers[i] = fmv_hd_test_start_thread(worker, NULL);
|
||||
#else
|
||||
s_workers[i] = SDL_CreateThread(worker, "psx-fmv-hd", NULL);
|
||||
#endif
|
||||
if (!s_workers[i]) { gpu_fmv_hd_shutdown(); s_init = 1; return; }
|
||||
}
|
||||
s_enabled = 1;
|
||||
}
|
||||
|
||||
void gpu_fmv_hd_shutdown(void) {
|
||||
s_enabled = 0;
|
||||
if (s_mutex) {
|
||||
SDL_LockMutex(s_mutex);
|
||||
s_stopping = 1;
|
||||
if (s_cond) SDL_CondBroadcast(s_cond);
|
||||
SDL_UnlockMutex(s_mutex);
|
||||
}
|
||||
for (int i = 0; i < FMV_WORKERS; ++i) if (s_workers[i]) {
|
||||
SDL_WaitThread(s_workers[i], NULL); s_workers[i] = NULL;
|
||||
}
|
||||
for (int i = 0; i < FMV_RING; ++i) {
|
||||
if (s_ring[i].rgba) stbi_image_free(s_ring[i].rgba);
|
||||
}
|
||||
for (int i = 0; i < s_nmovies; ++i) free(s_movies[i].sigs);
|
||||
memset(s_ring, 0, sizeof s_ring);
|
||||
memset(s_movies, 0, sizeof s_movies);
|
||||
if (s_cond) SDL_DestroyCond(s_cond);
|
||||
if (s_mutex) SDL_DestroyMutex(s_mutex);
|
||||
s_cond = NULL; s_mutex = NULL;
|
||||
free(s_shown); s_shown = NULL;
|
||||
s_shown_cap = s_decoded_bytes = s_index_bytes = 0;
|
||||
s_shown_w = s_shown_h = s_nmovies = s_init = s_stopping = 0;
|
||||
s_cur_movie = s_cur_src = s_shown_movie = s_shown_frame = -1;
|
||||
s_cur_since = s_shown_at = s_shown_serial = s_clock = 0;
|
||||
}
|
||||
|
||||
static void signature(const uint32_t *bgra, int src_w, int x0, int y0, int w, int h,
|
||||
int sw, int sh, uint8_t *out) {
|
||||
for (int gy = 0; gy < sh; ++gy) {
|
||||
const int ya = y0 + gy * h / sh, yb = y0 + (gy + 1) * h / sh;
|
||||
for (int gx = 0; gx < sw; ++gx) {
|
||||
const int xa = x0 + gx * w / sw, xb = x0 + (gx + 1) * w / sw;
|
||||
uint64_t sum = 0;
|
||||
uint32_t n = 0;
|
||||
for (int y = ya; y < yb; ++y)
|
||||
for (int x = xa; x < xb; ++x) {
|
||||
const uint32_t p = bgra[(size_t)y * src_w + x];
|
||||
const uint32_t r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255;
|
||||
sum += (r * 77 + g * 150 + b * 29) >> 8;
|
||||
++n;
|
||||
}
|
||||
out[gy * sw + gx] = (uint8_t)(n ? sum / n : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static unsigned distance(const uint8_t *a, const uint8_t *b, size_t n) {
|
||||
unsigned d = 0;
|
||||
for (size_t i = 0; i < n; ++i) d += (unsigned)abs((int)a[i] - (int)b[i]);
|
||||
return (unsigned)(d / n);
|
||||
}
|
||||
|
||||
static Slot *slot_for(int movie, int frame, int request) {
|
||||
for (int i = 0; i < FMV_RING; ++i)
|
||||
if (s_ring[i].movie == movie && s_ring[i].frame == frame) { s_ring[i].used = ++s_clock; return &s_ring[i]; }
|
||||
if (!request) return NULL;
|
||||
int victim = -1;
|
||||
for (int i = 0; i < FMV_RING; ++i) {
|
||||
if (s_ring[i].state == 4 || s_ring[i].state == 1) continue;
|
||||
if (victim < 0 || s_ring[i].used < s_ring[victim].used) victim = i;
|
||||
}
|
||||
if (victim < 0) return NULL;
|
||||
Slot *s = &s_ring[victim];
|
||||
if (s->rgba) { stbi_image_free(s->rgba); s->rgba = NULL; }
|
||||
s_decoded_bytes -= s->bytes; s->bytes = 0;
|
||||
s->movie = movie; s->frame = frame; s->state = 1; s->used = ++s_clock;
|
||||
SDL_CondSignal(s_cond);
|
||||
return s;
|
||||
}
|
||||
|
||||
static unsigned s_dbg_best = ~0u;
|
||||
static int s_dbg_src_w, s_dbg_src_h, s_dbg_frame = -1;
|
||||
|
||||
int gpu_fmv_hd_lookup(const uint32_t *bgra, int src_w, int src_h, int content_w, FmvHdImage *out) {
|
||||
init_once();
|
||||
if (!s_enabled || !bgra || src_w <= 0 || src_h <= 0 ||
|
||||
src_w > 16384 || src_h > 16384 || !out) return 0;
|
||||
++s_queries;
|
||||
s_dbg_src_w = src_w; s_dbg_src_h = src_h; s_dbg_best = ~0u;
|
||||
const int cw = (content_w > 0 && content_w < src_w) ? content_w : src_w;
|
||||
|
||||
/* Local search, then global. In a global search a nearly recognised movie is also tried
|
||||
* shifted by a few lines (each movie has its own position on screen); the offset found
|
||||
* is kept for the frames that follow. */
|
||||
static const int k_dys[] = { 0, -8, 8, -16, 16, -4, 4 };
|
||||
int best_m = -1, best_f = -1; unsigned best_d = ~0u;
|
||||
uint8_t sig[64 * 64];
|
||||
for (int pass = 0; pass < 2 && best_m < 0; ++pass) {
|
||||
for (int m = 0; m < s_nmovies; ++m) {
|
||||
Movie *mv = &s_movies[m];
|
||||
const size_t n = (size_t)mv->h.sig_w * mv->h.sig_h;
|
||||
if (n > sizeof sig) continue;
|
||||
int lo = 0, hi = (int)mv->h.frames - 1;
|
||||
if (pass == 0) {
|
||||
if (m != s_cur_movie || s_cur_src < 0) continue;
|
||||
lo = s_cur_src - FMV_LOCAL_BACK; hi = s_cur_src + FMV_LOCAL_AHEAD;
|
||||
if (lo < 0) lo = 0;
|
||||
if (hi > (int)mv->h.frames - 1) hi = (int)mv->h.frames - 1;
|
||||
}
|
||||
const int ntry = pass == 0 ? 1 : (int)(sizeof k_dys / sizeof k_dys[0]);
|
||||
unsigned m_d = ~0u; int m_f = -1, m_dy = mv->dy;
|
||||
for (int t = 0; t < ntry; ++t) {
|
||||
if (t == 1 && m_d > FMV_NEAR_GLOBAL) break;
|
||||
if (t > 0 && m_d <= FMV_ACCEPT_GLOBAL / 2) break;
|
||||
const int dy = mv->dy + k_dys[t];
|
||||
/* A src_w x src_h movie placed at (off_x, off_y) of a native disp_w x disp_h
|
||||
* display, presented here scaled to cw x src_h. */
|
||||
const int dw = mv->h.disp_w ? (int)mv->h.disp_w : cw;
|
||||
const int dh = mv->h.disp_h ? (int)mv->h.disp_h : src_h;
|
||||
int fw = (int)mv->h.src_w * cw / dw, fh = (int)mv->h.src_h * src_h / dh;
|
||||
if (fw <= 0 || fw > cw) fw = cw;
|
||||
if (fh <= 0 || fh > src_h) fh = src_h;
|
||||
const int oy = (mv->h.off_y < 0 ? ((int)dh - (int)mv->h.src_h) / 2 : mv->h.off_y) + dy;
|
||||
int x0 = mv->h.off_x < 0 ? (cw - fw) / 2 : mv->h.off_x * cw / dw;
|
||||
int y0 = oy * src_h / dh;
|
||||
if (x0 + fw > cw) x0 = cw - fw;
|
||||
if (y0 + fh > src_h || y0 < 0) continue;
|
||||
signature(bgra, src_w, x0, y0, fw, fh, (int)mv->h.sig_w, (int)mv->h.sig_h, sig);
|
||||
for (int f = lo; f <= hi; ++f) {
|
||||
const unsigned d = distance(sig, mv->sigs + (size_t)f * n, n);
|
||||
if (d < m_d) { m_d = d; m_f = f; m_dy = dy; }
|
||||
}
|
||||
}
|
||||
if (m_d < s_dbg_best) { s_dbg_best = m_d; s_dbg_frame = m_f; }
|
||||
if (m_d <= (unsigned)(pass == 0 ? FMV_ACCEPT_LOCAL : FMV_ACCEPT_GLOBAL) && m_d < best_d) {
|
||||
best_d = m_d; best_m = m; best_f = m_f; mv->dy = m_dy;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (best_m < 0) { ++s_misses; s_cur_movie = -1; s_cur_src = -1; return hold_last(out); }
|
||||
|
||||
const uint64_t now = SDL_GetPerformanceCounter();
|
||||
if (best_m != s_cur_movie || best_f != s_cur_src) {
|
||||
s_cur_movie = best_m; s_cur_src = best_f; s_cur_since = now;
|
||||
}
|
||||
const Movie *mv = &s_movies[best_m];
|
||||
int k = 0;
|
||||
if (mv->h.sub > 1 && mv->h.fps_num) {
|
||||
const double t = (double)(now - s_cur_since) / (double)SDL_GetPerformanceFrequency();
|
||||
const double phase = t * mv->h.fps_num / (mv->h.fps_den ? mv->h.fps_den : 1) * mv->h.sub;
|
||||
if (phase >= mv->h.sub) k = (int)mv->h.sub - 1;
|
||||
else if (phase > 0) k = (int)phase;
|
||||
}
|
||||
const int hd = best_f * (int)mv->h.sub + k;
|
||||
const int last = (int)(mv->h.frames * mv->h.sub) - 1;
|
||||
|
||||
SDL_LockMutex(s_mutex);
|
||||
/* Request the next frames ahead. */
|
||||
for (int a = 0; a < FMV_RING - 4 && hd + a <= last; ++a) slot_for(best_m, hd + a, 1);
|
||||
Slot *s = slot_for(best_m, hd, 0);
|
||||
/* Exact frame not ready yet: the previous decoded one, else the original. */
|
||||
for (int back = 0; (!s || s->state != 2) && back < (int)mv->h.sub * 2 && hd - back > 0; ++back)
|
||||
s = slot_for(best_m, hd - back - 1, 0);
|
||||
int ok = 0;
|
||||
if (s && s->state == 2 && s->rgba) {
|
||||
const size_t need = (size_t)s->w * s->h * 4;
|
||||
if (s->movie == s_shown_movie && s->frame == s_shown_frame && s_shown) {
|
||||
ok = 1; /* same frame as the previous refresh: no copy */
|
||||
} else {
|
||||
if (need > s_shown_cap) {
|
||||
uint8_t *next = (uint8_t *)realloc(s_shown, need);
|
||||
if (next) { s_shown = next; s_shown_cap = need; }
|
||||
else { SDL_UnlockMutex(s_mutex); return 0; }
|
||||
}
|
||||
if (s_shown) {
|
||||
memcpy(s_shown, s->rgba, need);
|
||||
s_shown_w = s->w; s_shown_h = s->h;
|
||||
s_shown_movie = s->movie; s_shown_frame = s->frame; ++s_shown_serial;
|
||||
ok = 1;
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
out->rgba = s_shown; out->w = s_shown_w; out->h = s_shown_h; out->serial = s_shown_serial;
|
||||
s_shown_at = now;
|
||||
}
|
||||
}
|
||||
SDL_UnlockMutex(s_mutex);
|
||||
if (ok) { ++s_hd_frames; return 1; }
|
||||
return hold_last(out);
|
||||
}
|
||||
|
||||
void gpu_fmv_hd_stats(uint64_t *hd_frames, uint64_t *queries, uint64_t *misses) {
|
||||
if (hd_frames) *hd_frames = s_hd_frames;
|
||||
if (queries) *queries = s_queries;
|
||||
if (misses) *misses = s_misses;
|
||||
}
|
||||
|
||||
int gpu_fmv_hd_debug_json(char *out, size_t cap) {
|
||||
return snprintf(out, cap,
|
||||
"\"init\":%d,\"enabled\":%d,\"movies\":%d,\"queries\":%llu,\"hd_frames\":%llu,"
|
||||
"\"misses\":%llu,\"last_src\":[%d,%d],\"last_best_dist\":%u,\"last_best_frame\":%d,"
|
||||
"\"cur_src\":%d,\"dy\":%d,\"held\":%llu",
|
||||
s_init, s_enabled, s_nmovies, (unsigned long long)s_queries, (unsigned long long)s_hd_frames,
|
||||
(unsigned long long)s_misses, s_dbg_src_w, s_dbg_src_h, s_dbg_best, s_dbg_frame, s_cur_src,
|
||||
s_cur_movie >= 0 ? s_movies[s_cur_movie].dy : 0, (unsigned long long)s_held);
|
||||
}
|
||||
@@ -78,6 +78,7 @@
|
||||
#include "gpu_render.h"
|
||||
#include "gl_scale_limits.h"
|
||||
#include "gpu_sw_renderer.h"
|
||||
#include "gpu_fmv_hd.h"
|
||||
#include "gpu_gl_renderer.h"
|
||||
#include "video_telemetry.h"
|
||||
#include "gpu_hd_textures.h"
|
||||
@@ -6579,8 +6580,10 @@ int gl_renderer_get_swap_interval(void) {
|
||||
static void pass_resources_release(void);
|
||||
|
||||
static void present_image_ring_gl_forget(void);
|
||||
static void fmv_hd_release(void);
|
||||
void gl_renderer_shutdown(void) {
|
||||
gl_renderer_render_thread_stop(); /* drains; the context comes back to this thread */
|
||||
fmv_hd_release();
|
||||
psx_host_set_hud_draw_cb(NULL, NULL);
|
||||
/* After the worker has stopped (it presents, and captures, on its own
|
||||
* thread) and before the context dies: forget the ring's GL names. */
|
||||
@@ -6635,10 +6638,70 @@ void gl_renderer_shutdown(void) {
|
||||
* force_4_3 pins the rect to native 4:3 regardless of the display aspect —
|
||||
* FMVs are authored 4:3 and have no GTE squash to compensate a stretch, so
|
||||
* widescreen presents them pillarboxed instead of distorted. */
|
||||
/* HD replacement of a movie frame (gpu_fmv_hd): presented at its own aspect ratio
|
||||
* (the pack's, e.g. 16:9), bilinear filtering, no chroma smoothing. */
|
||||
static GLuint s_fmvhd_tex = 0;
|
||||
int gl_renderer_fmv_hd_debug_json(char *out, size_t capacity) {
|
||||
GL_RT_SYNC("fmv_hd_debug_json");
|
||||
return gpu_fmv_hd_debug_json(out, capacity);
|
||||
}
|
||||
static int s_fmvhd_w = 0, s_fmvhd_h = 0;
|
||||
static uint64_t s_fmvhd_serial = 0;
|
||||
static void fmv_hd_release(void) {
|
||||
gpu_fmv_hd_shutdown();
|
||||
if (s_ctx && s_fmvhd_tex) glDeleteTextures(1, &s_fmvhd_tex);
|
||||
s_fmvhd_tex = 0;
|
||||
s_fmvhd_w = s_fmvhd_h = 0;
|
||||
s_fmvhd_serial = 0;
|
||||
}
|
||||
static void present_fmv_hd(const FmvHdImage *hd) {
|
||||
interp_reset_history();
|
||||
int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glViewport(0, 0, ww, wh);
|
||||
glClearColor(0.f, 0.f, 0.f, 1.f); glClear(GL_COLOR_BUFFER_BIT);
|
||||
int lx, ly, lw, lh;
|
||||
letterbox_rect_aspect(ww, wh, hd->w, hd->h, &lx, &ly, &lw, &lh);
|
||||
glViewport(lx, ly, lw, lh);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
if (!s_fmvhd_tex) glGenTextures(1, &s_fmvhd_tex);
|
||||
glBindTexture(GL_TEXTURE_2D, s_fmvhd_tex);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
if (hd->w != s_fmvhd_w || hd->h != s_fmvhd_h) {
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, hd->w, hd->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, hd->rgba);
|
||||
s_fmvhd_w = hd->w; s_fmvhd_h = hd->h; s_fmvhd_serial = hd->serial;
|
||||
} else if (hd->serial != s_fmvhd_serial) { /* 25 fps movie on a 60 Hz screen: upload on change only */
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, hd->w, hd->h, GL_RGBA, GL_UNSIGNED_BYTE, hd->rgba);
|
||||
s_fmvhd_serial = hd->serial;
|
||||
}
|
||||
p_glUseProgram(s_present_prog); p_glUniform1i(s_present_uTex, 0);
|
||||
present_set_gamma(s_present_uGamma, 1);
|
||||
present_set_sharp(0, hd->w, hd->h, lw, lh);
|
||||
PRESENT_SCANLINE(hd->h, hd->h, lh);
|
||||
p_glUniform4f(s_present_uUvRect, 0.5f / hd->w, 0.5f / hd->h, 1.f - 0.5f / hd->w, 1.f - 0.5f / hd->h);
|
||||
p_glBindVertexArray(s_present_vao); glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
p_glBindVertexArray(0); p_glUseProgram(0);
|
||||
pres_record(GL_PRES_CPU, 0, 0, hd->w, hd->h, lx, ly, lw, lh);
|
||||
hold_capture_drawable();
|
||||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||||
gl_swap_with_osd();
|
||||
latency_ring_mark(LAT_SWAP_END);
|
||||
s_probe_swap++;
|
||||
present_force_consumed();
|
||||
s_last_present_path = GL_PRES_CPU;
|
||||
}
|
||||
|
||||
void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linear,
|
||||
int force_4_3, int content_w) {
|
||||
GL_RT_SYNC("present");
|
||||
if (!s_ctx) return;
|
||||
if (s_present_d24) {
|
||||
FmvHdImage hd;
|
||||
if (gpu_fmv_hd_lookup(pixels, src_w, src_h, content_w, &hd)) { present_fmv_hd(&hd); return; }
|
||||
}
|
||||
interp_reset_history();
|
||||
int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#include "gpu_fmv_hd.h"
|
||||
#include "psx_sdl.h"
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "../third_party/stb_image.h"
|
||||
#include "../src/png_write.h"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
static std::atomic<int> allocation_failure{-1};
|
||||
static int thread_failure=-1, threads_started;
|
||||
static unsigned failures;
|
||||
extern "C" void *fmv_hd_test_malloc(size_t n) {
|
||||
return allocation_failure.fetch_sub(1)==0?nullptr:std::malloc(n);
|
||||
}
|
||||
extern "C" void *fmv_hd_test_realloc(void *p,size_t n) {
|
||||
return allocation_failure.fetch_sub(1)==0?nullptr:std::realloc(p,n);
|
||||
}
|
||||
extern "C" SDL_Thread *fmv_hd_test_start_thread(SDL_ThreadFunction fn,void *arg) {
|
||||
if(threads_started++==thread_failure)return nullptr;
|
||||
return SDL_CreateThread(fn,"fmv-test-decoder",arg);
|
||||
}
|
||||
static void check(bool ok,const char *why) {
|
||||
if(!ok){std::cerr<<why<<'\n';++failures;}
|
||||
}
|
||||
int main() {
|
||||
namespace fs=std::filesystem;
|
||||
const auto root=fs::temp_directory_path()/fs::path("psx-fmv-safety-"+
|
||||
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
fs::create_directories(root/"movie/frames");
|
||||
#ifdef _WIN32
|
||||
_putenv_s("PSX_FMV_HD_DIR",root.string().c_str());
|
||||
#else
|
||||
setenv("PSX_FMV_HD_DIR",root.string().c_str(),1);
|
||||
#endif
|
||||
std::ofstream(root/"liste.txt")<<"movie\n";
|
||||
FmvHdIndexHeader header{};
|
||||
std::memcpy(header.magic,"FMVHD002",8);
|
||||
header.frames=header.sig_w=header.sig_h=header.sub=header.fps_den=1;
|
||||
header.fps_num=30;header.src_w=header.src_h=header.disp_w=header.disp_h=2;
|
||||
auto index=[&](FmvHdIndexHeader h) {
|
||||
std::ofstream out(root/"movie/index.bin",std::ios::binary);
|
||||
out.write(reinterpret_cast<const char*>(&h),sizeof h);out.put(0);
|
||||
};
|
||||
const uint8_t rgba[16]={0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255};
|
||||
FILE *image=std::fopen((root/"movie/frames/000000.png").string().c_str(),"wb");
|
||||
check(image && png_write_rgba(image,rgba,2,2),"create valid tiny movie frame");
|
||||
if(image)std::fclose(image);
|
||||
uint32_t original[4]={0xff000000u,0xff000000u,0xff000000u,0xff000000u};
|
||||
FmvHdImage output{};
|
||||
auto lookup=[&]{return gpu_fmv_hd_lookup(original,2,2,2,&output);};
|
||||
for(unsigned variant=0;variant<5;++variant) {
|
||||
auto bad=header;
|
||||
if(variant==0)bad.sig_w=UINT32_MAX;
|
||||
if(variant==1)bad.frames=UINT32_MAX;
|
||||
if(variant==2)bad.sub=UINT32_MAX;
|
||||
if(variant==3)bad.off_x=INT32_MAX;
|
||||
if(variant==4)bad.frames=2; // truncated signature payload
|
||||
index(bad);check(!lookup(),"malformed index falls back to original");
|
||||
gpu_fmv_hd_shutdown();
|
||||
}
|
||||
index(header);
|
||||
for(int fail=0;fail<6;++fail) {
|
||||
allocation_failure=fail;threads_started=0;
|
||||
(void)lookup();gpu_fmv_hd_shutdown();
|
||||
}
|
||||
allocation_failure=-1;
|
||||
for(int fail=0;fail<6;++fail) {
|
||||
thread_failure=fail;threads_started=0;
|
||||
check(!lookup(),"partial thread startup falls back and joins workers");
|
||||
gpu_fmv_hd_shutdown();
|
||||
}
|
||||
thread_failure=-1;
|
||||
for(int restart=0;restart<3;++restart) {
|
||||
threads_started=0;bool found=false;
|
||||
for(int i=0;i<500 && !found;++i){found=lookup()!=0;if(!found)SDL_Delay(2);}
|
||||
check(found && output.w==2 && output.h==2 && output.rgba[3]==255,
|
||||
"valid frame decodes after failure or session restart");
|
||||
gpu_fmv_hd_shutdown();gpu_fmv_hd_shutdown();
|
||||
}
|
||||
fs::remove_all(root);
|
||||
if(!failures)std::cout<<"PASS FMV bounds, allocation/thread failure, real decode, joined shutdown and restart\n";
|
||||
return failures?1:0;
|
||||
}
|
||||
Reference in New Issue
Block a user