mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
GL: raise the internal-resolution ceiling to the driver's limits
The OpenGL backend capped supersampling at 4, the software mirror's limit, although its hr surface lives on the GPU. Raise the compile-time ceiling to 32 (gpu_render.h) and decide the real scale at context init: - Query GL_MAX_TEXTURE_SIZE, GL_MAX_RENDERBUFFER_SIZE and GL_MAX_VIEWPORT_DIMS and clamp S so the 1024*S x 512*S surface fits them and a 2 GiB memory budget (PSX_GL_VRAM_BUDGET_MB overrides it). Apple's GL on Metal reports 16384, so 16x is the most a full-VRAM surface can be. The clamp is logged with the limit that applied. - If the surface still fails to allocate, retry one scale lower inside GL. Only a 1x failure drops the backend to software, as before. - Draw lines above 1x as quads one native pixel thick covering both endpoints. glLineWidth(S) is capped at 1 on core profiles, so every line was one hr pixel thick (1/9 of native at 9x). - Present a supersampled source with an area resolve when it is more than 1.25x the output (1200 lines into a 1080 window, 480i menus), and inset the UV rect by half a texel instead of half a native pixel, which cropped and zoomed the image by (S-1)/2 texels per edge. - Size the copy scratch to the largest copy (copies stage at its origin) and rebuild the mask stencil in tiles, only over the primitive bbox union, so a large S does not cost another full-size surface. - Refuse a native-wide surface wider than the driver limit (uncapped Fit at a high scale) with one log line; the frame presents 4:3. - main.cpp: the offline and dual-raster clamps no longer cap GL at the software 4x. The CPU present path presents GL frames at 1x (its readout is native) and its staging buffer stays sized for at most 4x. - [video] supersampling accepts 1..32 in game.toml and settings.toml; the runtime clamps per backend. Every change is gated on S > 1: at 1x the renderer is byte-identical (same native VRAM and presented framebuffer digests as the previous source on a fixed GP0 scene). Tests: gl_scale_limits_test (clamp arithmetic), gl_internal_scale_guards (source guards + runner parsing), and gl_scale_invariance_test, a hidden real-GL run at 1,2,3,5,9x that requires identical guest-visible VRAM at every scale, one-native-pixel lines at internal resolution, and a 32x request that stays on GL at the driver's clamp (16x on Apple). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -450,6 +450,29 @@ a build compiled with Vulkan support. `offer_vulkan` controls launcher
|
||||
visibility only; it defaults to false so game projects must explicitly expose
|
||||
Vulkan after validating their visuals and stability.
|
||||
|
||||
### Internal resolution (`supersampling`)
|
||||
|
||||
`supersampling = N` renders at N times the native resolution per axis and
|
||||
downsamples to the window. It accepts 1 to 32 in both `game.toml` and the
|
||||
player's `settings.toml`; 1 (the default) is native and unchanged.
|
||||
|
||||
The runtime clamps N per backend:
|
||||
|
||||
- Software and Vulkan stop at 4.
|
||||
- OpenGL keeps VRAM as one `1024*N x 512*N` surface and clamps N at context
|
||||
init to the driver's `GL_MAX_TEXTURE_SIZE`, `GL_MAX_RENDERBUFFER_SIZE` and
|
||||
`GL_MAX_VIEWPORT_DIMS`, and to a memory budget of 2 GiB for that surface
|
||||
(`PSX_GL_VRAM_BUDGET_MB` overrides it; `0` removes it). Apple's OpenGL
|
||||
reports 16384, so 16 is the largest full-VRAM scale there. A surface that
|
||||
still fails to allocate is retried one scale lower; the backend never drops
|
||||
to software because of the scale. The log line
|
||||
`GL internal scale Nx clamped to Mx (...)` names the limit that applied.
|
||||
|
||||
Above 1x the OpenGL present averages the whole footprint of each output pixel
|
||||
when the internal image is more than 1.25 times larger than the window (for
|
||||
example 1200 internal lines into a 1080-line window), and lines are drawn one
|
||||
native pixel thick at any scale.
|
||||
|
||||
`offer_skip_fmv` defaults to true for compatibility with the shared PSX
|
||||
Settings surface. A game migrating Skip FMVs into its built-in mod catalog sets
|
||||
it to false. The runtime then hides the Settings row, ignores stale persisted
|
||||
|
||||
@@ -410,6 +410,12 @@ if(BUILD_TESTING)
|
||||
target_include_directories(display_scanout_test PRIVATE ../runtime/include)
|
||||
add_test(NAME display_scanout_test COMMAND display_scanout_test)
|
||||
|
||||
# Pure header arithmetic for the OpenGL internal-resolution clamp.
|
||||
add_executable(gl_scale_limits_test
|
||||
../runtime/tests/test_gl_scale_limits.c)
|
||||
target_include_directories(gl_scale_limits_test PRIVATE ../runtime/include)
|
||||
add_test(NAME gl_scale_limits_test COMMAND gl_scale_limits_test)
|
||||
|
||||
add_executable(cli_boot_path_test
|
||||
tests/cli_boot_path_test.cpp
|
||||
src/cli_boot_path.cpp
|
||||
@@ -822,6 +828,7 @@ if(BUILD_TESTING)
|
||||
hybrid_dev_any_input
|
||||
launcher_pad_mode_wiring
|
||||
jalr_link_register_guards
|
||||
gl_internal_scale_guards
|
||||
launcher_vulkan_option
|
||||
mod_controller_override
|
||||
mod_load_acceleration
|
||||
|
||||
@@ -574,9 +574,11 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
const toml::value& video = toml::find(cfg, "video");
|
||||
if (video.contains("supersampling")) {
|
||||
const auto n = toml::find<int64_t>(video, "supersampling");
|
||||
if (n < 1 || n > 4) {
|
||||
// 1..32: the runtime clamps per backend (software/Vulkan 4, OpenGL
|
||||
// to the driver's texture limits and a memory budget).
|
||||
if (n < 1 || n > 32) {
|
||||
throw std::runtime_error(fmt::format(
|
||||
"[video] supersampling out of range (1..4): {}", n));
|
||||
"[video] supersampling out of range (1..32): {}", n));
|
||||
}
|
||||
rt.video_supersampling = static_cast<int>(n);
|
||||
}
|
||||
@@ -2313,7 +2315,7 @@ UserSettings load_user_settings(const fs::path& path) {
|
||||
});
|
||||
if (v.contains("supersampling")) try_get([&]{
|
||||
const auto n = toml::find<int64_t>(v, "supersampling");
|
||||
if (n >= 1 && n <= 4) { s.supersampling = (int)n; s.has_supersampling = true; }
|
||||
if (n >= 1 && n <= 32) { s.supersampling = (int)n; s.has_supersampling = true; }
|
||||
});
|
||||
if (v.contains("window_width")) try_get([&]{
|
||||
const auto n = toml::find<int64_t>(v, "window_width");
|
||||
|
||||
@@ -378,9 +378,11 @@ struct RuntimeConfig {
|
||||
|
||||
// ---- [video] block — visual enhancement options ----
|
||||
// supersampling: internal-resolution SSAA factor (per axis). 1 = native
|
||||
// (default, behaves exactly as before). 2..4 render geometry/shading into
|
||||
// (default, behaves exactly as before). 2..32 render geometry/shading into
|
||||
// an N*-scaled mirror of VRAM and downsample on present — true ordered-grid
|
||||
// supersampling + edge anti-aliasing. Cost scales ~N^2 in fill rate.
|
||||
// supersampling + edge anti-aliasing. Cost scales ~N^2 in fill rate. The
|
||||
// runtime clamps per backend: software and Vulkan at 4, OpenGL at the
|
||||
// driver's texture limits and a memory budget (gl_scale_limits.h).
|
||||
int video_supersampling = 1;
|
||||
|
||||
// Optional initial window width declared by the title profile. Zero keeps
|
||||
@@ -1199,7 +1201,7 @@ struct UserSettings {
|
||||
|
||||
// [video]
|
||||
bool has_renderer = false; int renderer = DEFAULT_VIDEO_RENDERER; // 0=software,1=opengl,2=vulkan
|
||||
bool has_supersampling = false; int supersampling = 1; // 1..4
|
||||
bool has_supersampling = false; int supersampling = 1; // 1..32 (runtime clamps per backend)
|
||||
// Window size: width in px; height is always width*3/4 (PSX 4:3). Applies to
|
||||
// both the launcher and the emulator window so they boot at the same size.
|
||||
bool has_window_width = false; int window_width = 1280; // -> 1280x960
|
||||
|
||||
@@ -85,6 +85,30 @@ if(BUILD_TESTING)
|
||||
else()
|
||||
message(STATUS "gl_readback_region_test: not registered (requires MinGW, SDL3 headers/static library in PSX_GL_READBACK_SDL_ROOT and gcc.exe/g++.exe in PSX_GL_READBACK_COMPILER_BIN)")
|
||||
endif()
|
||||
# Real-GL internal-resolution invariance (macOS/Linux, hidden window): the
|
||||
# guest-visible VRAM is identical at every scale, lines stay one native
|
||||
# pixel thick, and an over-limit request clamps inside GL. Needs the SDL3
|
||||
# this tree fetched and a GPU session; labelled hardware like the readback
|
||||
# test above.
|
||||
if(NOT WIN32 AND Python3_Interpreter_FOUND AND TARGET SDL3::SDL3)
|
||||
if(DEFINED sdl3_SOURCE_DIR)
|
||||
set(_gl_scale_sdl_inc "${sdl3_SOURCE_DIR}/include")
|
||||
else()
|
||||
set(_gl_scale_sdl_inc "$<TARGET_PROPERTY:SDL3::Headers,INTERFACE_INCLUDE_DIRECTORIES>")
|
||||
endif()
|
||||
add_test(NAME gl_scale_invariance_test
|
||||
COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_scale_invariance.py"
|
||||
--fixture "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_scale_invariance.c"
|
||||
--cc "${CMAKE_C_COMPILER}"
|
||||
--sdl-include "${_gl_scale_sdl_inc}"
|
||||
--sdl-library "$<TARGET_FILE:SDL3::SDL3>"
|
||||
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-scale-results")
|
||||
set_tests_properties(gl_scale_invariance_test PROPERTIES LABELS "gpu;opengl;hardware" TIMEOUT 300)
|
||||
unset(_gl_scale_sdl_inc)
|
||||
else()
|
||||
message(STATUS "gl_scale_invariance_test: not registered (needs a non-Windows host, Python 3 and SDL3)")
|
||||
endif()
|
||||
add_executable(launcher_device_roundtrip_test
|
||||
tests/test_launcher_device_roundtrip.cpp)
|
||||
target_include_directories(launcher_device_roundtrip_test PRIVATE include)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
@@ -147,6 +147,26 @@ float gl_renderer_get_post_gamma(void);
|
||||
* path. Textured edge expansion needs the complete mirror surface. */
|
||||
void gl_renderer_set_wide_fast(int on);
|
||||
|
||||
/* Internal-resolution scale state of the live GL context (0 before init). */
|
||||
typedef struct GlScaleInfo {
|
||||
int requested; /* scale requested before context init (gr_set_scale) */
|
||||
int effective; /* scale the hr surface was allocated at */
|
||||
int max_scale; /* largest scale this context could allocate */
|
||||
int max_dim; /* min(GL_MAX_TEXTURE_SIZE, _RENDERBUFFER_SIZE, _VIEWPORT_DIMS) */
|
||||
int clamp_reason; /* PSX_GL_SCALE_* mask (gl_scale_limits.h) */
|
||||
int alloc_retries; /* FBO allocations that failed and stepped the scale down */
|
||||
int budget_mib; /* memory budget for the hr surface (0 = none) */
|
||||
int fbo_w, fbo_h; /* hr surface size in pixels */
|
||||
int drawable_w, drawable_h; /* window drawable in pixels */
|
||||
} GlScaleInfo;
|
||||
int gl_renderer_scale_info(GlScaleInfo *out);
|
||||
|
||||
/* Read the display rect at internal resolution from the hr FBO: (w*S) x (h*S)
|
||||
* ARGB8888, top row first. Returns the pixel count, 0 if unavailable or larger
|
||||
* than cap_px. Debug/verification only (screenshot_hires under GL). */
|
||||
int gl_renderer_read_display_hires(int x, int y, int w, int h, uint32_t *out,
|
||||
int cap_px, int *ow, int *oh);
|
||||
|
||||
void gl_renderer_shutdown(void);
|
||||
|
||||
/* Diagnostics (debug server): read GPU-side VRAM without touching the CPU
|
||||
|
||||
@@ -30,6 +30,12 @@ GrBackend gr_backend(void); /* effective backend after init */
|
||||
void gr_init(uint16_t *vram);
|
||||
void gr_set_scale(int scale);
|
||||
int gr_scale(void);
|
||||
|
||||
/* Compile-time ceiling for the OpenGL backend's internal-resolution scale. The
|
||||
* effective scale is decided at GL context init from the driver's size limits
|
||||
* and a memory budget (gl_scale_limits.h), so this is only an upper bound.
|
||||
* Software and Vulkan stay at SW_MAX_INTERNAL_SCALE (4). */
|
||||
#define GL_MAX_INTERNAL_SCALE 32
|
||||
void gr_set_texture_filter(int bilinear);
|
||||
int gr_texture_filter(void);
|
||||
|
||||
|
||||
+398
-44
@@ -52,19 +52,26 @@
|
||||
*
|
||||
* Known divergences from the software rasterizer (accepted, documented):
|
||||
* - GL triangle/line coverage rules differ from the PS1 DDA by ±1px on
|
||||
* edges; lines use GL_LINES (width S) instead of Bresenham.
|
||||
* edges; lines use GL_LINES at 1x and, above 1x, a quad one native pixel
|
||||
* thick that covers both endpoints (line_to_quad) instead of Bresenham.
|
||||
* - No dithering (the software path doesn't dither either).
|
||||
* - Gouraud interpolation happens at 8-bit precision instead of 5-bit
|
||||
* (smoother gradients; readback re-quantizes to 5-bit).
|
||||
* - VRAM-wrapping draws are clamped, except GP0(02h) fills which split
|
||||
* into wrapped segments. Wrapping copies/draws are unused by real SDKs.
|
||||
*
|
||||
* INTERNAL SCALE LIMITS. S is clamped at init to the driver's texture,
|
||||
* renderbuffer and viewport limits and to a memory budget (gl_scale_limits.h),
|
||||
* and an FBO that still fails to allocate is retried one scale lower. A large
|
||||
* S therefore never costs the GL backend itself.
|
||||
*
|
||||
* Init is all-or-nothing: if any shader/FBO fails, gl_renderer_init_context
|
||||
* returns 0 and the runtime falls back to the pure software renderer — no
|
||||
* half-GL hybrid. */
|
||||
|
||||
#include "gpu.h"
|
||||
#include "gpu_render.h"
|
||||
#include "gl_scale_limits.h"
|
||||
#include "gpu_sw_renderer.h"
|
||||
#include "gpu_gl_renderer.h"
|
||||
#include "mod_texture_banks.h"
|
||||
@@ -129,7 +136,16 @@
|
||||
|
||||
#define VRAM_W 1024
|
||||
#define VRAM_H 512
|
||||
#define GL_MAX_INTERNAL_SCALE 4
|
||||
/* GL_MAX_INTERNAL_SCALE (the compile-time ceiling) lives in gpu_render.h; the
|
||||
* effective scale is clamped to the driver and memory limits at init. */
|
||||
#ifndef PSXGL_MAX_RENDERBUFFER_SIZE
|
||||
#define PSXGL_MAX_RENDERBUFFER_SIZE 0x84E8
|
||||
#endif
|
||||
#ifndef GL_MAX_VIEWPORT_DIMS
|
||||
#define GL_MAX_VIEWPORT_DIMS 0x0D3A
|
||||
#endif
|
||||
/* Scratch tile edge (hr px) for the tiled stencil rebuild at S > 1. */
|
||||
#define GL_SCRATCH_TILE 2048
|
||||
|
||||
/* ---- Loaded modern-GL entry points ------------------------------------- */
|
||||
typedef GLuint (APIENTRY *PFN_glCreateShader)(GLenum);
|
||||
@@ -311,6 +327,13 @@ static int s_scale = 1; /* internal-res scale (hr FBO) */
|
||||
/* Netplay: SW@1× + GPU@s_scale dual write; CPU VRAM always authoritative. */
|
||||
static int s_cpu_auth_dual = 0;
|
||||
static int s_req_scale = 1; /* requested before context init */
|
||||
/* Driver limits queried at init: min(GL_MAX_TEXTURE_SIZE,
|
||||
* GL_MAX_RENDERBUFFER_SIZE, GL_MAX_VIEWPORT_DIMS); 0 before init. */
|
||||
static int s_gl_max_dim = 0;
|
||||
static int s_scale_clamp_reason = 0; /* PSX_GL_SCALE_* mask */
|
||||
static int s_scale_alloc_retries = 0; /* FBO failures stepped down */
|
||||
static uint64_t s_vram_budget = 0; /* bytes; 0 = none */
|
||||
static int s_wide_refused_logged = 0;
|
||||
|
||||
static GLuint s_present_tex = 0; /* CPU-readout present path (24bpp) */
|
||||
static int s_present_w = 0, s_present_h = 0;
|
||||
@@ -445,7 +468,7 @@ static GLint s_uBlitSrc = -1, s_uBlitPass = -1, s_uBlitMaskset = -1;
|
||||
static GLint s_uBlitSrcDiv = -1, s_uBlitSrcOff = -1;
|
||||
/* PACK program uniforms. */
|
||||
static GLint s_uPackHr = -1, s_uPackScale = -1;
|
||||
static GLint s_uStencilSrc = -1;
|
||||
static GLint s_uStencilSrc = -1, s_uStencilOff = -1;
|
||||
|
||||
static uint32_t *s_conv = NULL; /* RGBA8 staging for uploads */
|
||||
static int s_gpu_dirty = 0; /* CPU VRAM array may be stale */
|
||||
@@ -454,6 +477,10 @@ static int s_gpu_dirty = 0; /* CPU VRAM array may be stale */
|
||||
typedef struct { int x0, y0, x1, y1, set; } DirtyRect;
|
||||
static DirtyRect s_cpu_dirty; /* conservative GPU-written area not yet read into CPU VRAM */
|
||||
static DirtyRect s_pack_dirty; /* hr FBO content not in raw mirror */
|
||||
/* Native-coords union of primitive bboxes since the last stencil rebuild: the
|
||||
* only area whose stencil can lag its alpha (deferred-stencil draws). Used at
|
||||
* S > 1 so a mask-check toggle rebuilds that rect, not the whole surface. */
|
||||
static DirtyRect s_stencil_stale;
|
||||
|
||||
/* CPU writes not yet in the FBO — an EXACT rect list, NOT a single union.
|
||||
*
|
||||
@@ -786,7 +813,7 @@ static void letterbox_rect(int ww, int wh, int *x, int *y, int *w, int *h);
|
||||
static void present_target_quad(GLuint tex, int tex_w, int tex_h,
|
||||
int x, int y, int w, int h, int linear,
|
||||
int lx, int ly, int lw, int lh, int v_flip,
|
||||
int apply_gamma);
|
||||
int apply_gamma, int src_scale);
|
||||
|
||||
static void coh_record(int kind, int x0, int y0, int x1, int y1) {
|
||||
GlCohEvent *e = &s_coh_ring[s_coh_seq % GL_COH_RING_CAP];
|
||||
@@ -958,10 +985,33 @@ static const char *PRESENT_FS =
|
||||
" return r;\n"
|
||||
"}\n"
|
||||
PSX_SCANLINE_FUNC
|
||||
/* Area resolve (u_sharp == 3) for a supersampled source larger than the
|
||||
* output: average the whole footprint of the output pixel instead of one
|
||||
* tap, which skips source texels (aliasing) once the ratio passes ~1.25.
|
||||
* u_sharp_scale is output px per texel, so 1/u_sharp_scale is the
|
||||
* footprint in texels. Each tap is bilinear and taps are spaced up to two
|
||||
* texels apart, so every tap averages a 2x2 texel block: an exact box at
|
||||
* even ratios, a close tent otherwise. Taps clamp to u_uv_rect so the
|
||||
* neighbouring VRAM never bleeds in. */
|
||||
"vec4 area_resolve(vec2 uv){\n"
|
||||
" vec2 fp = 1.0 / max(u_sharp_scale, vec2(1.0e-6));\n"
|
||||
" vec2 nf = clamp(ceil(fp * 0.5), vec2(1.0), vec2(8.0));\n"
|
||||
" ivec2 n = ivec2(nf);\n"
|
||||
" vec2 st = fp / nf;\n"
|
||||
" vec2 base = uv * u_tex_size - 0.5 * fp + 0.5 * st;\n"
|
||||
" vec2 lo = min(u_uv_rect.xy, u_uv_rect.zw);\n"
|
||||
" vec2 hi = max(u_uv_rect.xy, u_uv_rect.zw);\n"
|
||||
" vec4 acc = vec4(0.0);\n"
|
||||
" for (int j = 0; j < n.y; j++)\n"
|
||||
" for (int i = 0; i < n.x; i++)\n"
|
||||
" acc += texture(u_tex, clamp((base + vec2(i, j) * st) / u_tex_size, lo, hi));\n"
|
||||
" return acc / float(n.x * n.y);\n"
|
||||
"}\n"
|
||||
"void main(){\n"
|
||||
" vec2 uv = v_uv;\n"
|
||||
" vec4 c;\n"
|
||||
" if (u_sharp == 2) { c = bicubic(uv); }\n"
|
||||
" else if (u_sharp == 3) { c = area_resolve(uv); }\n"
|
||||
" else {\n"
|
||||
" if (u_sharp == 1) {\n"
|
||||
" vec2 scale = max(u_sharp_scale, vec2(1.0));\n"
|
||||
@@ -1241,7 +1291,8 @@ static const char *PACK_FS =
|
||||
static const char *STENCIL_FS =
|
||||
"#version 330\n"
|
||||
"uniform sampler2D u_src; out vec4 frag;\n"
|
||||
"void main(){ vec4 c=texelFetch(u_src,ivec2(gl_FragCoord.xy),0);\n"
|
||||
"uniform ivec2 u_off; /* tile origin in the target; (0,0) for a full copy */\n"
|
||||
"void main(){ vec4 c=texelFetch(u_src,ivec2(gl_FragCoord.xy)-u_off,0);\n"
|
||||
" if(c.a<0.5) discard; frag=vec4(0.0); }\n";
|
||||
|
||||
static GLuint compile_shader(GLenum type, const char *src) {
|
||||
@@ -1480,14 +1531,100 @@ static void rebuild_target_stencil(GLuint target_fbo, int target_w, int target_h
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
/* Grow the shared scratch texture to at least w x h (never shrinks). The
|
||||
* scratch holds copy sources and stencil-rebuild tiles; at S > 1 it is sized
|
||||
* to what those operations actually need instead of the whole hr surface. */
|
||||
static void scratch_ensure(int w, int h) {
|
||||
if (w <= s_scratch_w && h <= s_scratch_h) return;
|
||||
if (w > s_scratch_w) s_scratch_w = w;
|
||||
if (h > s_scratch_h) s_scratch_h = h;
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, s_scratch_tex);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, s_scratch_w, s_scratch_h,
|
||||
0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
||||
}
|
||||
|
||||
/* Rebuild the stencil mask of [x, x+w) x [y, y+h) (target px) from alpha, in
|
||||
* tiles no larger than GL_SCRATCH_TILE so the scratch never has to match a
|
||||
* multi-gigapixel surface. Same result as rebuild_target_stencil over that
|
||||
* rect; used only at S > 1. */
|
||||
static void rebuild_target_stencil_tiled(GLuint target_fbo, int target_w, int target_h,
|
||||
int x, int y, int w, int h) {
|
||||
if (x < 0) { w += x; x = 0; }
|
||||
if (y < 0) { h += y; y = 0; }
|
||||
if (x + w > target_w) w = target_w - x;
|
||||
if (y + h > target_h) h = target_h - y;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
int tile_w = w < GL_SCRATCH_TILE ? w : GL_SCRATCH_TILE;
|
||||
int tile_h = h < GL_SCRATCH_TILE ? h : GL_SCRATCH_TILE;
|
||||
scratch_ensure(tile_w, tile_h);
|
||||
for (int ty = y; ty < y + h; ty += tile_h) {
|
||||
for (int tx = x; tx < x + w; tx += tile_w) {
|
||||
int cw = (x + w - tx) < tile_w ? (x + w - tx) : tile_w;
|
||||
int ch = (y + h - ty) < tile_h ? (y + h - ty) : tile_h;
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, target_fbo);
|
||||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_scratch_fbo);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
p_glBlitFramebuffer(tx, ty, tx + cw, ty + ch, 0, 0, cw, ch,
|
||||
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, target_fbo);
|
||||
glViewport(0, 0, target_w, target_h);
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(tx, ty, cw, ch);
|
||||
glDisable(GL_BLEND);
|
||||
glEnable(GL_STENCIL_TEST);
|
||||
glStencilMask(0x01);
|
||||
glClearStencil(0);
|
||||
glClear(GL_STENCIL_BUFFER_BIT);
|
||||
glStencilFunc(GL_ALWAYS, 1, 0x01);
|
||||
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
|
||||
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
|
||||
p_glUseProgram(s_stencil_prog);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, s_scratch_tex);
|
||||
p_glUniform1i(s_uStencilSrc, 0);
|
||||
p_glUniform2i(s_uStencilOff, tx, ty);
|
||||
p_glBindVertexArray(s_empty_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
}
|
||||
}
|
||||
p_glUniform2i(s_uStencilOff, 0, 0);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
p_glBindVertexArray(0);
|
||||
p_glUseProgram(0);
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
static void rebuild_mask_stencils(void) {
|
||||
if (s_stencil_valid || !s_raster_ok) return;
|
||||
int hw = VRAM_W * s_scale, hh = VRAM_H * s_scale;
|
||||
rebuild_target_stencil(s_hr_fbo, hw, hh);
|
||||
for (int i = 0; i < WIDE_MAX_SURF; i++) {
|
||||
if (s_wide_fbo[i])
|
||||
rebuild_target_stencil(s_wide_fbo[i], g_wide_w * s_scale, hh);
|
||||
if (s_scale <= 1) {
|
||||
/* Native: the historical whole-surface rebuild, unchanged. */
|
||||
rebuild_target_stencil(s_hr_fbo, hw, hh);
|
||||
for (int i = 0; i < WIDE_MAX_SURF; i++) {
|
||||
if (s_wide_fbo[i])
|
||||
rebuild_target_stencil(s_wide_fbo[i], g_wide_w * s_scale, hh);
|
||||
}
|
||||
} else {
|
||||
/* S > 1: only primitives can leave stencil behind alpha, so rebuild
|
||||
* their bbox union, tiled. Wide surfaces keep a full rebuild (their
|
||||
* margins are not in canonical coordinates), also tiled. */
|
||||
if (s_stencil_stale.set) {
|
||||
int S = s_scale;
|
||||
rebuild_target_stencil_tiled(s_hr_fbo, hw, hh,
|
||||
s_stencil_stale.x0 * S, s_stencil_stale.y0 * S,
|
||||
(s_stencil_stale.x1 - s_stencil_stale.x0 + 1) * S,
|
||||
(s_stencil_stale.y1 - s_stencil_stale.y0 + 1) * S);
|
||||
}
|
||||
for (int i = 0; i < WIDE_MAX_SURF; i++) {
|
||||
if (s_wide_fbo[i])
|
||||
rebuild_target_stencil_tiled(s_wide_fbo[i], g_wide_w * s_scale, hh,
|
||||
0, 0, g_wide_w * s_scale, hh);
|
||||
}
|
||||
}
|
||||
rect_clear(&s_stencil_stale);
|
||||
s_stencil_valid = 1;
|
||||
}
|
||||
|
||||
@@ -1607,6 +1744,7 @@ static void mark_prim_dirty(const int *xs, const int *ys, int n, int textured) {
|
||||
if (x1 > s_area_x2) x1 = s_area_x2;
|
||||
if (y1 > s_area_y2) y1 = s_area_y2;
|
||||
rect_add(&s_pack_dirty, x0, y0, x1, y1);
|
||||
if (s_scale > 1) rect_add(&s_stencil_stale, x0, y0, x1, y1);
|
||||
if (!s_cpu_auth_dual) rect_add(&s_cpu_dirty, x0, y0, x1, y1);
|
||||
/* Dual-raster keeps CPU current via SW writes — do not mark GPU-ahead. */
|
||||
if (!s_cpu_auth_dual)
|
||||
@@ -1954,6 +2092,44 @@ static void flush_flat_batch(void) {
|
||||
hr_end();
|
||||
}
|
||||
|
||||
/* A PS1 line as a quad one NATIVE pixel thick (used at S > 1).
|
||||
*
|
||||
* glLineWidth(S) was the old way to keep a line's thickness at internal scale,
|
||||
* but core-profile contexts only guarantee width 1 (macOS reports
|
||||
* GL_ALIASED_LINE_WIDTH_RANGE 1..1 and rejects anything wider), so every line
|
||||
* came out one HR pixel thick: 1/9 of native at S=9. The quad follows the PS1
|
||||
* rule instead: one pixel per step along the major axis, one pixel across the
|
||||
* minor axis. It spans pixel centres p0..p1 extended half a pixel along the
|
||||
* major axis at both ends (so both endpoint pixels are covered) and offset
|
||||
* +-0.5 px across the minor axis. Positions are native VRAM px, the space the
|
||||
* GEO shader already takes. v = two (x, y, r, g, b, a) vertices; q = six. */
|
||||
static void line_to_quad(const float *v, float *q) {
|
||||
float x0 = v[0] + 0.5f, y0 = v[1] + 0.5f;
|
||||
float x1 = v[6] + 0.5f, y1 = v[7] + 0.5f;
|
||||
float dx = x1 - x0, dy = y1 - y0;
|
||||
float ax = fabsf(dx), ay = fabsf(dy);
|
||||
float ex, ey, nx, ny; /* half-pixel extension along, offset across */
|
||||
if (ax >= ay) { /* X-major (also the single-pixel case) */
|
||||
float sx = dx < 0.0f ? -1.0f : 1.0f;
|
||||
ex = 0.5f * sx; ey = ax > 0.0f ? 0.5f * sx * dy / dx : 0.0f;
|
||||
nx = 0.0f; ny = 0.5f;
|
||||
} else { /* Y-major */
|
||||
float sy = dy < 0.0f ? -1.0f : 1.0f;
|
||||
ey = 0.5f * sy; ex = 0.5f * sy * dx / dy;
|
||||
nx = 0.5f; ny = 0.0f;
|
||||
}
|
||||
const float a[2] = { x0 - ex, y0 - ey }, b[2] = { x1 + ex, y1 + ey };
|
||||
/* corners: a-, b-, a+ / b-, a+, b+ ; colour follows the nearer endpoint */
|
||||
const float cx[6] = { a[0]-nx, b[0]-nx, a[0]+nx, b[0]-nx, a[0]+nx, b[0]+nx };
|
||||
const float cy[6] = { a[1]-ny, b[1]-ny, a[1]+ny, b[1]-ny, a[1]+ny, b[1]+ny };
|
||||
const int end[6] = { 0, 1, 0, 1, 0, 1 };
|
||||
for (int i = 0; i < 6; i++) {
|
||||
const float *src = v + end[i] * 6;
|
||||
q[i*6+0] = cx[i]; q[i*6+1] = cy[i];
|
||||
q[i*6+2] = src[2]; q[i*6+3] = src[3]; q[i*6+4] = src[4]; q[i*6+5] = src[5];
|
||||
}
|
||||
}
|
||||
|
||||
/* Flat / gouraud triangles and lines share the GEO program. mode: GL_TRIANGLES
|
||||
* or GL_LINES; verts are (x, y, r, g, b, a) tuples with colors as 1555. */
|
||||
static void gpu_geometry(GLenum mode, const int *xs, const int *ys,
|
||||
@@ -1977,16 +2153,26 @@ static void gpu_geometry(GLenum mode, const int *xs, const int *ys,
|
||||
verts[i*6+4] = (((cs[i] >> 10) & 0x1F) << 3) / 255.0f;
|
||||
verts[i*6+5] = mask_a;
|
||||
}
|
||||
/* S > 1: a line becomes a one-native-pixel quad (see line_to_quad);
|
||||
* native keeps GL_LINES exactly as before. */
|
||||
float quad[6 * 6];
|
||||
GLenum draw_mode = mode;
|
||||
const float *draw_verts = verts;
|
||||
int draw_n = n;
|
||||
if (mode == GL_LINES && n == 2 && s_scale > 1) {
|
||||
line_to_quad(verts, quad);
|
||||
draw_mode = GL_TRIANGLES; draw_verts = quad; draw_n = 6;
|
||||
}
|
||||
hr_begin(1);
|
||||
if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND);
|
||||
mask_stencil(s_mask_set);
|
||||
if (mode == GL_LINES) glLineWidth((float)s_scale);
|
||||
if (draw_mode == GL_LINES) glLineWidth((float)s_scale);
|
||||
p_glUseProgram(s_geo_prog);
|
||||
p_glBindVertexArray(s_geo_vao);
|
||||
p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo);
|
||||
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(n * 6 * sizeof(float)),
|
||||
verts, PSXGL_STREAM_DRAW);
|
||||
glDrawArrays(mode, 0, n);
|
||||
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(draw_n * 6 * sizeof(float)),
|
||||
draw_verts, PSXGL_STREAM_DRAW);
|
||||
glDrawArrays(draw_mode, 0, draw_n);
|
||||
if (g_wide_cur && !s_wide_suppress && s_ws_ablate != 1 &&
|
||||
!(!g_ws_bd_stretch_on && mirror_geo_center_only(xs, n))) {
|
||||
int dx = wide_dx();
|
||||
@@ -1994,7 +2180,7 @@ static void gpu_geometry(GLenum mode, const int *xs, const int *ys,
|
||||
gl_perf_mirror_begin();
|
||||
wide_target_begin(dx, s_geo_uXoff, s_geo_uXhalf);
|
||||
wide_set_bd_scale(s_geo_uXscale, s_geo_uXcenter);
|
||||
if (s_ws_ablate != 2) glDrawArrays(mode, 0, n);
|
||||
if (s_ws_ablate != 2) glDrawArrays(draw_mode, 0, draw_n);
|
||||
wide_clear_bd_scale(s_geo_uXscale, s_geo_uXcenter);
|
||||
wide_target_end(s_geo_uXoff, s_geo_uXhalf);
|
||||
gl_perf_mirror_end();
|
||||
@@ -2301,11 +2487,15 @@ static void gpu_copy_rect(int sx,int sy,int dx,int dy,int w,int h) {
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
int S = s_scale;
|
||||
/* Stage the source at the scratch ORIGIN (not at its own hr coords), so
|
||||
* the scratch only has to be as large as the biggest copy. The integer
|
||||
* texelFetch in BLIT_FS makes the placement invisible in the result. */
|
||||
scratch_ensure(w * S, h * S);
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo);
|
||||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_scratch_fbo);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
p_glBlitFramebuffer(sx*S, sy*S, (sx+w)*S, (sy+h)*S,
|
||||
sx*S, sy*S, (sx+w)*S, (sy+h)*S,
|
||||
0, 0, w*S, h*S,
|
||||
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
|
||||
@@ -2318,9 +2508,9 @@ static void gpu_copy_rect(int sx,int sy,int dx,int dy,int w,int h) {
|
||||
glBindTexture(GL_TEXTURE_2D, s_scratch_tex);
|
||||
p_glUniform1i(s_uBlitSrc, 0);
|
||||
p_glUniform1i(s_uBlitMaskset, s_mask_set);
|
||||
/* Scratch holds the source at its own hr coords: texel = frag + (src-dst)*S. */
|
||||
/* Scratch holds the source at its origin: texel = frag - dst*S. */
|
||||
p_glUniform1i(s_uBlitSrcDiv, 1);
|
||||
p_glUniform2i(s_uBlitSrcOff, (sx - dx) * S, (sy - dy) * S);
|
||||
p_glUniform2i(s_uBlitSrcOff, -dx * S, -dy * S);
|
||||
float fx0 = (float)dx, fy0 = (float)dy, fx1 = (float)(dx + w), fy1 = (float)(dy + h);
|
||||
float verts[6 * 2] = {
|
||||
fx0, fy0, fx1, fy0, fx0, fy1,
|
||||
@@ -2349,6 +2539,8 @@ static void glb_init(uint16_t *vram) { s_vram = vram; sw_renderer_init(vram); }
|
||||
* (software mirror, readbacks, screenshots) stays native, so the reported
|
||||
* scale is 1 and the software hi-res mirror stays off. */
|
||||
static void glb_set_scale(int s) {
|
||||
/* Only the compile-time ceiling here: the driver and memory limits are
|
||||
* known at context init (init_gpu_raster), which clamps further. */
|
||||
if (s < 1) s = 1;
|
||||
if (s > GL_MAX_INTERNAL_SCALE) s = GL_MAX_INTERNAL_SCALE;
|
||||
s_req_scale = s;
|
||||
@@ -2828,8 +3020,75 @@ static int make_fbo(GLuint *out_fbo, GLuint color_tex, GLuint stencil_rb) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Release the scale-dependent render targets (hr colour + depth-stencil +
|
||||
* copy scratch). Used when an allocation fails and init steps S down. */
|
||||
static void free_hr_targets(void) {
|
||||
if (s_hr_fbo) { p_glDeleteFramebuffers(1, &s_hr_fbo); s_hr_fbo = 0; }
|
||||
if (s_scratch_fbo) { p_glDeleteFramebuffers(1, &s_scratch_fbo); s_scratch_fbo = 0; }
|
||||
if (s_hr_rb) { p_glDeleteRenderbuffers(1, &s_hr_rb); s_hr_rb = 0; }
|
||||
if (s_hr_tex) { glDeleteTextures(1, &s_hr_tex); s_hr_tex = 0; }
|
||||
if (s_scratch_tex) { glDeleteTextures(1, &s_scratch_tex); s_scratch_tex = 0; }
|
||||
s_scratch_w = s_scratch_h = 0;
|
||||
}
|
||||
|
||||
/* Allocate the authoritative hr surface at scale S. Returns 1 when every
|
||||
* target is complete and the driver reported no error (a too-large texture
|
||||
* surfaces as GL_INVALID_VALUE or GL_OUT_OF_MEMORY, not always as an
|
||||
* incomplete FBO). */
|
||||
static int alloc_hr_targets(int S) {
|
||||
while (glGetError() != GL_NO_ERROR) {}
|
||||
int hw = VRAM_W * S, hh = VRAM_H * S;
|
||||
s_hr_tex = make_tex(GL_RGBA8, hw, hh, GL_RGBA, GL_UNSIGNED_BYTE);
|
||||
/* Copy/stencil scratch. Up to 2x it covers the whole surface as before
|
||||
* (<= 8 MiB); above that it starts at one tile and grows to the largest
|
||||
* copy on demand (scratch_ensure), instead of costing 4 MiB*S^2 up front. */
|
||||
if (S <= 2) { s_scratch_w = hw; s_scratch_h = hh; }
|
||||
else {
|
||||
s_scratch_w = hw < GL_SCRATCH_TILE ? hw : GL_SCRATCH_TILE;
|
||||
s_scratch_h = hh < GL_SCRATCH_TILE ? hh : GL_SCRATCH_TILE;
|
||||
}
|
||||
s_scratch_tex = make_tex(GL_RGBA8, s_scratch_w, s_scratch_h, GL_RGBA, GL_UNSIGNED_BYTE);
|
||||
p_glGenRenderbuffers(1, &s_hr_rb);
|
||||
p_glBindRenderbuffer(PSXGL_RENDERBUFFER, s_hr_rb);
|
||||
p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, hw, hh);
|
||||
p_glBindRenderbuffer(PSXGL_RENDERBUFFER, 0);
|
||||
int ok = glGetError() == GL_NO_ERROR;
|
||||
if (ok) ok = make_fbo(&s_hr_fbo, s_hr_tex, s_hr_rb);
|
||||
if (ok) ok = make_fbo(&s_scratch_fbo, s_scratch_tex, 0);
|
||||
if (ok) ok = glGetError() == GL_NO_ERROR;
|
||||
if (!ok) free_hr_targets();
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int init_gpu_raster(void) {
|
||||
s_scale = s_req_scale;
|
||||
/* Effective scale: the request, clamped to what the driver can allocate
|
||||
* for the full-VRAM surface and to the memory budget. Logged whenever it
|
||||
* differs from the request so a preset that cannot be honoured says so. */
|
||||
{
|
||||
GLint max_tex = 0, max_rb = 0, max_vp[2] = { 0, 0 };
|
||||
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_tex);
|
||||
glGetIntegerv(PSXGL_MAX_RENDERBUFFER_SIZE, &max_rb);
|
||||
glGetIntegerv(GL_MAX_VIEWPORT_DIMS, max_vp);
|
||||
while (glGetError() != GL_NO_ERROR) {}
|
||||
int lim = max_tex > 0 ? max_tex : 0;
|
||||
if (max_rb > 0 && (lim == 0 || max_rb < lim)) lim = max_rb;
|
||||
if (max_vp[0] > 0 && (lim == 0 || max_vp[0] < lim)) lim = max_vp[0];
|
||||
if (max_vp[1] > 0 && (lim == 0 || max_vp[1] < lim)) lim = max_vp[1];
|
||||
s_gl_max_dim = lim;
|
||||
s_vram_budget = psx_gl_budget_bytes_from_env(getenv("PSX_GL_VRAM_BUDGET_MB"));
|
||||
s_scale = psx_gl_clamp_full_vram_scale(s_req_scale, GL_MAX_INTERNAL_SCALE,
|
||||
s_gl_max_dim, s_vram_budget,
|
||||
&s_scale_clamp_reason);
|
||||
if (s_scale != s_req_scale)
|
||||
fprintf(stdout, "psxrecomp: GL internal scale %dx clamped to %dx "
|
||||
"(%s%s%s; GPU max texture %d, budget %llu MiB)\n",
|
||||
s_req_scale, s_scale,
|
||||
(s_scale_clamp_reason & PSX_GL_SCALE_TEXTURE) ? "texture limit " : "",
|
||||
(s_scale_clamp_reason & PSX_GL_SCALE_BUDGET) ? "memory budget " : "",
|
||||
(s_scale_clamp_reason & PSX_GL_SCALE_CEILING) ? "ceiling " : "",
|
||||
s_gl_max_dim, (unsigned long long)(s_vram_budget >> 20));
|
||||
s_scale_alloc_retries = 0;
|
||||
}
|
||||
|
||||
s_geo_prog = build_program(GEO_VS, GEO_FS);
|
||||
s_tex_prog = build_program_ex(TEX_VS, TEX_FS, 1);
|
||||
@@ -2841,11 +3100,6 @@ static int init_gpu_raster(void) {
|
||||
s_conv = (uint32_t *)malloc((size_t)VRAM_W * VRAM_H * sizeof(uint32_t));
|
||||
if (!s_conv) return 0;
|
||||
|
||||
int hw = VRAM_W * s_scale, hh = VRAM_H * s_scale;
|
||||
s_hr_tex = make_tex(GL_RGBA8, hw, hh, GL_RGBA, GL_UNSIGNED_BYTE);
|
||||
s_scratch_tex = make_tex(GL_RGBA8, hw, hh, GL_RGBA, GL_UNSIGNED_BYTE);
|
||||
s_scratch_w = hw;
|
||||
s_scratch_h = hh;
|
||||
s_up_tex = make_tex(GL_RGBA8, VRAM_W, VRAM_H, GL_RGBA, GL_UNSIGNED_BYTE);
|
||||
s_raw_tex = make_tex(PSXGL_R16UI, VRAM_W, VRAM_H, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT);
|
||||
/* Force the driver's first texture-upload allocation while the renderer is
|
||||
@@ -2863,14 +3117,18 @@ static int init_gpu_raster(void) {
|
||||
glFinish();
|
||||
}
|
||||
|
||||
p_glGenRenderbuffers(1, &s_hr_rb);
|
||||
p_glBindRenderbuffer(PSXGL_RENDERBUFFER, s_hr_rb);
|
||||
p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, hw, hh);
|
||||
p_glBindRenderbuffer(PSXGL_RENDERBUFFER, 0);
|
||||
|
||||
if (!make_fbo(&s_hr_fbo, s_hr_tex, s_hr_rb)) return 0;
|
||||
/* The hr surface: step the scale down inside GL when the driver cannot
|
||||
* allocate it (out of memory, or a limit it under-reported). Only a
|
||||
* failure at 1x takes the whole backend down, as before. */
|
||||
while (!alloc_hr_targets(s_scale)) {
|
||||
if (s_scale <= 1) return 0;
|
||||
fprintf(stdout, "psxrecomp: GL hr surface %dx%d (%dx) failed to allocate; "
|
||||
"retrying at %dx\n", VRAM_W * s_scale, VRAM_H * s_scale,
|
||||
s_scale, s_scale - 1);
|
||||
s_scale--;
|
||||
s_scale_alloc_retries++;
|
||||
}
|
||||
if (!make_fbo(&s_raw_fbo, s_raw_tex, 0)) return 0;
|
||||
if (!make_fbo(&s_scratch_fbo, s_scratch_tex, 0)) return 0;
|
||||
|
||||
s_uVram = p_glGetUniformLocation(s_tex_prog, "u_vram");
|
||||
s_uTpage = p_glGetUniformLocation(s_tex_prog, "u_tpage");
|
||||
@@ -2891,6 +3149,7 @@ static int init_gpu_raster(void) {
|
||||
s_uPackHr = p_glGetUniformLocation(s_pack_prog, "u_hr");
|
||||
s_uPackScale = p_glGetUniformLocation(s_pack_prog, "u_scale");
|
||||
s_uStencilSrc = p_glGetUniformLocation(s_stencil_prog, "u_src");
|
||||
s_uStencilOff = p_glGetUniformLocation(s_stencil_prog, "u_off");
|
||||
s_geo_uXoff = p_glGetUniformLocation(s_geo_prog, "u_xoff");
|
||||
s_geo_uXhalf = p_glGetUniformLocation(s_geo_prog, "u_xhalf");
|
||||
s_tex_uXoff = p_glGetUniformLocation(s_tex_prog, "u_xoff");
|
||||
@@ -2980,6 +3239,7 @@ static int init_gpu_raster(void) {
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||||
rect_clear(&s_pack_dirty);
|
||||
rect_clear(&s_stencil_stale);
|
||||
s_up_nrects = 0;
|
||||
up_add(0, 0, VRAM_W - 1, VRAM_H - 1);
|
||||
s_gpu_dirty = 0;
|
||||
@@ -3010,6 +3270,50 @@ static int init_gpu_raster(void) {
|
||||
|
||||
int gl_renderer_texture_banks_supported(void) { return s_raster_ok && !s_cpu_auth_dual; }
|
||||
|
||||
int gl_renderer_scale_info(GlScaleInfo *out) {
|
||||
if (!out) return 0;
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->requested = s_req_scale;
|
||||
out->effective = s_raster_ok ? s_scale : 0;
|
||||
out->max_dim = s_gl_max_dim;
|
||||
out->clamp_reason = s_scale_clamp_reason;
|
||||
out->alloc_retries = s_scale_alloc_retries;
|
||||
out->budget_mib = (int)(s_vram_budget >> 20);
|
||||
out->fbo_w = s_raster_ok ? VRAM_W * s_scale : 0;
|
||||
out->fbo_h = s_raster_ok ? VRAM_H * s_scale : 0;
|
||||
out->max_scale = psx_gl_clamp_full_vram_scale(GL_MAX_INTERNAL_SCALE,
|
||||
GL_MAX_INTERNAL_SCALE,
|
||||
s_gl_max_dim, s_vram_budget, NULL);
|
||||
if (s_ctx && s_win) SDL_GL_GetDrawableSize(s_win, &out->drawable_w, &out->drawable_h);
|
||||
return s_raster_ok;
|
||||
}
|
||||
|
||||
/* Read the display rect at internal resolution straight from the hr FBO:
|
||||
* (w*S) x (h*S) ARGB8888, top row first. The CPU-side hires path under GL is
|
||||
* native (the software mirror stays 1x), so this is the only capture that
|
||||
* shows what the internal resolution actually rendered. cap_px bounds the
|
||||
* output; returns the pixel count, 0 when unavailable or too large. */
|
||||
int gl_renderer_read_display_hires(int x, int y, int w, int h, uint32_t *out,
|
||||
int cap_px, int *ow, int *oh) {
|
||||
if (!s_raster_ok || !s_ctx || !out || w <= 0 || h <= 0) return 0;
|
||||
if (x < 0 || y < 0 || x + w > VRAM_W || y + h > VRAM_H) return 0;
|
||||
int S = s_scale, W = w * S, H = h * S;
|
||||
if ((int64_t)W * H > (int64_t)cap_px) return 0;
|
||||
flush_flat_batch();
|
||||
flush_tex_batch();
|
||||
flush_cpu_upload();
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
glReadPixels(x * S, y * S, W, H, GL_BGRA, GL_UNSIGNED_BYTE, out);
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||||
/* The hr FBO stores PS1 y=0 at GL row 0 and glReadPixels reads bottom-up,
|
||||
* so row 0 is already the display's top line (see glb_render_wide_display). */
|
||||
for (int64_t i = 0; i < (int64_t)W * H; i++) out[i] |= 0xFF000000u;
|
||||
if (ow) *ow = W;
|
||||
if (oh) *oh = H;
|
||||
return W * H;
|
||||
}
|
||||
|
||||
int gl_renderer_select_texture_bank(uint16_t id) {
|
||||
uint32_t width, height;
|
||||
const uint16_t* pixels;
|
||||
@@ -3478,6 +3782,21 @@ static GLuint wide_fbo_for(int base_x) {
|
||||
if (g_wide_w <= 0) return 0;
|
||||
for (int i = 0; i < WIDE_MAX_SURF; i++)
|
||||
if (s_wide_fbo[i] && s_wide_base[i] == base_x) return s_wide_fbo[i];
|
||||
/* An uncapped Fit aspect can ask for a surface wider than the driver can
|
||||
* allocate at this scale (about 38:9 at 18x on a 16384 limit). Refuse it
|
||||
* with one log line: the frame then presents pillarboxed 4:3 instead of
|
||||
* failing mid-draw. */
|
||||
if (s_gl_max_dim > 0 &&
|
||||
((int64_t)g_wide_w * s_scale > s_gl_max_dim ||
|
||||
(int64_t)VRAM_H * s_scale > s_gl_max_dim)) {
|
||||
if (!s_wide_refused_logged) {
|
||||
fprintf(stdout, "psxrecomp: GL native-wide surface %dx%d at %dx exceeds "
|
||||
"the GPU limit %d; presenting 4:3\n", g_wide_w * s_scale,
|
||||
VRAM_H * s_scale, s_scale, s_gl_max_dim);
|
||||
s_wide_refused_logged = 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
for (int i = 0; i < WIDE_MAX_SURF; i++) {
|
||||
if (!s_wide_fbo[i]) {
|
||||
int w = g_wide_w * s_scale, h = VRAM_H * s_scale;
|
||||
@@ -3518,7 +3837,7 @@ static void glb_wide_configure(int wide_w, int offset) {
|
||||
double t0 = cw_ms(); s_cw_wide_cfgs++;
|
||||
flush_tex_batch(); /* a queued batch's wide mirror targets the CURRENT surfaces */
|
||||
if (wide_w <= 0) { wide_free_all(); g_wide_w = 0; g_wide_off = 0; s_cw_wide_ms += cw_ms() - t0; return; }
|
||||
if (wide_w != g_wide_w) wide_free_all();
|
||||
if (wide_w != g_wide_w) { wide_free_all(); s_wide_refused_logged = 0; }
|
||||
g_wide_w = wide_w;
|
||||
g_wide_off = offset;
|
||||
s_cw_wide_ms += cw_ms() - t0;
|
||||
@@ -4293,23 +4612,52 @@ static void gl_swap_with_osd(void) {
|
||||
SDL_GL_SwapWindow(s_win);
|
||||
}
|
||||
|
||||
/* Output pixels per source texel below which a supersampled present switches
|
||||
* from one tap to the area resolve (1 / 1.25: the source is 25% larger than the
|
||||
* output). Only used for S > 1 sources, so native presents never change. */
|
||||
#define PRESENT_AREA_MIN_RATIO 1.25f
|
||||
|
||||
/* tex_w/tex_h and x/y/w/h are in the texture's NATIVE units; src_scale is how
|
||||
* many texels one native unit spans (the internal scale for the hr FBO and the
|
||||
* wide surfaces, 1 for textures that hold native or already-composed pixels).
|
||||
* src_scale <= 1 is exactly the historical behaviour. */
|
||||
static void present_target_quad(GLuint tex, int tex_w, int tex_h,
|
||||
int x, int y, int w, int h, int linear,
|
||||
int lx, int ly, int lw, int lh, int v_flip,
|
||||
int apply_gamma) {
|
||||
int apply_gamma, int src_scale) {
|
||||
float u0, v0, u1, v1;
|
||||
int area = 0;
|
||||
if (src_scale > 1 && lw > 0 && lh > 0 && w > 0 && h > 0) {
|
||||
float rx = (float)(w * src_scale) / (float)lw;
|
||||
float ry = (float)(h * src_scale) / (float)lh;
|
||||
area = (rx > PRESENT_AREA_MIN_RATIO || ry > PRESENT_AREA_MIN_RATIO);
|
||||
}
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||||
glViewport(lx, ly, lw, lh);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, tex);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST);
|
||||
/* The area resolve averages bilinear taps; it needs LINEAR whatever the
|
||||
* AA setting (the source is already the supersampled image). */
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, (linear || area) ? GL_LINEAR : GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, (linear || area) ? GL_LINEAR : GL_NEAREST);
|
||||
p_glUseProgram(s_present_prog);
|
||||
p_glUniform1i(s_present_uTex, 0);
|
||||
present_set_gamma(s_present_uGamma, apply_gamma);
|
||||
/* The rasterized path already renders at the internal scale, so it has no
|
||||
* low-res source to reconstruct — keep the plain sample. */
|
||||
present_set_sharp(0, 0, 0, 0, 0);
|
||||
* low-res source to reconstruct — keep the plain sample, unless the
|
||||
* internal image is larger than the output: then resolve its area. */
|
||||
if (area && s_present_uSharp >= 0) {
|
||||
p_glUniform1i(s_present_uSharp, 3);
|
||||
if (s_present_uTexSize >= 0)
|
||||
p_glUniform2f(s_present_uTexSize, (float)(tex_w * src_scale),
|
||||
(float)(tex_h * src_scale));
|
||||
if (s_present_uSharpScale >= 0) /* output px per source texel */
|
||||
p_glUniform2f(s_present_uSharpScale,
|
||||
(float)lw / (float)(w * src_scale),
|
||||
(float)lh / (float)(h * src_scale));
|
||||
} else {
|
||||
present_set_sharp(0, 0, 0, 0, 0);
|
||||
}
|
||||
/* Scanline at the native line grid. v_uv is normalized against tex_h (the
|
||||
* full VRAM/FBO texture), and one texel row is one PS1 scanline, so tex_h is
|
||||
* the phase pitch; h is the displayed line count for the output-scale gate.
|
||||
@@ -4320,10 +4668,16 @@ static void present_target_quad(GLuint tex, int tex_w, int tex_h,
|
||||
* dest pixels blend the border texel with VRAM outside the content rect
|
||||
* (visible edge stripe with AA on). Center-mapped UVs keep edge samples
|
||||
* inside the rect; interior sampling is unchanged. */
|
||||
u0 = ((float)x + 0.5f) / (float)tex_w;
|
||||
v0 = ((float)y + 0.5f) / (float)tex_h;
|
||||
u1 = ((float)(x + w) - 0.5f) / (float)tex_w;
|
||||
v1 = ((float)(y + h) - 0.5f) / (float)tex_h;
|
||||
/* The inset is half a TEXEL. For a supersampled source that is 0.5/S of a
|
||||
* native unit; the old 0.5 native units cropped (S-1)/2 texels per edge
|
||||
* (about 4 px at 9x) and zoomed the image. src_scale <= 1: unchanged. */
|
||||
{
|
||||
float in = src_scale > 1 ? 0.5f / (float)src_scale : 0.5f;
|
||||
u0 = ((float)x + in) / (float)tex_w;
|
||||
v0 = ((float)y + in) / (float)tex_h;
|
||||
u1 = ((float)(x + w) - in) / (float)tex_w;
|
||||
v1 = ((float)(y + h) - in) / (float)tex_h;
|
||||
}
|
||||
/* PRESENT_VS always samples with mix(v0,v1,1-p.y). For CPU/FBO guest
|
||||
* bands that is the correct PSX top-down → GL mapping (v_flip=1). For a
|
||||
* glCopyTexSubImage2D of the already-presented drawable, the texture
|
||||
@@ -4425,7 +4779,7 @@ int gl_renderer_present_hold_last(void) {
|
||||
}
|
||||
/* v_flip=0: drawable capture is already screen-oriented. */
|
||||
present_target_quad(s_hold_tex, s_hold_tw, s_hold_th,
|
||||
0, 0, s_hold_tw, s_hold_th, 0, lx, ly, lw, lh, 0, 0);
|
||||
0, 0, s_hold_tw, s_hold_th, 0, lx, ly, lw, lh, 0, 0, 1);
|
||||
} else {
|
||||
if (s_hold_force_4_3)
|
||||
letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh);
|
||||
@@ -4433,7 +4787,7 @@ int gl_renderer_present_hold_last(void) {
|
||||
letterbox_rect(ww, wh, &lx, &ly, &lw, &lh);
|
||||
present_target_quad(s_hold_tex, s_hold_tw, s_hold_th,
|
||||
0, 0, s_hold_tw, s_hold_th, s_hold_linear,
|
||||
lx, ly, lw, lh, 1, 1);
|
||||
lx, ly, lw, lh, 1, 1, 1);
|
||||
/* Upgrade to drawable after letterboxing once. Live+interp leaves
|
||||
* HOLD_NATIVE (no Swap on main); drawable path is the soak-proven
|
||||
* full-window image (GL without interp). */
|
||||
@@ -4498,7 +4852,7 @@ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear,
|
||||
}
|
||||
present_bezel(ww, wh, lx, ly, lw, lh);
|
||||
present_target_quad(s_hr_tex, VRAM_W, VRAM_H,
|
||||
disp_x, disp_y, w, h, linear, lx, ly, lw, lh, 1, 1);
|
||||
disp_x, disp_y, w, h, linear, lx, ly, lw, lh, 1, 1, s_scale);
|
||||
pres_record(GL_PRES_VRAM, disp_x, disp_y, w, h, lx, ly, lw, lh);
|
||||
hold_capture_drawable();
|
||||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||||
@@ -4602,7 +4956,7 @@ int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear)
|
||||
return 1;
|
||||
}
|
||||
present_target_quad(tex, g_wide_w, VRAM_H,
|
||||
0, disp_y, g_wide_w, disp_h, linear, lx, ly, lw, lh, 1, 1);
|
||||
0, disp_y, g_wide_w, disp_h, linear, lx, ly, lw, lh, 1, 1, s_scale);
|
||||
pres_record(GL_PRES_WIDE, disp_x, disp_y, g_wide_w, disp_h, lx, ly, lw, lh);
|
||||
hold_capture_drawable();
|
||||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||||
|
||||
+47
-14
@@ -403,6 +403,7 @@ static void apply_offline_pad_count(int game_players, bool multitap_enabled)
|
||||
/* ARGB8888 staging buffer. The 576-row maximum preserves the full interlaced
|
||||
* PAL active canvas. Allocated once the supersampling scale is known. */
|
||||
static uint32_t* sdl_pixel_buf = nullptr;
|
||||
static size_t s_pixel_buf_px = 0; /* sdl_pixel_buf capacity in pixels */
|
||||
|
||||
typedef void (*ModFrameHook)(void);
|
||||
|
||||
@@ -1939,10 +1940,11 @@ static int ensure_sw_sdl_present(void) {
|
||||
g_video_aa ? SDL_ScaleModeLinear : SDL_ScaleModeNearest);
|
||||
}
|
||||
if (!sdl_pixel_buf) {
|
||||
const int tex_scale = netplay_cpu_auth_gpu() ? 1 : g_video_scale;
|
||||
sdl_pixel_buf = (uint32_t*)std::malloc(
|
||||
(size_t)640 * tex_scale * PSX_DISPLAY_PRESENT_MAX_HEIGHT *
|
||||
tex_scale * sizeof(uint32_t));
|
||||
int tex_scale = netplay_cpu_auth_gpu() ? 1 : g_video_scale;
|
||||
if (tex_scale > SW_MAX_INTERNAL_SCALE) tex_scale = SW_MAX_INTERNAL_SCALE;
|
||||
s_pixel_buf_px = (size_t)640 * tex_scale * PSX_DISPLAY_PRESENT_MAX_HEIGHT *
|
||||
tex_scale;
|
||||
sdl_pixel_buf = (uint32_t*)std::malloc(s_pixel_buf_px * sizeof(uint32_t));
|
||||
if (!sdl_pixel_buf) {
|
||||
std::fprintf(stderr, "psxrecomp: netplay SW present: staging alloc failed\n");
|
||||
return -1;
|
||||
@@ -1960,8 +1962,10 @@ extern "C" void psx_frontend_netplay_force_sw_gpu(void) {
|
||||
#ifndef PSX_SDL_NO_RENDER
|
||||
/* Already locked on dual-raster GL quality present. */
|
||||
if (s_netplay_sw_gpu_locked && netplay_gl_dual_quality()) {
|
||||
/* The GL present surface is not bound by the software mirror's 4x
|
||||
* (dual-raster keeps SW at 1x); the backend clamps to its own limits. */
|
||||
int want = g_video_scale < 1 ? 1 : g_video_scale;
|
||||
if (want > SW_MAX_INTERNAL_SCALE) want = SW_MAX_INTERNAL_SCALE;
|
||||
if (want > GL_MAX_INTERNAL_SCALE) want = GL_MAX_INTERNAL_SCALE;
|
||||
gr_set_backend(GR_BACKEND_OPENGL);
|
||||
gl_renderer_set_cpu_auth_dual(1);
|
||||
/* FBO scale is fixed at context init — keep request in sync. */
|
||||
@@ -2001,8 +2005,10 @@ extern "C" void psx_frontend_netplay_force_sw_gpu(void) {
|
||||
#ifndef PSX_SDL_NO_RENDER
|
||||
if (g_gl_active) {
|
||||
/* Dual-raster: OPENGL backend, SW@1× + GPU@Nx, FBO present. */
|
||||
/* The GL present surface is not bound by the software mirror's 4x
|
||||
* (dual-raster keeps SW at 1x); the backend clamps to its own limits. */
|
||||
int want = g_video_scale < 1 ? 1 : g_video_scale;
|
||||
if (want > SW_MAX_INTERNAL_SCALE) want = SW_MAX_INTERNAL_SCALE;
|
||||
if (want > GL_MAX_INTERNAL_SCALE) want = GL_MAX_INTERNAL_SCALE;
|
||||
gr_set_backend(GR_BACKEND_OPENGL);
|
||||
gl_renderer_set_cpu_auth_dual(1);
|
||||
gr_set_scale(want);
|
||||
@@ -3166,7 +3172,7 @@ static void teardown_game_session_keep_lobby(void) {
|
||||
if (sdl_texture) { SDL_DestroyTexture(sdl_texture); sdl_texture = nullptr; }
|
||||
if (sdl_renderer) { SDL_DestroyRenderer(sdl_renderer); sdl_renderer = nullptr; }
|
||||
if (sdl_window) { SDL_DestroyWindow(sdl_window); sdl_window = nullptr; }
|
||||
if (sdl_pixel_buf) { std::free(sdl_pixel_buf); sdl_pixel_buf = nullptr; }
|
||||
if (sdl_pixel_buf) { std::free(sdl_pixel_buf); sdl_pixel_buf = nullptr; s_pixel_buf_px = 0; }
|
||||
/* Rematch must re-run force_sw (and prefer CPU-auth GL present again). */
|
||||
s_netplay_sw_gpu_locked = 0;
|
||||
s_netplay_gl_present = 0;
|
||||
@@ -7387,8 +7393,14 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
* native path for those frames (the present filter still upscales).
|
||||
* SW-only netplay present: native scanout. Dual-raster FBO present
|
||||
* already returned above at GL SSAA. */
|
||||
/* Under OpenGL the CPU-side display readout is native (the software
|
||||
* mirror stays 1x; the internal resolution lives in the GL FBO), so
|
||||
* this path presents at 1x. Treating that native image as S-scaled
|
||||
* showed a magnified top-left slice. */
|
||||
if (netplay_cpu_auth_gpu() && !netplay_gl_dual_quality())
|
||||
active_scale = 1;
|
||||
else if (g_gl_active)
|
||||
active_scale = 1;
|
||||
else
|
||||
active_scale = (g_video_scale > 1 && !di.depth24) ? g_video_scale : 1;
|
||||
|
||||
@@ -7406,8 +7418,12 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
int base_x = local_viewport_wide
|
||||
? gpu_ws_netplay_local_viewport_base_x()
|
||||
: (int)di.display_x;
|
||||
int n = gr_render_wide_display(sdl_pixel_buf, (int)(sw * sizeof(uint32_t)),
|
||||
base_x, (int)di.display_y, (int)h);
|
||||
/* The staging buffer is sized for at most SW_MAX_INTERNAL_SCALE;
|
||||
* a GL wide surface at a higher scale falls back to canonical. */
|
||||
int n = ((size_t)sw * (size_t)h * (size_t)s <= s_pixel_buf_px)
|
||||
? gr_render_wide_display(sdl_pixel_buf, (int)(sw * sizeof(uint32_t)),
|
||||
base_x, (int)di.display_y, (int)h)
|
||||
: 0;
|
||||
if (n > 0) {
|
||||
active_scale = s;
|
||||
} else {
|
||||
@@ -14837,7 +14853,20 @@ session_reboot:
|
||||
* Dual-raster: gr_set_scale(N) arms GL hr FBO @ N× while glb_set_scale
|
||||
* keeps SW at 1×. SW-only netplay: force scale 1. Offline: full SSAA. */
|
||||
if (g_video_scale < 1) g_video_scale = 1;
|
||||
if (g_video_scale > SW_MAX_INTERNAL_SCALE) g_video_scale = SW_MAX_INTERNAL_SCALE;
|
||||
{
|
||||
/* Per-backend ceiling. OpenGL allocates its hr surface at context init
|
||||
* and clamps there to the driver's texture limits and a memory budget
|
||||
* (gl_scale_limits.h), so only its compile-time bound applies here.
|
||||
* Software keeps a CPU mirror of 1 MiB*S^2 and Vulkan accepts 1..4. */
|
||||
const int ceiling = (g_video_renderer == 1) ? GL_MAX_INTERNAL_SCALE
|
||||
: SW_MAX_INTERNAL_SCALE;
|
||||
if (g_video_scale > ceiling) {
|
||||
std::fprintf(stdout, "psxrecomp: supersampling %dx clamped to %dx "
|
||||
"(%s maximum)\n", g_video_scale, ceiling,
|
||||
g_video_renderer == 2 ? "vulkan" : "software");
|
||||
g_video_scale = ceiling;
|
||||
}
|
||||
}
|
||||
if (net_cfg.enabled && s_netplay_gl_present && gl_renderer_cpu_auth_dual()) {
|
||||
gr_set_scale(g_video_scale);
|
||||
if (g_video_scale > 1) {
|
||||
@@ -15425,10 +15454,14 @@ session_reboot:
|
||||
/* Staging buffer + backing texture preserve the 576-row interlaced PAL
|
||||
* canvas, times the supersampling factor. Netplay: 1×. */
|
||||
{
|
||||
const int tex_scale = netplay_cpu_auth_gpu() ? 1 : g_video_scale;
|
||||
sdl_pixel_buf = (uint32_t*)std::malloc(
|
||||
(size_t)640 * tex_scale * PSX_DISPLAY_PRESENT_MAX_HEIGHT *
|
||||
tex_scale * sizeof(uint32_t));
|
||||
/* The CPU present path only runs above 1x on the software backend
|
||||
* (<= SW_MAX_INTERNAL_SCALE); under OpenGL it is native (see
|
||||
* active_scale), so an 8K GL scale never sizes this buffer by S^2. */
|
||||
int tex_scale = netplay_cpu_auth_gpu() ? 1 : g_video_scale;
|
||||
if (tex_scale > SW_MAX_INTERNAL_SCALE) tex_scale = SW_MAX_INTERNAL_SCALE;
|
||||
s_pixel_buf_px = (size_t)640 * tex_scale * PSX_DISPLAY_PRESENT_MAX_HEIGHT *
|
||||
tex_scale;
|
||||
sdl_pixel_buf = (uint32_t*)std::malloc(s_pixel_buf_px * sizeof(uint32_t));
|
||||
if (!sdl_pixel_buf) {
|
||||
std::fprintf(stderr, "failed to allocate %dx staging buffer\n", tex_scale);
|
||||
return 1;
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Build and run test_gl_scale_invariance.c on a hidden real OpenGL context.
|
||||
|
||||
The fixture runs once per internal scale; the guest-visible (native) VRAM
|
||||
digest must be identical at every scale, and each run checks line thickness
|
||||
at internal resolution. Two clamp runs check that an over-limit request (32x,
|
||||
and a tiny memory budget) stays on the GL backend at the clamped scale.
|
||||
|
||||
macOS/Linux: pass the SDL3 include directory and static library (for example
|
||||
from a runtime build tree's _deps/sdl3-src/include and
|
||||
_deps/sdl3-build/libSDL3.a) and a C compiler. Evidence (commands, output) is
|
||||
written to receipt.json under --output.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
MAC_FRAMEWORKS = ["Cocoa", "OpenGL", "IOKit", "CoreVideo", "CoreAudio", "AudioToolbox",
|
||||
"Carbon", "ForceFeedback", "GameController", "Metal", "QuartzCore",
|
||||
"CoreMedia", "AVFoundation", "Foundation", "CoreHaptics",
|
||||
"UniformTypeIdentifiers"]
|
||||
|
||||
|
||||
def parse_run(stdout):
|
||||
"""Return (checks, failures, digest or None) from a fixture run."""
|
||||
summary = re.search(r"^checks=(\d+) failures=(\d+)$", stdout, re.M)
|
||||
digest = re.search(r"^digest=([0-9a-f]{16})$", stdout, re.M)
|
||||
if not summary:
|
||||
return None
|
||||
return int(summary[1]), int(summary[2]), digest[1] if digest else None
|
||||
|
||||
|
||||
def digests_agree(results):
|
||||
"""results: {scale: digest}. All present and equal."""
|
||||
values = list(results.values())
|
||||
return bool(values) and all(v is not None for v in values) and len(set(values)) == 1
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--cc", default="cc")
|
||||
ap.add_argument("--sdl-include", required=True)
|
||||
ap.add_argument("--sdl-library", required=True)
|
||||
ap.add_argument("--output", required=True)
|
||||
ap.add_argument("--fixture", type=pathlib.Path)
|
||||
ap.add_argument("--scales", default="1,2,3,5,9")
|
||||
args = ap.parse_args()
|
||||
# ';'-separated when CMake hands over a target's include list.
|
||||
sdl_includes = [str(pathlib.Path(d).resolve()) for d in args.sdl_include.split(";") if d]
|
||||
args.sdl_library = str(pathlib.Path(args.sdl_library).resolve())
|
||||
framework = pathlib.Path(__file__).resolve().parents[2]
|
||||
fixture = args.fixture or framework / "runtime/tests/test_gl_scale_invariance.c"
|
||||
out_root = pathlib.Path(args.output).resolve()
|
||||
out_root.mkdir(parents=True, exist_ok=True)
|
||||
dest = pathlib.Path(tempfile.mkdtemp(prefix="scale-", dir=out_root))
|
||||
print("Evidence directory:", dest)
|
||||
receipt = []
|
||||
|
||||
def run(command, env=None):
|
||||
command = [str(c) for c in command]
|
||||
r = subprocess.run(command, cwd=dest, capture_output=True, text=True,
|
||||
encoding="utf-8", errors="replace", env=env)
|
||||
receipt.append({"cmd": command, "exit": r.returncode,
|
||||
"stdout": r.stdout, "stderr": r.stderr[-4000:]})
|
||||
(dest / "receipt.json").write_text(json.dumps(receipt, indent=2), encoding="utf-8")
|
||||
return r
|
||||
|
||||
includes = ["-I", framework / "runtime/include", "-I", framework / "runtime/src"]
|
||||
for d in sdl_includes:
|
||||
includes += ["-I", d]
|
||||
objs = []
|
||||
for name, src in (("probe", fixture), ("sw", framework / "runtime/src/gpu_sw_renderer.c")):
|
||||
o = dest / (name + ".o")
|
||||
r = run([args.cc, "-std=gnu11", "-O1", "-DPSX_SDL3=1", "-DPSX_NO_DEBUG_TOOLS=1",
|
||||
"-DGL_SILENCE_DEPRECATION=1", "-w", *includes, "-c", src, "-o", o])
|
||||
if r.returncode:
|
||||
print(r.stderr[-3000:])
|
||||
return 2
|
||||
objs.append(o)
|
||||
link = [args.cc, *objs, args.sdl_library, "-o", dest / "probe"]
|
||||
if platform.system() == "Darwin":
|
||||
for f in MAC_FRAMEWORKS:
|
||||
link += ["-framework", f]
|
||||
link += ["-liconv", "-lm", "-Wl,-undefined,dynamic_lookup", "-Wl,-dead_strip"]
|
||||
else:
|
||||
link += ["-lGL", "-lm", "-ldl", "-lpthread", "-Wl,--unresolved-symbols=ignore-all",
|
||||
"-Wl,--gc-sections"]
|
||||
r = run(link)
|
||||
if r.returncode:
|
||||
print(r.stderr[-3000:])
|
||||
return 2
|
||||
|
||||
ok = True
|
||||
digests = {}
|
||||
for s in [int(v) for v in args.scales.split(",") if v]:
|
||||
r = run([dest / "probe", s])
|
||||
parsed = parse_run(r.stdout)
|
||||
print(f"scale {s}: exit={r.returncode}", r.stdout.strip().splitlines()[-2:],
|
||||
r.stderr.strip()[-600:])
|
||||
if r.returncode or not parsed or parsed[1]:
|
||||
ok = False
|
||||
digests[s] = parsed[2] if parsed else None
|
||||
if not digests_agree(digests):
|
||||
print("FAIL native VRAM digest differs across scales:", digests)
|
||||
ok = False
|
||||
env = os.environ.copy()
|
||||
for label, s, budget in (("over-limit", 32, None), ("budget", 12, "40")):
|
||||
e = dict(env)
|
||||
if budget is not None:
|
||||
e["PSX_GL_VRAM_BUDGET_MB"] = budget
|
||||
r = run([dest / "probe", s, "clamp"], env=e)
|
||||
parsed = parse_run(r.stdout)
|
||||
print(f"clamp {label}: exit={r.returncode}", r.stdout.strip().splitlines()[-2:],
|
||||
r.stderr.strip()[-600:])
|
||||
if r.returncode or not parsed or parsed[1]:
|
||||
ok = False
|
||||
print("PASS" if ok else "FAIL")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Source guards for the OpenGL internal-resolution scale (source-only).
|
||||
|
||||
The real-GL behaviour is covered by run_gl_scale_invariance.py (needs a GPU and
|
||||
SDL3). These guards pin the structural promises that keep native (1x) output
|
||||
byte-identical and keep a large scale from costing the GL backend:
|
||||
- no local 4x cap in the GL backend; the ceiling lives in gpu_render.h;
|
||||
- init queries the driver limits and clamps, and only a 1x allocation
|
||||
failure can drop the backend to software;
|
||||
- line quads, the area resolve and the half-texel inset change are all
|
||||
gated on a scale above 1;
|
||||
- the dual-raster re-arm and the offline clamp no longer cap GL at the
|
||||
software mirror's 4x.
|
||||
Also unit-tests the invariance runner's result parsing.
|
||||
"""
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
GL = (ROOT / "runtime/src/gpu_gl_renderer.c").read_text(encoding="utf-8")
|
||||
MAIN = (ROOT / "runtime/src/main.cpp").read_text(encoding="utf-8")
|
||||
RENDER_H = (ROOT / "runtime/include/gpu_render.h").read_text(encoding="utf-8")
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
||||
from run_gl_scale_invariance import digests_agree, parse_run # noqa: E402
|
||||
|
||||
|
||||
def body(src, signature):
|
||||
start = src.index(signature)
|
||||
brace = src.index("{", start)
|
||||
depth = 0
|
||||
for i in range(brace, len(src)):
|
||||
if src[i] == "{":
|
||||
depth += 1
|
||||
elif src[i] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return src[brace:i + 1]
|
||||
raise AssertionError("unbalanced " + signature)
|
||||
|
||||
|
||||
class GlScaleGuards(unittest.TestCase):
|
||||
def test_ceiling_is_shared(self):
|
||||
self.assertIsNone(re.search(r"#define\s+GL_MAX_INTERNAL_SCALE\s+4\b", GL))
|
||||
self.assertRegex(RENDER_H, r"#define\s+GL_MAX_INTERNAL_SCALE\s+32\b")
|
||||
|
||||
def test_init_clamps_to_driver_limits(self):
|
||||
init = body(GL, "static int init_gpu_raster(void)")
|
||||
for token in ("GL_MAX_TEXTURE_SIZE", "PSXGL_MAX_RENDERBUFFER_SIZE",
|
||||
"GL_MAX_VIEWPORT_DIMS", "psx_gl_clamp_full_vram_scale"):
|
||||
self.assertIn(token, init)
|
||||
# Retry lower inside GL; only a 1x failure returns 0 (software fallback).
|
||||
self.assertRegex(init, r"while \(!alloc_hr_targets\(s_scale\)\) \{\s*if \(s_scale <= 1\) return 0;")
|
||||
|
||||
def test_s1_paths_unchanged(self):
|
||||
geo = body(GL, "static void gpu_geometry(")
|
||||
self.assertIn("if (mode == GL_LINES && n == 2 && s_scale > 1) {", geo)
|
||||
quad = body(GL, "static void present_target_quad(GLuint tex, int tex_w, int tex_h,\n"
|
||||
" int x, int y, int w, int h, int linear,\n"
|
||||
" int lx, int ly, int lw, int lh, int v_flip,\n"
|
||||
" int apply_gamma, int src_scale) {")
|
||||
self.assertIn("if (src_scale > 1 && lw > 0", quad)
|
||||
self.assertIn("float in = src_scale > 1 ? 0.5f / (float)src_scale : 0.5f;", quad)
|
||||
stencil = body(GL, "static void rebuild_mask_stencils(void)")
|
||||
self.assertIn("if (s_scale <= 1) {", stencil)
|
||||
|
||||
def test_main_does_not_cap_gl_at_software_limit(self):
|
||||
self.assertNotIn("if (want > SW_MAX_INTERNAL_SCALE) want = SW_MAX_INTERNAL_SCALE;", MAIN)
|
||||
self.assertIn("(g_video_renderer == 1) ? GL_MAX_INTERNAL_SCALE", MAIN)
|
||||
|
||||
|
||||
class RunnerParsing(unittest.TestCase):
|
||||
def test_parse(self):
|
||||
self.assertEqual(parse_run("driver=x\ndigest=0123456789abcdef\nchecks=7 failures=0\n"),
|
||||
(7, 0, "0123456789abcdef"))
|
||||
self.assertIsNone(parse_run("garbage"))
|
||||
|
||||
def test_digests(self):
|
||||
self.assertTrue(digests_agree({1: "a", 3: "a"}))
|
||||
self.assertFalse(digests_agree({1: "a", 3: "b"}))
|
||||
self.assertFalse(digests_agree({1: "a", 3: None}))
|
||||
self.assertFalse(digests_agree({}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,189 @@
|
||||
/* Internal-resolution invariance on a real, hidden OpenGL context.
|
||||
*
|
||||
* Run once per scale by run_gl_scale_invariance.py, which compares the
|
||||
* digests across runs. Checks, per scale:
|
||||
* - the native VRAM the game can read back (pack of the hr surface) is the
|
||||
* same at every scale: supersampling must never change guest-visible
|
||||
* pixels (lines excepted, see LINE_BAND, and checked separately);
|
||||
* - a line is one NATIVE pixel thick at internal resolution (S hr rows);
|
||||
* - a request the driver cannot hold is clamped inside GL, never dropped to
|
||||
* the software renderer.
|
||||
* Original source-owned scene; no retail payload. */
|
||||
#include "gpu_gl_renderer.c"
|
||||
#include "mod_texture_banks.c"
|
||||
uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t n,uint32_t a){(void)n;(void)a;return 0;}
|
||||
uint32_t psx_mod_read_word(uint32_t a){(void)a;return 0;}
|
||||
int g_psx_vram_dirty_tracking=0;
|
||||
uint64_t s_frame_count=0;
|
||||
void gpu_vram_dirty_mark_row_impl(uint32_t y){(void)y;}
|
||||
void gpu_vram_dirty_mark_rect(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;}
|
||||
void gpu_vram_dirty_mark_all(void){}
|
||||
int psx_netplay_active(void){return 0;}
|
||||
int gpu_display_is_depth24(void){return 0;}
|
||||
void gpu_get_display_info(GpuDisplayInfo *out){memset(out,0,sizeof(*out));out->width=320;out->height=240;}
|
||||
int psx_ws_prim_in_backdrop(void){return 0;}
|
||||
int gpu_ws_nw_flat_backdrop_enabled(void){return 0;}
|
||||
int g_ws_tex_edge_pct=0;
|
||||
int psx_ws_prim_is_tagged(void){return 0;}
|
||||
void psx_ws_dbg_gate_frame_snapshot(void){}
|
||||
void gpu_depth24_upload_span_reset(void){}
|
||||
void frame_interpolation_schedule_reset(FrameInterpolationSchedule *p){memset(p,0,sizeof(*p));}
|
||||
|
||||
static uint16_t vram[1024*512], peek[1024*512];
|
||||
static int checks, failures;
|
||||
static void check(int ok, const char *label) {
|
||||
checks++;
|
||||
if (!ok) { fprintf(stderr, "FAIL %s\n", label); failures++; }
|
||||
}
|
||||
|
||||
/* Lines are rasterised differently by design above 1x (a one-native-pixel quad
|
||||
* instead of GL_LINES, whose width is capped at 1 on core profiles), so their
|
||||
* band is excluded from the cross-scale digest and checked on its own. */
|
||||
#define LINE_X0 600
|
||||
#define LINE_Y0 300
|
||||
#define LINE_X1 700
|
||||
#define LINE_Y1 360
|
||||
|
||||
static uint64_t fnv(const uint16_t *p, int n, uint64_t h) {
|
||||
for (int i = 0; i < n; i++) { h ^= p[i]; h *= 0x100000001b3ull; }
|
||||
return h;
|
||||
}
|
||||
|
||||
static void scene(void) {
|
||||
/* A 4-bit texture page at (512,0) with a 16-entry CLUT at (512,256). */
|
||||
static uint16_t page[64*64], clut[16];
|
||||
for (int i = 0; i < 64*64; i++) page[i] = (uint16_t)((i * 0x1357u) ^ (i >> 3));
|
||||
for (int i = 0; i < 16; i++) clut[i] = (uint16_t)(i ? (0x0421u * (uint16_t)i) | ((i & 1) << 15) : 0);
|
||||
glb_vram_transfer_in(512, 0, 64, 64, page);
|
||||
glb_vram_transfer_in(512, 256, 16, 1, clut);
|
||||
|
||||
glb_set_draw_area(0, 0, 1023, 511);
|
||||
glb_set_draw_offset(0, 0);
|
||||
glb_set_mask_bits(0, 0);
|
||||
glb_set_semi_transparency(0, 0);
|
||||
glb_set_color_modulation(128, 128, 128, 0);
|
||||
|
||||
/* Framebuffer-like region 0..319 x 0..239. */
|
||||
glb_fill_rect(0, 0, 320, 240, 0x1084);
|
||||
glb_draw_flat_rect(10, 10, 100, 60, 0x03e0);
|
||||
glb_draw_gouraud_triangle(20, 100, 0x001f, 180, 120, 0x7c00, 60, 230, 0x03ff);
|
||||
glb_draw_flat_triangle(200, 20, 310, 90, 230, 200, 0x5294);
|
||||
/* Textured rect (4-bit, CLUT at 512,256) and a textured triangle. */
|
||||
glb_draw_textured_rect(120, 12, 64, 48, 0, 0, 512, 256, 0x0008);
|
||||
glb_draw_shaded_textured_triangle(150, 140, 0, 0, 0x808080,
|
||||
300, 150, 63, 4, 0x6090b0,
|
||||
170, 235, 8, 63, 0xb09060, 512, 256, 0x0008, 0);
|
||||
/* Semi-transparency, every mode. */
|
||||
for (int mode = 0; mode < 4; mode++) {
|
||||
glb_set_semi_transparency(1, mode);
|
||||
glb_draw_flat_rect(20 + mode * 30, 180, 40, 30, (uint16_t)(0x2108 + mode * 0x0842));
|
||||
}
|
||||
glb_set_semi_transparency(0, 0);
|
||||
/* Mask set, then mask-check: the second rect must not overwrite masked px. */
|
||||
glb_set_mask_bits(1, 0);
|
||||
glb_draw_flat_rect(250, 150, 30, 30, 0x4210);
|
||||
glb_set_mask_bits(0, 1);
|
||||
glb_draw_flat_rect(240, 140, 50, 50, 0x7fff);
|
||||
glb_set_mask_bits(0, 0);
|
||||
/* Clipped primitive. */
|
||||
glb_set_draw_area(40, 40, 90, 90);
|
||||
glb_draw_flat_rect(0, 0, 200, 200, 0x3def);
|
||||
glb_set_draw_area(0, 0, 1023, 511);
|
||||
/* Copies: overlapping inside the frame, and from an off-screen area in. */
|
||||
glb_copy_rect(10, 10, 14, 13, 80, 50);
|
||||
glb_draw_flat_rect(400, 300, 32, 32, 0x7c1f);
|
||||
glb_copy_rect(400, 300, 280, 200, 32, 32);
|
||||
/* A render-to-texture round trip: draw, then sample what was drawn. */
|
||||
glb_draw_flat_rect(576, 64, 32, 32, 0x03e0);
|
||||
glb_draw_textured_rect(200, 200, 32, 32, 0, 64, 512, 256, 0x0009 | (1 << 7) * 0);
|
||||
|
||||
/* Lines, inside LINE_BAND only. */
|
||||
glb_draw_line(LINE_X0 + 5, LINE_Y0 + 10, LINE_X0 + 80, LINE_Y0 + 10, 0x7fff); /* horizontal */
|
||||
glb_draw_line(LINE_X0 + 5, LINE_Y0 + 20, LINE_X0 + 45, LINE_Y0 + 50, 0x03ff); /* diagonal */
|
||||
glb_draw_shaded_line(LINE_X0 + 90, LINE_Y0 + 5, 0x001f, LINE_X0 + 92, LINE_Y0 + 55, 0x7c00); /* steep */
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int scale = argc > 1 ? atoi(argv[1]) : 1;
|
||||
const char *mode = argc > 2 ? argv[2] : "scene";
|
||||
if (SDL_Init(SDL_INIT_VIDEO) != 0) return 2;
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
#if defined(__APPLE__)
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
|
||||
#endif
|
||||
SDL_Window *win = SDL_CreateWindow("Scale invariance hidden test", 0, 0, 128, 128,
|
||||
SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN);
|
||||
if (!win) return 2;
|
||||
for (int i = 0; i < 1024*512; i++) vram[i] = 0;
|
||||
glb_init(vram);
|
||||
glb_set_scale(scale);
|
||||
gl_renderer_set_swap_interval(0);
|
||||
if (!gl_renderer_init_context(win)) { fprintf(stderr, "FAIL context\n"); return 2; }
|
||||
GlScaleInfo si;
|
||||
gl_renderer_scale_info(&si);
|
||||
printf("driver=%s max_dim=%d requested=%d effective=%d\n",
|
||||
(const char *)glGetString(GL_VERSION), si.max_dim, si.requested, si.effective);
|
||||
|
||||
if (!strcmp(mode, "clamp")) {
|
||||
/* Whatever was requested, the backend stays GL and never exceeds what
|
||||
* the limits allow. */
|
||||
int want = psx_gl_clamp_full_vram_scale(scale, GL_MAX_INTERNAL_SCALE, si.max_dim,
|
||||
psx_gl_budget_bytes_from_env(getenv("PSX_GL_VRAM_BUDGET_MB")),
|
||||
NULL);
|
||||
check(s_raster_ok == 1, "GL pipeline kept (no software fallback)");
|
||||
check(si.effective == want, "effective scale is the limit-clamped request");
|
||||
check(psx_gl_full_vram_fits(si.effective, si.max_dim, 0), "allocated surface within driver limit");
|
||||
check(glGetError() == GL_NO_ERROR, "GL error");
|
||||
printf("checks=%d failures=%d\n", checks, failures);
|
||||
gl_renderer_shutdown(); SDL_DestroyWindow(win); SDL_Quit();
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
||||
check(si.effective == scale, "requested scale allocated");
|
||||
scene();
|
||||
gl_renderer_sync_cpu();
|
||||
check(gl_renderer_fbo_peek(0, 0, 1024, 512, peek), "native peek");
|
||||
/* Digest of guest-visible VRAM, the line band masked out. */
|
||||
for (int y = LINE_Y0; y < LINE_Y1; y++)
|
||||
for (int x = LINE_X0; x < LINE_X1; x++) peek[y * 1024 + x] = 0;
|
||||
uint64_t digest = fnv(peek, 1024*512, 0xcbf29ce484222325ull);
|
||||
check(memcmp(vram, vram, 2) == 0, "cpu mirror readable");
|
||||
|
||||
/* Line thickness at internal resolution: the horizontal line covers S hr
|
||||
* rows (native 1 row); the steep line S hr columns. */
|
||||
{
|
||||
int w = 100, h = 60;
|
||||
uint32_t *img = (uint32_t *)malloc((size_t)w * scale * h * scale * 4);
|
||||
int ow = 0, oh = 0;
|
||||
int n = img ? gl_renderer_read_display_hires(LINE_X0, LINE_Y0, w, h, img,
|
||||
w * scale * h * scale, &ow, &oh) : 0;
|
||||
check(n == w * scale * h * scale && ow == w * scale && oh == h * scale,
|
||||
"hires readback size");
|
||||
if (n) {
|
||||
int col = (30) * scale + scale / 2, rows = 0; /* inside the horizontal line */
|
||||
for (int yy = 0; yy < oh; yy++) {
|
||||
uint32_t p = img[yy * ow + col] & 0xFFFFFFu;
|
||||
if (p == 0xF8F8F8u) rows++;
|
||||
}
|
||||
if (rows != scale) fprintf(stderr, "horizontal line rows=%d scale=%d\n", rows, scale);
|
||||
check(rows == scale, "horizontal line is one native pixel thick");
|
||||
int row = 30 * scale + scale / 2, cols = 0; /* across the steep line */
|
||||
for (int xx = 85 * scale; xx < 98 * scale; xx++) {
|
||||
uint32_t p = img[row * ow + xx];
|
||||
if ((p & 0xFFFFFFu) != 0) cols++;
|
||||
}
|
||||
if (cols != scale) fprintf(stderr, "steep line cols=%d scale=%d\n", cols, scale);
|
||||
check(cols == scale, "steep line is one native pixel thick");
|
||||
}
|
||||
free(img);
|
||||
}
|
||||
check(glGetError() == GL_NO_ERROR, "GL error");
|
||||
printf("digest=%016llx\n", (unsigned long long)digest);
|
||||
printf("checks=%d failures=%d\n", checks, failures);
|
||||
gl_renderer_shutdown();
|
||||
SDL_DestroyWindow(win);
|
||||
SDL_Quit();
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/* gl_scale_limits.h: the OpenGL backend clamps an internal-resolution request
|
||||
* to the driver's size limit and a memory budget instead of failing context
|
||||
* init (which used to drop the whole backend to software). Pure arithmetic. */
|
||||
#include "gl_scale_limits.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
static int g_failures = 0;
|
||||
|
||||
static void expect_int(const char *what, long long got, long long want) {
|
||||
if (got != want) {
|
||||
printf("FAIL %s: got %lld want %lld\n", what, got, want);
|
||||
++g_failures;
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const uint64_t MiB = 1ull << 20;
|
||||
const uint64_t budget = (uint64_t)PSX_GL_DEFAULT_BUDGET_MB * MiB;
|
||||
int why = -1;
|
||||
|
||||
/* Surface bytes: colour + depth-stencil, 8 B per internal pixel. */
|
||||
expect_int("bytes 1x", (long long)psx_gl_full_vram_bytes(1), 4 * MiB);
|
||||
expect_int("bytes 9x", (long long)psx_gl_full_vram_bytes(9), 324 * MiB);
|
||||
|
||||
/* Apple GL on Metal reports 16384: 16x is the largest full-VRAM scale. */
|
||||
expect_int("16384 -> 16", psx_gl_clamp_full_vram_scale(18, 32, 16384, budget, &why), 16);
|
||||
expect_int("16384 reason", why, PSX_GL_SCALE_TEXTURE);
|
||||
expect_int("16384 fits 16", psx_gl_full_vram_fits(16, 16384, budget), 1);
|
||||
expect_int("16384 fits 17", psx_gl_full_vram_fits(17, 16384, budget), 0);
|
||||
/* 32768-limit GPUs: 18x fits the texture limit; the 2 GiB budget allows 22. */
|
||||
expect_int("32768 -> 18", psx_gl_clamp_full_vram_scale(18, 32, 32768, budget, &why), 18);
|
||||
expect_int("32768 reason", why, PSX_GL_SCALE_OK);
|
||||
expect_int("32768 budget", psx_gl_clamp_full_vram_scale(32, 32, 32768, budget, &why), 22);
|
||||
expect_int("32768 budget reason", why, PSX_GL_SCALE_BUDGET);
|
||||
/* Ceiling. */
|
||||
expect_int("ceiling", psx_gl_clamp_full_vram_scale(40, 32, 0, 0, &why), 32);
|
||||
expect_int("ceiling reason", why, PSX_GL_SCALE_CEILING);
|
||||
/* Preset requests that fit stay exact. */
|
||||
const int presets[] = { 1, 3, 5, 6, 9, 12 };
|
||||
for (unsigned i = 0; i < sizeof presets / sizeof presets[0]; i++) {
|
||||
expect_int("preset exact", psx_gl_clamp_full_vram_scale(presets[i], 32, 16384, budget, &why),
|
||||
presets[i]);
|
||||
expect_int("preset reason", why, PSX_GL_SCALE_OK);
|
||||
}
|
||||
/* Never below 1, whatever the limits. */
|
||||
expect_int("tiny limit", psx_gl_clamp_full_vram_scale(9, 32, 512, budget, NULL), 1);
|
||||
expect_int("zero req", psx_gl_clamp_full_vram_scale(0, 32, 16384, budget, NULL), 1);
|
||||
/* Unknown limit (0) and no budget: only the ceiling applies. */
|
||||
expect_int("unknown", psx_gl_clamp_full_vram_scale(20, 32, 0, 0, NULL), 20);
|
||||
|
||||
/* Native-wide surfaces: widest width that still fits the limit. */
|
||||
expect_int("wide 16x", psx_gl_max_wide_width(16, 16384), 1024);
|
||||
expect_int("wide 18x", psx_gl_max_wide_width(18, 16384), 910);
|
||||
expect_int("wide too tall", psx_gl_max_wide_width(40, 16384), 0);
|
||||
|
||||
/* Budget override parsing. */
|
||||
expect_int("env null", (long long)psx_gl_budget_bytes_from_env(NULL), (long long)budget);
|
||||
expect_int("env 64", (long long)psx_gl_budget_bytes_from_env("64"), (long long)(64 * MiB));
|
||||
expect_int("env 0", (long long)psx_gl_budget_bytes_from_env("0"), 0);
|
||||
expect_int("env junk", (long long)psx_gl_budget_bytes_from_env("12x"), (long long)budget);
|
||||
|
||||
if (g_failures) return 1;
|
||||
printf("gl_scale_limits: all checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user