mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Dynamic resolution: allocate native-wide surfaces at the current level
A native-wide surface was allocated at the dynamic-resolution ceiling and rendered into its lower-left corner. On GL-on-Metal every render pass loads and stores the whole attachment, so the wide surfaces cost the ceiling's bandwidth at every level. Allocate them at the level they render at and reallocate at each step, rescaling the presented rows and margins from the old surface. The limit check stays at the ceiling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -176,9 +176,15 @@ resolution (below), not of this layer.
|
||||
## Dynamic resolution (`[video] dynamic_resolution`)
|
||||
|
||||
Opt-in as before (`dynamic_resolution = true`, `dynamic_resolution_min`;
|
||||
`PSX_DYNRES=0/1`, `PSX_DYNRES_MIN` for one run); no new keys. The surfaces
|
||||
are allocated at the configured internal resolution (the ceiling, e.g. Match
|
||||
display) and the level steps between that and the minimum. With the render
|
||||
`PSX_DYNRES=0/1`, `PSX_DYNRES_MIN` for one run); no new keys. The hr
|
||||
(VRAM) surface is allocated at the configured internal resolution (the
|
||||
ceiling, e.g. Match display) and the level steps between that and the
|
||||
minimum. The native-wide surfaces are allocated at the current level and
|
||||
reallocated at every step (the presented rows and margins rescaled from the
|
||||
old surface into the new one): on Apple's GL-on-Metal every render pass into
|
||||
an attachment loads and stores the whole attachment, so a wide surface held
|
||||
at the ceiling cost the ceiling's bandwidth at every level (R4 2P VS: about
|
||||
4 s at 35-40 Hz after the race starts at a 10x ceiling, see the PR). With the render
|
||||
thread off nothing changes: the emulation thread's wall-time controller
|
||||
(`dynres_*` in `dynamic_resolution.c`) decides. With it on, the guest's
|
||||
frame no longer pays for GL, so its wall time says nothing about whether the
|
||||
|
||||
@@ -323,6 +323,7 @@ typedef struct GlDynresStats {
|
||||
* the reseed, the rects back, the native-wide surfaces. */
|
||||
double last_ms, last_prep_ms, last_seed_ms, last_rects_ms, last_wide_ms;
|
||||
int last_rects; /* rects kept at full detail */
|
||||
uint64_t wide_reallocs; /* native-wide surfaces reallocated at a step */
|
||||
} GlDynresStats;
|
||||
void gl_renderer_dynres_stats(GlDynresStats *out);
|
||||
/* Host time the renderer spent, as running totals in performance-counter
|
||||
|
||||
@@ -449,11 +449,11 @@ static int s_hr_scale = 1;
|
||||
/* Dynamic internal resolution ([video] dynamic_resolution; the step block
|
||||
* near the end of this file). 0 = off, and then every surface is allocated at
|
||||
* the scale it renders at, exactly as before. Otherwise the scale the hr
|
||||
* surface and the native-wide surfaces were allocated at (the ceiling): the
|
||||
* current level (s_hr_scale == s_out_scale, never above it) renders into
|
||||
* their lower-left VRAM*S corner. Only allocation and the present's
|
||||
* normalised UVs read it; everything else keeps taking the two scales above
|
||||
* at use. */
|
||||
* surface was allocated at (the ceiling): the current level (s_hr_scale ==
|
||||
* s_out_scale, never above it) renders into its lower-left VRAM*S corner.
|
||||
* Only allocation and the present's normalised UVs read it; everything else
|
||||
* keeps taking the two scales above at use. Native-wide surfaces are
|
||||
* allocated at the level instead (s_wide_as) and reallocated at each step. */
|
||||
static int s_alloc_scale = 0;
|
||||
/* The scale a surface rendering at `cur` is (to be) allocated at. */
|
||||
static inline int alloc_scale_for(int cur) {
|
||||
@@ -827,6 +827,12 @@ static int s_wide_base[WIDE_MAX_SURF]; /* base_x per surface (-1 = free) *
|
||||
/* Native rows [y0, y1) each surface has presented (dynamic resolution: the
|
||||
* rows a scale step rescales; y1 <= y0 = none yet, then all of them). */
|
||||
static int s_dyn_wide_y0[WIDE_MAX_SURF], s_dyn_wide_y1[WIDE_MAX_SURF];
|
||||
/* The scale each surface is allocated at. Dynamic resolution allocates a wide
|
||||
* surface at the level it renders at (not the ceiling) and reallocates it at
|
||||
* every step: on Apple's GL-on-Metal every render pass into an attachment
|
||||
* loads and stores the whole attachment, so a surface allocated at the
|
||||
* ceiling costs the ceiling's bandwidth at every level. */
|
||||
static int s_wide_as[WIDE_MAX_SURF];
|
||||
static int g_wide_w = 0; /* wide width (native px); 0 = disabled */
|
||||
static int g_wide_off = 0; /* centering OFFSET (native px) */
|
||||
static GLuint g_wide_cur = 0; /* active mirror FBO (0 = no mirror) */
|
||||
@@ -5782,11 +5788,43 @@ static void wide_free_all(void) {
|
||||
if (s_wide_tex[i]) { glDeleteTextures(1, &s_wide_tex[i]); s_wide_tex[i] = 0; }
|
||||
if (s_wide_rb[i]) { p_glDeleteRenderbuffers(1, &s_wide_rb[i]); s_wide_rb[i] = 0; }
|
||||
s_wide_base[i] = -1;
|
||||
s_wide_as[i] = 0;
|
||||
wst_clear(i);
|
||||
}
|
||||
g_wide_cur = 0;
|
||||
}
|
||||
|
||||
/* Create a native-wide surface (colour texture, depth-stencil RB, FBO) at
|
||||
* scale S, cleared to black with a zero stencil. 0 on failure (nothing left
|
||||
* allocated). */
|
||||
static int wide_alloc_surface(int S, GLuint *tex, GLuint *rb, GLuint *fbo) {
|
||||
int w = g_wide_w * S, h = VRAM_H * S;
|
||||
s_cw_fbo_creates++;
|
||||
*tex = make_tex(GL_RGBA8, w, h, GL_RGBA, GL_UNSIGNED_BYTE);
|
||||
/* Depth-stencil RB, same as the hr FBO: the stencil carries the PSX
|
||||
* mask-bit mirror for the wide surface, and (the hard lesson) a
|
||||
* stencil-less FBO turns every stencil-enabled mirror draw into ~0.6ms of
|
||||
* driver-side work — the 16:9 GL perf collapse. */
|
||||
p_glGenRenderbuffers(1, rb);
|
||||
p_glBindRenderbuffer(PSXGL_RENDERBUFFER, *rb);
|
||||
p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, w, h);
|
||||
if (!make_fbo(fbo, *tex, *rb)) {
|
||||
glDeleteTextures(1, tex); *tex = 0;
|
||||
p_glDeleteRenderbuffers(1, rb); *rb = 0;
|
||||
*fbo = 0;
|
||||
return 0;
|
||||
}
|
||||
/* Clear to black so unwritten margins are clean (not stale). */
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, *fbo);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearColor(0, 0, 0, 0);
|
||||
glClearStencil(0);
|
||||
glStencilMask(0xFF);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Lazily allocate (or find) the wide FBO+tex for base_x. Returns the FBO id, or
|
||||
* 0 on failure / more distinct buffers than WIDE_MAX_SURF. */
|
||||
static GLuint wide_fbo_for(int base_x) {
|
||||
@@ -5801,8 +5839,8 @@ static GLuint wide_fbo_for(int base_x) {
|
||||
* Refuse that with one log line instead of failing mid-draw: the frame
|
||||
* then presents 4:3 through the CPU path at 1x, without the native-wide
|
||||
* margins. */
|
||||
/* Dynamic resolution: allocated at the ceiling (alloc_scale_for), like
|
||||
* the hr surface, so a step never reallocates it. */
|
||||
/* Dynamic resolution: the limit is checked at the ceiling
|
||||
* (alloc_scale_for), which every level may step up to. */
|
||||
const int AS = alloc_scale_for(s_out_scale);
|
||||
if (s_gl_max_dim > 0 &&
|
||||
((int64_t)g_wide_w * AS > s_gl_max_dim ||
|
||||
@@ -5817,29 +5855,13 @@ static GLuint wide_fbo_for(int base_x) {
|
||||
}
|
||||
for (int i = 0; i < WIDE_MAX_SURF; i++) {
|
||||
if (!s_wide_fbo[i]) {
|
||||
int w = g_wide_w * AS, h = VRAM_H * AS;
|
||||
s_cw_fbo_creates++;
|
||||
s_wide_tex[i] = make_tex(GL_RGBA8, w, h, GL_RGBA, GL_UNSIGNED_BYTE);
|
||||
/* Depth-stencil RB, same as the hr FBO: the stencil carries the
|
||||
* PSX mask-bit mirror for the wide surface, and (the hard lesson)
|
||||
* a stencil-less FBO turns every stencil-enabled mirror draw into
|
||||
* ~0.6ms of driver-side work — the 16:9 GL perf collapse. */
|
||||
p_glGenRenderbuffers(1, &s_wide_rb[i]);
|
||||
p_glBindRenderbuffer(PSXGL_RENDERBUFFER, s_wide_rb[i]);
|
||||
p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, w, h);
|
||||
if (!make_fbo(&s_wide_fbo[i], s_wide_tex[i], s_wide_rb[i])) {
|
||||
glDeleteTextures(1, &s_wide_tex[i]); s_wide_tex[i] = 0;
|
||||
p_glDeleteRenderbuffers(1, &s_wide_rb[i]); s_wide_rb[i] = 0;
|
||||
/* Allocated at the level it renders at; a dynamic-resolution step
|
||||
* reallocates it (dyn_apply). The limit above is checked at the
|
||||
* ceiling, so no step can exceed it. */
|
||||
if (!wide_alloc_surface(s_out_scale, &s_wide_tex[i], &s_wide_rb[i],
|
||||
&s_wide_fbo[i]))
|
||||
return 0;
|
||||
}
|
||||
/* Clear to black so unwritten margins are clean (not stale). */
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_wide_fbo[i]);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearColor(0, 0, 0, 0);
|
||||
glClearStencil(0);
|
||||
glStencilMask(0xFF);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||||
s_wide_as[i] = s_out_scale;
|
||||
s_wide_base[i] = base_x;
|
||||
s_dyn_wide_y0[i] = s_dyn_wide_y1[i] = 0; /* nothing presented yet */
|
||||
wst_clear(i); /* colour and stencil cleared together */
|
||||
@@ -8599,9 +8621,10 @@ static int present_wide_fbo_impl(int disp_x, int disp_y, int disp_h, int linear)
|
||||
}
|
||||
if (!s_ctx || !s_raster_ok || g_wide_w <= 0) return 0;
|
||||
GLuint fbo = 0, tex = 0;
|
||||
int wide_as = 0;
|
||||
for (int i = 0; i < WIDE_MAX_SURF; i++)
|
||||
if (s_wide_fbo[i] && s_wide_base[i] == disp_x) {
|
||||
fbo = s_wide_fbo[i]; tex = s_wide_tex[i];
|
||||
fbo = s_wide_fbo[i]; tex = s_wide_tex[i]; wide_as = s_wide_as[i];
|
||||
if (s_alloc_scale) { /* the rows a scale step rescales */
|
||||
int y0 = disp_y < 0 ? 0 : disp_y;
|
||||
int y1 = disp_y + disp_h > VRAM_H ? VRAM_H : disp_y + disp_h;
|
||||
@@ -8661,8 +8684,10 @@ static int present_wide_fbo_impl(int disp_x, int disp_y, int disp_h, int linear)
|
||||
s_last_dw = g_wide_w; s_last_dh = disp_h;
|
||||
return 1;
|
||||
}
|
||||
present_target_quad(tex, present_alloc_extent(g_wide_w, s_out_scale),
|
||||
present_alloc_extent(VRAM_H, s_out_scale),
|
||||
/* The surface is allocated at its own scale (s_wide_as), the level. */
|
||||
const float wext = (wide_as > s_out_scale && s_out_scale >= 1)
|
||||
? (float)wide_as / (float)s_out_scale : 1.0f;
|
||||
present_target_quad(tex, (float)g_wide_w * wext, (float)VRAM_H * wext,
|
||||
0, disp_y, g_wide_w, disp_h, linear, lx, ly, lw, lh, 1, 1, s_out_scale);
|
||||
pres_record(GL_PRES_WIDE, disp_x, disp_y, g_wide_w, disp_h, lx, ly, lw, lh);
|
||||
gpu_timeline_note(GTL_PRESENT, GTL_PATH_WIDE_FBO,
|
||||
@@ -9044,8 +9069,35 @@ static int dyn_apply(int snew) {
|
||||
if (!s_wide_fbo[i]) continue;
|
||||
int y0 = s_dyn_wide_y0[i], y1 = s_dyn_wide_y1[i];
|
||||
if (y1 <= y0) { y0 = 0; y1 = VRAM_H; }
|
||||
dyn_rescale_in_place(s_wide_fbo[i], g_wide_w, (const int (*)[2])cols, nc,
|
||||
y0, y1, sold, snew);
|
||||
/* Reallocated at the new level (s_wide_as): the same rows and
|
||||
* columns rescaled from the old surface into a cleared new one.
|
||||
* If the allocation fails, a step down rescales in place as
|
||||
* before and the surface keeps its larger allocation; a step up
|
||||
* past it drops the surface (recreated at the next target). */
|
||||
GLuint ntex = 0, nrb = 0, nfbo = 0;
|
||||
if (wide_alloc_surface(snew, &ntex, &nrb, &nfbo)) {
|
||||
for (int c = 0; c < nc; c++)
|
||||
if (cols[c][1] > cols[c][0])
|
||||
dyn_rescale_rect(s_wide_tex[i], cols[c][0] * sold, y0 * sold, sold,
|
||||
nfbo, cols[c][0], y0, cols[c][1] - cols[c][0],
|
||||
y1 - y0, snew);
|
||||
if (g_wide_cur == s_wide_fbo[i]) g_wide_cur = nfbo;
|
||||
p_glDeleteFramebuffers(1, &s_wide_fbo[i]);
|
||||
glDeleteTextures(1, &s_wide_tex[i]);
|
||||
p_glDeleteRenderbuffers(1, &s_wide_rb[i]);
|
||||
s_wide_fbo[i] = nfbo; s_wide_tex[i] = ntex; s_wide_rb[i] = nrb;
|
||||
s_wide_as[i] = snew;
|
||||
s_dyn_stats.wide_reallocs++;
|
||||
} else if (snew <= s_wide_as[i]) {
|
||||
dyn_rescale_in_place(s_wide_fbo[i], g_wide_w, (const int (*)[2])cols, nc,
|
||||
y0, y1, sold, snew);
|
||||
} else {
|
||||
if (g_wide_cur == s_wide_fbo[i]) g_wide_cur = 0;
|
||||
p_glDeleteFramebuffers(1, &s_wide_fbo[i]); s_wide_fbo[i] = 0;
|
||||
glDeleteTextures(1, &s_wide_tex[i]); s_wide_tex[i] = 0;
|
||||
p_glDeleteRenderbuffers(1, &s_wide_rb[i]); s_wide_rb[i] = 0;
|
||||
s_wide_base[i] = -1; s_wide_as[i] = 0;
|
||||
}
|
||||
wst_all(i); /* colour only */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9556,7 +9556,7 @@ extern "C" int psx_dynres_status_json(char *out, int cap) {
|
||||
"\"held_windows\":%llu,\"steps\":%llu,\"deferred\":%llu,\"last_from\":%d,"
|
||||
"\"last_to\":%d,\"last_ms\":%.3f,\"last_interval_ms\":%.3f,\"last_prep_ms\":%.3f,"
|
||||
"\"last_seed_ms\":%.3f,\"last_rects_ms\":%.3f,\"last_wide_ms\":%.3f,"
|
||||
"\"step_cost_ms\":%.3f,\"down_blocked_s\":%.1f,\"up_blocked\":{%s}",
|
||||
"\"wide_reallocs\":%llu,\"step_cost_ms\":%.3f,\"down_blocked_s\":%.1f,\"up_blocked\":{%s}",
|
||||
g_dynres.active ? 1 : 0, dynres_requested(), st.ceiling,
|
||||
g_dynres.active ? c.floor : 0, st.level, st.level * g_video_ref_lines, c.forced,
|
||||
c.last_load, c.last_late, c.last_vblank_hz, c.f,
|
||||
@@ -9565,7 +9565,7 @@ extern "C" int psx_dynres_status_json(char *out, int cap) {
|
||||
c.windows, c.held_windows, (unsigned long long)st.steps,
|
||||
(unsigned long long)st.deferred, st.last_from, st.last_to, st.last_ms,
|
||||
g_dynres.step_interval_ms, st.last_prep_ms, st.last_seed_ms, st.last_rects_ms,
|
||||
st.last_wide_ms, c.step_cost_s * 1000.0,
|
||||
st.last_wide_ms, (unsigned long long)st.wide_reallocs, c.step_cost_s * 1000.0,
|
||||
c.down_block_until > now_s ? c.down_block_until - now_s : 0.0, blocked);
|
||||
}
|
||||
|
||||
|
||||
@@ -62,8 +62,9 @@
|
||||
* gl_renderer_pass_begin opens nothing there: a pass backs up and restores
|
||||
* only the authoritative surface, never the window's tiles.
|
||||
* - mode "steps" (dynamic resolution, argv: scale = the ceiling, then a
|
||||
* comma list of levels, then "fresh" or "chain"): the surfaces stay
|
||||
* allocated at the ceiling and the scale steps at run time.
|
||||
* comma list of levels, then "fresh" or "chain"): the hr surface stays
|
||||
* allocated at the ceiling, the native-wide surface at the current level
|
||||
* (reallocated at each step), and the scale steps at run time.
|
||||
* "fresh" steps before anything is drawn, then renders the scene: the
|
||||
* runner requires the native digest, the frame at S and the wide surface
|
||||
* to equal the fixed-scale run at the final level (rendering into the
|
||||
@@ -978,15 +979,17 @@ static uint64_t steps_wide_digest(uint32_t **keep) {
|
||||
return d;
|
||||
}
|
||||
|
||||
/* The native-wide surface is allocated at the ceiling, whatever the level
|
||||
* when it was created: a later step up must not outgrow it. */
|
||||
static void steps_check_wide_alloc(int ceiling) {
|
||||
/* The native-wide surface is allocated at the level it renders at (not the
|
||||
* ceiling, whose size every render pass would load and store on
|
||||
* GL-on-Metal), and a step reallocates it at the new level. */
|
||||
static void steps_check_wide_alloc(int level) {
|
||||
GLint tw = 0, th = 0;
|
||||
glBindTexture(GL_TEXTURE_2D, s_wide_tex[0]);
|
||||
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &tw);
|
||||
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &th);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
check(tw == 426 * ceiling && th == 512 * ceiling, "wide surface allocated at the ceiling");
|
||||
check(tw == 426 * level && th == 512 * level && s_wide_as[0] == level,
|
||||
"wide surface allocated at the level");
|
||||
}
|
||||
|
||||
static int steps_main(int ceiling, int window, const char *chain, int fresh) {
|
||||
@@ -1018,7 +1021,7 @@ static int steps_main(int ceiling, int window, const char *chain, int fresh) {
|
||||
uint64_t digest = steps_native_digest(1);
|
||||
uint64_t hires = steps_hires_digest(0, 0, FRAME_W, FRAME_H);
|
||||
steps_wide_part();
|
||||
steps_check_wide_alloc(ceiling); /* created below the ceiling */
|
||||
steps_check_wide_alloc(s_out_scale); /* created below the ceiling */
|
||||
uint64_t wide = steps_wide_digest(NULL);
|
||||
check(glGetError() == GL_NO_ERROR, "GL error");
|
||||
printf("level=%d\n", s_out_scale);
|
||||
@@ -1030,7 +1033,7 @@ static int steps_main(int ceiling, int window, const char *chain, int fresh) {
|
||||
}
|
||||
scene();
|
||||
steps_wide_part();
|
||||
steps_check_wide_alloc(ceiling);
|
||||
steps_check_wide_alloc(ceiling); /* the level is the ceiling here */
|
||||
/* A back buffer with its own content, the draw area while stepping. */
|
||||
glb_set_draw_area(0, STEPS_BACK_Y, FRAME_W - 1, STEPS_BACK_Y + FRAME_H - 1);
|
||||
glb_wide_disable_target();
|
||||
@@ -1062,6 +1065,7 @@ static int steps_main(int ceiling, int window, const char *chain, int fresh) {
|
||||
char label[128];
|
||||
snprintf(label, sizeof label, "step %dx -> %dx applied", sold, snew);
|
||||
check(ok && s_out_scale == snew && s_hr_scale == snew, label);
|
||||
steps_check_wide_alloc(snew);
|
||||
GlDynresStats st;
|
||||
gl_renderer_dynres_stats(&st);
|
||||
uint64_t d1 = steps_native_digest(0);
|
||||
|
||||
Reference in New Issue
Block a user