mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
runtime: keep temporal blending on the render thread
This commit is contained in:
@@ -1,3 +1,14 @@
|
||||
# Unreleased
|
||||
|
||||
- Replaces OpenGL temporal frame blending's shared second context and
|
||||
presentation thread with a deadline-driven path on the renderer's original
|
||||
thread and context. All OpenGL swaps now have one owner, no application mutex
|
||||
spans pacing or driver work, and the stock path is unchanged when disabled.
|
||||
- Documents the feature as temporal blending rather than motion-vector frame
|
||||
generation and adds cadence plus context-ownership regression tests.
|
||||
|
||||
---
|
||||
|
||||
# PSXRecomp OpenBIOS integration
|
||||
|
||||
OpenBIOS is now a first-class, redistributable runtime backend.
|
||||
|
||||
@@ -357,6 +357,16 @@ if(BUILD_TESTING)
|
||||
target_include_directories(frame_pacing_test PRIVATE include)
|
||||
add_test(NAME frame_pacing_test COMMAND frame_pacing_test)
|
||||
|
||||
add_executable(frame_interpolation_schedule_test
|
||||
tests/test_frame_interpolation_schedule.c
|
||||
src/frame_interpolation.c)
|
||||
target_include_directories(frame_interpolation_schedule_test PRIVATE include)
|
||||
if(NOT MSVC)
|
||||
target_link_libraries(frame_interpolation_schedule_test PRIVATE m)
|
||||
endif()
|
||||
add_test(NAME frame_interpolation_schedule_test
|
||||
COMMAND frame_interpolation_schedule_test)
|
||||
|
||||
add_executable(gpu_primitive_reject_test tests/test_gpu_primitive_reject.c)
|
||||
target_include_directories(gpu_primitive_reject_test PRIVATE include)
|
||||
add_test(NAME gpu_primitive_reject_test COMMAND gpu_primitive_reject_test)
|
||||
@@ -464,6 +474,9 @@ if(BUILD_TESTING)
|
||||
|
||||
find_package(Python3 COMPONENTS Interpreter)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME frame_interpolation_context_ownership_test
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_frame_interpolation_context_ownership.py)
|
||||
add_test(NAME mod_bezel_test
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_mod_bezel.py)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef PSX_FRAME_INTERPOLATION_H
|
||||
#define PSX_FRAME_INTERPOLATION_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct FrameInterpolationSchedule {
|
||||
double source_deadline;
|
||||
double next_present_deadline;
|
||||
double frame_start;
|
||||
double frame_end;
|
||||
double target_period;
|
||||
} FrameInterpolationSchedule;
|
||||
|
||||
/* Plan one stock guest-frame interval. Presentation deadlines remain anchored
|
||||
* across calls so guest work consumes part of the interval instead of slowing
|
||||
* the simulation. Returns zero when the rates or host clock are invalid. */
|
||||
int frame_interpolation_schedule_begin(FrameInterpolationSchedule *schedule,
|
||||
uint64_t now, uint64_t frequency,
|
||||
double source_hz, double target_hz);
|
||||
|
||||
/* Return each presentation deadline in the current source interval. Alpha is
|
||||
* the temporal blend position from the previous completed frame to the current
|
||||
* one. Deadlines missed by more than one output period are coalesced. */
|
||||
int frame_interpolation_schedule_next(FrameInterpolationSchedule *schedule,
|
||||
uint64_t now, uint64_t *deadline,
|
||||
float *alpha);
|
||||
|
||||
uint64_t frame_interpolation_schedule_end(
|
||||
const FrameInterpolationSchedule *schedule);
|
||||
void frame_interpolation_schedule_reset(FrameInterpolationSchedule *schedule);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -23,11 +23,13 @@ int gl_renderer_init_context(struct SDL_Window *win);
|
||||
* Safe before or after context creation; applies live when a context exists. */
|
||||
void gl_renderer_set_swap_interval(int interval);
|
||||
|
||||
/* Presentation-only frame interpolation. High-refresh sub-presents blend the
|
||||
* two most recent stable display images; guest simulation timing is unchanged. */
|
||||
/* Presentation-only temporal blending. High-refresh sub-presents blend the two
|
||||
* most recent stable display images on the owning render thread/context; this
|
||||
* does not generate motion vectors or true intermediate object positions. */
|
||||
void gl_renderer_set_interpolation(int enabled, double host_hz, double target_hz,
|
||||
int blend_mode);
|
||||
double source_hz, int blend_mode);
|
||||
void gl_renderer_set_interpolation_suspended(int suspended);
|
||||
int gl_renderer_interpolation_owns_cadence(void);
|
||||
void gl_renderer_interpolation_diag(int *enabled, int *suspended,
|
||||
int *history_frames,
|
||||
double *host_hz, double *target_hz,
|
||||
|
||||
@@ -136,13 +136,15 @@ int psx_mod_set_native_vblank_rate(uint32_t frames_per_second);
|
||||
/*
|
||||
* Enable presentation-only frame interpolation while leaving guest VBlank,
|
||||
* game logic, timers, and audio at their stock cadence. The OpenGL presenter
|
||||
* blends between completed guest frames at the requested output rate.
|
||||
* temporally blends completed guest frames at the requested output rate on its
|
||||
* owning render thread/context. It does not derive motion vectors or generate
|
||||
* true intermediate object positions.
|
||||
* A value of zero follows the measured host-display refresh rate.
|
||||
*/
|
||||
int psx_mod_set_frame_interpolation(uint32_t frames_per_second);
|
||||
/*
|
||||
* Choose how the OpenGL presenter combines completed frames. Linear is the
|
||||
* legacy full-frame crossfade. Motion-adaptive retains interpolation for
|
||||
* Choose how the OpenGL presenter combines completed frames. Linear is a
|
||||
* full-frame crossfade. Motion-adaptive retains temporal blending for
|
||||
* small temporal changes but switches large changes cleanly to reduce the
|
||||
* double-image trails produced by moving objects.
|
||||
*/
|
||||
|
||||
@@ -262,6 +262,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/timers.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/interrupts.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/frame_pacing.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/frame_interpolation.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/host_time.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_fiber.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/sio.c
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "frame_interpolation.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
void frame_interpolation_schedule_reset(FrameInterpolationSchedule *schedule) {
|
||||
if (schedule) memset(schedule, 0, sizeof(*schedule));
|
||||
}
|
||||
|
||||
int frame_interpolation_schedule_begin(FrameInterpolationSchedule *schedule,
|
||||
uint64_t now, uint64_t frequency,
|
||||
double source_hz, double target_hz) {
|
||||
double source_period;
|
||||
double target_period;
|
||||
double now_d = (double)now;
|
||||
|
||||
if (!schedule || frequency == 0 || !isfinite(source_hz) ||
|
||||
!isfinite(target_hz) || source_hz < 1.0 || target_hz < source_hz ||
|
||||
source_hz > 1000.0 || target_hz > 1000.0) {
|
||||
if (schedule) frame_interpolation_schedule_reset(schedule);
|
||||
return 0;
|
||||
}
|
||||
|
||||
source_period = (double)frequency / source_hz;
|
||||
target_period = (double)frequency / target_hz;
|
||||
if (source_period < 1.0 || target_period < 1.0) {
|
||||
frame_interpolation_schedule_reset(schedule);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Re-anchor after startup, clock discontinuity, or a sustained overrun.
|
||||
* Normal guest work lands after the prior deadline but before the next one;
|
||||
* retaining the old anchor makes that work consume the pacing budget. */
|
||||
if (schedule->source_deadline <= 0.0 ||
|
||||
now_d + source_period < schedule->source_deadline ||
|
||||
now_d > schedule->source_deadline + source_period * 4.0) {
|
||||
schedule->frame_start = now_d;
|
||||
schedule->source_deadline = now_d + source_period;
|
||||
schedule->next_present_deadline = now_d + target_period;
|
||||
} else {
|
||||
schedule->frame_start = schedule->source_deadline;
|
||||
schedule->source_deadline += source_period;
|
||||
}
|
||||
|
||||
schedule->frame_end = schedule->source_deadline;
|
||||
schedule->target_period = target_period;
|
||||
|
||||
/* Coalesce stale output deadlines. Keep the newest missed deadline so an
|
||||
* over-budget guest frame can update the window once, then resume cadence. */
|
||||
while (schedule->next_present_deadline + target_period <= now_d)
|
||||
schedule->next_present_deadline += target_period;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int frame_interpolation_schedule_next(FrameInterpolationSchedule *schedule,
|
||||
uint64_t now, uint64_t *deadline,
|
||||
float *alpha) {
|
||||
double d;
|
||||
double span;
|
||||
double a;
|
||||
|
||||
if (!schedule || schedule->target_period <= 0.0 ||
|
||||
schedule->next_present_deadline > schedule->frame_end + 0.5)
|
||||
return 0;
|
||||
|
||||
d = schedule->next_present_deadline;
|
||||
schedule->next_present_deadline += schedule->target_period;
|
||||
if (d < (double)now) d = (double)now;
|
||||
if (d > schedule->frame_end) d = schedule->frame_end;
|
||||
|
||||
span = schedule->frame_end - schedule->frame_start;
|
||||
a = span > 0.0 ? (d - schedule->frame_start) / span : 1.0;
|
||||
if (a < 0.0) a = 0.0;
|
||||
if (a > 1.0) a = 1.0;
|
||||
if (deadline) *deadline = (uint64_t)(d + 0.5);
|
||||
if (alpha) *alpha = (float)a;
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint64_t frame_interpolation_schedule_end(
|
||||
const FrameInterpolationSchedule *schedule) {
|
||||
if (!schedule || schedule->frame_end <= 0.0) return 0;
|
||||
return (uint64_t)(schedule->frame_end + 0.5);
|
||||
}
|
||||
+170
-285
@@ -65,8 +65,9 @@
|
||||
|
||||
#include "gpu.h"
|
||||
#include "gpu_render.h"
|
||||
#include "gpu_sw_renderer.h"
|
||||
#include "gpu_gl_renderer.h"
|
||||
#include "gpu_sw_renderer.h"
|
||||
#include "gpu_gl_renderer.h"
|
||||
#include "frame_interpolation.h"
|
||||
#include "host_osd.h"
|
||||
#include "psx_savestate_menu.h"
|
||||
#include "host_time.h"
|
||||
@@ -117,8 +118,6 @@
|
||||
#define PSXGL_CONSTANT_ALPHA 0x8003
|
||||
#define PSXGL_UNPACK_ROW_LENGTH 0x0CF2
|
||||
#define PSXGL_SRC1_ALPHA 0x8589
|
||||
#define PSXGL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
|
||||
#define PSXGL_TIMEOUT_IGNORED 0xFFFFFFFFFFFFFFFFull
|
||||
|
||||
#ifndef APIENTRY
|
||||
#define APIENTRY
|
||||
@@ -160,9 +159,6 @@ typedef void (APIENTRY *PFN_glBufferData)(GLenum, ptrdiff_t, const void *, GLe
|
||||
typedef void (APIENTRY *PFN_glVertexAttribPointer)(GLuint, GLint, GLenum, GLboolean, GLsizei, const void *);
|
||||
typedef void (APIENTRY *PFN_glEnableVertexAttribArray)(GLuint);
|
||||
typedef void (APIENTRY *PFN_glBindFragDataLocationIndexed)(GLuint, GLuint, GLuint, const char *);
|
||||
typedef GLsync (APIENTRY *PFN_glFenceSync)(GLenum, GLbitfield);
|
||||
typedef void (APIENTRY *PFN_glWaitSync)(GLsync, GLbitfield, GLuint64);
|
||||
typedef void (APIENTRY *PFN_glDeleteSync)(GLsync);
|
||||
typedef void (APIENTRY *PFN_glGenFramebuffers)(GLsizei, GLuint *);
|
||||
typedef void (APIENTRY *PFN_glDeleteFramebuffers)(GLsizei, const GLuint *);
|
||||
typedef void (APIENTRY *PFN_glBindFramebuffer)(GLenum, GLuint);
|
||||
@@ -226,9 +222,6 @@ static PFN_glBufferData p_glBufferData;
|
||||
static PFN_glVertexAttribPointer p_glVertexAttribPointer;
|
||||
static PFN_glEnableVertexAttribArray p_glEnableVertexAttribArray;
|
||||
static PFN_glBindFragDataLocationIndexed p_glBindFragDataLocationIndexed;
|
||||
static PFN_glFenceSync p_glFenceSync;
|
||||
static PFN_glWaitSync p_glWaitSync;
|
||||
static PFN_glDeleteSync p_glDeleteSync;
|
||||
static PFN_glGenFramebuffers p_glGenFramebuffers;
|
||||
static PFN_glDeleteFramebuffers p_glDeleteFramebuffers;
|
||||
static PFN_glBindFramebuffer p_glBindFramebuffer;
|
||||
@@ -281,9 +274,6 @@ static int load_modern_gl(void) {
|
||||
LOAD(p_glVertexAttribPointer, "glVertexAttribPointer");
|
||||
LOAD(p_glEnableVertexAttribArray, "glEnableVertexAttribArray");
|
||||
LOAD(p_glBindFragDataLocationIndexed, "glBindFragDataLocationIndexed");
|
||||
LOAD(p_glFenceSync, "glFenceSync");
|
||||
LOAD(p_glWaitSync, "glWaitSync");
|
||||
LOAD(p_glDeleteSync, "glDeleteSync");
|
||||
LOAD(p_glGenFramebuffers, "glGenFramebuffers"); LOAD(p_glBindFramebuffer, "glBindFramebuffer");
|
||||
LOAD(p_glDeleteFramebuffers, "glDeleteFramebuffers");
|
||||
LOAD(p_glFramebufferTexture2D, "glFramebufferTexture2D");
|
||||
@@ -327,9 +317,7 @@ 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;
|
||||
static GLuint s_interp_prog = 0, s_interp_tex[3];
|
||||
static GLint s_interp_uPrev = -1, s_interp_uCurr = -1;
|
||||
static GLint s_interp_uAlpha = -1, s_interp_uUvRect = -1;
|
||||
static GLint s_interp_uBlendMode = -1;
|
||||
@@ -339,21 +327,18 @@ static int s_interp_blend_mode = 0;
|
||||
static int s_interp_prev = 0, s_interp_cur = 0;
|
||||
static int s_interp_w = 0, s_interp_h = 0, s_interp_linear = 0;
|
||||
static int s_interp_force_4_3 = 0, s_interp_source_path = -1;
|
||||
static uint64_t s_interp_start = 0, s_interp_duration = 1;
|
||||
static uint64_t s_interp_last_capture = 0, s_interp_swaps = 0;
|
||||
static uint64_t s_interp_captures = 0;
|
||||
static int s_interp_diag = 0;
|
||||
static double s_interp_host_hz = 0.0;
|
||||
static double s_interp_target_hz = 0.0;
|
||||
static SDL_GLContext s_interp_ctx = NULL;
|
||||
static SDL_Thread *s_interp_thread = NULL;
|
||||
static SDL_mutex *s_interp_mutex = NULL;
|
||||
static SDL_atomic_t s_interp_thread_run;
|
||||
static GLuint s_interp_thread_vao = 0;
|
||||
static void interp_reset_history(void);
|
||||
static int interp_thread_main(void *opaque);
|
||||
static int interp_present(void);
|
||||
static void interp_draw_quad(float alpha, int lx, int ly, int lw, int lh);
|
||||
static uint64_t s_interp_swaps = 0;
|
||||
static uint64_t s_interp_captures = 0;
|
||||
static int s_interp_diag = 0;
|
||||
static double s_interp_host_hz = 0.0;
|
||||
static double s_interp_target_hz = 0.0;
|
||||
static double s_interp_source_hz = 0.0;
|
||||
static FrameInterpolationSchedule s_interp_schedule;
|
||||
static void interp_reset_history(void);
|
||||
static int interp_present(float alpha);
|
||||
static void interp_present_source_interval(void);
|
||||
static void interp_draw_quad(float alpha, int lx, int ly, int lw, int lh);
|
||||
static void present_bezel(int ww, int wh, int lx, int ly, int lw, int lh);
|
||||
|
||||
static int s_raster_ok = 0; /* full GPU pipeline available */
|
||||
|
||||
@@ -2905,35 +2890,11 @@ void gl_renderer_set_swap_interval(int interval) {
|
||||
}
|
||||
}
|
||||
|
||||
void gl_renderer_shutdown(void) {
|
||||
if (s_interp_thread) {
|
||||
SDL_AtomicSet(&s_interp_thread_run, 0);
|
||||
SDL_WaitThread(s_interp_thread, NULL);
|
||||
s_interp_thread = NULL;
|
||||
}
|
||||
if (s_interp_ctx) {
|
||||
SDL_GL_DeleteContext(s_interp_ctx);
|
||||
s_interp_ctx = NULL;
|
||||
}
|
||||
if (s_ctx) {
|
||||
SDL_GL_MakeCurrent(s_win, s_ctx);
|
||||
if (s_interp_draw_fence) {
|
||||
p_glDeleteSync(s_interp_draw_fence);
|
||||
s_interp_draw_fence = NULL;
|
||||
}
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (s_interp_fence[i]) {
|
||||
p_glDeleteSync(s_interp_fence[i]);
|
||||
s_interp_fence[i] = NULL;
|
||||
}
|
||||
}
|
||||
ensure_cpu();
|
||||
SDL_GL_DeleteContext(s_ctx); s_ctx = NULL;
|
||||
}
|
||||
if (s_interp_mutex) {
|
||||
SDL_DestroyMutex(s_interp_mutex);
|
||||
s_interp_mutex = NULL;
|
||||
}
|
||||
void gl_renderer_shutdown(void) {
|
||||
if (s_ctx) {
|
||||
ensure_cpu();
|
||||
SDL_GL_DeleteContext(s_ctx); s_ctx = NULL;
|
||||
}
|
||||
free(s_conv); s_conv = NULL;
|
||||
s_raster_ok = 0;
|
||||
/* New context regenerates s_present_tex empty; a stale size makes
|
||||
@@ -3726,107 +3687,71 @@ int gl_renderer_perf_aggregate(int wide_filter, double out[18]) {
|
||||
void gl_renderer_set_ws_ablate(int mode) { s_ws_ablate = (mode >= 0 && mode <= 3) ? mode : 0; }
|
||||
int gl_renderer_get_ws_ablate(void) { return s_ws_ablate; }
|
||||
|
||||
static void interp_reset_history_unlocked(void) {
|
||||
s_interp_valid = 0;
|
||||
s_interp_w = s_interp_h = 0;
|
||||
s_interp_start = s_interp_last_capture = 0;
|
||||
s_interp_duration = 1;
|
||||
s_interp_source_path = -1;
|
||||
}
|
||||
|
||||
static void interp_reset_history(void) {
|
||||
if (s_interp_mutex) SDL_LockMutex(s_interp_mutex);
|
||||
interp_reset_history_unlocked();
|
||||
if (s_interp_mutex) SDL_UnlockMutex(s_interp_mutex);
|
||||
}
|
||||
|
||||
void gl_renderer_set_interpolation(int enabled, double host_hz, double target_hz,
|
||||
int blend_mode) {
|
||||
double effective_hz = target_hz < 0.0
|
||||
? -1.0
|
||||
: (target_hz >= 60.0 ? target_hz : host_hz);
|
||||
int active = (enabled &&
|
||||
(effective_hz < 0.0 || effective_hz >= 50.0)) ? 1 : 0;
|
||||
const char *diag = getenv("PSX_GL_INTERP_DIAG");
|
||||
s_interp_diag = diag && diag[0] && diag[0] != '0';
|
||||
if (active && !s_interp_ctx && s_ctx) {
|
||||
if (!s_interp_mutex) s_interp_mutex = SDL_CreateMutex();
|
||||
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);
|
||||
s_interp_ctx = SDL_GL_CreateContext(s_win);
|
||||
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0);
|
||||
SDL_GL_MakeCurrent(s_win, s_ctx);
|
||||
/* The wall-clock pacer and interpolation scheduler own cadence. Keeping
|
||||
* driver vsync on the main context adds a 6-11 ms block whenever an
|
||||
* FMV temporarily suspends interpolation and the main context presents. */
|
||||
SDL_GL_SetSwapInterval(0);
|
||||
if (s_interp_ctx && s_interp_mutex) {
|
||||
SDL_AtomicSet(&s_interp_thread_run, 1);
|
||||
s_interp_thread = SDL_CreateThread(interp_thread_main,
|
||||
"psx-gl-interp", NULL);
|
||||
}
|
||||
if (!s_interp_thread) {
|
||||
SDL_AtomicSet(&s_interp_thread_run, 0);
|
||||
if (s_interp_ctx) SDL_GL_DeleteContext(s_interp_ctx);
|
||||
s_interp_ctx = NULL;
|
||||
active = 0;
|
||||
}
|
||||
}
|
||||
if (s_interp_mutex) SDL_LockMutex(s_interp_mutex);
|
||||
if (active != s_interp_enabled) interp_reset_history_unlocked();
|
||||
s_interp_enabled = active;
|
||||
s_interp_host_hz = host_hz;
|
||||
s_interp_target_hz = active ? effective_hz : 0.0;
|
||||
s_interp_blend_mode = blend_mode == 1 ? 1 : 0;
|
||||
if (s_interp_mutex) SDL_UnlockMutex(s_interp_mutex);
|
||||
if (active && effective_hz < 0.0)
|
||||
fprintf(stdout, "psxrecomp: GL frame interpolation enabled: uncapped "
|
||||
"target on %.1f Hz display (%s blend)\n", host_hz,
|
||||
s_interp_blend_mode ? "motion-adaptive" : "linear");
|
||||
else if (active)
|
||||
fprintf(stdout, "psxrecomp: GL frame interpolation enabled: %.1f FPS "
|
||||
"target on %.1f Hz display (%s blend)\n", effective_hz, host_hz,
|
||||
s_interp_blend_mode ? "motion-adaptive" : "linear");
|
||||
else
|
||||
fprintf(stdout, "psxrecomp: GL frame interpolation disabled (host %.1f Hz)\n", host_hz);
|
||||
}
|
||||
|
||||
void gl_renderer_set_interpolation_suspended(int suspended) {
|
||||
suspended = suspended ? 1 : 0;
|
||||
if (s_interp_mutex) SDL_LockMutex(s_interp_mutex);
|
||||
if (suspended != s_interp_suspended) interp_reset_history_unlocked();
|
||||
s_interp_suspended = suspended;
|
||||
if (s_interp_mutex) SDL_UnlockMutex(s_interp_mutex);
|
||||
}
|
||||
static void interp_reset_history_unlocked(void) {
|
||||
s_interp_valid = 0;
|
||||
s_interp_w = s_interp_h = 0;
|
||||
s_interp_source_path = -1;
|
||||
frame_interpolation_schedule_reset(&s_interp_schedule);
|
||||
}
|
||||
|
||||
static void interp_reset_history(void) {
|
||||
interp_reset_history_unlocked();
|
||||
}
|
||||
|
||||
void gl_renderer_set_interpolation(int enabled, double host_hz, double target_hz,
|
||||
double source_hz, int blend_mode) {
|
||||
double effective_hz = target_hz > 0.0 ? target_hz : host_hz;
|
||||
if (effective_hz < source_hz) effective_hz = source_hz;
|
||||
int active = (enabled && source_hz >= 1.0 && source_hz <= 1000.0 &&
|
||||
effective_hz >= source_hz && effective_hz <= 1000.0) ? 1 : 0;
|
||||
const char *diag = getenv("PSX_GL_INTERP_DIAG");
|
||||
s_interp_diag = diag && diag[0] && diag[0] != '0';
|
||||
if (active != s_interp_enabled || source_hz != s_interp_source_hz ||
|
||||
effective_hz != s_interp_target_hz)
|
||||
interp_reset_history_unlocked();
|
||||
s_interp_enabled = active;
|
||||
s_interp_host_hz = host_hz;
|
||||
s_interp_target_hz = active ? effective_hz : 0.0;
|
||||
s_interp_source_hz = active ? source_hz : 0.0;
|
||||
s_interp_blend_mode = blend_mode == 1 ? 1 : 0;
|
||||
if (active)
|
||||
fprintf(stdout, "psxrecomp: GL temporal frame blending enabled: %.1f "
|
||||
"presents/s from %.3f guest frames/s on the render thread "
|
||||
"(%s blend; no motion vectors)\n",
|
||||
effective_hz, source_hz,
|
||||
s_interp_blend_mode ? "change-adaptive" : "linear");
|
||||
else
|
||||
fprintf(stdout, "psxrecomp: GL temporal frame blending disabled "
|
||||
"(host %.1f Hz)\n", host_hz);
|
||||
}
|
||||
|
||||
void gl_renderer_set_interpolation_suspended(int suspended) {
|
||||
suspended = suspended ? 1 : 0;
|
||||
if (suspended != s_interp_suspended) interp_reset_history_unlocked();
|
||||
s_interp_suspended = suspended;
|
||||
}
|
||||
|
||||
int gl_renderer_interpolation_owns_cadence(void) {
|
||||
return s_ctx && s_interp_enabled && !s_interp_suspended;
|
||||
}
|
||||
|
||||
void gl_renderer_interpolation_diag(int *enabled, int *suspended,
|
||||
int *history_frames,
|
||||
double *host_hz, double *target_hz,
|
||||
uint64_t *swaps) {
|
||||
if (s_interp_mutex) SDL_LockMutex(s_interp_mutex);
|
||||
if (enabled) *enabled = s_interp_enabled;
|
||||
if (enabled) *enabled = s_interp_enabled;
|
||||
if (suspended) *suspended = s_interp_suspended;
|
||||
if (history_frames) *history_frames = s_interp_valid;
|
||||
if (host_hz) *host_hz = s_interp_host_hz;
|
||||
if (target_hz) *target_hz = s_interp_target_hz;
|
||||
if (swaps) *swaps = s_interp_swaps;
|
||||
if (s_interp_mutex) SDL_UnlockMutex(s_interp_mutex);
|
||||
}
|
||||
if (target_hz) *target_hz = s_interp_target_hz;
|
||||
if (swaps) *swaps = s_interp_swaps;
|
||||
}
|
||||
|
||||
/* Copy a stable display image out of the mutable VRAM/wide render target.
|
||||
* Returns true once both previous and current images are available. */
|
||||
/* Copy a stable display image out of the mutable VRAM/wide render target.
|
||||
* Returns true when temporal blending owns this source-frame interval. */
|
||||
static int interp_capture(GLuint fbo, int x, int y, int w, int h,
|
||||
int linear, int force_4_3, int source_path) {
|
||||
if (!s_interp_enabled || s_interp_suspended || !fbo || w <= 0 || h <= 0) return 0;
|
||||
SDL_LockMutex(s_interp_mutex);
|
||||
/* The presentation context may still have a draw queued which samples one
|
||||
* of the shared history textures. Order this context's next allocation or
|
||||
* copy after that draw before recycling a texture. glWaitSync keeps the
|
||||
* dependency on the GPU; unlike glFinish it does not stall the guest CPU. */
|
||||
if (s_interp_draw_fence) {
|
||||
p_glWaitSync(s_interp_draw_fence, 0, PSXGL_TIMEOUT_IGNORED);
|
||||
p_glDeleteSync(s_interp_draw_fence);
|
||||
s_interp_draw_fence = NULL;
|
||||
}
|
||||
if (!s_interp_enabled || s_interp_suspended || !fbo || w <= 0 || h <= 0) return 0;
|
||||
int pw = w * s_scale, ph = h * s_scale;
|
||||
if (pw != s_interp_w || ph != s_interp_h ||
|
||||
source_path != s_interp_source_path || force_4_3 != s_interp_force_4_3) {
|
||||
@@ -3843,30 +3768,11 @@ static int interp_capture(GLuint fbo, int x, int y, int w, int h,
|
||||
int dst = 0;
|
||||
if (s_interp_valid == 1) dst = s_interp_cur == 0 ? 1 : 0;
|
||||
else if (s_interp_valid >= 2) dst = s_interp_prev;
|
||||
if (s_interp_fence[dst]) {
|
||||
p_glDeleteSync(s_interp_fence[dst]);
|
||||
s_interp_fence[dst] = NULL;
|
||||
}
|
||||
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbo);
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbo);
|
||||
glBindTexture(GL_TEXTURE_2D, s_interp_tex[dst]);
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0,
|
||||
x * s_scale, y * s_scale, pw, ph);
|
||||
s_interp_fence[dst] = p_glFenceSync(PSXGL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
glFlush();
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||||
|
||||
uint64_t now = SDL_GetPerformanceCounter();
|
||||
uint64_t freq = SDL_GetPerformanceFrequency();
|
||||
if (s_interp_valid > 0 && s_interp_last_capture && now > s_interp_last_capture) {
|
||||
uint64_t d = now - s_interp_last_capture;
|
||||
uint64_t lo = freq / 240u, hi = freq / 10u;
|
||||
if (d < lo) d = lo;
|
||||
if (d > hi) d = hi;
|
||||
s_interp_duration = d;
|
||||
}
|
||||
s_interp_last_capture = now;
|
||||
s_interp_start = now;
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||||
if (s_interp_valid == 0) {
|
||||
s_interp_cur = dst;
|
||||
s_interp_valid = 1;
|
||||
@@ -3877,19 +3783,13 @@ static int interp_capture(GLuint fbo, int x, int y, int w, int h,
|
||||
}
|
||||
s_interp_linear = linear;
|
||||
s_interp_force_4_3 = force_4_3;
|
||||
s_interp_source_path = source_path;
|
||||
s_interp_captures++;
|
||||
int ready = s_interp_valid >= 2;
|
||||
SDL_UnlockMutex(s_interp_mutex);
|
||||
return ready;
|
||||
}
|
||||
|
||||
static void interp_draw_quad(float alpha, int lx, int ly, int lw, int lh) {
|
||||
int prev = s_interp_prev, curr = s_interp_cur;
|
||||
if (s_interp_fence[prev])
|
||||
p_glWaitSync(s_interp_fence[prev], 0, PSXGL_TIMEOUT_IGNORED);
|
||||
if (s_interp_fence[curr])
|
||||
p_glWaitSync(s_interp_fence[curr], 0, PSXGL_TIMEOUT_IGNORED);
|
||||
s_interp_source_path = source_path;
|
||||
s_interp_captures++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void interp_draw_quad(float alpha, int lx, int ly, int lw, int lh) {
|
||||
int prev = s_interp_prev, curr = s_interp_cur;
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||||
glViewport(lx, ly, lw, lh);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
@@ -3906,25 +3806,16 @@ static void interp_draw_quad(float alpha, int lx, int ly, int lw, int lh) {
|
||||
p_glUniform1f(s_interp_uAlpha, alpha);
|
||||
p_glUniform1i(s_interp_uBlendMode, s_interp_blend_mode);
|
||||
p_glUniform4f(s_interp_uUvRect, 0.f, 0.f, 1.f, 1.f);
|
||||
p_glBindVertexArray(s_interp_thread_vao);
|
||||
p_glBindVertexArray(s_present_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
p_glBindVertexArray(0);
|
||||
p_glUseProgram(0);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
}
|
||||
|
||||
static int interp_present(void) {
|
||||
if (!s_ctx || !s_interp_enabled || s_interp_suspended || s_interp_valid < 2) return 0;
|
||||
uint64_t now = SDL_GetPerformanceCounter();
|
||||
if (now <= s_interp_start || !s_interp_duration) return 0;
|
||||
double a = (double)(now - s_interp_start) / (double)s_interp_duration;
|
||||
/* Keep swapping at the host cadence after the blend completes. Holding
|
||||
* alpha at one is visually identical to leaving the current image on the
|
||||
* front buffer, but avoids an irregular 2/3-swap pattern on 120/144/165 Hz
|
||||
* displays while the next 59.94 Hz guest frame is being produced. */
|
||||
if (a > 1.0) a = 1.0;
|
||||
if (a < 0.0) a = 0.0;
|
||||
|
||||
static int interp_present(float alpha) {
|
||||
if (!s_ctx || !s_interp_enabled || s_interp_suspended || s_interp_valid < 1)
|
||||
return 0;
|
||||
int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||||
int lx, ly, lw, lh;
|
||||
if (s_interp_force_4_3)
|
||||
@@ -3933,78 +3824,72 @@ static int interp_present(void) {
|
||||
letterbox_rect(ww, wh, &lx, &ly, &lw, &lh);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glViewport(0, 0, ww, wh);
|
||||
if (lx != 0 || ly != 0 || lw != ww || lh != wh) {
|
||||
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
interp_draw_quad((float)a, lx, ly, lw, lh);
|
||||
if (s_interp_draw_fence) p_glDeleteSync(s_interp_draw_fence);
|
||||
s_interp_draw_fence = p_glFenceSync(PSXGL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
glFlush();
|
||||
if (lx != 0 || ly != 0 || lw != ww || lh != wh) {
|
||||
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
present_bezel(ww, wh, lx, ly, lw, lh);
|
||||
interp_draw_quad(alpha, lx, ly, lw, lh);
|
||||
pres_record(GL_PRES_INTERP, 0, 0, s_interp_w, s_interp_h,
|
||||
lx, ly, lw, lh);
|
||||
gl_swap_with_osd();
|
||||
gl_swap_with_osd();
|
||||
s_interp_swaps++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int interp_thread_main(void *opaque) {
|
||||
(void)opaque;
|
||||
if (SDL_GL_MakeCurrent(s_win, s_interp_ctx) != 0) return -1;
|
||||
SDL_GL_SetSwapInterval(0); /* host-period scheduler owns cadence */
|
||||
p_glGenVertexArrays(1, &s_interp_thread_vao);
|
||||
uint64_t freq = SDL_GetPerformanceFrequency();
|
||||
uint64_t deadline = SDL_GetPerformanceCounter();
|
||||
uint64_t diag_start = deadline, diag_swaps = 0, diag_captures = 0;
|
||||
|
||||
while (SDL_AtomicGet(&s_interp_thread_run)) {
|
||||
SDL_LockMutex(s_interp_mutex);
|
||||
double hz = s_interp_target_hz;
|
||||
SDL_UnlockMutex(s_interp_mutex);
|
||||
int uncapped = hz < 0.0;
|
||||
uint64_t now = SDL_GetPerformanceCounter();
|
||||
if (!uncapped) {
|
||||
if (hz < 50.0) hz = 60.0;
|
||||
uint64_t period = (uint64_t)((double)freq / hz);
|
||||
if (!period) period = 1;
|
||||
deadline += period;
|
||||
if (now > deadline + period * 4u) deadline = now + period;
|
||||
for (;;) {
|
||||
now = SDL_GetPerformanceCounter();
|
||||
if (now >= deadline) break;
|
||||
uint64_t remain = deadline - now;
|
||||
uint32_t ms = (uint32_t)((remain * 1000u) /
|
||||
(freq ? freq : 1u));
|
||||
if (ms > 1) psx_host_sleep_ms(ms - 1);
|
||||
}
|
||||
while (SDL_GetPerformanceCounter() < deadline) {}
|
||||
now = SDL_GetPerformanceCounter();
|
||||
} else {
|
||||
deadline = now;
|
||||
}
|
||||
|
||||
SDL_LockMutex(s_interp_mutex);
|
||||
int presented = 0;
|
||||
if (SDL_AtomicGet(&s_interp_thread_run) && s_interp_enabled)
|
||||
presented = interp_present();
|
||||
if (s_interp_diag && now - diag_start >= freq * 5u) {
|
||||
double seconds = (double)(now - diag_start) / (double)freq;
|
||||
fprintf(stdout, "psxrecomp: GL interpolation cadence: "
|
||||
"%.2f captures/s, %.2f presents/s\n",
|
||||
(double)(s_interp_captures - diag_captures) / seconds,
|
||||
(double)(s_interp_swaps - diag_swaps) / seconds);
|
||||
fflush(stdout);
|
||||
diag_start = now;
|
||||
diag_captures = s_interp_captures;
|
||||
diag_swaps = s_interp_swaps;
|
||||
}
|
||||
SDL_UnlockMutex(s_interp_mutex);
|
||||
if (uncapped && !presented) psx_host_sleep_ms(1);
|
||||
}
|
||||
p_glBindVertexArray(0);
|
||||
SDL_GL_MakeCurrent(s_win, NULL);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void interp_wait_until(uint64_t deadline, uint64_t frequency) {
|
||||
uint64_t now;
|
||||
if (!deadline || !frequency) return;
|
||||
for (;;) {
|
||||
now = SDL_GetPerformanceCounter();
|
||||
if (now >= deadline) return;
|
||||
uint64_t remain = deadline - now;
|
||||
uint32_t ms = (uint32_t)((remain * 1000u) / frequency);
|
||||
if (ms > 1) psx_host_sleep_ms(ms - 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void interp_present_source_interval(void) {
|
||||
static uint64_t diag_start, diag_swaps, diag_captures;
|
||||
uint64_t frequency = SDL_GetPerformanceFrequency();
|
||||
uint64_t now = SDL_GetPerformanceCounter();
|
||||
uint64_t deadline;
|
||||
float alpha;
|
||||
|
||||
if (!frame_interpolation_schedule_begin(
|
||||
&s_interp_schedule, now, frequency,
|
||||
s_interp_source_hz, s_interp_target_hz))
|
||||
return;
|
||||
|
||||
while (frame_interpolation_schedule_next(
|
||||
&s_interp_schedule, SDL_GetPerformanceCounter(),
|
||||
&deadline, &alpha)) {
|
||||
interp_wait_until(deadline, frequency);
|
||||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||||
(void)interp_present(alpha);
|
||||
latency_ring_mark(LAT_SWAP_END);
|
||||
}
|
||||
interp_wait_until(frame_interpolation_schedule_end(&s_interp_schedule),
|
||||
frequency);
|
||||
|
||||
now = SDL_GetPerformanceCounter();
|
||||
if (!diag_start) {
|
||||
diag_start = now;
|
||||
diag_swaps = s_interp_swaps;
|
||||
diag_captures = s_interp_captures;
|
||||
} else if (s_interp_diag && frequency &&
|
||||
now - diag_start >= frequency * 5u) {
|
||||
double seconds = (double)(now - diag_start) / (double)frequency;
|
||||
fprintf(stdout, "psxrecomp: GL temporal-blend cadence: "
|
||||
"%.2f captures/s, %.2f presents/s (single context)\n",
|
||||
(double)(s_interp_captures - diag_captures) / seconds,
|
||||
(double)(s_interp_swaps - diag_swaps) / seconds);
|
||||
fflush(stdout);
|
||||
diag_start = now;
|
||||
diag_captures = s_interp_captures;
|
||||
diag_swaps = s_interp_swaps;
|
||||
}
|
||||
}
|
||||
|
||||
/* Draw one host OSD ARGB image into the default framebuffer at (vx,vy)
|
||||
* in top-left window coordinates (y down). Bitmap is ow×oh; viewport is
|
||||
@@ -4052,11 +3937,7 @@ static void gl_draw_osd_image(const uint32_t *px, int ow, int oh,
|
||||
* cancel for already-oriented captures and made toasts upside-down. */
|
||||
p_glUniform4f(s_present_uUvRect, 0.f, 0.f, 1.f, 1.f);
|
||||
{
|
||||
GLuint vao = s_present_vao;
|
||||
if (s_interp_ctx && SDL_GL_GetCurrentContext() == s_interp_ctx &&
|
||||
s_interp_thread_vao)
|
||||
vao = s_interp_thread_vao;
|
||||
p_glBindVertexArray(vao);
|
||||
p_glBindVertexArray(s_present_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
p_glBindVertexArray(0);
|
||||
}
|
||||
@@ -4252,12 +4133,13 @@ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear,
|
||||
flush_flat_batch();
|
||||
flush_tex_batch();
|
||||
flush_cpu_upload();
|
||||
if (s_force_present_remaining <= 0 &&
|
||||
s_last_present_path == GL_PRES_VRAM &&
|
||||
s_last_dx == disp_x && s_last_dy == disp_y &&
|
||||
s_last_dw == w && s_last_dh == h &&
|
||||
!present_dirty_test(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1) &&
|
||||
!host_osd_needs_present()) {
|
||||
if (s_force_present_remaining <= 0 &&
|
||||
s_last_present_path == GL_PRES_VRAM &&
|
||||
s_last_dx == disp_x && s_last_dy == disp_y &&
|
||||
s_last_dw == w && s_last_dh == h &&
|
||||
!present_dirty_test(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1) &&
|
||||
!host_osd_needs_present() &&
|
||||
!gl_renderer_interpolation_owns_cadence()) {
|
||||
s_probe_skip++;
|
||||
gl_perf_present_enter();
|
||||
gl_perf_present_exit(0);
|
||||
@@ -4282,10 +4164,11 @@ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear,
|
||||
}
|
||||
int interp_pair = interp_capture(s_hr_fbo, disp_x, disp_y, w, h,
|
||||
linear, force_4_3, GL_PRES_VRAM);
|
||||
if (interp_pair) {
|
||||
/* Interp owns Swap — still snapshot the band so resim hold-last has
|
||||
* a frozen frame if an episode opens before the next main-thread Swap. */
|
||||
hold_capture_native_fbo(s_hr_fbo, disp_x, disp_y, w, h, force_4_3, linear);
|
||||
if (interp_pair) {
|
||||
/* Temporal blending owns this stock frame interval. Capture hold-last
|
||||
* before pacing; every blend and Swap remains on this context/thread. */
|
||||
hold_capture_native_fbo(s_hr_fbo, disp_x, disp_y, w, h, force_4_3, linear);
|
||||
interp_present_source_interval();
|
||||
gl_perf_present_exit(0);
|
||||
present_dirty_rect(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1, 0);
|
||||
present_force_consumed();
|
||||
@@ -4360,12 +4243,13 @@ int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear)
|
||||
flush_flat_batch();
|
||||
flush_tex_batch();
|
||||
flush_cpu_upload();
|
||||
if (s_force_present_remaining <= 0 &&
|
||||
s_last_present_path == GL_PRES_WIDE &&
|
||||
if (s_force_present_remaining <= 0 &&
|
||||
s_last_present_path == GL_PRES_WIDE &&
|
||||
s_last_dx == disp_x && s_last_dy == disp_y &&
|
||||
s_last_dw == g_wide_w && s_last_dh == disp_h &&
|
||||
!present_dirty_test(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1) &&
|
||||
!host_osd_needs_present()) {
|
||||
!host_osd_needs_present() &&
|
||||
!gl_renderer_interpolation_owns_cadence()) {
|
||||
s_probe_skip++;
|
||||
gl_perf_present_enter();
|
||||
gl_perf_present_exit(1);
|
||||
@@ -4385,8 +4269,9 @@ int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear)
|
||||
}
|
||||
int interp_pair = interp_capture(fbo, 0, disp_y, g_wide_w, disp_h,
|
||||
linear, 0, GL_PRES_WIDE);
|
||||
if (interp_pair) {
|
||||
hold_capture_native_fbo(fbo, 0, disp_y, g_wide_w, disp_h, 0, linear);
|
||||
if (interp_pair) {
|
||||
hold_capture_native_fbo(fbo, 0, disp_y, g_wide_w, disp_h, 0, linear);
|
||||
interp_present_source_interval();
|
||||
gl_perf_present_exit(1);
|
||||
present_dirty_rect(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1, 0);
|
||||
present_force_consumed();
|
||||
|
||||
+13
-5
@@ -1368,13 +1368,13 @@ extern "C" int psx_mod_set_frame_interpolation(
|
||||
|
||||
if (frames_per_second) {
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: mod selected presentation-only interpolation at "
|
||||
"%u FPS; guest timing remains stock\n",
|
||||
"psxrecomp: mod selected presentation-only temporal blending at "
|
||||
"%u presents/s; guest timing remains stock (no motion vectors)\n",
|
||||
(unsigned)frames_per_second);
|
||||
} else {
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: mod selected display-refresh presentation-only "
|
||||
"interpolation; guest timing remains stock\n");
|
||||
"psxrecomp: mod selected display-refresh presentation-only temporal "
|
||||
"blending; guest timing remains stock (no motion vectors)\n");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -2634,6 +2634,11 @@ static int present_effective_swap_interval(void) {
|
||||
}
|
||||
|
||||
static int present_should_wall_pace(void) {
|
||||
/* The single-context temporal-blend path subdivides this same stock frame
|
||||
* interval. Do not run the outer pacer as well or guest timing doubles. */
|
||||
if (g_frame_interpolation && g_gl_active &&
|
||||
gl_renderer_interpolation_owns_cadence())
|
||||
return 0;
|
||||
return g_frame_period_ms > 0.0 && !present_vsync_owns_cadence();
|
||||
}
|
||||
|
||||
@@ -13048,7 +13053,10 @@ session_reboot:
|
||||
g_video_scale = gr_scale();
|
||||
gl_renderer_set_interpolation(g_frame_interpolation, g_host_refresh_hz,
|
||||
(double)g_frame_interpolation_fps,
|
||||
/*blend_mode*/ 0);
|
||||
g_frame_period_ms > 0.0
|
||||
? 1000.0 / g_frame_period_ms
|
||||
: 59.94,
|
||||
g_frame_interpolation_blend);
|
||||
}
|
||||
/* Vulkan backend: create the instance/device/swapchain on the
|
||||
* SDL_WINDOW_VULKAN window. On failure, fall back to software (vkb_init
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Pin temporal blending to the renderer's original thread and GL context."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
RENDERER = (ROOT / "runtime" / "src" / "gpu_gl_renderer.c").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert RENDERER.count("SDL_GL_CreateContext(") == 1, (
|
||||
"temporal blending must not create a second GL context"
|
||||
)
|
||||
assert "SDL_CreateThread(" not in RENDERER, (
|
||||
"temporal blending must not create a presentation thread"
|
||||
)
|
||||
assert "SDL_LockMutex(" not in RENDERER, (
|
||||
"the GL renderer must not hold an application mutex across driver work"
|
||||
)
|
||||
assert RENDERER.count("SDL_GL_SwapWindow(") == 1, (
|
||||
"all OpenGL presentation must converge on one owned swap call"
|
||||
)
|
||||
assert "gl_renderer_interpolation_owns_cadence" in RENDERER
|
||||
assert "no motion vectors" in RENDERER
|
||||
|
||||
print("single-context temporal blending ownership guard passed")
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Guard the mod API's zero sentinel: follow the display, never busy-loop."""
|
||||
"""Guard the mod API's zero sentinel: follow the measured display rate."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@@ -17,9 +17,7 @@ assert EXPECTED_ASSIGNMENT in MAIN, (
|
||||
"psx_mod_set_frame_interpolation(0) must preserve the 0 sentinel so "
|
||||
"gl_renderer_set_interpolation resolves it to the measured host refresh"
|
||||
)
|
||||
assert "frames_per_second ? (int)frames_per_second : -1" not in MAIN, (
|
||||
"the -1 sentinel selects the uncapped interpolation busy loop"
|
||||
)
|
||||
assert "frames_per_second ? (int)frames_per_second : -1" not in MAIN
|
||||
assert "zero follows the measured host-display refresh rate" in HEADER, (
|
||||
"the public plugin contract must document the display-rate sentinel"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "frame_interpolation.h"
|
||||
|
||||
static int failures;
|
||||
#define CHECK(condition, message) do { \
|
||||
if (!(condition)) { fprintf(stderr, "FAIL: %s\n", message); failures++; } \
|
||||
} while (0)
|
||||
|
||||
static int run_interval(FrameInterpolationSchedule *schedule, uint64_t now,
|
||||
double source_hz, double target_hz,
|
||||
float *first_alpha, float *last_alpha) {
|
||||
uint64_t deadline;
|
||||
float alpha;
|
||||
int count = 0;
|
||||
|
||||
CHECK(frame_interpolation_schedule_begin(
|
||||
schedule, now, 1000000u, source_hz, target_hz),
|
||||
"valid rates should produce an interval");
|
||||
while (frame_interpolation_schedule_next(
|
||||
schedule, now, &deadline, &alpha)) {
|
||||
CHECK(deadline >= now, "presentation deadline must not be in the past");
|
||||
CHECK(deadline <= frame_interpolation_schedule_end(schedule),
|
||||
"presentation deadline must stay inside the source interval");
|
||||
if (count == 0 && first_alpha) *first_alpha = alpha;
|
||||
if (last_alpha) *last_alpha = alpha;
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
FrameInterpolationSchedule schedule = {0};
|
||||
float first = 0.0f, last = 0.0f;
|
||||
uint64_t end;
|
||||
|
||||
CHECK(run_interval(&schedule, 1000000u, 60.0, 120.0,
|
||||
&first, &last) == 2,
|
||||
"120 Hz should schedule two presents per 60 Hz source interval");
|
||||
CHECK(fabsf(first - 0.5f) < 0.001f && fabsf(last - 1.0f) < 0.001f,
|
||||
"120 Hz blend phases should be one-half and complete");
|
||||
|
||||
frame_interpolation_schedule_reset(&schedule);
|
||||
end = 2000000u;
|
||||
int total = 0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
total += run_interval(&schedule, end, 60.0, 144.0, NULL, NULL);
|
||||
end = frame_interpolation_schedule_end(&schedule) + 1000u;
|
||||
}
|
||||
CHECK(total == 12,
|
||||
"144 Hz should retain fractional cadence across five source frames");
|
||||
|
||||
frame_interpolation_schedule_reset(&schedule);
|
||||
CHECK(run_interval(&schedule, 3000000u, 60.0, 165.0, NULL, NULL) >= 2,
|
||||
"165 Hz should schedule multiple presents");
|
||||
end = frame_interpolation_schedule_end(&schedule);
|
||||
CHECK(frame_interpolation_schedule_begin(
|
||||
&schedule, end + 500u, 1000000u, 60.0, 165.0),
|
||||
"normal guest work should keep the existing cadence anchor");
|
||||
CHECK(frame_interpolation_schedule_end(&schedule) == end + 16667u ||
|
||||
frame_interpolation_schedule_end(&schedule) == end + 16666u,
|
||||
"guest work must consume, not extend, the next source interval");
|
||||
|
||||
CHECK(!frame_interpolation_schedule_begin(
|
||||
&schedule, 0u, 0u, 60.0, 120.0),
|
||||
"zero host frequency should be rejected");
|
||||
CHECK(!frame_interpolation_schedule_begin(
|
||||
&schedule, 0u, 1000000u, 60.0, 59.0),
|
||||
"output below source cadence should be rejected");
|
||||
|
||||
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user