Files
Shokunin 2211ecabf3 GL: true 8K past a 16384 texture limit with a high-resolution window
8K is 18x at a 240-line reference, and a full-VRAM surface at 18x is
18432 px wide: over the 16384 limit of Apple's GL, Intel and MoltenVK. A
title only needs its displayed frame at 18x, so past the full-VRAM limit
the GL backend now splits the work:

- The authoritative VRAM stays the ordinary hr surface at 1x, i.e. the
  native renderer unchanged: pack, CPU readback, render-to-texture and VRAM
  copies are exactly the native results (s_hr_scale; s_scale stays the
  presented scale gr_scale reports).
- A presentation-only surface at S covers the displayed columns W =
  [x0, x1) x all 512 rows (R4: 0..319, 5760x9216 at 18x, 405 MiB). Every
  GPU write touching W is mirrored into it: textured and flat batches,
  lines (quads), fills, uploads, the depth24 clear, and VRAM copies (hi to
  hi when the source is in W, otherwise the 1x source upscaled; both staged
  before drawing so overlapping copies read pre-copy pixels). It keeps its
  own stencil. A separate blit program carries the window projection, so
  the canonical shaders are untouched.
- W grows (64-column aligned) the first time a display rectangle outside it
  is presented, seeded by upscaling the 1x content. Present, hold-last,
  interpolation capture, screenshot_hires and the native-wide centre read
  it; a display it cannot hold presents at 1x.

It engages only when the full-VRAM surface cannot hold the request (driver
limit or budget), sized so a 320-wide window fits both; up to 16x on the
M4 nothing changes. PSX_GL_HIRES_WINDOW=0/1 disables or forces it. The
launcher note and video_info (hr_scale, windowed, hires_window_*, hires_fbo)
report it.

Default-off: at 1x and whenever the full-VRAM surface fits, the paths are
the previous ones (same native-VRAM and present digests on a fixed scene at
1x and 2x).

Tests: gl_scale_invariance_test now also forces the window at 2/3/5/9x and
requires its frame at internal resolution to be byte-identical to the
full-VRAM surface at the same scale (copies inside, into and across the
window edge, fills/uploads across it, mask, all blend modes) with native
VRAM identical to 1x, and runs it at 18x; gl_scale_limits_test covers the
window clamp; guards pin that it only engages past the full-VRAM limit.
Verified in R4 on an M4: the 8K preset renders 5760x4320 in a race (rear-
view mirror present) and 5760x8640 on a 480i menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:52:23 -07:00

155 lines
6.7 KiB
C

/* gl_scale_limits.h — how large an internal-resolution scale the OpenGL
* backend may allocate.
*
* Pure arithmetic, no GL or SDL, so a test can pin it. The GL backend keeps the
* authoritative VRAM as one 1024*S x 512*S colour texture plus a same-size
* depth-stencil renderbuffer (gpu_gl_renderer.c). Two things bound S:
*
* - the driver's size limits: GL_MAX_TEXTURE_SIZE, GL_MAX_RENDERBUFFER_SIZE
* and GL_MAX_VIEWPORT_DIMS (the caller passes their minimum). Apple's GL
* on Metal reports 16384, so the full-VRAM surface tops out at S = 16;
* - a memory budget. On unified-memory machines this is system RAM.
*
* A request that does not fit is clamped here, never turned into a failed
* context: the old behaviour (allocation failure -> the whole GL backend fell
* back to the software renderer) lost far more than the resolution. */
#pragma once
#include <stdint.h>
#define PSX_GL_VRAM_W 1024
#define PSX_GL_VRAM_H 512
/* RGBA8 colour + D24S8 depth-stencil per internal pixel. The copy scratch is
* sized lazily to the largest copy, so it is not part of the fixed cost. */
#define PSX_GL_HR_BYTES_PER_PX 8u
/* Default memory budget for the full-VRAM surface: 2 GiB (S = 22 by memory;
* the texture limit binds first on 16384-limit GPUs). */
#define PSX_GL_DEFAULT_BUDGET_MB 2048u
/* Why a scale was reduced. */
enum {
PSX_GL_SCALE_OK = 0,
PSX_GL_SCALE_TEXTURE = 1, /* 1024*S exceeds the driver size limit */
PSX_GL_SCALE_BUDGET = 2, /* surface exceeds the memory budget */
PSX_GL_SCALE_CEILING = 4 /* above the compile-time ceiling */
};
/* Bytes of the full-VRAM hr surface (colour + depth-stencil) at scale s. */
static inline uint64_t psx_gl_full_vram_bytes(int s) {
if (s < 1) s = 1;
return (uint64_t)PSX_GL_VRAM_W * (uint64_t)s *
(uint64_t)PSX_GL_VRAM_H * (uint64_t)s * PSX_GL_HR_BYTES_PER_PX;
}
/* 1 if a full-VRAM surface at scale s fits max_dim and budget_bytes.
* max_dim <= 0 means "unknown" (no size check). budget_bytes == 0 means no
* budget. */
static inline int psx_gl_full_vram_fits(int s, int max_dim, uint64_t budget_bytes) {
if (s < 1) return 0;
if (max_dim > 0 && (int64_t)PSX_GL_VRAM_W * s > (int64_t)max_dim) return 0;
if (max_dim > 0 && (int64_t)PSX_GL_VRAM_H * s > (int64_t)max_dim) return 0;
if (budget_bytes && psx_gl_full_vram_bytes(s) > budget_bytes) return 0;
return 1;
}
/* Largest scale <= req whose full-VRAM surface fits. Never below 1. When
* reason is non-NULL it receives a PSX_GL_SCALE_* mask of what bound it. */
static inline int psx_gl_clamp_full_vram_scale(int req, int ceiling, int max_dim,
uint64_t budget_bytes, int *reason) {
int why = PSX_GL_SCALE_OK;
int s = req < 1 ? 1 : req;
if (ceiling > 0 && s > ceiling) { s = ceiling; why |= PSX_GL_SCALE_CEILING; }
while (s > 1) {
int tex_ok = !(max_dim > 0 &&
((int64_t)PSX_GL_VRAM_W * s > (int64_t)max_dim ||
(int64_t)PSX_GL_VRAM_H * s > (int64_t)max_dim));
int mem_ok = !(budget_bytes && psx_gl_full_vram_bytes(s) > budget_bytes);
if (tex_ok && mem_ok) break;
if (!tex_ok) why |= PSX_GL_SCALE_TEXTURE;
if (!mem_ok) why |= PSX_GL_SCALE_BUDGET;
s--;
}
if (reason) *reason = why;
return s;
}
/* Widest native-wide surface (in native px) that fits max_dim at scale s.
* The GL wide surfaces are wide_w*S x 512*S. Returns 0 when nothing fits. */
static inline int psx_gl_max_wide_width(int s, int max_dim) {
if (s < 1) s = 1;
if (max_dim <= 0) return 1 << 30;
if ((int64_t)PSX_GL_VRAM_H * s > (int64_t)max_dim) return 0;
return max_dim / s;
}
/* Native-wide surface width (native px) for a display disp_w px wide at aspect
* num:den: the display plus two margins of disp_w*(3*num - 4*den)/(8*den),
* rounded exactly as gpu.c's ws_nw_configured_offset rounds them (16:9, 21:9
* and 32:9 at 320 wide give 426, 560 and 854). */
static inline int psx_gl_native_wide_width(int disp_w, int num, int den) {
if (disp_w <= 0 || num <= 0 || den <= 0 ||
(int64_t)num * 3 <= (int64_t)den * 4)
return disp_w;
int64_t numr = (int64_t)3 * num - (int64_t)4 * den;
return disp_w + 2 * (int)(((int64_t)disp_w * numr + (int64_t)4 * den) /
((int64_t)8 * den));
}
/* Narrow num:den, if needed, to the widest aspect whose native-wide surface
* is at most max_w native px (psx_gl_max_wide_width) at display width disp_w;
* 4:3 when not even one more column fits. 4*(max_w-1) : 3*disp_w rounds to at
* most max_w columns. Returns 1 when it changed num:den. */
static inline int psx_gl_fit_wide_aspect(int disp_w, int max_w, int *num, int *den) {
if (disp_w <= 0 || max_w <= 0 || !num || !den ||
psx_gl_native_wide_width(disp_w, *num, *den) <= max_w)
return 0;
int64_t n = 4, d = 3;
if (max_w - 1 > disp_w) {
n = 4 * (int64_t)(max_w - 1);
d = 3 * (int64_t)disp_w;
int64_t a = n, b = d;
while (b) { int64_t t = a % b; a = b; b = t; }
n /= a; d /= a;
}
*num = (int)n;
*den = (int)d;
return 1;
}
/* Windowed high-resolution mode: the authoritative VRAM stays at 1x and only
* a window of the displayed columns (window_w native px, full 512-row height)
* is kept at S. Largest S <= req whose smallest useful window (min_window_w,
* e.g. a 320-px framebuffer) fits max_dim and budget_bytes. */
static inline uint64_t psx_gl_window_bytes(int s, int window_w) {
if (s < 1) s = 1;
return (uint64_t)window_w * (uint64_t)s * (uint64_t)PSX_GL_VRAM_H * (uint64_t)s *
PSX_GL_HR_BYTES_PER_PX;
}
static inline int psx_gl_clamp_window_scale(int req, int ceiling, int max_dim,
uint64_t budget_bytes, int min_window_w) {
int s = req < 1 ? 1 : req;
if (ceiling > 0 && s > ceiling) s = ceiling;
while (s > 1) {
int dim_ok = !(max_dim > 0 &&
((int64_t)PSX_GL_VRAM_H * s > (int64_t)max_dim ||
(int64_t)min_window_w * s > (int64_t)max_dim));
int mem_ok = !(budget_bytes && psx_gl_window_bytes(s, min_window_w) > budget_bytes);
if (dim_ok && mem_ok) break;
s--;
}
return s;
}
/* Parse a budget override in MiB (PSX_GL_VRAM_BUDGET_MB). NULL/empty/invalid
* returns the default. "0" disables the budget. */
static inline uint64_t psx_gl_budget_bytes_from_env(const char *mb) {
uint64_t v = 0;
const char *p = mb;
if (!p || !*p) return (uint64_t)PSX_GL_DEFAULT_BUDGET_MB << 20;
while (*p >= '0' && *p <= '9') { v = v * 10u + (uint64_t)(*p - '0'); p++; if (v > (1u << 24)) break; }
if (*p) return (uint64_t)PSX_GL_DEFAULT_BUDGET_MB << 20;
return v << 20;
}