mirror of
https://github.com/ARMSX2/ARMSX1.git
synced 2026-08-24 16:53:35 -07:00
5384 lines
228 KiB
C
5384 lines
228 KiB
C
#include "gpu_hw_gl.h"
|
|
|
|
#if defined(USE_HARDWARE) && defined(ARMSX_ENABLE_GL) && defined(__ANDROID__)
|
|
#define ARMSX_HW_GL_BUILD 1
|
|
#endif
|
|
|
|
#ifdef USE_HARDWARE
|
|
|
|
#include <stdlib.h>
|
|
|
|
/*
|
|
Reads the ARMSX_GL_MASK_BIT escape hatch. Outside ARMSX_HW_GL_BUILD with the predicate
|
|
below, so the whole chain — environment to decision — is exercisable on the host; the
|
|
env read was otherwise the one link between "the opt-in exists" and "the opt-in works"
|
|
that no test could reach.
|
|
|
|
Exact "1" only. A prefix match would make ARMSX_GL_MASK_BIT=0... or a stray "10" enable
|
|
an unproven rasterizer path, and this switch exists precisely because that path silently
|
|
took over a session once already.
|
|
*/
|
|
int armsx_hw_gl_mask_bit_opt_in(void) {
|
|
const char* v = getenv("ARMSX_GL_MASK_BIT");
|
|
|
|
return v && v[0] == '1' && v[1] == '\0';
|
|
}
|
|
|
|
/*
|
|
Deliberately OUTSIDE ARMSX_HW_GL_BUILD: it touches no GL, and a host test has to be able
|
|
to compile this file and link the rule on a machine with no GLES at all. See the contract
|
|
in gpu_hw_gl.h and the truth table in tests/gpu_rasterizer_select.c.
|
|
|
|
The opt-in is checked FIRST and on its own line because it is the whole point: without it
|
|
this must return 0 for every hardware combination, including the one that reports perfect
|
|
support. That is what keeps a session with accurate_mask_bit on the CPU rasterizer, which
|
|
is the rasterizer against which mask-from-texel was validated.
|
|
*/
|
|
int armsx_hw_gl_mask_bit_supported(int have_fbfetch, int driver_trusted, int is_angle,
|
|
int opt_in) {
|
|
if (!opt_in)
|
|
return 0;
|
|
|
|
/* Framebuffer fetch is the only mechanism in GLES that hands a fragment shader the
|
|
destination, which the mask CHECK needs. MediaTek Mali advertises it and returns zero
|
|
or stale destination colour, and ANGLE has been seen to crash the compiler on it —
|
|
either way the mask check would silently do nothing. */
|
|
return have_fbfetch && driver_trusted && !is_angle;
|
|
}
|
|
|
|
int armsx_hw_gl_use_cpu_fallback(int rasterizer_mode, int internal_scale) {
|
|
return rasterizer_mode == 2 || (rasterizer_mode == 1 && internal_scale > 1);
|
|
}
|
|
#endif
|
|
|
|
#ifdef ARMSX_HW_GL_BUILD
|
|
|
|
#include <dlfcn.h>
|
|
#include <stdarg.h>
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
|
|
/* Types and constants only; every entry point is dlsym'd out of the same provider the
|
|
present path bound, exactly as frontend/render_gl.cpp does. */
|
|
#include <EGL/egl.h>
|
|
|
|
#include "diagnostics.h"
|
|
#include "gpu_profile.h"
|
|
#include "render.h"
|
|
|
|
#if !defined(EGL_OPENGL_ES3_BIT_KHR)
|
|
#define EGL_OPENGL_ES3_BIT_KHR 0x00000040
|
|
#endif
|
|
|
|
/*
|
|
============================================================================================
|
|
The software rasterizer, in a fragment shader.
|
|
============================================================================================
|
|
|
|
Everything below reproduces psx/dev/gpu.c's three live rasterizers on the GPU. The
|
|
unusual part — and the reason this is not a textbook GPU_HW bring-up — is that it does
|
|
NOT use hardware attribute interpolation.
|
|
|
|
A GPU interpolates at the fragment centre (x+0.5, y+0.5). gpu_render_triangle evaluates
|
|
its barycentrics at the integer coordinate (x, y) (gpu.c:331-332). That half-pixel is a
|
|
guaranteed parity failure for every Gouraud and every textured polygon, and it is
|
|
invisible at S=1 only if you never look. So instead:
|
|
|
|
* every vertex carries ALL THREE of the triangle's vertices, colours and UVs as flat
|
|
attributes;
|
|
* the fragment shader recomputes the edge functions itself at
|
|
pf = gl_FragCoord.xy / S - 0.5
|
|
which at S == 1 is EXACTLY (x, y) — the software's own sample point — and at S > 1
|
|
is the natural centred sub-pixel position, so one shader serves both;
|
|
* the top-left fill rule (gpu.c:266-267) and the half-open bounding box
|
|
(gpu.c:320-321) are evaluated in the shader too, so coverage matches by
|
|
construction instead of by trusting the GPU's fill rule to agree;
|
|
* gpu_fetch_texel_bilinear (gpu.c:225), which polygons use unconditionally, is ported
|
|
verbatim as four texelFetches. the backend deviation #1 is therefore
|
|
REPRODUCED, not "fixed" — parity first.
|
|
|
|
That costs ~10 extra ALU ops per fragment, which no GPU notices, and it is what makes
|
|
"1x is pixel-identical" a design property rather than a hope.
|
|
|
|
Blending (the backend, refined). With blending permanently enabled as
|
|
(ONE, SRC_ALPHA) + FUNC_ADD:
|
|
|
|
opaque fragment rgb = F, a = 0 -> F*1 + B*0 = F (a plain write)
|
|
semi-transparency 0 rgb = F*0.5, a = 0.5 -> 0.5F + 0.5B
|
|
semi-transparency 1 rgb = F, a = 1 -> F + B
|
|
semi-transparency 3 rgb = F*0.25, a = 1 -> 0.25F + B
|
|
|
|
so opaque primitives, modes 0/1/3, and the per-texel STP mix INSIDE one primitive all
|
|
coexist in a single draw call with no state change at all. Only mode 2 (B - F,
|
|
FUNC_REVERSE_SUBTRACT) needs its own range, and only textured mode-2 primitives need
|
|
the two-pass split. The render target is RGBA8 rather than RGB5_A1 precisely because
|
|
the 8-bit arithmetic then works out exactly: the shader emits 5-bit values
|
|
pre-multiplied to 8 bits (c5*8), every blend result stays a multiple of 4, and the
|
|
>> 3 at readback recovers the software's integer answer.
|
|
|
|
Known divergence, recorded rather than hidden: a pixel that is blended MORE THAN ONCE
|
|
keeps 8-bit precision in the render target where the software path re-truncates to 5
|
|
bits after every blend. Single-layer transparency is exact; the second layer can differ
|
|
by one 5-bit step. Fixing it needs programmable blending (framebuffer fetch), which is
|
|
driver-gated on this platform and is deliberately not the baseline.
|
|
|
|
MASK MODE is the exception, and it is where all of the above changes shape. Once
|
|
PSX_GPU_ACCURACY_MASK_BIT is on, `.a` has to carry VRAM bit 15 and can no longer be the
|
|
SRC_ALPHA blend factor, so the blend moves into the fragment shader via
|
|
GL_EXT_shader_framebuffer_fetch — which the mask CHECK needs anyway, being a read of the
|
|
destination. The arithmetic is the same four modes with the same `a`, plus the 5-bit
|
|
re-truncation the paragraph above says is missing, so multi-layer transparency becomes
|
|
exact there for free. Without the extension the backend declines and the CPU rasterizer
|
|
takes the session. the backend.
|
|
|
|
VRAM coherency (the backend) is one-way here, because
|
|
PSX_GPU_BACKEND_SOFTWARE_SHADOW is set and gpu->vram is therefore always correct. The
|
|
only job is keeping the native-resolution vram texture in sync with host VRAM, which is
|
|
a pure upload problem with a known-good source. See the dirty-tile block below.
|
|
*/
|
|
|
|
/* ---- GL types and enums ------------------------------------------------------------- */
|
|
/* Declared here rather than included, exactly as frontend/render_gl.cpp does, so this file
|
|
needs no GL headers on any platform and cannot drift out of sync with the NDK's. */
|
|
|
|
typedef unsigned int GLenum;
|
|
typedef unsigned char GLboolean;
|
|
typedef unsigned int GLbitfield;
|
|
typedef int GLint;
|
|
typedef unsigned int GLuint;
|
|
typedef int GLsizei;
|
|
typedef float GLfloat;
|
|
typedef char GLchar;
|
|
typedef intptr_t GLintptr;
|
|
typedef intptr_t GLsizeiptr;
|
|
|
|
#define GL_NO_ERROR 0
|
|
#define GL_FALSE 0
|
|
#define GL_TRUE 1
|
|
#define GL_TRIANGLES 0x0004
|
|
#define GL_TRIANGLE_STRIP 0x0005
|
|
#define GL_ZERO 0
|
|
#define GL_ONE 1
|
|
#define GL_SRC_ALPHA 0x0302
|
|
#define GL_FUNC_ADD 0x8006
|
|
#define GL_FUNC_REVERSE_SUBTRACT 0x800B
|
|
#define GL_BLEND 0x0BE2
|
|
#define GL_SCISSOR_TEST 0x0C11
|
|
#define GL_DEPTH_TEST 0x0B71
|
|
#define GL_STENCIL_TEST 0x0B90
|
|
#define GL_CULL_FACE 0x0B44
|
|
#define GL_DITHER 0x0BD0
|
|
#define GL_COLOR_BUFFER_BIT 0x00004000
|
|
#define GL_TEXTURE_2D 0x0DE1
|
|
#define GL_TEXTURE0 0x84C0
|
|
#define GL_TEXTURE1 0x84C1
|
|
#define GL_TEXTURE_MAG_FILTER 0x2800
|
|
#define GL_TEXTURE_MIN_FILTER 0x2801
|
|
#define GL_TEXTURE_WRAP_S 0x2802
|
|
#define GL_TEXTURE_WRAP_T 0x2803
|
|
#define GL_NEAREST 0x2600
|
|
#define GL_CLAMP_TO_EDGE 0x812F
|
|
#define GL_RGBA 0x1908
|
|
#define GL_RGBA8 0x8058
|
|
#define GL_RG 0x8227
|
|
#define GL_RG8 0x822B
|
|
#define GL_RED_INTEGER 0x8D94
|
|
#define GL_R16UI 0x8234
|
|
#define GL_UNSIGNED_BYTE 0x1401
|
|
#define GL_UNSIGNED_SHORT 0x1403
|
|
#define GL_UNSIGNED_INT 0x1405
|
|
#define GL_FLOAT 0x1406
|
|
#define GL_ARRAY_BUFFER 0x8892
|
|
#define GL_STREAM_DRAW 0x88E0
|
|
#define GL_VERTEX_SHADER 0x8B31
|
|
#define GL_FRAGMENT_SHADER 0x8B30
|
|
#define GL_COMPILE_STATUS 0x8B81
|
|
#define GL_LINK_STATUS 0x8B82
|
|
#define GL_INFO_LOG_LENGTH 0x8B84
|
|
#define GL_FRAMEBUFFER 0x8D40
|
|
#define GL_READ_FRAMEBUFFER 0x8CA8
|
|
#define GL_DRAW_FRAMEBUFFER 0x8CA9
|
|
#define GL_COLOR_ATTACHMENT0 0x8CE0
|
|
#define GL_FRAMEBUFFER_COMPLETE 0x8CD5
|
|
#define GL_UNPACK_ALIGNMENT 0x0CF5
|
|
#define GL_UNPACK_ROW_LENGTH 0x0CF2
|
|
#define GL_PACK_ALIGNMENT 0x0D05
|
|
#define GL_PACK_ROW_LENGTH 0x0D02
|
|
#define GL_MAX_TEXTURE_SIZE 0x0D33
|
|
#define GL_IMPLEMENTATION_COLOR_READ_TYPE 0x8B9A
|
|
#define GL_IMPLEMENTATION_COLOR_READ_FORMAT 0x8B9B
|
|
#define GL_VENDOR 0x1F00
|
|
#define GL_VERSION 0x1F02
|
|
#define GL_RENDERER 0x1F01
|
|
#define GL_EXTENSIONS 0x1F03
|
|
|
|
/* ---- entry points ------------------------------------------------------------------- */
|
|
|
|
typedef struct {
|
|
void (*ActiveTexture)(GLenum);
|
|
void (*AttachShader)(GLuint, GLuint);
|
|
void (*BindAttribLocation)(GLuint, GLuint, const GLchar*);
|
|
void (*BindBuffer)(GLenum, GLuint);
|
|
void (*BindFramebuffer)(GLenum, GLuint);
|
|
void (*BindTexture)(GLenum, GLuint);
|
|
void (*BindVertexArray)(GLuint);
|
|
void (*BlendEquation)(GLenum);
|
|
void (*BlendFunc)(GLenum, GLenum);
|
|
void (*BlitFramebuffer)(GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLint,
|
|
GLbitfield, GLenum);
|
|
void (*BufferData)(GLenum, GLsizeiptr, const void*, GLenum);
|
|
GLenum (*CheckFramebufferStatus)(GLenum);
|
|
void (*Clear)(GLbitfield);
|
|
void (*ClearColor)(GLfloat, GLfloat, GLfloat, GLfloat);
|
|
void (*ColorMask)(GLboolean, GLboolean, GLboolean, GLboolean);
|
|
void (*CompileShader)(GLuint);
|
|
GLuint (*CreateProgram)(void);
|
|
GLuint (*CreateShader)(GLenum);
|
|
void (*DeleteBuffers)(GLsizei, const GLuint*);
|
|
void (*DeleteFramebuffers)(GLsizei, const GLuint*);
|
|
void (*DeleteProgram)(GLuint);
|
|
void (*DeleteShader)(GLuint);
|
|
void (*DeleteTextures)(GLsizei, const GLuint*);
|
|
void (*DeleteVertexArrays)(GLsizei, const GLuint*);
|
|
void (*Disable)(GLenum);
|
|
void (*DrawArrays)(GLenum, GLint, GLsizei);
|
|
void (*Enable)(GLenum);
|
|
void (*EnableVertexAttribArray)(GLuint);
|
|
void (*Finish)(void);
|
|
/* Trailing underscore only because the macro that loads it takes the field name and the
|
|
symbol name separately, and this keeps them visually distinct. */
|
|
void (*FramebufferTexture2D_)(GLenum, GLenum, GLenum, GLuint, GLint);
|
|
void (*GenBuffers)(GLsizei, GLuint*);
|
|
void (*GenFramebuffers)(GLsizei, GLuint*);
|
|
void (*GenTextures)(GLsizei, GLuint*);
|
|
void (*GenVertexArrays)(GLsizei, GLuint*);
|
|
GLenum (*GetError)(void);
|
|
void (*GetIntegerv)(GLenum, GLint*);
|
|
void (*GetProgramInfoLog)(GLuint, GLsizei, GLsizei*, GLchar*);
|
|
void (*GetProgramiv)(GLuint, GLenum, GLint*);
|
|
void (*GetShaderInfoLog)(GLuint, GLsizei, GLsizei*, GLchar*);
|
|
void (*GetShaderiv)(GLuint, GLenum, GLint*);
|
|
const unsigned char* (*GetString)(GLenum);
|
|
GLint (*GetUniformLocation)(GLuint, const GLchar*);
|
|
void (*LinkProgram)(GLuint);
|
|
void (*PixelStorei)(GLenum, GLint);
|
|
void (*ReadPixels)(GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, void*);
|
|
void (*Scissor)(GLint, GLint, GLsizei, GLsizei);
|
|
void (*ShaderSource)(GLuint, GLsizei, const GLchar* const*, const GLint*);
|
|
void (*TexImage2D)(GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum,
|
|
const void*);
|
|
void (*TexParameteri)(GLenum, GLenum, GLint);
|
|
void (*TexSubImage2D)(GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum,
|
|
const void*);
|
|
void (*Uniform1f)(GLint, GLfloat);
|
|
void (*Uniform1i)(GLint, GLint);
|
|
void (*Uniform1iv)(GLint, GLsizei, const GLint*);
|
|
void (*Uniform2f)(GLint, GLfloat, GLfloat);
|
|
void (*Uniform2i)(GLint, GLint, GLint);
|
|
void (*Uniform4f)(GLint, GLfloat, GLfloat, GLfloat, GLfloat);
|
|
void (*UseProgram)(GLuint);
|
|
void (*VertexAttribIPointer)(GLuint, GLint, GLenum, GLsizei, const void*);
|
|
void (*VertexAttribPointer)(GLuint, GLint, GLenum, GLboolean, GLsizei, const void*);
|
|
void (*Viewport)(GLint, GLint, GLsizei, GLsizei);
|
|
} gl_api_t;
|
|
|
|
/* ---- vertex format ------------------------------------------------------------------ */
|
|
|
|
/* 72 bytes. Every triangle's three vertices carry the whole primitive, because the
|
|
fragment shader does its own barycentric evaluation (see the file header). PS1 frames
|
|
are a few thousand triangles at most, so ~200 bytes per triangle is not the constraint;
|
|
draw-call count is. */
|
|
typedef struct {
|
|
float pos[2]; /* this vertex, native coords, drawing offset already applied */
|
|
float tri0[4]; /* x0,y0,x1,y1 -- post-winding-swap, offset applied */
|
|
float tri1[2]; /* x2,y2 */
|
|
float triw[3]; /* PGXP: GTE w (SZ3) of a/b/c; {1,1,1} marks an integer triangle,
|
|
which the shader reads as "affine, use the exact bdiv path". */
|
|
uint8_t col0[4]; /* 0xBBGGRR of vertex a, as gpu.c stores it */
|
|
uint8_t col1[4];
|
|
uint8_t col2[4];
|
|
uint16_t uv01[4]; /* u0,v0,u1,v1 */
|
|
uint16_t uv2[4]; /* u2,v2,texpage,clut */
|
|
uint16_t misc[4]; /* flags, transp_mode, -, - */
|
|
uint8_t texwin[4]; /* mx,my,ox,oy, all pre-shifted by 3 by GP0(E2) */
|
|
/*
|
|
Texture replacement (psx/texrep.h), packed:
|
|
[0] atlas x | (atlas y << 16) -- where the image sits in repl_tex
|
|
[1] u0 | (v0 << 8) | (S << 16) -- folded rect origin and the integer upscale
|
|
[2] (nw-1) | ((nh-1) << 16) -- folded rect size, for the shader's clamp
|
|
[3] reserved, always 0
|
|
|
|
S == 0 means "no replacement", which is what every vertex carries unless the feature
|
|
is on AND a pack file matched THIS primitive. It is per-VERTEX for the same reason
|
|
GLF_MASK_CHECK is per vertex: a uniform would be per-DRAW and would break the batch at
|
|
every primitive, and PS1 games change texture page constantly.
|
|
*/
|
|
uint32_t repl[4];
|
|
} gl_vertex_t;
|
|
|
|
/* Mirrors the PA_ and RA_ attribute bits where their values coincide, so the CPU side can
|
|
pass gpu.c's attrib byte through almost unchanged and only add the two bits gpu.c has no
|
|
concept of. */
|
|
enum {
|
|
GLF_RAW = 0x0001,
|
|
GLF_TRANSP = 0x0002,
|
|
GLF_TEXTURED = 0x0004,
|
|
GLF_SHADED = 0x0010,
|
|
GLF_SPRITE = 0x0100, /* point-sampled, UV from the position delta, never dithered */
|
|
GLF_DITHER = 0x0200,
|
|
/*
|
|
GP0(E6), resolved per PRIMITIVE by psx_gpu_mask_check()/psx_gpu_mask_set() and carried
|
|
per VERTEX rather than as a uniform — which is the one design decision that makes the
|
|
mask bit free here. the backend maps it to the stencil test, whose
|
|
reference value is per-DRAW; that would break the batch at every GP0(E6), and PS1 games
|
|
toggle it between primitives (Silent Hill's fog does it per object). In the vertex it
|
|
costs nothing and never splits a range.
|
|
*/
|
|
GLF_MASK_CHECK = 0x0400,
|
|
GLF_MASK_SET = 0x0800
|
|
};
|
|
|
|
/* Blend state a draw range runs under. Only mode 2 needs the second one — see the header. */
|
|
enum { GL_RANGE_ADD = 0, GL_RANGE_SUB = 1 };
|
|
|
|
typedef struct {
|
|
int first; /* first vertex */
|
|
int count;
|
|
uint8_t blend; /* GL_RANGE_* */
|
|
uint8_t stp_pass; /* 0 = all fragments, 1 = opaque only, 2 = semi-transparent only */
|
|
int16_t sx, sy, sw, sh; /* scissor, native coords */
|
|
} gl_range_t;
|
|
|
|
/* ---- dirty-tile tracking ------------------------------------------------------------ */
|
|
|
|
/* 16x16 native texels per tile -> 64 x 32 tiles over VRAM, one uint64 per tile row.
|
|
A bounding rectangle alone is not enough: a game with its framebuffer at the top-left
|
|
and its textures at the bottom-right would produce a box covering all of VRAM and force
|
|
a full 1 MB upload every frame (the backend). */
|
|
#define TILE_SHIFT 4
|
|
#define TILES_X 64
|
|
#define TILES_Y 32
|
|
|
|
typedef struct { uint64_t row[TILES_Y]; } tilemap_t;
|
|
|
|
/* ---- backend ------------------------------------------------------------------------ */
|
|
|
|
typedef struct {
|
|
psx_gpu_backend_t base;
|
|
psx_gpu_t* gpu;
|
|
|
|
gl_api_t gl;
|
|
void* gles_library;
|
|
|
|
/* Only populated when we had to make our own context because the presentation backend
|
|
is not OpenGL. See gl_acquire_context(). */
|
|
void* egl_library;
|
|
EGLDisplay egl_display;
|
|
EGLContext egl_context;
|
|
EGLSurface egl_surface;
|
|
int owns_context;
|
|
|
|
int scale;
|
|
int rt_w, rt_h;
|
|
|
|
GLuint rt_tex, rt_fbo;
|
|
GLuint vram_tex;
|
|
GLuint vram_fbo; /* vram_tex as a TARGET: row 1's GPU->GPU resolve */
|
|
int vram_fbo_state; /* 0 untried, 1 complete, -1 unusable */
|
|
GLuint scratch_tex, scratch_fbo; /* GP0(80) needs a bounce; GL forbids self-blit */
|
|
int scratch_w, scratch_h;
|
|
GLuint resolve_tex, resolve_fbo; /* packed BGR555 scanout, read straight to the host */
|
|
int resolve_w, resolve_h;
|
|
GLuint xfer_tex, xfer_fbo; /* packed BGR555 at NATIVE size, for GPU->host reads */
|
|
int xfer_w, xfer_h;
|
|
|
|
/*
|
|
Texture replacement atlas (psx/texrep.h). ONE RGBA8 texture, shelf-packed, sampled
|
|
with texelFetch. Created lazily the first time a replacement is actually bound, so a
|
|
session with the feature off never allocates it and the whole block below stays zero.
|
|
|
|
No eviction. When a frame needs more replacement pixels than the atlas holds, the
|
|
PENDING BATCH is flushed -- which is the only thing that still needs the pixels
|
|
currently in it -- and the packer starts over. That is why residency is a generation
|
|
(`repl_gen`) rather than a flag: resetting the atlas is one increment, and every
|
|
psx_texrep_image_t stamped with an older generation is implicitly evicted.
|
|
|
|
`repl_epoch` tracks psx_texrep_epoch(), so switching packs in the settings screen
|
|
drops the atlas without this file having to be told.
|
|
*/
|
|
GLuint repl_tex;
|
|
int repl_dim; /* square edge; 0 = never created */
|
|
int repl_x, repl_y, repl_shelf;
|
|
uint32_t repl_gen;
|
|
uint32_t repl_epoch;
|
|
uint32_t cur_repl[4]; /* a_repl for the primitive being emitted */
|
|
int repl_full_logged;
|
|
|
|
GLuint vbo, vao, quad_vao;
|
|
|
|
GLuint prog_draw;
|
|
GLint u_draw_rt_size, u_draw_scale, u_draw_vram, u_draw_dither, u_draw_dither_on,
|
|
u_draw_stp_pass, u_draw_paint, u_draw_filter, u_draw_tex_trunc, u_draw_mask_texel,
|
|
u_draw_repl;
|
|
GLuint prog_xfer;
|
|
GLint u_xfer_rect, u_xfer_rt_size, u_xfer_scale, u_xfer_vram;
|
|
GLuint prog_resolve;
|
|
GLint u_res_rt, u_res_origin, u_res_limit, u_res_step, u_res_box, u_res_mask;
|
|
GLuint prog_gres; /* the same resolve, with a uint output */
|
|
GLint u_gres_rt, u_gres_limit, u_gres_step, u_gres_half, u_gres_mask;
|
|
|
|
/*
|
|
PSX_GPU_ACCURACY_MASK_BIT is on AND this backend can serve it, i.e. the draw shader was
|
|
compiled with its mask stage and the render target's ALPHA CHANNEL now carries VRAM bit
|
|
15 (the backend). Everything the flag switches:
|
|
|
|
* the fragment shader does its own blending (framebuffer fetch) instead of the
|
|
fixed-function unit, because alpha can only be the blend factor or the mask bit and
|
|
not both — "alpha-channel conflict", resolved in favour of the mask bit;
|
|
* GL_BLEND is therefore OFF for the draw program;
|
|
* both resolve shaders reconstruct bit 15 from alpha instead of writing 0.
|
|
|
|
Off, every one of those reverts textually to what shipped, so the default path is
|
|
byte-identical.
|
|
*/
|
|
int mask_mode;
|
|
|
|
gl_vertex_t* verts;
|
|
int vert_count, vert_cap;
|
|
gl_range_t* ranges;
|
|
int range_count, range_cap;
|
|
|
|
tilemap_t dirty; /* host VRAM the vram texture has not seen yet */
|
|
tilemap_t sampled; /* VRAM the pending batch samples; a write here forces a flush */
|
|
int any_dirty, any_sampled;
|
|
|
|
/*
|
|
row 1 / `gpu_dirty`: regions the RASTERIZER wrote, which therefore exist
|
|
ONLY in the render target. A textured draw that samples one of these must not be fed
|
|
from host VRAM — that is stale the moment the software shadow stops running — so
|
|
those tiles are resolved render target -> vram_tex on the GPU, with no CPU round trip.
|
|
|
|
Maintained unconditionally, because the bitmap costs nothing; it is only ACTED ON
|
|
when `gpu_own` is set (see below), so the shadow-on build's behaviour is unchanged
|
|
to the instruction unless the marker arms it.
|
|
*/
|
|
tilemap_t gpu_dirty;
|
|
int any_gpu_dirty;
|
|
|
|
/*
|
|
"the render target owns rendered content", i.e. draws mark gpu_dirty rather than
|
|
dirty and gl_note_sample() resolves from the render target. Implied by !shadow;
|
|
forceable by `hwgl_gpu_resolve` WHILE the shadow is still on, which is the whole
|
|
point — the 1x parity gate then becomes a direct oracle for the resolve, with the
|
|
software shadow still installed as the reference. See .
|
|
*/
|
|
int gpu_own;
|
|
int dbg_gpu_resolve;
|
|
|
|
uint8_t* readback; /* packed BGR555, w*S x h*S */
|
|
size_t readback_cap;
|
|
GLenum read_format, read_type; /* the 2-bytes-per-pixel fast path, when offered */
|
|
int read_packed;
|
|
int read_probed;
|
|
|
|
uint8_t* native_rb; /* staging for gl_readback_rect(), packed BGR555 at native size */
|
|
size_t native_rb_cap;
|
|
|
|
/* Set from base.flags at create(). While it is on, gpu->vram is authoritative (the core
|
|
runs the software rasterizer too) and every GPU->host read below is diagnostic rather
|
|
than load-bearing. Clearing it is what proper means; every path is already written
|
|
for both worlds so that switch stays a one-liner. */
|
|
int shadow;
|
|
|
|
int rt_bound;
|
|
int have_context; /* teardown must not issue GL calls without one */
|
|
int failed;
|
|
int error_streak;
|
|
|
|
/* Diagnostic switches, read once at create() from marker FILES next to the diag log,
|
|
so a single build can be A/B'd from adb (`run-as <pkg> touch files/logs/<name>`)
|
|
without a rebuild or a settings-schema change. Both default ON — the ON state is the
|
|
shipping behaviour and the OFF state exists only to reproduce the pre-fix build as a
|
|
control. See the backend. */
|
|
int dbg_tri_bbox; /* off: submit the triangle itself, i.e. the coverage bug */
|
|
int dbg_upscale_parity; /* `hwgl_upscale_parity`: one upscaled-vs-shadow report */
|
|
int dbg_paint; /* `hwgl_paint_reject`: paint coverage-rejected fragments magenta */
|
|
int dbg_defer_dirty; /* off: mark the destination dirty before note_sample */
|
|
int dbg_dump; /* dump one scanout + the software shadow at frame 3000 */
|
|
|
|
/* `hwgl_vram_diff`: run the whole-VRAM readback-vs-shadow diff every kVramDiffPeriod
|
|
frames instead of only on the downgrade path, so it can be taken at a BUSY frame and
|
|
at more than one moment. single 0/524288 was real but was one sample, and
|
|
the second copy of it landed on a black CD-load screen (softnonzero=0) and is
|
|
vacuous. Only meaningful while the shadow is on — it IS the reference. */
|
|
int dbg_vram_diff;
|
|
|
|
/* `hwgl_geom`: one shot at frame 3000, the first 8 textured samples with their page
|
|
rectangle, the drawing area, the display origin and which of the two tilemaps they
|
|
intersect. Reasoning about which regions overlap which produced three wrong answers
|
|
in a row; this prints them. */
|
|
int dbg_geom;
|
|
int geom_left;
|
|
int geom_draw_left;
|
|
|
|
/*
|
|
automatic downgrade, and the two markers that make it TESTABLE.
|
|
|
|
The rule itself is option 1: a running average of GP0(C0) readback bytes over
|
|
the last 60 frames above `c0_limit` means this game reads VRAM faster than a GPU
|
|
rasterizer can serve it, so seed host VRAM from the render target, say so in the log,
|
|
and hand the session back to a CPU rasterizer for good.
|
|
|
|
The markers exist because the trigger CANNOT fire on its own here: with the software
|
|
shadow in place GP0(C0) costs nothing, and a whole Crash Bandicoot session moves 96
|
|
bytes through it (). A fallback that has never executed is not a fallback, so:
|
|
|
|
* hwgl_c0_trip — c0_limit = 0, i.e. ANY GP0(C0) traffic inside the 60-frame
|
|
window trips it. Fires from REAL game traffic, so it proves
|
|
the counter, the window and the trigger are wired to the
|
|
emulated GPU and not just to each other.
|
|
* hwgl_force_downgrade — trips unconditionally at frame 900 whether or not the game
|
|
ever touches GP0(C0). Deterministic, so it proves the LADDER
|
|
(seed, teardown, fallback rasterizer, picture keeps moving)
|
|
in a game that reads no VRAM at all.
|
|
*/
|
|
int dbg_c0_trip;
|
|
int dbg_force_downgrade;
|
|
uint64_t c0_limit; /* bytes per frame, averaged over the window */
|
|
uint32_t c0_frame_bytes; /* GP0(C0) bytes in the frame being emulated now */
|
|
uint32_t c0_window[60]; /* per-frame history; the running average's ring */
|
|
uint64_t c0_window_sum;
|
|
int c0_window_idx;
|
|
int downgraded; /* the ladder has already been walked; never twice */
|
|
|
|
/* Diagnostics. Reported every kStatsPeriod frames; this is the only instrumentation
|
|
that exists for "is the GPU path actually batching", which says is the whole
|
|
difference between 60 fps and 12 on a tiler. */
|
|
uint64_t frames;
|
|
uint32_t stat_draws, stat_ranges, stat_prims, stat_syncs, stat_sync_px;
|
|
uint32_t stat_adopted; /* frames presented straight off the GPU, no readback */
|
|
uint32_t stat_reads, stat_read_px; /* gl_readback_rect(): stall, when it happens */
|
|
uint32_t stat_gres, stat_gres_px; /* row 1's GPU->GPU resolve; NOT a stall */
|
|
uint64_t stat_readback_bytes; /* GP0(C0), for accounting */
|
|
|
|
/* The brokered seam (armsx_hw_gl_present_texture). Sticky: a present backend that
|
|
cannot take a GL texture is a property of the run, not of the frame, so it is asked
|
|
once and then left alone — otherwise every frame would pay a wasted resolve. */
|
|
int adopt_disabled;
|
|
int adopt_logged;
|
|
|
|
/* The 1x parity gate. make test-gpu cannot reach a GL backend, so this measures it.
|
|
The buckets matter more than the total: "0.2% of pixels differ" is not actionable,
|
|
but "every one of them is BLANK where the software has content" and "every one of
|
|
them is within one 5-bit step" point at completely different bugs — a primitive the
|
|
backend never drew versus rounding in the blend. FAR is the only bucket that means
|
|
the rasterization itself disagrees. */
|
|
uint64_t parity_pixels, parity_diff;
|
|
uint64_t parity_blank; /* GPU 0, software non-zero: the backend missed a write */
|
|
uint64_t parity_extra; /* GPU non-zero, software 0: the backend drew something extra */
|
|
uint64_t parity_near; /* every channel within one 5-bit step: rounding / dither */
|
|
uint64_t parity_far; /* genuinely different colours */
|
|
/* MASK BIT ONLY: the colour agrees and bit 15 does not. Its own bucket because it is the
|
|
one bit the GL path cannot get right by accident — it comes from a framebuffer-fetch
|
|
read of the destination, not from the arithmetic every other bucket measures. Counted
|
|
(and included in parity_diff) only while mask_mode is on; the comparison masks bit 15
|
|
off otherwise, exactly as it always did. */
|
|
uint64_t parity_mask;
|
|
/* Four samples rather than one. A single coordinate cannot distinguish "one broken
|
|
region" from "speckle spread over the whole frame", and that distinction is the
|
|
difference between hunting a transfer path and hunting a coverage rule. */
|
|
uint16_t parity_blank_x[4], parity_blank_y[4], parity_blank_want[4];
|
|
int parity_blank_n;
|
|
uint16_t parity_far_x[4], parity_far_y[4], parity_far_got[4], parity_far_want[4];
|
|
int parity_far_n;
|
|
/* Mismatches with an orthogonal neighbour that also mismatches. High means clustered
|
|
(a region the backend got wrong); low means isolated speckle (an edge rule). */
|
|
uint64_t parity_clustered;
|
|
/* Worst single scanout in the window, and how many scanouts were perfectly clean.
|
|
"1.1% of pixels over 1800 frames" is a completely different bug depending on whether
|
|
every frame is 1.1% wrong or forty frames are 50% wrong and the rest are exact. */
|
|
uint64_t parity_worst_frame;
|
|
uint64_t parity_clean_frames, parity_frames;
|
|
int parity_done;
|
|
} hw_gl_t;
|
|
|
|
static char g_status[192] = "not attempted";
|
|
|
|
static void gl_status(const char* fmt, ...) {
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
vsnprintf(g_status, sizeof(g_status), fmt, args);
|
|
va_end(args);
|
|
psxe_diag_logf("hwgl", "%s", g_status);
|
|
}
|
|
|
|
const char* armsx_hw_gl_status(void) {
|
|
return g_status;
|
|
}
|
|
|
|
/*
|
|
[video] texture_filter / downsample / line_detect.
|
|
|
|
Process-global rather than per-instance for one concrete reason: this backend is
|
|
DESTROYED AND RECREATED behind the frontend's back — checkRasterizerHealth() does it on
|
|
any GL failure, and a scale change does it too. Options that lived only on hw_gl_t would
|
|
silently revert to their defaults there, which is the "setting worked until something
|
|
else happened" failure this project has shipped before. The instance reads these every
|
|
time it needs them, so a create() in between changes nothing.
|
|
|
|
Plain ints, no lock: each is written by the UI thread and read by the emulation thread,
|
|
both as a single aligned word, and every value is independently valid — a torn read is
|
|
not expressible. All three are 0 (= off / nearest / disabled) by default.
|
|
*/
|
|
static int g_opt_texture_filter = 0; /* 0 nearest, 1 bilinear, 2 xBR-style */
|
|
static int g_opt_downsample = 0; /* 0 or 1 = off, 2..8 = requested box factor */
|
|
static int g_opt_line_detect = 0; /* 0 disabled, 1 quads, 2 basic */
|
|
/* Mirrors PSX_GPU_ACCURACY_TEX_MODULATE for the shader; re-read from the gpu each flush
|
|
so it cannot go stale the way a create()-time copy would. */
|
|
static int g_opt_tex_trunc = 0;
|
|
/* Mirrors psx_gpu_mask_from_texel() != 0 for the shader, re-read per primitive for the same
|
|
reason. It is the third argument of PSX_GPU_MASK_WRITE and the ONE that is not carried per
|
|
vertex, because it follows the accuracy flag rather than GPUSTAT. */
|
|
static int g_opt_mask_texel = 0;
|
|
|
|
void armsx_hw_gl_set_video_options(int texture_filter, int downsample, int line_detect) {
|
|
g_opt_texture_filter = (texture_filter >= 0 && texture_filter <= 2) ? texture_filter : 0;
|
|
g_opt_downsample = (downsample < 2) ? 0 : (downsample > 8 ? 8 : downsample);
|
|
g_opt_line_detect = (line_detect >= 0 && line_detect <= 2) ? line_detect : 0;
|
|
}
|
|
|
|
/* Builds <diag log directory>/<name>. The diag log path is the one app-private location
|
|
this file can reach without new plumbing, and it is exactly where `run-as` already
|
|
works. Returns 0 when diagnostics are off, which is the shipping default. */
|
|
static int gl_debug_path(char* out, size_t cap, const char* name) {
|
|
const char* logp = psxe_diag_log_path();
|
|
const char* slash;
|
|
size_t dir_len;
|
|
|
|
if (!logp || !logp[0])
|
|
return 0;
|
|
|
|
slash = strrchr(logp, '/');
|
|
|
|
if (!slash)
|
|
return 0;
|
|
|
|
dir_len = (size_t)(slash - logp) + 1u;
|
|
|
|
if ((dir_len + strlen(name) + 1u) > cap)
|
|
return 0;
|
|
|
|
memcpy(out, logp, dir_len);
|
|
memcpy(out + dir_len, name, strlen(name) + 1u);
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int gl_debug_marker(const char* name) {
|
|
char path[1024];
|
|
FILE* f;
|
|
|
|
if (!gl_debug_path(path, sizeof(path), name))
|
|
return 0;
|
|
|
|
f = fopen(path, "rb");
|
|
|
|
if (!f)
|
|
return 0;
|
|
|
|
fclose(f);
|
|
|
|
return 1;
|
|
}
|
|
|
|
static hw_gl_t* gl_self(psx_gpu_backend_t* be) {
|
|
return (hw_gl_t*)be;
|
|
}
|
|
|
|
/* ---- shaders ------------------------------------------------------------------------- */
|
|
|
|
static const char* kDrawVS =
|
|
"#version 300 es\n"
|
|
"precision highp float;\n"
|
|
"precision highp int;\n"
|
|
"in vec2 a_pos;\n"
|
|
"in vec4 a_tri0;\n"
|
|
"in vec2 a_tri1;\n"
|
|
"in uvec4 a_col0;\n"
|
|
"in uvec4 a_col1;\n"
|
|
"in uvec4 a_col2;\n"
|
|
"in uvec4 a_uv01;\n"
|
|
"in uvec4 a_uv2;\n"
|
|
"in uvec4 a_misc;\n"
|
|
"in uvec4 a_texwin;\n"
|
|
"in uvec4 a_repl;\n"
|
|
"in vec3 a_triw;\n"
|
|
"uniform vec2 u_rt_size;\n"
|
|
"uniform float u_scale;\n"
|
|
"flat out vec4 v_tri0;\n"
|
|
"flat out vec2 v_tri1;\n"
|
|
"flat out uvec4 v_col0;\n"
|
|
"flat out uvec4 v_col1;\n"
|
|
"flat out uvec4 v_col2;\n"
|
|
"flat out uvec4 v_uv01;\n"
|
|
"flat out uvec4 v_uv2;\n"
|
|
"flat out uvec4 v_misc;\n"
|
|
"flat out uvec4 v_texwin;\n"
|
|
"flat out uvec4 v_repl;\n"
|
|
"flat out vec3 v_triw;\n"
|
|
"void main() {\n"
|
|
" v_tri0 = a_tri0; v_tri1 = a_tri1;\n"
|
|
" v_col0 = a_col0; v_col1 = a_col1; v_col2 = a_col2;\n"
|
|
" v_uv01 = a_uv01; v_uv2 = a_uv2; v_misc = a_misc; v_texwin = a_texwin;\n"
|
|
" v_repl = a_repl;\n"
|
|
" v_triw = a_triw;\n"
|
|
" vec2 rt = a_pos * u_scale;\n"
|
|
" gl_Position = vec4(rt / u_rt_size * 2.0 - 1.0, 0.0, 1.0);\n"
|
|
"}\n";
|
|
|
|
/*
|
|
NOTE: no `#version` line. This is the BODY only — gl_build_draw_fs() prepends the version,
|
|
and, when the mask bit is on, the framebuffer-fetch `#extension`, `#define ARMSX_GL_MASK`
|
|
and PSX_GPU_MASK_GLSL (psx/dev/gpu.h), which is where the two mask expressions come from.
|
|
Both directives are required to precede any other token, so they cannot live here.
|
|
*/
|
|
static const char* kDrawFS =
|
|
"precision highp float;\n"
|
|
"precision highp int;\n"
|
|
"precision highp usampler2D;\n"
|
|
"uniform highp usampler2D u_vram;\n"
|
|
"uniform float u_scale;\n"
|
|
"uniform int u_dither[16];\n"
|
|
"uniform int u_dither_on;\n"
|
|
"uniform int u_stp_pass;\n"
|
|
"uniform int u_tex_trunc;\n"
|
|
/* psx_gpu_mask_from_texel() != 0, i.e. the accuracy flag itself. Read only inside the mask
|
|
stage; declared unconditionally so the uniform location lookup is one code path. */
|
|
"uniform int u_mask_texel;\n"
|
|
"uniform int u_paint;\n"
|
|
/* [video] texture_filter: 0 = nearest/legacy, 1 = bilinear, 2 = xBR-style. A uniform rather
|
|
than a recompile because this file has no runtime shader-rebuild path (the four programs
|
|
are linked once in create()), and because the setting is meant to be live-toggleable.
|
|
0 is the shipping default and takes the identical branch the file had before filtering
|
|
existed. */
|
|
"uniform int u_filter;\n"
|
|
/* The replacement atlas (psx/texrep.h). Declared unconditionally so the uniform lookup is one
|
|
code path; sampled only when v_repl says a replacement is bound, which is never unless the
|
|
feature is on. texelFetch, so no sampler filtering state can leak in. */
|
|
"uniform highp sampler2D u_repl;\n"
|
|
"flat in vec4 v_tri0;\n"
|
|
"flat in vec2 v_tri1;\n"
|
|
"flat in uvec4 v_col0;\n"
|
|
"flat in uvec4 v_col1;\n"
|
|
"flat in uvec4 v_col2;\n"
|
|
"flat in uvec4 v_uv01;\n"
|
|
"flat in uvec4 v_uv2;\n"
|
|
"flat in uvec4 v_misc;\n"
|
|
"flat in uvec4 v_texwin;\n"
|
|
"flat in uvec4 v_repl;\n"
|
|
"flat in vec3 v_triw;\n"
|
|
/* `inout` is how GL_EXT_shader_framebuffer_fetch spells "this output starts out holding the
|
|
destination pixel". That read is the mask CHECK, and it is also what lets the blend happen
|
|
here rather than in the fixed-function unit — see the mask stage at the bottom. */
|
|
"#ifdef ARMSX_GL_MASK\n"
|
|
"inout vec4 o_color;\n"
|
|
"#else\n"
|
|
"out vec4 o_color;\n"
|
|
"#endif\n"
|
|
"const uint F_RAW = 1u, F_TRANSP = 2u, F_TEXTURED = 4u, F_SHADED = 16u;\n"
|
|
"const uint F_SPRITE = 256u, F_DITHER = 512u;\n"
|
|
/* GLF_MASK_CHECK / GLF_MASK_SET. */
|
|
"const uint F_MASK_CHECK = 1024u, F_MASK_SET = 2048u;\n"
|
|
/* gpu.c indexes VRAM linearly and does NOT wrap the texture page at x=1024
|
|
(the backend deviation #8). Reproducing that means addressing linearly
|
|
here too; the row mask only keeps a pathological page from reading past the surface,
|
|
which is a latent overrun in the software path itself. */
|
|
"uint vram_at(int lin) {\n"
|
|
" return texelFetch(u_vram, ivec2(lin & 1023, (lin >> 10) & 511), 0).r;\n"
|
|
"}\n"
|
|
"uint fetch_texel(int tx, int ty) {\n"
|
|
" int mx = int(v_texwin.x), my = int(v_texwin.y);\n"
|
|
" tx = (tx & ~mx) | (int(v_texwin.z) & mx);\n"
|
|
" ty = (ty & ~my) | (int(v_texwin.w) & my);\n"
|
|
" tx &= 0xff; ty &= 0xff;\n"
|
|
" int texp = int(v_uv2.z), clut = int(v_uv2.w);\n"
|
|
" int tpx = (texp & 0xf) << 6;\n"
|
|
" int tpy = (texp & 0x10) << 4;\n"
|
|
" int clutx = (clut & 0x3f) << 4;\n"
|
|
" int cluty = (clut >> 6) & 0x1ff;\n"
|
|
" int depth = (texp >> 7) & 3;\n"
|
|
" if (depth == 0) {\n"
|
|
" uint w = vram_at((tpx + (tx >> 2)) + ((tpy + ty) << 10));\n"
|
|
" int idx = int((w >> uint((tx & 3) << 2)) & 0xfu);\n"
|
|
" return vram_at((clutx + idx) + (cluty << 10));\n"
|
|
" }\n"
|
|
" if (depth == 1) {\n"
|
|
" uint w = vram_at((tpx + (tx >> 1)) + ((tpy + ty) << 10));\n"
|
|
" int idx = int((w >> uint((tx & 1) << 3)) & 0xffu);\n"
|
|
" return vram_at((clutx + idx) + (cluty << 10));\n"
|
|
" }\n"
|
|
" return vram_at((tpx + tx) + ((tpy + ty) << 10));\n"
|
|
"}\n"
|
|
/* ---- texture replacement (psx/texrep.h) -------------------------------------------------
|
|
The window transform is the SAME two lines fetch_texel() opens with, because the folded
|
|
rectangle recorded in v_repl is in post-window texel space -- the transform is idempotent,
|
|
so applying it here lands inside the image by construction and the clamp inside texrep_at()
|
|
only catches interpolation that strayed outside the vertex UV box.
|
|
|
|
The atlas mapping and the RGBA8 -> BGR555 pack both come from PSX_TEXREP_GLSL, the same
|
|
text psx_texrep_sample() implements in C for the two CPU rasterizers. One formula, one
|
|
place: three copies of a formula is how , and all happened.
|
|
|
|
fx/fy are the SUB-TEXEL, in 0..S-1. A replacement is S times finer than the native texel
|
|
grid, and without this every SxS block would show one replacement pixel -- an upscaled pack
|
|
would look exactly like the original. */
|
|
"uint fetch_repl(float tx, float ty, vec2 frac) {\n"
|
|
" int mx = int(v_texwin.x), my = int(v_texwin.y);\n"
|
|
" int itx = ((int(floor(tx)) & ~mx) | (int(v_texwin.z) & mx)) & 0xff;\n"
|
|
" int ity = ((int(floor(ty)) & ~my) | (int(v_texwin.w) & my)) & 0xff;\n"
|
|
" int s = int(v_repl.y >> 16);\n"
|
|
" int u0 = int(v_repl.y & 0xffu);\n"
|
|
" int v0 = int((v_repl.y >> 8) & 0xffu);\n"
|
|
" int nw = int(v_repl.z & 0xffffu) + 1;\n"
|
|
" int nh = int((v_repl.z >> 16) & 0xffffu) + 1;\n"
|
|
" int ax = int(v_repl.x & 0xffffu);\n"
|
|
" int ay = int((v_repl.x >> 16) & 0xffffu);\n"
|
|
" int fx = int(floor(frac.x * float(s)));\n"
|
|
" int fy = int(floor(frac.y * float(s)));\n"
|
|
" ivec2 p = texrep_at(itx, ity, u0, v0, nw, nh, s, ax, ay, fx, fy);\n"
|
|
" return texrep_pack(texelFetch(u_repl, p, 0));\n"
|
|
"}\n"
|
|
/* gpu_fetch_texel_bilinear, gpu.c:225. Polygons use it unconditionally; the early-out on
|
|
the top-left tap and the OR of all four mask bits are both load-bearing for parity. */
|
|
/* ---- [video] texture_filter ------------------------------------------------------------
|
|
These run on the TEXTURE SAMPLE and nothing else. Coverage has already been decided by the
|
|
time control reaches them (: once per NATIVE pixel at pc = vec2(pn)), so no filter
|
|
mode can move an edge, open a seam or change which fragments survive. The only fragment
|
|
any of them can kill is one whose nearest texel is 0 — the transparent-texel discard that
|
|
was already there, kept deliberately so a filtered edge cannot bleed a cut-out shape.
|
|
|
|
u_filter == 0 is untouched legacy (fetch_bilinear below, the software rasterizer's own
|
|
sub-texel blend) and is the ONLY mode the 1x parity gate means anything under. */
|
|
"vec3 c5(uint v) {\n"
|
|
" return vec3(float(v & 31u), float((v >> 5) & 31u), float((v >> 10) & 31u));\n"
|
|
"}\n"
|
|
/* Weighted 2x2 around the TEXEL CENTRE (tx - 0.5), with transparent taps dropped from the
|
|
weight sum instead of averaged in as black — averaging them in is what puts a dark halo
|
|
around every cut-out sprite. */
|
|
"uint fetch_smooth(float tx, float ty) {\n"
|
|
" if (fetch_texel(int(floor(tx)), int(floor(ty))) == 0u) return 0u;\n"
|
|
" float bx = tx - 0.5, by = ty - 0.5;\n"
|
|
" float x0 = floor(bx), y0 = floor(by);\n"
|
|
" float fx = bx - x0, fy = by - y0;\n"
|
|
" uint s00 = fetch_texel(int(x0), int(y0));\n"
|
|
" uint s10 = fetch_texel(int(x0) + 1, int(y0));\n"
|
|
" uint s01 = fetch_texel(int(x0), int(y0) + 1);\n"
|
|
" uint s11 = fetch_texel(int(x0) + 1, int(y0) + 1);\n"
|
|
" vec4 w = vec4((1.0 - fx) * (1.0 - fy), fx * (1.0 - fy),\n"
|
|
" (1.0 - fx) * fy, fx * fy);\n"
|
|
" w *= vec4(s00 != 0u ? 1.0 : 0.0, s10 != 0u ? 1.0 : 0.0,\n"
|
|
" s01 != 0u ? 1.0 : 0.0, s11 != 0u ? 1.0 : 0.0);\n"
|
|
" float ws = w.x + w.y + w.z + w.w;\n"
|
|
" if (ws <= 0.0) return 0u;\n"
|
|
" vec3 acc = w.x * c5(s00) + w.y * c5(s10) + w.z * c5(s01) + w.w * c5(s11);\n"
|
|
" uvec3 q = uvec3(clamp(acc / ws, 0.0, 31.0));\n"
|
|
" return q.x | (q.y << 5) | (q.z << 10) | ((s00 | s10 | s01 | s11) & 0x8000u);\n"
|
|
"}\n"
|
|
/* xBR-style: edge-DIRECTED, which is the family's whole idea — interpolate ALONG a detected
|
|
edge and stay sharp across it, instead of blurring both ways like a plain box. The two
|
|
diagonals of the same 2x2 are compared in luma; the one with the smaller difference is the
|
|
direction the edge runs, and only that pair is blended. When neither diagonal wins clearly
|
|
there is no edge to follow and it degrades to fetch_smooth().
|
|
|
|
This is a 2x2 kernel, NOT the full multi-pass xBR rule set: it costs the same four taps as
|
|
bilinear and needs no extra passes, and it keeps hard pixel-art boundaries that a box blur
|
|
softens. Named "xBR-style" in the UI for exactly that reason. */
|
|
"uint fetch_xbr(float tx, float ty) {\n"
|
|
" if (fetch_texel(int(floor(tx)), int(floor(ty))) == 0u) return 0u;\n"
|
|
" float bx = tx - 0.5, by = ty - 0.5;\n"
|
|
" float x0 = floor(bx), y0 = floor(by);\n"
|
|
" float fx = bx - x0, fy = by - y0;\n"
|
|
" uint s00 = fetch_texel(int(x0), int(y0));\n"
|
|
" uint s10 = fetch_texel(int(x0) + 1, int(y0));\n"
|
|
" uint s01 = fetch_texel(int(x0), int(y0) + 1);\n"
|
|
" uint s11 = fetch_texel(int(x0) + 1, int(y0) + 1);\n"
|
|
" if (s00 == 0u || s10 == 0u || s01 == 0u || s11 == 0u) return fetch_smooth(tx, ty);\n"
|
|
" vec3 lw = vec3(0.299, 0.587, 0.114);\n"
|
|
" float lA = abs(dot(c5(s00), lw) - dot(c5(s11), lw));\n"
|
|
" float lB = abs(dot(c5(s10), lw) - dot(c5(s01), lw));\n"
|
|
" if (abs(lA - lB) < 1.0) return fetch_smooth(tx, ty);\n"
|
|
" vec3 acc;\n"
|
|
" if (lA < lB) {\n"
|
|
" float t = clamp((fx + fy) * 0.5, 0.0, 1.0);\n"
|
|
" acc = mix(c5(s00), c5(s11), t);\n"
|
|
" } else {\n"
|
|
" float t = clamp((fx - fy + 1.0) * 0.5, 0.0, 1.0);\n"
|
|
" acc = mix(c5(s01), c5(s10), t);\n"
|
|
" }\n"
|
|
" uvec3 q = uvec3(clamp(acc, 0.0, 31.0));\n"
|
|
" return q.x | (q.y << 5) | (q.z << 10) | ((s00 | s10 | s01 | s11) & 0x8000u);\n"
|
|
"}\n"
|
|
"uint fetch_bilinear(float tx, float ty) {\n"
|
|
" float txf = floor(tx), tyf = floor(ty);\n"
|
|
" float txc = txf + 1.0, tyc = tyf + 1.0;\n"
|
|
" uint s0 = fetch_texel(int(txf), int(tyf));\n"
|
|
" if (s0 == 0u) return 0u;\n"
|
|
" uint s1 = fetch_texel(int(txc), int(tyf));\n"
|
|
" uint s2 = fetch_texel(int(txf), int(tyc));\n"
|
|
" uint s3 = fetch_texel(int(txc), int(tyc));\n"
|
|
" vec3 c0 = vec3(float(s0 & 31u), float((s0 >> 5) & 31u), float((s0 >> 10) & 31u));\n"
|
|
" vec3 c1 = vec3(float(s1 & 31u), float((s1 >> 5) & 31u), float((s1 >> 10) & 31u));\n"
|
|
" vec3 c2 = vec3(float(s2 & 31u), float((s2 >> 5) & 31u), float((s2 >> 10) & 31u));\n"
|
|
" vec3 c3 = vec3(float(s3 & 31u), float((s3 >> 5) & 31u), float((s3 >> 10) & 31u));\n"
|
|
" vec3 q1 = c0 * (txc - tx) + c1 * (tx - txf);\n"
|
|
" vec3 q2 = c2 * (txc - tx) + c3 * (tx - txf);\n"
|
|
" uvec3 q = uvec3(q1 * (tyc - ty) + q2 * (ty - tyf));\n"
|
|
" return q.x | (q.y << 5) | (q.z << 10) | ((s0 | s1 | s2 | s3) & 0x8000u);\n"
|
|
"}\n"
|
|
/*
|
|
Barycentric divide, with the one case that matters made exact.
|
|
|
|
gpu.c divides in C, where IEEE-754 division is correctly rounded to 0.5 ULP. GLSL ES 3.0
|
|
only requires highp division to be accurate to 2.5 ULP (spec 4.5.1). Everywhere that does
|
|
not matter — except at a TEXEL BOUNDARY, where the exact quotient is an integer and the
|
|
two implementations therefore land on opposite sides of floor(). One flipped floor()
|
|
selects a different texel, and adjacent texels in a font atlas are "opaque" and
|
|
"transparent", so the disagreement is not a rounding step, it is a whole pixel appearing
|
|
or vanishing. That is what the residual far/blank/extra buckets in
|
|
the backend turned out to be, and why they show up as one-pixel-tall
|
|
runs along scanlines.
|
|
|
|
When the true quotient is the integer r, num == r*den exactly. Both sides of that test
|
|
are exact for real PS1 triangle sizes (|z| and |area| stay well inside 2^24), so the
|
|
comparison is reliable, and a correctly-rounded division of an exactly representable
|
|
value returns it unchanged — which is precisely what gpu.c gets. When the test fails,
|
|
nothing is claimed and the ordinary quotient stands, so this can only ever agree more.
|
|
*/
|
|
"float bdiv(float num, float den) {\n"
|
|
" float q = num / den;\n"
|
|
" float r = floor(q + 0.5);\n"
|
|
" return (r * den == num) ? r : q;\n"
|
|
"}\n"
|
|
"bool tl(float z, vec2 a, vec2 b) {\n"
|
|
" return (z < 0.0) || ((z == 0.0) && ((b.y > a.y) || ((b.y == a.y) && (b.x < a.x))));\n"
|
|
"}\n"
|
|
"void main() {\n"
|
|
" uint flags = v_misc.x;\n"
|
|
" int mode = int(v_misc.y);\n"
|
|
" vec2 pf = gl_FragCoord.xy / u_scale - 0.5;\n"
|
|
" ivec2 pn = ivec2(floor(gl_FragCoord.xy / u_scale));\n"
|
|
" vec2 A = v_tri0.xy, B = v_tri0.zw, C = v_tri1;\n"
|
|
" float z0 = 0.0, z1 = 0.0, z2 = 0.0, area = 1.0;\n"
|
|
" bool sprite = (flags & F_SPRITE) != 0u;\n"
|
|
" if (!sprite) {\n"
|
|
" vec2 lo = min(A, min(B, C));\n"
|
|
" vec2 hi = max(A, max(B, C));\n"
|
|
/* pf — the SUBPIXEL position — because the three edge tests below use pf, and the two halves
|
|
of one coverage test must agree about granularity.
|
|
With pn (the native pixel index) a subpixel genuinely inside the triangle could still be
|
|
thrown away: a triangle starting at x = 100.2 owns the subpixel at 100.33, but pn is 100 and
|
|
100 < 100.2 rejected the whole native pixel. The neighbouring triangle ends at 100.2 and does
|
|
not cover it either, so nothing drew it — a gap along every shared edge, which is the seam.
|
|
Identical at 1x, where gl_FragCoord.x = px + 0.5 makes pf.x exactly px. */
|
|
/* Coverage rejection is ACCUMULATED rather than discarded piecemeal so the probe below can
|
|
paint rejected fragments instead of dropping them. With u_paint == 0 (shipping) the final
|
|
discard fires for exactly the same set of fragments as the old early discards — the only
|
|
cost is computing z1/z2 for fragments the bbox already rejected, which is noise. */
|
|
/* COVERAGE IS DECIDED ONCE PER NATIVE PIXEL, at the native pixel's own coordinate, exactly
|
|
as psx/dev/gpu.c decides it — and the decision applies to every subpixel of the block.
|
|
|
|
This is the resolution of the seam hunt, and the reasoning is worth keeping:
|
|
the original code tested the bounding box at native granularity (pn) but the top-left edge
|
|
rule at subpixel positions (pf); a later attempt made both subpixel. NEITHER matches the
|
|
software rasterizer, and mixed or subpixel rules can strand a subpixel that no primitive
|
|
claims — the probe (hwgl_paint_reject) showed exactly that: dark unclaimed hairlines
|
|
between abutting accepted quads. Native-granularity coverage is the one rule PROVEN
|
|
watertight: it reproduces software coverage identically at every scale (1x parity 0.005%),
|
|
so a hole here requires a hole in the software image too.
|
|
|
|
The cost is honest: polygon edges quantize to native pixels at upscale — textures and
|
|
Gouraud shading still gain from the higher resolution (interpolation below stays at pf),
|
|
but edge silhouettes step at native granularity. Smooth subpixel edges need real GPU
|
|
rasterization with a watertight fill rule, which is the planned PGXP-era design, not a
|
|
per-fragment discard rule.
|
|
|
|
zc* are the coverage barycentrics at the native coordinate; z0..z2 (at pf) remain the
|
|
INTERPOLATION barycentrics. float(pn) and integer vertices keep every zc product inside
|
|
float-exact range, so the tie-break comparisons below are exact at every scale. */
|
|
" vec2 pc = vec2(pn);\n"
|
|
" float zc0, zc1, zc2;\n"
|
|
" bool rej = false;\n"
|
|
" if (pc.x < lo.x || pc.x >= hi.x) rej = true;\n"
|
|
" if (pc.y < lo.y || pc.y >= hi.y) rej = true;\n"
|
|
" zc0 = (C.x - B.x) * (pc.y - B.y) - (C.y - B.y) * (pc.x - B.x);\n"
|
|
" if (tl(zc0, B, C)) rej = true;\n"
|
|
" zc1 = (A.x - C.x) * (pc.y - C.y) - (A.y - C.y) * (pc.x - C.x);\n"
|
|
" if (tl(zc1, C, A)) rej = true;\n"
|
|
" zc2 = (B.x - A.x) * (pc.y - A.y) - (B.y - A.y) * (pc.x - A.x);\n"
|
|
" if (tl(zc2, A, B)) rej = true;\n"
|
|
" z0 = (C.x - B.x) * (pf.y - B.y) - (C.y - B.y) * (pf.x - B.x);\n"
|
|
" z1 = (A.x - C.x) * (pf.y - C.y) - (A.y - C.y) * (pf.x - C.x);\n"
|
|
" z2 = (B.x - A.x) * (pf.y - A.y) - (B.y - A.y) * (pf.x - A.x);\n"
|
|
" area = (B.x - A.x) * (C.y - A.y) - (B.y - A.y) * (C.x - A.x);\n"
|
|
" if (area == 0.0) rej = true;\n"
|
|
/* A native-accepted subpixel can sit slightly outside the triangle, where the pf
|
|
barycentrics go negative — Gouraud/UV would EXTRAPOLATE there, and a weight clamp would
|
|
darken (weights stop summing to the area: the same artefact class being fixed). Fall back
|
|
to the native-centre barycentrics instead: zc* passed coverage, so they are inside by
|
|
construction, and the edge subpixel takes exactly the value software gives that pixel.
|
|
Interior subpixels keep smooth subpixel interpolation. At 1x, pf == pc, so nothing moves. */
|
|
" if (min(z0, min(z1, z2)) < 0.0) { z0 = zc0; z1 = zc1; z2 = zc2; }\n"
|
|
/* Probe v2. v1 painted REJECTED fragments magenta and was misleading by construction: a later
|
|
triangle's rejected bbox fragments overwrote an earlier triangle's correct pixels, so any
|
|
dense mesh drowned in magenta regardless of correctness. Inverted: ACCEPTED non-sprite
|
|
geometry paints flat green (sprites flat blue, below), as a plain write immune to blend
|
|
state, BEFORE the texture/STP stages so texel-transparency cannot hide geometric coverage.
|
|
Reading the hairlines is then unambiguous and order-proof:
|
|
green hairline -> the pixel WAS drawn by accepted geometry; the bug is in the value
|
|
path (texture fetch / interpolation / blending / pass split);
|
|
dark hairline -> no geometry accepted the pixel: a genuine coverage hole (or it was
|
|
never generated at all). */
|
|
" if (rej) discard;\n"
|
|
" if (u_paint != 0) { o_color = vec4(0.0, 248.0/255.0, 0.0, 0.0); return; }\n"
|
|
/* z0/z1/z2 stay as computed at pf and are reused for interpolation. They were briefly
|
|
re-evaluated at the native pixel centre, which made things worse in a subtle way: coverage
|
|
is decided per subpixel, so a triangle can own subpixels of a pixel whose CENTRE lies in its
|
|
neighbour, and the barycentrics for that centre are negative — colour and UV then extrapolate
|
|
outside the triangle. A subpixel that passed the tests above is by definition inside, so
|
|
interpolating at the same position it was tested at is the only self-consistent choice. */
|
|
" }\n"
|
|
" if (sprite && u_paint != 0) { o_color = vec4(0.0, 0.0, 248.0/255.0, 0.0); return; }\n"
|
|
/* ---- mask CHECK, GP0(E6) bit 1 ----------------------------------------------------------
|
|
"check mask before draw": the write is skipped where the DESTINATION already has bit 15.
|
|
o_color is the destination here (framebuffer fetch), and the render target's alpha is the
|
|
mask bit — 0.0 or 1.0 exactly, written by this shader and by nothing else that blends.
|
|
|
|
Placed after coverage and before the texture fetch, mirroring the order in gpu.c:1180 and
|
|
gpu_hw_rt.c:170 — where it is also a `continue` before anything is sampled. Per FRAGMENT,
|
|
not per native pixel: gpu_hw_rt.c tests the render-target pixel too, and at 1x the two are
|
|
the same pixel. coverage contract is about GEOMETRY and is untouched by this. */
|
|
"#ifdef ARMSX_GL_MASK\n"
|
|
" if (PSX_GPU_MASK_SKIP((flags & F_MASK_CHECK) != 0u, o_color.a >= 0.5)) discard;\n"
|
|
"#endif\n"
|
|
" vec3 md;\n"
|
|
" if ((flags & F_SHADED) != 0u) {\n"
|
|
" vec3 n = z0 * vec3(v_col0.xyz) + z1 * vec3(v_col1.xyz) + z2 * vec3(v_col2.xyz);\n"
|
|
" vec3 c = vec3(bdiv(n.x, area), bdiv(n.y, area), bdiv(n.z, area));\n"
|
|
/* Indexed by the ABSOLUTE native VRAM coordinate, matching gpu.c and gpu_hw_rt.c. It used
|
|
to subtract the primitive's bounding-box origin, which re-phased the 4x4 kernel per
|
|
primitive and put a seam at every shared edge of a gradient. pn is already the native
|
|
coordinate (gl_FragCoord / u_scale), so this is scale-invariant for free. */
|
|
" if (u_dither_on != 0 && (flags & F_DITHER) != 0u) {\n"
|
|
" int dx = pn.x & 3;\n"
|
|
" int dy = pn.y & 3;\n"
|
|
" c += float(u_dither[dx + dy * 4]);\n"
|
|
" }\n"
|
|
" md = floor(clamp(c, 0.0, 255.0) + 0.5);\n"
|
|
" } else {\n"
|
|
" md = vec3(v_col0.xyz);\n"
|
|
" }\n"
|
|
" bool transp = (flags & F_TRANSP) != 0u;\n"
|
|
/* The SOURCE TEXEL's bit 15, which is the other half of GP0(E6) bit 0 and the half
|
|
found missing. Untextured primitives leave it false and therefore write a 0 mask bit, which
|
|
is what gpu.c does (its `stp` is initialised to 0 per pixel and only the textured branch
|
|
assigns it). The filtered fetches return the OR of their taps' bit 15, exactly as
|
|
gpu_fetch_texel_bilinear does, so this is the same bit the software path would take. */
|
|
" bool stp = false;\n"
|
|
" vec3 col;\n"
|
|
" if ((flags & F_TEXTURED) != 0u) {\n"
|
|
" uint texel;\n"
|
|
/* v_repl.y's high half is the replacement's integer upscale, and 0 means "none" -- which is
|
|
what it is on every vertex unless [video] texture_replacements is on AND a pack file
|
|
matched this primitive. psx/texrep.h. */
|
|
" bool repl = (v_repl.y >> 16) != 0u;\n"
|
|
" if (sprite) {\n"
|
|
/* A sprite walks one texel per NATIVE pixel, so the fragment's position inside its native
|
|
pixel IS the sub-texel. Native space and gl_FragCoord share an orientation here (the
|
|
vertex shader puts native y=0 at the bottom, where gl_FragCoord.y is 0), so no flip. */
|
|
" if (repl) {\n"
|
|
" texel = fetch_repl(float(int(v_uv01.x) + (pn.x - int(A.x))),\n"
|
|
" float(int(v_uv01.y) + (pn.y - int(A.y))),\n"
|
|
" fract(gl_FragCoord.xy / u_scale));\n"
|
|
" } else {\n"
|
|
" texel = fetch_texel(int(v_uv01.x) + (pn.x - int(A.x)),\n"
|
|
" int(v_uv01.y) + (pn.y - int(A.y)));\n"
|
|
" }\n"
|
|
" } else {\n"
|
|
/* PGXP perspective-correct texturing. zN are the same unnormalized barycentrics the affine
|
|
path uses; dividing each by its vertex w and renormalizing by the sum is the standard
|
|
1/w-interpolation, so `area` is not needed in that branch. Every non-PGXP triangle ships
|
|
triw = {1,1,1}, which keeps the exact bdiv path byte-identical — the 1x parity gate is
|
|
meaningful only with PGXP off, by design (PGXP moves geometry on purpose). The UV clamp
|
|
below applies to BOTH branches. */
|
|
" float tx, ty;\n"
|
|
" if (v_triw.x != v_triw.y || v_triw.x != v_triw.z) {\n"
|
|
" float iw0 = z0 / v_triw.x, iw1 = z1 / v_triw.y, iw2 = z2 / v_triw.z;\n"
|
|
" float wsum = iw0 + iw1 + iw2;\n"
|
|
" tx = (iw0 * float(v_uv01.x) + iw1 * float(v_uv01.z) + iw2 * float(v_uv2.x)) / wsum;\n"
|
|
" ty = (iw0 * float(v_uv01.y) + iw1 * float(v_uv01.w) + iw2 * float(v_uv2.y)) / wsum;\n"
|
|
" } else {\n"
|
|
" tx = bdiv(z0 * float(v_uv01.x) + z1 * float(v_uv01.z) + z2 * float(v_uv2.x), area);\n"
|
|
" ty = bdiv(z0 * float(v_uv01.y) + z1 * float(v_uv01.w) + z2 * float(v_uv2.y), area);\n"
|
|
" }\n"
|
|
/* Clamp UV to the triangle's OWN vertex range.
|
|
Coverage is decided per subpixel while colour and UV are interpolated at the native pixel
|
|
CENTRE, so a triangle can legitimately own subpixels of a pixel whose centre lies just
|
|
inside its neighbour. The barycentrics for that centre are then slightly negative and the
|
|
interpolated UV walks off the end of the intended texels, into whatever sits next to them
|
|
in VRAM — a dark fringe on exactly the pixels along a shared edge.
|
|
Clamping to the triangle's own three UVs cannot change a correctly-interpolated pixel,
|
|
because any point genuinely inside the triangle already interpolates within that range. It
|
|
only bounds the extrapolated ones. */
|
|
" float ulo = min(float(v_uv01.x), min(float(v_uv01.z), float(v_uv2.x)));\n"
|
|
" float uhi = max(float(v_uv01.x), max(float(v_uv01.z), float(v_uv2.x)));\n"
|
|
" float vlo = min(float(v_uv01.y), min(float(v_uv01.w), float(v_uv2.y)));\n"
|
|
" float vhi = max(float(v_uv01.y), max(float(v_uv01.w), float(v_uv2.y)));\n"
|
|
" tx = clamp(tx, ulo, uhi);\n"
|
|
" ty = clamp(ty, vlo, vhi);\n"
|
|
/* Polygons only. Sprites keep the integer texel walk above: their UV never has a fractional
|
|
part, so a filter would be a no-op on them without also synthesising a sub-texel position —
|
|
and doing that softens exactly the 2D HUD/text a player wants pixel-exact. */
|
|
/* A bound replacement BYPASSES u_filter and is point-sampled at its own resolution -- the
|
|
same rule psx_gpu_filter_active() states for the two CPU rasterizers, and for the same two
|
|
reasons: the filter exists to hide the size of a NATIVE texel, and three filter kernels
|
|
would otherwise have to agree over the atlas as well as over VRAM. psx/texrep.h. */
|
|
" if (repl) texel = fetch_repl(tx, ty, fract(vec2(tx, ty)));\n"
|
|
" else if (u_filter == 1) texel = fetch_smooth(tx, ty);\n"
|
|
" else if (u_filter == 2) texel = fetch_xbr(tx, ty);\n"
|
|
/* Mode 0 is NEAREST and must actually point-sample — it previously called fetch_bilinear(),
|
|
so "nearest" filtered, matching the other two rasterizers' identical bug. All three now
|
|
point-sample at 0, so the 1x parity gate still compares like with like. */
|
|
" else texel = fetch_texel(int(floor(tx)), int(floor(ty)));\n"
|
|
" }\n"
|
|
" if (texel == 0u) discard;\n"
|
|
" stp = (texel & 0x8000u) != 0u;\n"
|
|
" if ((flags & F_TRANSP) != 0u) transp = stp;\n"
|
|
" if ((flags & F_RAW) != 0u) {\n"
|
|
" col = vec3(float(texel & 31u), float((texel >> 5) & 31u), float((texel >> 10) & 31u));\n"
|
|
" } else {\n"
|
|
" vec3 t = vec3(float((texel & 31u) << 3u), float(((texel >> 5) & 31u) << 3u),\n"
|
|
" float(((texel >> 10) & 31u) << 3u));\n"
|
|
/* Hardware divides by 128 with integer truncation (psx-spx). This shader rounded, which
|
|
makes levels 1..8 fixed points and a frame-feedback trail permanent — see
|
|
psx_gpu_modulate_channel() in psx/dev/gpu.h and the backend. t and md
|
|
are integer-valued and t*md <= 63240, so t*md/128.0 is EXACT in float (128 is a power of
|
|
two) and floor() cannot land a level low. */
|
|
" vec3 c = (u_tex_trunc != 0) ? floor(clamp(t * md / 128.0, 0.0, 255.0))\n"
|
|
" : floor(clamp(t * md / 128.0, 0.0, 255.0) + 0.5);\n"
|
|
" col = floor(c / 8.0);\n"
|
|
" }\n"
|
|
" } else {\n"
|
|
" col = floor(md / 8.0);\n"
|
|
" }\n"
|
|
" if (u_stp_pass == 1 && transp) discard;\n"
|
|
" if (u_stp_pass == 2 && !transp) discard;\n"
|
|
/* col is 0..31 per channel. Emitting c5*8 keeps every blend result a multiple of 4, so
|
|
the >> 3 at scanout recovers the software's integer answer exactly. */
|
|
" vec3 F = col * 8.0;\n"
|
|
" float a = 0.0;\n"
|
|
" if (transp) {\n"
|
|
" if (mode == 0) { F *= 0.5; a = 0.5; }\n"
|
|
" else if (mode == 3) { F *= 0.25; a = 1.0; }\n"
|
|
" else { a = 1.0; }\n"
|
|
" }\n"
|
|
"#ifdef ARMSX_GL_MASK\n"
|
|
/* ---- mask SET, GP0(E6) bit 0 -- and, unavoidably, the blend --------------------------------
|
|
"alpha-channel conflict", stated there and resolved here: `.a` is either the
|
|
fixed-function SRC_ALPHA blend factor or the mask bit, and it cannot be both. The mask bit
|
|
wins, so the blend moves into the shader — which framebuffer fetch already made possible,
|
|
since o_color arrives holding the destination.
|
|
|
|
The arithmetic is the SAME `a` the fixed-function unit would have used, so this reproduces
|
|
the four modes without a second table: ADD is `F + B*a` and mode 2's reverse-subtract is
|
|
`B*a - F`. `transp && mode == 2` selects exactly the fragments the SUB range covered, so
|
|
the existing u_stp_pass split stays correct and is left alone.
|
|
|
|
B is recovered as an EXACT 8-bit integer, and the result is re-truncated to 5 bits before
|
|
it is stored. That second step is not cosmetic: psx/dev/gpu.c blends in 8 bits and then
|
|
packs to BGR555, so its destination is 5-bit, while the RGBA8 target kept 8 — the "a pixel
|
|
blended more than once can differ by one 5-bit step" divergence in the file header. Doing
|
|
the blend here makes matching it free, so the mask path does. */
|
|
/* `dst`, not `B` — A/B/C are already the triangle's three vertices in this scope. */
|
|
" vec3 dst = floor(o_color.rgb * 255.0 + 0.5);\n"
|
|
" vec3 blended = (transp && mode == 2) ? (dst * a - F) : (F + dst * a);\n"
|
|
" blended = floor(clamp(blended, 0.0, 255.0) + 0.5);\n"
|
|
" blended = floor(blended / 8.0) * 8.0;\n"
|
|
" bool mbit = PSX_GPU_MASK_WRITE((flags & F_MASK_SET) != 0u, u_mask_texel != 0, stp);\n"
|
|
" o_color = vec4(blended / 255.0, mbit ? 1.0 : 0.0);\n"
|
|
"#else\n"
|
|
/* The shipping path, textually unchanged: the fixed-function unit does the blend, `.a` is its
|
|
factor, and the render target has nowhere to keep bit 15. */
|
|
" o_color = vec4(F / 255.0, a);\n"
|
|
"#endif\n"
|
|
"}\n";
|
|
|
|
/* GP0(A0) upload and the initial/load-state seed: replicate a native VRAM rectangle into
|
|
the upscaled target. Issued with GL_BLEND OFF (gl_blit_from_vram), so alpha is free to
|
|
carry the source halfword's bit 15 straight into the target's mask channel — which is what
|
|
gpu.c:2282 does, it stores the uploaded halfword verbatim, bit 15 included. With the mask
|
|
bit off nothing ever reads that alpha and the RGB written is identical either way. */
|
|
static const char* kXferVS =
|
|
"#version 300 es\n"
|
|
"precision highp float;\n"
|
|
"uniform vec4 u_rect;\n"
|
|
"uniform vec2 u_rt_size;\n"
|
|
"uniform float u_scale;\n"
|
|
"void main() {\n"
|
|
" vec2 c = vec2(float(gl_VertexID & 1), float((gl_VertexID >> 1) & 1));\n"
|
|
" vec2 p = (u_rect.xy + c * u_rect.zw) * u_scale;\n"
|
|
" gl_Position = vec4(p / u_rt_size * 2.0 - 1.0, 0.0, 1.0);\n"
|
|
"}\n";
|
|
|
|
static const char* kXferFS =
|
|
"#version 300 es\n"
|
|
"precision highp float;\n"
|
|
"precision highp int;\n"
|
|
"precision highp usampler2D;\n"
|
|
"uniform highp usampler2D u_vram;\n"
|
|
"uniform float u_scale;\n"
|
|
"out vec4 o_color;\n"
|
|
"void main() {\n"
|
|
" ivec2 pn = ivec2(floor(gl_FragCoord.xy / u_scale));\n"
|
|
" uint t = texelFetch(u_vram, ivec2(pn.x & 1023, pn.y & 511), 0).r;\n"
|
|
" o_color = vec4(float((t & 31u) << 3u) / 255.0, float(((t >> 5) & 31u) << 3u) / 255.0,\n"
|
|
" float(((t >> 10) & 31u) << 3u) / 255.0, float((t >> 15) & 1u));\n"
|
|
"}\n";
|
|
|
|
/* Resolve: pack a region of the render target back down to BGR555 on the GPU so a readback
|
|
is 2 bytes per pixel and needs no CPU conversion at all. Two callers, one shader:
|
|
|
|
* SCANOUT — u_step = 1. Every render-target texel becomes one output texel, so the
|
|
result is the upscaled display region, which is what both the present texture and the
|
|
old glReadPixels path want.
|
|
* NATIVE READBACK — u_step = S. One output texel per NATIVE VRAM texel, sampling the
|
|
sub-texel at +S/2 (the centre-ish one; at S == 1 that is +0, so 1x is bit-for-bit the
|
|
scanout path and the parity gate still measures the same pixels). This is what
|
|
GP0(C0), the save-state flush and downgrade seed need: host VRAM is native, so
|
|
the downsample has to happen somewhere and doing it on the GPU keeps the transfer to
|
|
2 bytes per native texel instead of 2·S².
|
|
*/
|
|
static const char* kResolveVS =
|
|
"#version 300 es\n"
|
|
"precision highp float;\n"
|
|
"void main() {\n"
|
|
" vec2 c = vec2(float(gl_VertexID & 1), float((gl_VertexID >> 1) & 1));\n"
|
|
" gl_Position = vec4(c * 2.0 - 1.0, 0.0, 1.0);\n"
|
|
"}\n";
|
|
|
|
static const char* kResolveFS =
|
|
"#version 300 es\n"
|
|
"precision highp float;\n"
|
|
"precision highp int;\n"
|
|
"uniform sampler2D u_rt;\n"
|
|
"uniform ivec2 u_origin;\n"
|
|
"uniform ivec2 u_limit;\n"
|
|
"uniform int u_step;\n"
|
|
/* [video] downsample. 1 = the historical single-tap behaviour, textually unchanged in the
|
|
else-branch below so the scanout and the native readback are bit-identical to before the
|
|
key existed. N > 1 box-averages the NxN render-target block whose top-left corner the step
|
|
landed on — the same average gl_parity_check_upscaled() computes on the CPU (
|
|
instrument), which is why that harness is the reference for this resolve.
|
|
The loop is bounded by a compile-time constant with a break, not by u_box directly: GLSL ES
|
|
3.00 allows a dynamic bound, but drivers unroll a constant one and this shader runs once
|
|
per output pixel of every frame. */
|
|
"uniform int u_box;\n"
|
|
/* 1 while the mask bit is on: the target's alpha IS VRAM bit 15, so put it back. 0 restores
|
|
the original packing exactly, because with the mask bit off that alpha is the residue of
|
|
the fixed-function blend factor and means nothing. The box branch cannot produce a
|
|
meaningful mask bit (it averages S x S native pixels) and writes 0 there. */
|
|
"uniform int u_mask;\n"
|
|
"out vec4 o_color;\n"
|
|
"void main() {\n"
|
|
" ivec2 p = ivec2(gl_FragCoord.xy) * u_step + u_origin;\n"
|
|
" vec4 c;\n"
|
|
" if (u_box > 1) {\n"
|
|
" vec3 acc = vec3(0.0);\n"
|
|
" for (int by = 0; by < 8; by++) {\n"
|
|
" if (by >= u_box) break;\n"
|
|
" for (int bx = 0; bx < 8; bx++) {\n"
|
|
" if (bx >= u_box) break;\n"
|
|
" acc += texelFetch(u_rt, clamp(p + ivec2(bx, by), ivec2(0), u_limit), 0).rgb;\n"
|
|
" }\n"
|
|
" }\n"
|
|
" c = vec4(acc / float(u_box * u_box), 0.0);\n"
|
|
" } else {\n"
|
|
" c = texelFetch(u_rt, clamp(p, ivec2(0), u_limit), 0);\n"
|
|
" }\n"
|
|
" uint r = uint(c.r * 255.0 + 0.5) >> 3;\n"
|
|
" uint g = uint(c.g * 255.0 + 0.5) >> 3;\n"
|
|
" uint b = uint(c.b * 255.0 + 0.5) >> 3;\n"
|
|
" uint v = r | (g << 5) | (b << 10);\n"
|
|
" if (u_mask != 0 && c.a >= 0.5) v |= 0x8000u;\n"
|
|
" o_color = vec4(float(v & 255u) / 255.0, float((v >> 8) & 255u) / 255.0, 0.0, 1.0);\n"
|
|
"}\n";
|
|
|
|
/*
|
|
GPU-to-GPU resolve needed before the software shadow can be dropped.
|
|
|
|
Identical arithmetic to kResolveFS, but it writes a UINT rather than two normalised
|
|
bytes, because its destination is vram_tex — the R16UI native VRAM mirror the draw
|
|
shader samples. R16UI is colour-renderable in GLES 3.0, so this is an FBO and a draw:
|
|
no glReadPixels, no CPU round trip, no pipeline stall. The 5-bit values recovered here
|
|
are exactly what a readback would produce (: the draw shader emits c5*8, every blend
|
|
result stays a multiple of 4, and >> 3 recovers the software's integer answer).
|
|
|
|
Bit 15 comes from the target's ALPHA once the mask bit is on (u_mask), and is written as 0
|
|
otherwise — which is the KNOWN divergence the file shipped with: with no mask channel the
|
|
RGBA8 render target cannot keep it, and gpu.c:800 does preserve a source texel's bit 15
|
|
through a raw textured write, so a game that renders a sprite sheet on the GPU and then
|
|
samples it back with semi-transparency loses the per-texel STP flag. The parity gate masks
|
|
bit 15 off on both sides in that state. With the mask bit on there is a real channel to
|
|
read and the round trip is lossless.
|
|
*/
|
|
static const char* kGResFS =
|
|
"#version 300 es\n"
|
|
"precision highp float;\n"
|
|
"precision highp int;\n"
|
|
"uniform sampler2D u_rt;\n"
|
|
"uniform ivec2 u_limit;\n"
|
|
"uniform int u_step;\n"
|
|
"uniform int u_half;\n"
|
|
"uniform int u_mask;\n"
|
|
"out uvec4 o_val;\n"
|
|
"void main() {\n"
|
|
" ivec2 p = clamp(ivec2(gl_FragCoord.xy) * u_step + ivec2(u_half), ivec2(0), u_limit);\n"
|
|
" vec4 c = texelFetch(u_rt, p, 0);\n"
|
|
" uint r = uint(c.r * 255.0 + 0.5) >> 3;\n"
|
|
" uint g = uint(c.g * 255.0 + 0.5) >> 3;\n"
|
|
" uint b = uint(c.b * 255.0 + 0.5) >> 3;\n"
|
|
" uint v = r | (g << 5) | (b << 10);\n"
|
|
" if (u_mask != 0 && c.a >= 0.5) v |= 0x8000u;\n"
|
|
" o_val = uvec4(v, 0u, 0u, 0u);\n"
|
|
"}\n";
|
|
|
|
/* ---- tile helpers -------------------------------------------------------------------- */
|
|
|
|
static void tiles_clear(tilemap_t* t) {
|
|
memset(t, 0, sizeof(*t));
|
|
}
|
|
|
|
/* Marks the tiles a native-coordinate rectangle touches. Coordinates are clamped rather
|
|
than wrapped: a transfer that runs off the edge is dropped by the core too. */
|
|
static void tiles_mark(tilemap_t* t, int x, int y, int w, int h) {
|
|
int x0, y0, x1, y1, ty;
|
|
|
|
if ((w <= 0) || (h <= 0))
|
|
return;
|
|
|
|
x0 = x < 0 ? 0 : x;
|
|
y0 = y < 0 ? 0 : y;
|
|
x1 = x + w; if (x1 > 1024) x1 = 1024;
|
|
y1 = y + h; if (y1 > 512) y1 = 512;
|
|
|
|
if ((x0 >= x1) || (y0 >= y1))
|
|
return;
|
|
|
|
x0 >>= TILE_SHIFT; y0 >>= TILE_SHIFT;
|
|
x1 = (x1 - 1) >> TILE_SHIFT; y1 = (y1 - 1) >> TILE_SHIFT;
|
|
|
|
for (ty = y0; ty <= y1; ty++) {
|
|
uint64_t mask;
|
|
|
|
if (x1 >= 63)
|
|
mask = ~(uint64_t)0 << x0;
|
|
else
|
|
mask = ((((uint64_t)1 << (x1 + 1)) - 1) & ~((((uint64_t)1 << x0) - 1)));
|
|
|
|
t->row[ty] |= mask;
|
|
}
|
|
}
|
|
|
|
static int tiles_intersects(const tilemap_t* t, int x, int y, int w, int h) {
|
|
int x0, y0, x1, y1, ty;
|
|
|
|
if ((w <= 0) || (h <= 0))
|
|
return 0;
|
|
|
|
x0 = x < 0 ? 0 : x;
|
|
y0 = y < 0 ? 0 : y;
|
|
x1 = x + w; if (x1 > 1024) x1 = 1024;
|
|
y1 = y + h; if (y1 > 512) y1 = 512;
|
|
|
|
if ((x0 >= x1) || (y0 >= y1))
|
|
return 0;
|
|
|
|
x0 >>= TILE_SHIFT; y0 >>= TILE_SHIFT;
|
|
x1 = (x1 - 1) >> TILE_SHIFT; y1 = (y1 - 1) >> TILE_SHIFT;
|
|
|
|
for (ty = y0; ty <= y1; ty++) {
|
|
uint64_t mask;
|
|
|
|
if (x1 >= 63)
|
|
mask = ~(uint64_t)0 << x0;
|
|
else
|
|
mask = ((((uint64_t)1 << (x1 + 1)) - 1) & ~((((uint64_t)1 << x0) - 1)));
|
|
|
|
if (t->row[ty] & mask)
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* ---- program helpers ----------------------------------------------------------------- */
|
|
|
|
static GLuint gl_compile(hw_gl_t* g, GLenum type, const char* src, const char* label) {
|
|
GLuint shader = g->gl.CreateShader(type);
|
|
GLint ok = 0;
|
|
|
|
if (!shader)
|
|
return 0;
|
|
|
|
g->gl.ShaderSource(shader, 1, &src, NULL);
|
|
g->gl.CompileShader(shader);
|
|
g->gl.GetShaderiv(shader, GL_COMPILE_STATUS, &ok);
|
|
|
|
if (!ok) {
|
|
char log[1024];
|
|
GLsizei written = 0;
|
|
|
|
log[0] = '\0';
|
|
g->gl.GetShaderInfoLog(shader, (GLsizei)sizeof(log), &written, log);
|
|
log[sizeof(log) - 1] = '\0';
|
|
gl_status("shader %s failed to compile: %s", label, log);
|
|
g->gl.DeleteShader(shader);
|
|
|
|
return 0;
|
|
}
|
|
|
|
return shader;
|
|
}
|
|
|
|
static const char* kDrawAttribs[] = {
|
|
"a_pos", "a_tri0", "a_tri1", "a_col0", "a_col1", "a_col2",
|
|
"a_uv01", "a_uv2", "a_misc", "a_texwin", "a_triw", "a_repl"
|
|
};
|
|
|
|
static GLuint gl_link(hw_gl_t* g, const char* vs_src, const char* fs_src,
|
|
const char* const* attribs, int attrib_count, const char* label) {
|
|
GLuint vs = gl_compile(g, GL_VERTEX_SHADER, vs_src, label);
|
|
GLuint fs = 0;
|
|
GLuint prog = 0;
|
|
GLint ok = 0;
|
|
int i;
|
|
|
|
if (!vs)
|
|
return 0;
|
|
|
|
fs = gl_compile(g, GL_FRAGMENT_SHADER, fs_src, label);
|
|
|
|
if (!fs) {
|
|
g->gl.DeleteShader(vs);
|
|
return 0;
|
|
}
|
|
|
|
prog = g->gl.CreateProgram();
|
|
|
|
if (!prog) {
|
|
g->gl.DeleteShader(vs);
|
|
g->gl.DeleteShader(fs);
|
|
return 0;
|
|
}
|
|
|
|
g->gl.AttachShader(prog, vs);
|
|
g->gl.AttachShader(prog, fs);
|
|
|
|
/* Explicit binding rather than layout(location=) qualifiers: both are legal in GLSL ES
|
|
3.00, but this keeps the C-side attribute indices and the shader in one place. */
|
|
for (i = 0; i < attrib_count; i++)
|
|
g->gl.BindAttribLocation(prog, (GLuint)i, attribs[i]);
|
|
|
|
g->gl.LinkProgram(prog);
|
|
g->gl.GetProgramiv(prog, GL_LINK_STATUS, &ok);
|
|
g->gl.DeleteShader(vs);
|
|
g->gl.DeleteShader(fs);
|
|
|
|
if (!ok) {
|
|
char log[1024];
|
|
GLsizei written = 0;
|
|
|
|
log[0] = '\0';
|
|
g->gl.GetProgramInfoLog(prog, (GLsizei)sizeof(log), &written, log);
|
|
log[sizeof(log) - 1] = '\0';
|
|
gl_status("program %s failed to link: %s", label, log);
|
|
g->gl.DeleteProgram(prog);
|
|
|
|
return 0;
|
|
}
|
|
|
|
return prog;
|
|
}
|
|
|
|
/* Whole-word search of GL_EXTENSIONS. glGetStringi() is the GLES 3.0 way, but glGetString()
|
|
still returns the space-separated list there and this file dlsym's its entry points one by
|
|
one — one fewer symbol to load, and a substring match would say yes to
|
|
GL_EXT_shader_framebuffer_fetch_non_coherent, which needs a barrier this backend does not
|
|
issue and would therefore read stale destination colour. */
|
|
static int gl_has_extension(hw_gl_t* g, const char* name) {
|
|
const unsigned char* list = g->gl.GetString ? g->gl.GetString(GL_EXTENSIONS) : NULL;
|
|
const char* p = (const char*)list;
|
|
const size_t n = strlen(name);
|
|
|
|
while (p && *p) {
|
|
const char* end;
|
|
|
|
while (*p == ' ')
|
|
p++;
|
|
|
|
end = strchr(p, ' ');
|
|
|
|
if (!end)
|
|
end = p + strlen(p);
|
|
|
|
if (((size_t)(end - p) == n) && (memcmp(p, name, n) == 0))
|
|
return 1;
|
|
|
|
p = end;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
The draw fragment shader's source, assembled.
|
|
|
|
Two things have to precede every other token in a GLSL ES translation unit — `#version`
|
|
and `#extension` — so neither can live in kDrawFS, and the mask stage's two expressions
|
|
come from psx/dev/gpu.h rather than from this file (PSX_GPU_MASK_GLSL: the SAME macro
|
|
bodies the software rasterizer's contract test drives, stringified). The caller owns the
|
|
result and frees it after linking.
|
|
*/
|
|
static char* gl_build_draw_fs(int mask_mode) {
|
|
static const char* kVersion = "#version 300 es\n";
|
|
static const char* kMaskHead =
|
|
"#extension GL_EXT_shader_framebuffer_fetch : require\n"
|
|
"#define ARMSX_GL_MASK 1\n"
|
|
PSX_GPU_MASK_GLSL;
|
|
/* Unconditional: fetch_repl() in kDrawFS calls texrep_at()/texrep_pack(), and the
|
|
replacement path is switched off by v_repl carrying zero, not by recompiling. Same
|
|
construction as the mask stage — the ONE definition of the atlas mapping lives in
|
|
psx/texrep.h and both the shader and psx_texrep_sample() are built from it. */
|
|
static const char* kTexrep = PSX_TEXREP_GLSL;
|
|
const char* head = mask_mode ? kMaskHead : "";
|
|
const size_t len = strlen(kVersion) + strlen(head) + strlen(kTexrep) +
|
|
strlen(kDrawFS) + 1u;
|
|
char* out = (char*)malloc(len);
|
|
|
|
if (!out)
|
|
return NULL;
|
|
|
|
out[0] = '\0';
|
|
strcat(out, kVersion);
|
|
strcat(out, head);
|
|
strcat(out, kTexrep);
|
|
strcat(out, kDrawFS);
|
|
|
|
return out;
|
|
}
|
|
|
|
/* ---- render-target binding ----------------------------------------------------------- */
|
|
|
|
/*
|
|
frontend/render_gl.cpp's present path never calls glBindFramebuffer and its loader table
|
|
contains no framebuffer entry points at all — it structurally assumes FBO 0 is bound. If
|
|
this backend leaves its own FBO bound on the way out, present renders into it and the
|
|
screen goes black with NO GL error. Rebinding 0 is entirely on us, which is what
|
|
gl_release() is for, and it is called from both end_frame() and display_buffer().
|
|
*/
|
|
static void gl_bind_rt(hw_gl_t* g) {
|
|
if (g->rt_bound)
|
|
return;
|
|
|
|
g->gl.BindFramebuffer(GL_FRAMEBUFFER, g->rt_fbo);
|
|
g->gl.Viewport(0, 0, g->rt_w, g->rt_h);
|
|
g->gl.Disable(GL_DEPTH_TEST);
|
|
g->gl.Disable(GL_STENCIL_TEST);
|
|
g->gl.Disable(GL_CULL_FACE);
|
|
g->gl.Disable(GL_DITHER);
|
|
|
|
/* In mask mode the fragment shader does the blend itself (framebuffer fetch), because the
|
|
alpha channel is the mask bit and can no longer be the SRC_ALPHA factor. Leaving the
|
|
blend unit on would then apply a SECOND blend on top of the shader's answer, with the
|
|
mask bit as its factor — a corruption that looks like "transparency is wrong sometimes",
|
|
which is why it is turned off in the one place every path passes through. */
|
|
if (g->mask_mode) {
|
|
g->gl.Disable(GL_BLEND);
|
|
} else {
|
|
g->gl.Enable(GL_BLEND);
|
|
g->gl.BlendFunc(GL_ONE, GL_SRC_ALPHA);
|
|
g->gl.BlendEquation(GL_FUNC_ADD);
|
|
}
|
|
|
|
g->gl.ColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
|
g->rt_bound = 1;
|
|
}
|
|
|
|
/* Deliberately NOT short-circuited on rt_bound. gl_display_buffer() and the two
|
|
ensure_*() helpers bind a DIFFERENT framebuffer of ours and clear rt_bound, so an
|
|
early-out here would leave that one bound and present would render into it — a black
|
|
screen with no GL error, which is precisely the failure the comment above describes. */
|
|
static void gl_release(hw_gl_t* g) {
|
|
g->gl.Disable(GL_SCISSOR_TEST);
|
|
g->gl.Disable(GL_BLEND);
|
|
g->gl.BindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
g->gl.BindVertexArray(0);
|
|
g->gl.UseProgram(0);
|
|
g->gl.BindBuffer(GL_ARRAY_BUFFER, 0);
|
|
g->rt_bound = 0;
|
|
}
|
|
|
|
/* ---- vram texture sync ---------------------------------------------------------------- */
|
|
|
|
/* Uploads every dirty tile that overlaps the requested rectangle and clears those bits.
|
|
Called only when a textured primitive is about to sample a region host VRAM has written
|
|
since the last sync — the framebuffer being permanently dirty costs nothing as long as
|
|
nothing samples it, which is the normal case. */
|
|
static void gl_sync_vram(hw_gl_t* g, int x, int y, int w, int h) {
|
|
const uint16_t* vram = g->gpu->vram;
|
|
int x0, y0, x1, y1, ty;
|
|
|
|
if (!g->any_dirty || (w <= 0) || (h <= 0))
|
|
return;
|
|
|
|
x0 = x < 0 ? 0 : x;
|
|
y0 = y < 0 ? 0 : y;
|
|
x1 = x + w; if (x1 > 1024) x1 = 1024;
|
|
y1 = y + h; if (y1 > 512) y1 = 512;
|
|
|
|
if ((x0 >= x1) || (y0 >= y1))
|
|
return;
|
|
|
|
x0 >>= TILE_SHIFT; y0 >>= TILE_SHIFT;
|
|
x1 = (x1 - 1) >> TILE_SHIFT; y1 = (y1 - 1) >> TILE_SHIFT;
|
|
|
|
g->gl.ActiveTexture(GL_TEXTURE0);
|
|
g->gl.BindTexture(GL_TEXTURE_2D, g->vram_tex);
|
|
g->gl.PixelStorei(GL_UNPACK_ALIGNMENT, 2);
|
|
g->gl.PixelStorei(GL_UNPACK_ROW_LENGTH, 1024);
|
|
|
|
for (ty = y0; ty <= y1; ty++) {
|
|
uint64_t mask;
|
|
uint64_t hit;
|
|
int lo;
|
|
|
|
if (x1 >= 63)
|
|
mask = ~(uint64_t)0 << x0;
|
|
else
|
|
mask = ((((uint64_t)1 << (x1 + 1)) - 1) & ~((((uint64_t)1 << x0) - 1)));
|
|
|
|
hit = g->dirty.row[ty] & mask;
|
|
|
|
if (!hit)
|
|
continue;
|
|
|
|
/*
|
|
One upload per CONTIGUOUS RUN of dirty tiles, not one per row-span.
|
|
|
|
The span version — first set bit to last set bit, everything in between along
|
|
for the ride — was correct only because host VRAM was authoritative everywhere,
|
|
so re-uploading a clean tile was a no-op. Once the render target owns rendered
|
|
content that stops being true: a clean tile inside the span may hold texels that
|
|
exist ONLY on the GPU, and uploading stale host VRAM over it silently corrupts
|
|
them with nothing left marked to repair it. Exact runs never touch a tile that
|
|
was not marked.
|
|
*/
|
|
lo = x0;
|
|
|
|
while (lo <= x1) {
|
|
int hi;
|
|
int px, py, pw, ph;
|
|
|
|
if (!(hit & ((uint64_t)1 << lo))) {
|
|
lo++;
|
|
continue;
|
|
}
|
|
|
|
hi = lo;
|
|
|
|
while ((hi + 1 <= x1) && (hit & ((uint64_t)1 << (hi + 1))))
|
|
hi++;
|
|
|
|
px = lo << TILE_SHIFT;
|
|
py = ty << TILE_SHIFT;
|
|
pw = ((hi + 1) << TILE_SHIFT) - px;
|
|
ph = 1 << TILE_SHIFT;
|
|
|
|
if (px + pw > 1024) pw = 1024 - px;
|
|
if (py + ph > 512) ph = 512 - py;
|
|
|
|
g->gl.TexSubImage2D(GL_TEXTURE_2D, 0, px, py, pw, ph, GL_RED_INTEGER,
|
|
GL_UNSIGNED_SHORT, vram + px + (size_t)py * 1024);
|
|
|
|
g->stat_syncs++;
|
|
g->stat_sync_px += (uint32_t)(pw * ph);
|
|
|
|
lo = hi + 1;
|
|
}
|
|
|
|
g->dirty.row[ty] &= ~hit;
|
|
}
|
|
|
|
g->gl.PixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
|
|
|
g->any_dirty = 0;
|
|
for (ty = 0; ty < TILES_Y; ty++) {
|
|
if (g->dirty.row[ty]) {
|
|
g->any_dirty = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void gl_mark_dirty(hw_gl_t* g, int x, int y, int w, int h) {
|
|
tiles_mark(&g->dirty, x, y, w, h);
|
|
g->any_dirty = 1;
|
|
}
|
|
|
|
/* ---- row 1: the GPU -> GPU resolve ------------------------------------------------ */
|
|
|
|
/* ladder, defined next to the seed it walks. A resolve that cannot be served is a
|
|
correctness failure once the shadow is gone, so this is reachable from here. */
|
|
static void gl_downgrade(hw_gl_t* g, const char* reason);
|
|
|
|
/* vram_tex as a render target. Lazily, because it is only ever needed once something
|
|
actually samples a region the rasterizer drew, and a driver that refuses R16UI as a
|
|
colour attachment must not take the whole backend down at create() time on a build where
|
|
the shadow is still doing the work. */
|
|
static int gl_ensure_vram_fbo(hw_gl_t* g) {
|
|
if (g->vram_fbo_state)
|
|
return g->vram_fbo_state > 0;
|
|
|
|
g->gl.GenFramebuffers(1, &g->vram_fbo);
|
|
|
|
if (!g->vram_fbo) {
|
|
g->vram_fbo_state = -1;
|
|
return 0;
|
|
}
|
|
|
|
g->gl.BindFramebuffer(GL_FRAMEBUFFER, g->vram_fbo);
|
|
g->gl.FramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
|
g->vram_tex, 0);
|
|
g->rt_bound = 0;
|
|
|
|
if (g->gl.CheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
|
psxe_diag_logf("hwgl", "vram_tex (R16UI) is not framebuffer-complete; row 1's "
|
|
"GPU->GPU resolve is unavailable");
|
|
g->gl.BindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
g->gl.DeleteFramebuffers(1, &g->vram_fbo);
|
|
g->vram_fbo = 0;
|
|
g->vram_fbo_state = -1;
|
|
return 0;
|
|
}
|
|
|
|
g->gl.BindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
g->vram_fbo_state = 1;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
Mirror image of gl_sync_vram(): same tile walk,
|
|
same "one span per tile row" collapse, but the source is the render target and the
|
|
transfer never leaves the GPU.
|
|
|
|
The caller must have flushed — this binds a different framebuffer and rebinds TEXTURE0,
|
|
and rendering into vram_tex while the draw program still samples it is undefined.
|
|
*/
|
|
static void gl_resolve_gpu_tiles(hw_gl_t* g, int x, int y, int w, int h) {
|
|
int x0, y0, x1, y1, ty;
|
|
|
|
if (!g->any_gpu_dirty || (w <= 0) || (h <= 0))
|
|
return;
|
|
|
|
x0 = x < 0 ? 0 : x;
|
|
y0 = y < 0 ? 0 : y;
|
|
x1 = x + w; if (x1 > 1024) x1 = 1024;
|
|
y1 = y + h; if (y1 > 512) y1 = 512;
|
|
|
|
if ((x0 >= x1) || (y0 >= y1))
|
|
return;
|
|
|
|
x0 >>= TILE_SHIFT; y0 >>= TILE_SHIFT;
|
|
x1 = (x1 - 1) >> TILE_SHIFT; y1 = (y1 - 1) >> TILE_SHIFT;
|
|
|
|
if (!gl_ensure_vram_fbo(g)) {
|
|
/* Nothing correct can be done here. With the shadow on, host VRAM is still right
|
|
and the caller's gl_sync_vram() has already produced the right texels, so this is
|
|
only a lost optimisation. Without it, the picture would be silently wrong, which
|
|
is precisely what ladder exists for. */
|
|
if (!g->shadow)
|
|
gl_downgrade(g, "vram_tex is not renderable, so row 1 cannot be served");
|
|
|
|
g->any_gpu_dirty = 0;
|
|
tiles_clear(&g->gpu_dirty);
|
|
return;
|
|
}
|
|
|
|
g->gl.BindFramebuffer(GL_FRAMEBUFFER, g->vram_fbo);
|
|
g->rt_bound = 0;
|
|
g->gl.Viewport(0, 0, 1024, 512);
|
|
g->gl.Disable(GL_BLEND);
|
|
g->gl.Enable(GL_SCISSOR_TEST);
|
|
g->gl.UseProgram(g->prog_gres);
|
|
g->gl.Uniform1i(g->u_gres_rt, 0);
|
|
g->gl.Uniform2i(g->u_gres_limit, g->rt_w - 1, g->rt_h - 1);
|
|
g->gl.Uniform1i(g->u_gres_step, g->scale);
|
|
/* Same sub-texel choice as gl_readback_rect(): the centre-ish one, +0 at S == 1. */
|
|
g->gl.Uniform1i(g->u_gres_half, g->scale / 2);
|
|
g->gl.Uniform1i(g->u_gres_mask, g->mask_mode);
|
|
g->gl.ActiveTexture(GL_TEXTURE0);
|
|
g->gl.BindTexture(GL_TEXTURE_2D, g->rt_tex);
|
|
g->gl.BindVertexArray(g->quad_vao);
|
|
|
|
for (ty = y0; ty <= y1; ty++) {
|
|
uint64_t mask;
|
|
uint64_t hit;
|
|
int lo;
|
|
|
|
if (x1 >= 63)
|
|
mask = ~(uint64_t)0 << x0;
|
|
else
|
|
mask = ((((uint64_t)1 << (x1 + 1)) - 1) & ~((((uint64_t)1 << x0) - 1)));
|
|
|
|
hit = g->gpu_dirty.row[ty] & mask;
|
|
|
|
if (!hit)
|
|
continue;
|
|
|
|
/*
|
|
EXACT RUNS, and this is the whole correctness of the pass.
|
|
|
|
The first version resolved first-set-bit to last-set-bit, which drags every
|
|
clean tile in between through the render target. That round trip is not
|
|
lossless: the RGBA8 target has no bit 15, so a 16-bit texture texel loses its
|
|
STP flag and — far worse — a 4bpp texture word loses the high bit of one of its
|
|
four PALETTE INDICES. Sampling a page whose untouched half had been laundered
|
|
that way is how a 0.2 % whole-VRAM divergence turned into 4.92 % of the display
|
|
window disagreeing (measured, ). A tile that the rasterizer did not write
|
|
must never be touched.
|
|
*/
|
|
lo = x0;
|
|
|
|
while (lo <= x1) {
|
|
int hi;
|
|
int px, py, pw, ph;
|
|
|
|
if (!(hit & ((uint64_t)1 << lo))) {
|
|
lo++;
|
|
continue;
|
|
}
|
|
|
|
hi = lo;
|
|
|
|
while ((hi + 1 <= x1) && (hit & ((uint64_t)1 << (hi + 1))))
|
|
hi++;
|
|
|
|
px = lo << TILE_SHIFT;
|
|
py = ty << TILE_SHIFT;
|
|
pw = ((hi + 1) << TILE_SHIFT) - px;
|
|
ph = 1 << TILE_SHIFT;
|
|
|
|
if (px + pw > 1024) pw = 1024 - px;
|
|
if (py + ph > 512) ph = 512 - py;
|
|
|
|
g->gl.Scissor(px, py, pw, ph);
|
|
g->gl.DrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
|
|
g->stat_gres++;
|
|
g->stat_gres_px += (uint32_t)(pw * ph);
|
|
|
|
lo = hi + 1;
|
|
}
|
|
|
|
g->gpu_dirty.row[ty] &= ~hit;
|
|
|
|
/* A tile whose content now comes from the render target must NOT be overwritten by
|
|
a later host upload of the same tile: host VRAM does not have the rendered
|
|
texels. gl_sync_vram() runs first by construction (gl_note_sample), so clearing
|
|
the CPU-dirty bit here is the correct resolution of "both maps claim this tile". */
|
|
g->dirty.row[ty] &= ~hit;
|
|
}
|
|
|
|
g->any_gpu_dirty = 0;
|
|
for (ty = 0; ty < TILES_Y; ty++) {
|
|
if (g->gpu_dirty.row[ty]) {
|
|
g->any_gpu_dirty = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
g->any_dirty = 0;
|
|
for (ty = 0; ty < TILES_Y; ty++) {
|
|
if (g->dirty.row[ty]) {
|
|
g->any_dirty = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
g->gl.Disable(GL_SCISSOR_TEST);
|
|
g->gl.BindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
}
|
|
|
|
/*
|
|
A region the RASTERIZER just wrote.
|
|
|
|
With the shadow on, gpu.c has already put those texels into host VRAM, so marking them
|
|
CPU-dirty and re-uploading is both correct and cheaper than a resolve. With the shadow
|
|
off (or `hwgl_gpu_resolve` armed) the render target is the only copy that has them, and
|
|
the same mark has to go into gpu_dirty instead — nothing else in this file changes.
|
|
*/
|
|
static void gl_mark_drawn(hw_gl_t* g, int x, int y, int w, int h) {
|
|
if (g->gpu_own) {
|
|
/*
|
|
CLIPPED TO THE DRAWING AREA, and that clip is load-bearing rather than tidy.
|
|
|
|
A primitive's bounding box is not what it writes: gpu.c:322-325 rejects every
|
|
pixel outside [draw_x1,draw_x2] x [draw_y1,draw_y2] and gl_range_for() scissors
|
|
to the same rectangle, so a large polygon clipped to a 512x240 drawing area can
|
|
have a bounding box covering half of VRAM while touching none of it.
|
|
|
|
Over-marking is free when the mark means "re-upload this from host VRAM" — the
|
|
data is identical, so the extra work is wasted, not wrong. It is NOT free when
|
|
the mark means "launder this tile through the render target": the RGBA8 target
|
|
has no bit 15, so an untouched tile carrying a 4bpp TEXTURE loses the high bit
|
|
of one palette index in four and an untouched 16-bit texel loses its STP flag.
|
|
That is what took the 1x parity window from 0.4708 % to 4.918 % () — not
|
|
a rasterization bug at all, a tilemap that claimed more than the rasterizer
|
|
wrote.
|
|
*/
|
|
const psx_gpu_t* gpu = g->gpu;
|
|
int x0 = x, y0 = y, x1 = x + w, y1 = y + h;
|
|
const int dx0 = (int)gpu->draw_x1;
|
|
const int dy0 = (int)gpu->draw_y1;
|
|
const int dx1 = (int)gpu->draw_x2 + 1; /* draw_x2 is INCLUSIVE */
|
|
const int dy1 = (int)gpu->draw_y2 + 1;
|
|
|
|
if (x0 < dx0) x0 = dx0;
|
|
if (y0 < dy0) y0 = dy0;
|
|
if (x1 > dx1) x1 = dx1;
|
|
if (y1 > dy1) y1 = dy1;
|
|
|
|
if (g->dbg_geom && (g->frames == 3500) && (g->geom_draw_left > 0) &&
|
|
((y + h > 256) || (y0 >= 256))) {
|
|
g->geom_draw_left--;
|
|
psxe_diag_logf("hwgl",
|
|
"geom-draw: bbox=(%d,%d %dx%d) clipped=(%d,%d %dx%d) "
|
|
"drawarea=(%u,%u)-(%u,%u)",
|
|
x, y, w, h, x0, y0, x1 - x0, y1 - y0,
|
|
gpu->draw_x1, gpu->draw_y1, gpu->draw_x2, gpu->draw_y2);
|
|
}
|
|
|
|
if ((x0 >= x1) || (y0 >= y1))
|
|
return;
|
|
|
|
tiles_mark(&g->gpu_dirty, x0, y0, x1 - x0, y1 - y0);
|
|
g->any_gpu_dirty = 1;
|
|
return;
|
|
}
|
|
|
|
gl_mark_dirty(g, x, y, w, h);
|
|
}
|
|
|
|
/* GP0(80)'s destination, which the drawing area does NOT clip (gpu.c:1977-1984). */
|
|
static void gl_mark_gpu_dirty(hw_gl_t* g, int x, int y, int w, int h) {
|
|
tiles_mark(&g->gpu_dirty, x, y, w, h);
|
|
g->any_gpu_dirty = 1;
|
|
}
|
|
|
|
/* ---- batching -------------------------------------------------------------------------- */
|
|
|
|
static void gl_flush(hw_gl_t* g);
|
|
|
|
/* A write into VRAM that the pending batch is going to sample is a read-after-write hazard:
|
|
the batch has to go out before host VRAM changes underneath it. This is only the flush
|
|
half; the two halves have to straddle gl_note_sample() on the drawing paths, which is
|
|
why they are separable. See gl_triangle(). */
|
|
static void gl_write_flush(hw_gl_t* g, int x, int y, int w, int h) {
|
|
if (g->any_sampled && tiles_intersects(&g->sampled, x, y, w, h))
|
|
gl_flush(g);
|
|
}
|
|
|
|
/* Both halves, for the VRAM transfer commands: there host VRAM either already holds the
|
|
new contents (GP0(A0)) or is written by the software shadow immediately after, and
|
|
nothing in between can sample and re-clear the tile. */
|
|
static void gl_write_hazard(hw_gl_t* g, int x, int y, int w, int h) {
|
|
gl_write_flush(g, x, y, w, h);
|
|
gl_mark_dirty(g, x, y, w, h);
|
|
}
|
|
|
|
static int gl_reserve(hw_gl_t* g, int verts) {
|
|
if (g->vert_count + verts > g->vert_cap) {
|
|
int cap = g->vert_cap ? g->vert_cap * 2 : 4096;
|
|
gl_vertex_t* grown;
|
|
|
|
while (cap < g->vert_count + verts)
|
|
cap *= 2;
|
|
|
|
/* 64k triangles is far past any real PS1 frame; flushing is always correct, so an
|
|
upper bound costs nothing but keeps a runaway display list from eating the heap. */
|
|
if (cap > 196608) {
|
|
gl_flush(g);
|
|
cap = g->vert_cap ? g->vert_cap : 4096;
|
|
|
|
if (g->vert_count + verts > cap)
|
|
return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
grown = (gl_vertex_t*)realloc(g->verts, (size_t)cap * sizeof(gl_vertex_t));
|
|
|
|
if (!grown)
|
|
return 0;
|
|
|
|
g->verts = grown;
|
|
g->vert_cap = cap;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static gl_range_t* gl_range_for(hw_gl_t* g, int blend, int stp_pass) {
|
|
const psx_gpu_t* gpu = g->gpu;
|
|
int sx = (int)gpu->draw_x1;
|
|
int sy = (int)gpu->draw_y1;
|
|
int sw = (int)gpu->draw_x2 - sx + 1;
|
|
int sh = (int)gpu->draw_y2 - sy + 1;
|
|
gl_range_t* r;
|
|
|
|
if (g->range_count) {
|
|
r = &g->ranges[g->range_count - 1];
|
|
|
|
if ((r->blend == blend) && (r->stp_pass == stp_pass) &&
|
|
(r->sx == sx) && (r->sy == sy) && (r->sw == sw) && (r->sh == sh) &&
|
|
(r->first + r->count == g->vert_count)) {
|
|
return r;
|
|
}
|
|
}
|
|
|
|
if (g->range_count + 1 > g->range_cap) {
|
|
int cap = g->range_cap ? g->range_cap * 2 : 256;
|
|
gl_range_t* grown = (gl_range_t*)realloc(g->ranges, (size_t)cap * sizeof(gl_range_t));
|
|
|
|
if (!grown)
|
|
return NULL;
|
|
|
|
g->ranges = grown;
|
|
g->range_cap = cap;
|
|
}
|
|
|
|
r = &g->ranges[g->range_count++];
|
|
r->first = g->vert_count;
|
|
r->count = 0;
|
|
r->blend = (uint8_t)blend;
|
|
r->stp_pass = (uint8_t)stp_pass;
|
|
r->sx = (int16_t)sx;
|
|
r->sy = (int16_t)sy;
|
|
r->sw = (int16_t)sw;
|
|
r->sh = (int16_t)sh;
|
|
|
|
return r;
|
|
}
|
|
|
|
static void gl_flush(hw_gl_t* g) {
|
|
int i;
|
|
|
|
if (!g->range_count) {
|
|
g->vert_count = 0;
|
|
tiles_clear(&g->sampled);
|
|
g->any_sampled = 0;
|
|
return;
|
|
}
|
|
|
|
gl_bind_rt(g);
|
|
|
|
g->gl.BindVertexArray(g->vao);
|
|
g->gl.BindBuffer(GL_ARRAY_BUFFER, g->vbo);
|
|
g->gl.BufferData(GL_ARRAY_BUFFER, (GLsizeiptr)((size_t)g->vert_count * sizeof(gl_vertex_t)),
|
|
g->verts, GL_STREAM_DRAW);
|
|
|
|
g->gl.UseProgram(g->prog_draw);
|
|
g->gl.Uniform2f(g->u_draw_rt_size, (GLfloat)g->rt_w, (GLfloat)g->rt_h);
|
|
g->gl.Uniform1f(g->u_draw_scale, (GLfloat)g->scale);
|
|
g->gl.Uniform1i(g->u_draw_vram, 0);
|
|
/* Per flush, not per create: the filter is live-toggleable and the backend is not
|
|
recreated when it changes. One Uniform1i per flush is noise next to the draw calls. */
|
|
g->gl.Uniform1i(g->u_draw_filter, g_opt_texture_filter);
|
|
g->gl.Uniform1i(g->u_draw_tex_trunc, g_opt_tex_trunc);
|
|
g->gl.Uniform1i(g->u_draw_mask_texel, g_opt_mask_texel);
|
|
/* Texture unit 1 is the replacement atlas (psx/texrep.h). Bound unconditionally so the
|
|
sampler is never left pointing at unit 0's usampler2D — a GLES driver may reject two
|
|
samplers of different types on one unit even when the branch that reads the second is
|
|
never taken. Costs one Uniform1i and, when the atlas exists, one BindTexture. */
|
|
g->gl.Uniform1i(g->u_draw_repl, 1);
|
|
|
|
if (g->repl_tex) {
|
|
g->gl.ActiveTexture(GL_TEXTURE1);
|
|
g->gl.BindTexture(GL_TEXTURE_2D, g->repl_tex);
|
|
}
|
|
|
|
g->gl.ActiveTexture(GL_TEXTURE0);
|
|
g->gl.BindTexture(GL_TEXTURE_2D, g->vram_tex);
|
|
g->gl.Enable(GL_SCISSOR_TEST);
|
|
|
|
/* See gl_bind_rt(): in mask mode the shader blends and the blend unit must stay off. The
|
|
ranges still carry their ADD/SUB split — the shader reproduces the same arithmetic from
|
|
`transp` and `mode`, and the stp_pass split that goes with it is still what decides
|
|
which fragments each range keeps. */
|
|
if (g->mask_mode) {
|
|
g->gl.Disable(GL_BLEND);
|
|
} else {
|
|
g->gl.Enable(GL_BLEND);
|
|
g->gl.BlendFunc(GL_ONE, GL_SRC_ALPHA);
|
|
}
|
|
|
|
for (i = 0; i < g->range_count; i++) {
|
|
const gl_range_t* r = &g->ranges[i];
|
|
|
|
/* An inverted drawing area (draw_x2 < draw_x1) is legal for the game to set and
|
|
draws nothing. glScissor would reject the negative width with GL_INVALID_VALUE,
|
|
which end_frame() would read as a broken pipeline. */
|
|
if ((r->count <= 0) || (r->sw <= 0) || (r->sh <= 0))
|
|
continue;
|
|
|
|
g->gl.Scissor(r->sx * g->scale, r->sy * g->scale,
|
|
r->sw * g->scale, r->sh * g->scale);
|
|
|
|
if (!g->mask_mode)
|
|
g->gl.BlendEquation(r->blend == GL_RANGE_SUB ? GL_FUNC_REVERSE_SUBTRACT
|
|
: GL_FUNC_ADD);
|
|
|
|
g->gl.Uniform1i(g->u_draw_stp_pass, r->stp_pass);
|
|
g->gl.DrawArrays(GL_TRIANGLES, r->first, r->count);
|
|
g->stat_draws++;
|
|
}
|
|
|
|
g->gl.BlendEquation(GL_FUNC_ADD);
|
|
g->stat_ranges += (uint32_t)g->range_count;
|
|
|
|
g->vert_count = 0;
|
|
g->range_count = 0;
|
|
tiles_clear(&g->sampled);
|
|
g->any_sampled = 0;
|
|
}
|
|
|
|
/* ---- texture replacement atlas (psx/texrep.h) ------------------------------------------- */
|
|
|
|
/* Created on FIRST USE, not in create(): a session with the feature off never pays for it,
|
|
and the size is a plain 2048 square (16 MiB) capped by whatever the driver will give us.
|
|
2048 holds 1024 native 64x64 textures at 1x, or 64 of them at 4x; when it runs out the
|
|
packer resets, which costs draw calls and not correctness. */
|
|
static int gl_repl_ready(hw_gl_t* g) {
|
|
GLint max_dim = 0;
|
|
int dim = 2048;
|
|
|
|
if (g->repl_dim)
|
|
return 1;
|
|
|
|
if (!g->gl.GenTextures || !g->gl.TexSubImage2D)
|
|
return 0;
|
|
|
|
g->gl.GetIntegerv(GL_MAX_TEXTURE_SIZE, &max_dim);
|
|
|
|
if (max_dim > 0 && max_dim < dim)
|
|
dim = (int)max_dim;
|
|
|
|
if (dim < 512)
|
|
return 0;
|
|
|
|
g->gl.GenTextures(1, &g->repl_tex);
|
|
|
|
if (!g->repl_tex)
|
|
return 0;
|
|
|
|
g->gl.ActiveTexture(GL_TEXTURE1);
|
|
g->gl.BindTexture(GL_TEXTURE_2D, g->repl_tex);
|
|
/* NEAREST/CLAMP for tidiness only — every read is a texelFetch, which ignores both. */
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
g->gl.TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, dim, dim, 0, GL_RGBA,
|
|
GL_UNSIGNED_BYTE, NULL);
|
|
g->gl.ActiveTexture(GL_TEXTURE0);
|
|
g->gl.BindTexture(GL_TEXTURE_2D, g->vram_tex);
|
|
|
|
g->repl_dim = dim;
|
|
g->repl_gen = 1;
|
|
psxe_diag_logf("hwgl", "texture replacement atlas %dx%d", dim, dim);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* Shelf packer. One pass, then a flush-and-reset, then one more pass — an image that does not
|
|
fit an EMPTY atlas can never fit, so two attempts is the whole loop. */
|
|
static int gl_repl_pack(hw_gl_t* g, psx_texrep_image_t* im) {
|
|
const int w = (int)im->pw;
|
|
const int h = (int)im->ph;
|
|
int attempt;
|
|
|
|
if (w > g->repl_dim || h > g->repl_dim) {
|
|
if (!g->repl_full_logged) {
|
|
g->repl_full_logged = 1;
|
|
psxe_diag_logf("hwgl", "replacement %dx%d does not fit a %d atlas; skipped",
|
|
w, h, g->repl_dim);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
for (attempt = 0; attempt < 2; attempt++) {
|
|
if ((g->repl_x + w) > g->repl_dim) {
|
|
g->repl_x = 0;
|
|
g->repl_y += g->repl_shelf;
|
|
g->repl_shelf = 0;
|
|
}
|
|
|
|
if ((g->repl_y + h) <= g->repl_dim) {
|
|
g->gl.ActiveTexture(GL_TEXTURE1);
|
|
g->gl.BindTexture(GL_TEXTURE_2D, g->repl_tex);
|
|
g->gl.TexSubImage2D(GL_TEXTURE_2D, 0, g->repl_x, g->repl_y, w, h,
|
|
GL_RGBA, GL_UNSIGNED_BYTE, im->rgba);
|
|
g->gl.ActiveTexture(GL_TEXTURE0);
|
|
g->gl.BindTexture(GL_TEXTURE_2D, g->vram_tex);
|
|
|
|
im->be_x = (uint16_t)g->repl_x;
|
|
im->be_y = (uint16_t)g->repl_y;
|
|
im->be_resident = 1;
|
|
im->be_epoch = g->repl_gen;
|
|
|
|
g->repl_x += w;
|
|
|
|
if (h > g->repl_shelf)
|
|
g->repl_shelf = h;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* Full. The pending batch is the only thing that still needs what is in the atlas,
|
|
so draw it and start over. Everything stamped with the old generation is thereby
|
|
evicted without touching it. */
|
|
gl_flush(g);
|
|
g->repl_gen++;
|
|
g->repl_x = 0;
|
|
g->repl_y = 0;
|
|
g->repl_shelf = 0;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
Resolves gpu->texrep_bind into the packed a_repl attribute for the primitive about to be
|
|
emitted. Called at the top of every draw hook, so `cur_repl` is authoritative and stale
|
|
values from an earlier primitive cannot leak into an untextured one.
|
|
|
|
Zero-cost while the feature is off: texrep_bind.img is NULL, and this returns after one
|
|
branch without touching GL.
|
|
*/
|
|
static void gl_repl_resolve(hw_gl_t* g, psx_gpu_t* gpu) {
|
|
const psx_texrep_image_t* im = gpu->texrep_bind.img;
|
|
psx_texrep_image_t* mut;
|
|
uint32_t epoch;
|
|
|
|
g->cur_repl[0] = 0;
|
|
g->cur_repl[1] = 0;
|
|
g->cur_repl[2] = 0;
|
|
g->cur_repl[3] = 0;
|
|
|
|
if (!im)
|
|
return;
|
|
|
|
if (!gl_repl_ready(g))
|
|
return;
|
|
|
|
epoch = psx_texrep_epoch(gpu);
|
|
|
|
if (epoch != g->repl_epoch) {
|
|
/* The pack was rebuilt (settings change): every be_* stamp now describes an atlas
|
|
layout that no longer applies. */
|
|
g->repl_epoch = epoch;
|
|
g->repl_gen++;
|
|
g->repl_x = 0;
|
|
g->repl_y = 0;
|
|
g->repl_shelf = 0;
|
|
}
|
|
|
|
/* be_x/be_y/be_resident/be_epoch are declared BACKEND-OWNED in psx/texrep.h; the module
|
|
zeroes them at load and never reads them. The bind hands out a const pointer because
|
|
nothing else may write the image, so the cast is the ownership statement. */
|
|
mut = (psx_texrep_image_t*)im;
|
|
|
|
if (!(mut->be_resident && mut->be_epoch == g->repl_gen)) {
|
|
if (!gl_repl_pack(g, mut))
|
|
return;
|
|
}
|
|
|
|
g->cur_repl[0] = (uint32_t)mut->be_x | ((uint32_t)mut->be_y << 16);
|
|
g->cur_repl[1] = (uint32_t)gpu->texrep_bind.u0 |
|
|
((uint32_t)gpu->texrep_bind.v0 << 8) |
|
|
((uint32_t)mut->scale << 16);
|
|
g->cur_repl[2] = (uint32_t)(mut->nw - 1u) | ((uint32_t)(mut->nh - 1u) << 16);
|
|
g->cur_repl[3] = 0;
|
|
}
|
|
|
|
/* Region of VRAM a textured primitive reads: the whole texture page plus the CLUT row.
|
|
Deliberately coarse — narrowing it to the primitive's UV box is a later optimisation and
|
|
being too WIDE only costs an unnecessary sync, never correctness. */
|
|
static void gl_note_sample(hw_gl_t* g, uint16_t texp, uint16_t clut) {
|
|
int tpx = (texp & 0xf) << 6;
|
|
int tpy = (texp & 0x10) << 4;
|
|
int depth = (texp >> 7) & 3;
|
|
int clutx = (clut & 0x3f) << 4;
|
|
int cluty = (clut >> 6) & 0x1ff;
|
|
int words = (depth == 0) ? 64 : ((depth == 1) ? 128 : 256);
|
|
|
|
if (g->dbg_geom && (g->frames == 3500) && (g->geom_left > 0)) {
|
|
g->geom_left--;
|
|
psxe_diag_logf("hwgl",
|
|
"geom: page=(%d,%d %dx256) depth=%d clut=(%d,%d) draw=(%u,%u)-(%u,%u) "
|
|
"disp=(%u,%u) cpudirty=%d gpudirty=%d",
|
|
tpx, tpy, words, depth, clutx, cluty,
|
|
g->gpu->draw_x1, g->gpu->draw_y1, g->gpu->draw_x2, g->gpu->draw_y2,
|
|
g->gpu->disp_x, g->gpu->disp_y,
|
|
tiles_intersects(&g->dirty, tpx, tpy, words, 256),
|
|
tiles_intersects(&g->gpu_dirty, tpx, tpy, words, 256));
|
|
}
|
|
|
|
if (tiles_intersects(&g->dirty, tpx, tpy, words, 256)) {
|
|
gl_flush(g);
|
|
gl_sync_vram(g, tpx, tpy, words, 256);
|
|
}
|
|
|
|
/* row 1, and the ORDER is load-bearing: the host upload above may have rewritten
|
|
whole 16x16 tiles that the rasterizer also drew into, so the render target's copy has
|
|
to land afterwards to win. gl_resolve_gpu_tiles() clears the CPU-dirty bit for every
|
|
tile it serves, which is what stops the next upload from undoing it again. */
|
|
if (g->any_gpu_dirty && tiles_intersects(&g->gpu_dirty, tpx, tpy, words, 256)) {
|
|
gl_flush(g);
|
|
gl_resolve_gpu_tiles(g, tpx, tpy, words, 256);
|
|
}
|
|
|
|
if (depth < 2) {
|
|
if (tiles_intersects(&g->dirty, clutx, cluty, 256, 1)) {
|
|
gl_flush(g);
|
|
gl_sync_vram(g, clutx, cluty, 256, 1);
|
|
}
|
|
|
|
if (g->any_gpu_dirty && tiles_intersects(&g->gpu_dirty, clutx, cluty, 256, 1)) {
|
|
gl_flush(g);
|
|
gl_resolve_gpu_tiles(g, clutx, cluty, 256, 1);
|
|
}
|
|
|
|
tiles_mark(&g->sampled, clutx, cluty, 256, 1);
|
|
}
|
|
|
|
tiles_mark(&g->sampled, tpx, tpy, words, 256);
|
|
g->any_sampled = 1;
|
|
}
|
|
|
|
/* ---- primitive emission ---------------------------------------------------------------- */
|
|
|
|
static void gl_fill_common(gl_vertex_t* v, const float tri[6], const float triw[3],
|
|
const uint32_t col[3],
|
|
const uint16_t uv[6], uint16_t texp, uint16_t clut,
|
|
uint16_t flags, uint16_t mode, const psx_gpu_t* gpu) {
|
|
v->tri0[0] = tri[0]; v->tri0[1] = tri[1];
|
|
v->tri0[2] = tri[2]; v->tri0[3] = tri[3];
|
|
v->tri1[0] = tri[4]; v->tri1[1] = tri[5];
|
|
v->triw[0] = triw[0]; v->triw[1] = triw[1]; v->triw[2] = triw[2];
|
|
|
|
v->col0[0] = (uint8_t)(col[0] & 0xff);
|
|
v->col0[1] = (uint8_t)((col[0] >> 8) & 0xff);
|
|
v->col0[2] = (uint8_t)((col[0] >> 16) & 0xff);
|
|
v->col0[3] = 0;
|
|
v->col1[0] = (uint8_t)(col[1] & 0xff);
|
|
v->col1[1] = (uint8_t)((col[1] >> 8) & 0xff);
|
|
v->col1[2] = (uint8_t)((col[1] >> 16) & 0xff);
|
|
v->col1[3] = 0;
|
|
v->col2[0] = (uint8_t)(col[2] & 0xff);
|
|
v->col2[1] = (uint8_t)((col[2] >> 8) & 0xff);
|
|
v->col2[2] = (uint8_t)((col[2] >> 16) & 0xff);
|
|
v->col2[3] = 0;
|
|
|
|
v->uv01[0] = uv[0]; v->uv01[1] = uv[1];
|
|
v->uv01[2] = uv[2]; v->uv01[3] = uv[3];
|
|
v->uv2[0] = uv[4]; v->uv2[1] = uv[5];
|
|
v->uv2[2] = texp; v->uv2[3] = clut;
|
|
|
|
v->misc[0] = flags;
|
|
v->misc[1] = mode;
|
|
v->misc[2] = 0;
|
|
v->misc[3] = 0;
|
|
|
|
v->texwin[0] = (uint8_t)gpu->texw_mx;
|
|
v->texwin[1] = (uint8_t)gpu->texw_my;
|
|
v->texwin[2] = (uint8_t)gpu->texw_ox;
|
|
v->texwin[3] = (uint8_t)gpu->texw_oy;
|
|
}
|
|
|
|
/*
|
|
Emits one triangle, split across as many draw ranges as the blend state needs. `tri` is
|
|
already winding-corrected and offset-applied, in the software's (a, b, c) order.
|
|
|
|
THE GEOMETRY SUBMITTED IS THE BOUNDING BOX, NOT THE TRIANGLE. This is not an
|
|
optimisation gone wrong; it is the only construction under which 1x can be
|
|
pixel-identical, and submitting the triangle itself is what made it not be.
|
|
|
|
The hardware rasterizer decides coverage by testing the FRAGMENT CENTRE (x+0.5, y+0.5)
|
|
against the submitted primitive. gpu_render_triangle tests the INTEGER CORNER (x, y)
|
|
(gpu.c:331-332). Those two tests do not select the same pixels, and the shader cannot
|
|
repair the difference: it can discard a fragment the hardware produced, but it can
|
|
never invent one the hardware declined to produce. Every pixel whose corner is inside
|
|
the triangle while its centre is outside is a pixel the software rasterizer writes and
|
|
the GPU silently leaves alone — which is precisely the `blank` bucket (a pixel nothing
|
|
ever drew) and the `far` bucket (a pixel where an older primitive's colour survives),
|
|
64% and 32% of the 1x mismatches measured in the backend.
|
|
|
|
Shifting the triangle by half a pixel would line the two sample grids up, but it makes
|
|
correctness depend on GL's fill rule agreeing with gpu.c's TL() macro at samples that
|
|
land exactly on an edge — and it does not: this vertex shader maps native y=0 to the
|
|
BOTTOM of the framebuffer, so GL's "top" edge is gpu.c's bottom one and the two rules
|
|
are mirrored on every horizontal edge.
|
|
|
|
Rasterizing the bounding box removes the hardware's fill rule from the answer entirely.
|
|
The box spans native [xmin, xmax) x [ymin, ymax), so at scale S the fragments generated
|
|
are exactly the render-target pixels the software's (and gpu_hw_rt.c's) loop visits,
|
|
and the shader's own bounding-box test, three edge functions and TL rule decide every
|
|
pixel — which is what the file header already claimed and what the code did not do.
|
|
|
|
The cost is fragments discarded outside the triangle, ~2x the triangle's own area. The
|
|
measurements say that is affordable: 1x and 2x both ran at ~153% uncapped with
|
|
emu ~10.8 ms, i.e. quadrupling the fragment count cost nothing measurable, so doubling
|
|
it at 1x costs nothing either. Narrowing this back down later means offsetting the
|
|
three edge lines outward and intersecting them, which is exact for fat triangles and
|
|
numerically nasty for slivers; it is an optimisation, not a fix, and it is not needed
|
|
to ship.
|
|
*/
|
|
static void gl_emit_tri(hw_gl_t* g, const float tri[6], const float triw[3],
|
|
const uint32_t col[3],
|
|
const uint16_t uv[6], uint16_t texp, uint16_t clut,
|
|
uint16_t flags, uint16_t mode) {
|
|
const psx_gpu_t* gpu = g->gpu;
|
|
float lo_x, lo_y, hi_x, hi_y;
|
|
int passes[2];
|
|
int blends[2];
|
|
int npass = 1;
|
|
int p;
|
|
|
|
lo_x = hi_x = tri[0];
|
|
lo_y = hi_y = tri[1];
|
|
|
|
for (p = 1; p < 3; p++) {
|
|
if (tri[p * 2 + 0] < lo_x) lo_x = tri[p * 2 + 0];
|
|
if (tri[p * 2 + 0] > hi_x) hi_x = tri[p * 2 + 0];
|
|
if (tri[p * 2 + 1] < lo_y) lo_y = tri[p * 2 + 1];
|
|
if (tri[p * 2 + 1] > hi_y) hi_y = tri[p * 2 + 1];
|
|
}
|
|
|
|
/* Half-open in both axes, so a zero-width or zero-height box draws nothing — exactly
|
|
as gpu.c's `for (x = xmin; x < xmax; x++)` does. */
|
|
if ((hi_x <= lo_x) || (hi_y <= lo_y))
|
|
return;
|
|
|
|
/* Modes 0, 1 and 3 and every opaque primitive share one blend state, so they never
|
|
break a batch. Only mode 2's reverse-subtract needs its own range, and only a
|
|
TEXTURED mode-2 primitive needs the two-pass split, because there the per-texel STP
|
|
bit means both kinds of fragment live in the same primitive. */
|
|
if ((flags & GLF_TRANSP) && (mode == 2)) {
|
|
if (flags & GLF_TEXTURED) {
|
|
npass = 2;
|
|
passes[0] = 1; blends[0] = GL_RANGE_ADD;
|
|
passes[1] = 2; blends[1] = GL_RANGE_SUB;
|
|
} else {
|
|
passes[0] = 0; blends[0] = GL_RANGE_SUB;
|
|
}
|
|
} else {
|
|
passes[0] = 0; blends[0] = GL_RANGE_ADD;
|
|
}
|
|
|
|
for (p = 0; p < npass; p++) {
|
|
gl_range_t* r;
|
|
gl_vertex_t* v;
|
|
static const int kx[6] = {0, 1, 1, 0, 1, 0};
|
|
static const int ky[6] = {0, 0, 1, 0, 1, 1};
|
|
const int nvert = g->dbg_tri_bbox ? 6 : 3;
|
|
int i;
|
|
|
|
if (!gl_reserve(g, nvert))
|
|
return;
|
|
|
|
r = gl_range_for(g, blends[p], passes[p]);
|
|
|
|
if (!r)
|
|
return;
|
|
|
|
v = &g->verts[g->vert_count];
|
|
|
|
/* The real triangle still travels in tri0/tri1 as flat attributes, so the fragment
|
|
shader is unchanged; only the geometry that generates the fragments differs. */
|
|
for (i = 0; i < nvert; i++) {
|
|
gl_fill_common(&v[i], tri, triw, col, uv, texp, clut, flags, mode, gpu);
|
|
v[i].repl[0] = g->cur_repl[0];
|
|
v[i].repl[1] = g->cur_repl[1];
|
|
v[i].repl[2] = g->cur_repl[2];
|
|
v[i].repl[3] = g->cur_repl[3];
|
|
|
|
if (g->dbg_tri_bbox) {
|
|
v[i].pos[0] = kx[i] ? hi_x : lo_x;
|
|
v[i].pos[1] = ky[i] ? hi_y : lo_y;
|
|
} else {
|
|
v[i].pos[0] = tri[i * 2 + 0];
|
|
v[i].pos[1] = tri[i * 2 + 1];
|
|
}
|
|
}
|
|
|
|
g->vert_count += nvert;
|
|
r->count += nvert;
|
|
}
|
|
|
|
g->stat_prims++;
|
|
}
|
|
|
|
/* Axis-aligned quad in native coordinates, covering [x0, x1) x [y0, y1). Sprites and the
|
|
per-pixel quads a Bresenham line expands into both come through here. */
|
|
static void gl_emit_quad(hw_gl_t* g, int x0, int y0, int x1, int y1,
|
|
uint32_t color, uint16_t u0, uint16_t v0_,
|
|
uint16_t texp, uint16_t clut, uint16_t flags, uint16_t mode) {
|
|
/* Sprites, fills and line-expansion quads are integer by definition. */
|
|
static const float kNoW[3] = {1.0f, 1.0f, 1.0f};
|
|
float tri[6];
|
|
uint32_t col[3];
|
|
uint16_t uv[6];
|
|
const psx_gpu_t* gpu = g->gpu;
|
|
int passes[2];
|
|
int blends[2];
|
|
int npass = 1;
|
|
int p;
|
|
|
|
if ((x1 <= x0) || (y1 <= y0))
|
|
return;
|
|
|
|
/* tri0.xy carries the box origin, which the sprite path in the fragment shader uses as
|
|
the UV reference point; tri0.zw / tri1 are unused there. */
|
|
tri[0] = (float)x0; tri[1] = (float)y0;
|
|
tri[2] = (float)x1; tri[3] = (float)y1;
|
|
tri[4] = 0.0f; tri[5] = 0.0f;
|
|
|
|
col[0] = color; col[1] = color; col[2] = color;
|
|
uv[0] = u0; uv[1] = v0_; uv[2] = 0; uv[3] = 0; uv[4] = 0; uv[5] = 0;
|
|
|
|
flags |= GLF_SPRITE;
|
|
|
|
if ((flags & GLF_TRANSP) && (mode == 2)) {
|
|
if (flags & GLF_TEXTURED) {
|
|
npass = 2;
|
|
passes[0] = 1; blends[0] = GL_RANGE_ADD;
|
|
passes[1] = 2; blends[1] = GL_RANGE_SUB;
|
|
} else {
|
|
passes[0] = 0; blends[0] = GL_RANGE_SUB;
|
|
}
|
|
} else {
|
|
passes[0] = 0; blends[0] = GL_RANGE_ADD;
|
|
}
|
|
|
|
for (p = 0; p < npass; p++) {
|
|
gl_range_t* r;
|
|
gl_vertex_t* v;
|
|
static const int kx[6] = {0, 1, 1, 0, 1, 0};
|
|
static const int ky[6] = {0, 0, 1, 0, 1, 1};
|
|
int i;
|
|
|
|
if (!gl_reserve(g, 6))
|
|
return;
|
|
|
|
r = gl_range_for(g, blends[p], passes[p]);
|
|
|
|
if (!r)
|
|
return;
|
|
|
|
v = &g->verts[g->vert_count];
|
|
|
|
for (i = 0; i < 6; i++) {
|
|
gl_fill_common(&v[i], tri, kNoW, col, uv, texp, clut, flags, mode, gpu);
|
|
v[i].repl[0] = g->cur_repl[0];
|
|
v[i].repl[1] = g->cur_repl[1];
|
|
v[i].repl[2] = g->cur_repl[2];
|
|
v[i].repl[3] = g->cur_repl[3];
|
|
v[i].pos[0] = (float)(kx[i] ? x1 : x0);
|
|
v[i].pos[1] = (float)(ky[i] ? y1 : y0);
|
|
}
|
|
|
|
g->vert_count += 6;
|
|
r->count += 6;
|
|
}
|
|
|
|
g->stat_prims++;
|
|
}
|
|
|
|
/* ---- drawing hooks ---------------------------------------------------------------------- */
|
|
|
|
static int gl_min3(int a, int b, int c) { int m = (a <= b) ? a : b; return (m <= c) ? m : c; }
|
|
static int gl_max3(int a, int b, int c) { int m = (a > b) ? a : b; return (m > c) ? m : c; }
|
|
|
|
#define GL_EDGE(ax, ay, bx, by, cx, cy) \
|
|
(((bx) - (ax)) * ((cy) - (ay)) - ((by) - (ay)) * ((cx) - (ax)))
|
|
|
|
static void gl_triangle(hw_gl_t* g, psx_gpu_t* gpu, const poly_data_t* poly,
|
|
vertex_t v0, vertex_t v1, vertex_t v2) {
|
|
vertex_t a, b, c;
|
|
float tri[6];
|
|
float triw[3];
|
|
int pgxp;
|
|
uint32_t col[3];
|
|
uint16_t uv[6];
|
|
uint16_t flags = 0;
|
|
uint16_t mode;
|
|
int xmin, ymin, xmax, ymax;
|
|
|
|
a = v0;
|
|
|
|
/* PGXP gate, decided BEFORE the winding swap so the swap itself can honour it.
|
|
|
|
All-or-nothing per triangle: mixing one precise vertex with two integer ones would crack
|
|
the shared edges sealed. On top of precise_valid (core-validated source word,
|
|
psx/pgxp.c), each precise coordinate must agree with its own integer truncation within
|
|
one pixel — a stale cache attach shows up as a vertex teleporting somewhere plausible,
|
|
and the tolerance is what turns "bizarre geometry" into "falls back to integer".
|
|
pw > 0 guards the shader's 1/w divisions. */
|
|
{
|
|
const float tol = 1.0f;
|
|
|
|
pgxp = v0.precise_valid && v1.precise_valid && v2.precise_valid &&
|
|
(v0.pw > 0.0f) && (v1.pw > 0.0f) && (v2.pw > 0.0f) &&
|
|
(fabsf(v0.px - (float)v0.x) <= tol) && (fabsf(v0.py - (float)v0.y) <= tol) &&
|
|
(fabsf(v1.px - (float)v1.x) <= tol) && (fabsf(v1.py - (float)v1.y) <= tol) &&
|
|
(fabsf(v2.px - (float)v2.x) <= tol) && (fabsf(v2.py - (float)v2.y) <= tol);
|
|
}
|
|
|
|
/* gpu.c:289-295 — enforce positive area, then apply the drawing offset.
|
|
|
|
For a PRECISE triangle the swap must follow the precise edge sign, not the integer one:
|
|
near-degenerate slivers can flip sign between the two, and a triangle whose float area
|
|
is negative fails every top-left test in the shader — it does not distort, it VANISHES
|
|
(missing face polys on the Crash title screen, found the hard way). */
|
|
if (pgxp) {
|
|
const float pe = (v1.px - v0.px) * (v2.py - v0.py) -
|
|
(v1.py - v0.py) * (v2.px - v0.px);
|
|
|
|
if (pe < 0.0f) {
|
|
b = v2;
|
|
c = v1;
|
|
} else {
|
|
b = v1;
|
|
c = v2;
|
|
}
|
|
} else if (GL_EDGE(v0.x, v0.y, v1.x, v1.y, v2.x, v2.y) < 0) {
|
|
b = v2;
|
|
c = v1;
|
|
} else {
|
|
b = v1;
|
|
c = v2;
|
|
}
|
|
|
|
a.x += (int16_t)gpu->off_x; b.x += (int16_t)gpu->off_x; c.x += (int16_t)gpu->off_x;
|
|
a.y += (int16_t)gpu->off_y; b.y += (int16_t)gpu->off_y; c.y += (int16_t)gpu->off_y;
|
|
|
|
|
|
xmin = gl_min3(a.x, b.x, c.x);
|
|
ymin = gl_min3(a.y, b.y, c.y);
|
|
xmax = gl_max3(a.x, b.x, c.x);
|
|
ymax = gl_max3(a.y, b.y, c.y);
|
|
|
|
/* Hardware rejects at 1023x511; this core historically rejected at 2048x1024, twice as
|
|
permissive in both axes. psx_gpu_prim_oversize() holds the rule for all three
|
|
rasterizers and picks between the two on PSX_GPU_ACCURACY_PRIM_SIZE. */
|
|
if (psx_gpu_prim_oversize(gpu, xmax - xmin, ymax - ymin))
|
|
return;
|
|
|
|
if (pgxp) {
|
|
/* A precise triangle can poke up to a pixel outside its integer bounding box; widen
|
|
what gets flushed and marked so the tile bookkeeping can never miss it. The size
|
|
REJECT above stays on the integer box on purpose — same accept/reject decision as
|
|
the software shadow. */
|
|
xmin -= 1; ymin -= 1; xmax += 1; ymax += 1;
|
|
}
|
|
|
|
if (poly->attrib & PA_RAW) flags |= GLF_RAW;
|
|
if (poly->attrib & PA_TRANSP) flags |= GLF_TRANSP;
|
|
if (poly->attrib & PA_TEXTURED) flags |= GLF_TEXTURED;
|
|
if (poly->attrib & PA_SHADED) flags |= GLF_SHADED;
|
|
if (psx_gpu_dither_enabled(gpu)) flags |= GLF_DITHER;
|
|
/* GP0(E6), from the SAME two helpers gpu.c:1151 and gpu_hw_rt.c:139 resolve it with, so
|
|
the three rasterizers cannot disagree about what the game asked for. Per primitive
|
|
because that is what they are; carried per vertex because a uniform would break the
|
|
batch at every GP0(E6) (see GLF_MASK_CHECK). */
|
|
if (psx_gpu_mask_check(gpu)) flags |= GLF_MASK_CHECK;
|
|
if (psx_gpu_mask_set(gpu)) flags |= GLF_MASK_SET;
|
|
|
|
mode = (poly->attrib & PA_TEXTURED) ? (uint16_t)((poly->texp >> 5) & 3)
|
|
: (uint16_t)((gpu->gpustat >> 5) & 3);
|
|
|
|
/* Order matters, in two directions.
|
|
|
|
The flush has to come FIRST: it clears the sampled map, so a sample noted before it
|
|
is thrown away and a later write into that texture would not force a resync.
|
|
|
|
The dirty MARK has to come LAST, after the emit, and that is why the two halves are
|
|
separated. Marking the destination before gl_note_sample() lets a primitive that
|
|
samples a page overlapping its own destination hit dirty ∩ page, sync that region
|
|
out of host VRAM — which does not yet contain this primitive, the software shadow
|
|
runs after this hook returns (gpu.c:1213-1225) — and CLEAR the tile as a side
|
|
effect. Nothing re-marks it, so every later sample of that page reads stale texels
|
|
until something else happens to write there. */
|
|
gl_write_flush(g, xmin, ymin, xmax - xmin, ymax - ymin);
|
|
|
|
if (!g->dbg_defer_dirty)
|
|
gl_mark_drawn(g, xmin, ymin, xmax - xmin, ymax - ymin);
|
|
|
|
if (poly->attrib & PA_TEXTURED)
|
|
gl_note_sample(g, poly->texp, poly->clut);
|
|
|
|
if (pgxp) {
|
|
/* Precise coords are pre-offset SXY space; apply the drawing offset exactly as the
|
|
integer path did above. */
|
|
const float offx = (float)(int16_t)gpu->off_x;
|
|
const float offy = (float)(int16_t)gpu->off_y;
|
|
|
|
tri[0] = a.px + offx; tri[1] = a.py + offy;
|
|
tri[2] = b.px + offx; tri[3] = b.py + offy;
|
|
tri[4] = c.px + offx; tri[5] = c.py + offy;
|
|
/* Texture correction only when the w's are mutually sane. One outlier w smears the
|
|
whole triangle's texture (the perspective divide amplifies it); positions stay
|
|
precise either way, UV just falls back to the exact affine path. 32x within one
|
|
triangle is far beyond anything a legitimate PS1 scene produces. */
|
|
{
|
|
float wmin = a.pw, wmax = a.pw;
|
|
|
|
if (b.pw < wmin) wmin = b.pw;
|
|
if (b.pw > wmax) wmax = b.pw;
|
|
if (c.pw < wmin) wmin = c.pw;
|
|
if (c.pw > wmax) wmax = c.pw;
|
|
|
|
if (wmax > wmin * 32.0f) {
|
|
triw[0] = 1.0f; triw[1] = 1.0f; triw[2] = 1.0f;
|
|
} else {
|
|
triw[0] = a.pw; triw[1] = b.pw; triw[2] = c.pw;
|
|
}
|
|
}
|
|
} else {
|
|
tri[0] = (float)a.x; tri[1] = (float)a.y;
|
|
tri[2] = (float)b.x; tri[3] = (float)b.y;
|
|
tri[4] = (float)c.x; tri[5] = (float)c.y;
|
|
triw[0] = 1.0f; triw[1] = 1.0f; triw[2] = 1.0f;
|
|
}
|
|
|
|
/* Non-shaded primitives modulate with data.v[0].c (gpu.c:379), which is always the
|
|
ORIGINAL v0 — and a == v0 because the winding swap only exchanges v1 and v2. */
|
|
col[0] = a.c; col[1] = b.c; col[2] = c.c;
|
|
uv[0] = a.tx; uv[1] = a.ty;
|
|
uv[2] = b.tx; uv[3] = b.ty;
|
|
uv[4] = c.tx; uv[5] = c.ty;
|
|
|
|
gl_emit_tri(g, tri, triw, col, uv, poly->texp, poly->clut, flags, mode);
|
|
|
|
if (g->dbg_defer_dirty)
|
|
gl_mark_drawn(g, xmin, ymin, xmax - xmin, ymax - ymin);
|
|
}
|
|
|
|
/*
|
|
[video] line_detect — games draw thin lines as flat polygons, and a polygon with ZERO
|
|
extent in one axis draws nothing at all: gpu.c's `for (y = ymin; y < ymax; y++)` is
|
|
half-open, and gl_emit_tri() bails on `hi_y <= lo_y` to match it. That is the polygon
|
|
that "disappears", and rescuing it is what this setting is for.
|
|
|
|
What it does NOT do is change the coverage RULE. contract is that coverage is
|
|
decided once per native pixel from the triangle's own edges; this only moves the
|
|
triangle's vertices before that, exactly as a game moving them itself would. Coverage
|
|
stays watertight and stays equal to the software rasterizer's for the same geometry —
|
|
the geometry is simply no longer degenerate.
|
|
|
|
Which vertices move: the ones already at the far edge of the thin axis. When the extent
|
|
is zero every vertex is at that edge, so instead the SECOND HALF of the strip moves —
|
|
PS1 quads arrive in strip order (v0,v1 one edge, v2,v3 the other), so moving v2/v3 turns
|
|
a zero-height quad into a one-pixel-tall rectangle whose two triangles tile it exactly.
|
|
A triangle moves v1/v2, which recovers a wedge rather than a full line: strictly better
|
|
than vanishing, and honestly less than a real line renderer would do.
|
|
|
|
quads — quads only, and only the vanishing (zero-extent) case. Conservative: it can
|
|
only ever add pixels that a real console showed and this backend did not.
|
|
basic — triangles too, and also polygons that are already exactly one pixel thin,
|
|
which widens them to two. That is the aggressive setting: on this backend a
|
|
1px poly does NOT vanish when upscaling (coverage is native-granular), so
|
|
`basic` deliberately makes such lines thicker rather than fixing a defect.
|
|
*/
|
|
static int gl_expand_thin_poly(poly_data_t* p, int nv, int mode) {
|
|
int i, xmin, xmax, ymin, ymax, thin_limit;
|
|
|
|
xmin = xmax = p->v[0].x;
|
|
ymin = ymax = p->v[0].y;
|
|
|
|
for (i = 1; i < nv; i++) {
|
|
if (p->v[i].x < xmin) xmin = p->v[i].x;
|
|
if (p->v[i].x > xmax) xmax = p->v[i].x;
|
|
if (p->v[i].y < ymin) ymin = p->v[i].y;
|
|
if (p->v[i].y > ymax) ymax = p->v[i].y;
|
|
}
|
|
|
|
thin_limit = (mode == 2) ? 1 : 0;
|
|
|
|
/* The long axis must actually be long: a 1x1 dot is not a line, and widening it would
|
|
double every point sprite a game draws as a degenerate poly. */
|
|
if (((ymax - ymin) <= thin_limit) && ((xmax - xmin) >= 2)) {
|
|
if (ymax == ymin) {
|
|
for (i = nv / 2; i < nv; i++)
|
|
p->v[i].y = (int16_t)(p->v[i].y + 1);
|
|
} else {
|
|
for (i = 0; i < nv; i++)
|
|
if (p->v[i].y == ymax)
|
|
p->v[i].y = (int16_t)(p->v[i].y + 1);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
if (((xmax - xmin) <= thin_limit) && ((ymax - ymin) >= 2)) {
|
|
if (xmax == xmin) {
|
|
for (i = nv / 2; i < nv; i++)
|
|
p->v[i].x = (int16_t)(p->v[i].x + 1);
|
|
} else {
|
|
for (i = 0; i < nv; i++)
|
|
if (p->v[i].x == xmax)
|
|
p->v[i].x = (int16_t)(p->v[i].x + 1);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void gl_draw_poly(psx_gpu_backend_t* be, psx_gpu_t* gpu, const poly_data_t* poly) {
|
|
hw_gl_t* g = gl_self(be);
|
|
poly_data_t adjusted;
|
|
|
|
if (g->failed)
|
|
return;
|
|
|
|
/* Keep the shader's blend rounding in step with psx_gpu_modulate_channel(). Re-read
|
|
per primitive rather than cached at create(): one predictable store, and it can never
|
|
be stale if the accuracy flags are set after the backend attaches. */
|
|
g_opt_tex_trunc = (psx_gpu_accuracy_flags(gpu) & PSX_GPU_ACCURACY_TEX_MODULATE) ? 1 : 0;
|
|
g_opt_mask_texel = psx_gpu_mask_from_texel(gpu) ? 1 : 0;
|
|
|
|
/* Texture replacement, decided by gpu_poly()/gpu_rect() BEFORE this hook ran, so the
|
|
shader and the two CPU rasterizers are answering the same question. Returns after one
|
|
branch when nothing is bound, which is always unless the feature is on. psx/texrep.h. */
|
|
gl_repl_resolve(g, gpu);
|
|
|
|
|
|
/* Off by default, and when off this is one compare against a global before the original
|
|
code runs untouched — the copy only happens for a polygon that actually qualifies. */
|
|
if (g_opt_line_detect &&
|
|
((poly->attrib & PA_QUAD) || (g_opt_line_detect == 2))) {
|
|
const int nv = (poly->attrib & PA_QUAD) ? 4 : 3;
|
|
|
|
adjusted = *poly;
|
|
|
|
if (gl_expand_thin_poly(&adjusted, nv, g_opt_line_detect)) {
|
|
/* Precise (PGXP) coordinates describe the ORIGINAL vertex; keeping them would
|
|
put gl_triangle's 1px attach tolerance in charge of undoing the expansion.
|
|
Dropping them makes the expanded polygon plainly integer, which is what it
|
|
now is. */
|
|
int i;
|
|
|
|
for (i = 0; i < 4; i++)
|
|
adjusted.v[i].precise_valid = 0;
|
|
|
|
poly = &adjusted;
|
|
}
|
|
}
|
|
|
|
if (poly->attrib & PA_QUAD) {
|
|
gl_triangle(g, gpu, poly, poly->v[0], poly->v[1], poly->v[2]);
|
|
gl_triangle(g, gpu, poly, poly->v[1], poly->v[2], poly->v[3]);
|
|
} else {
|
|
gl_triangle(g, gpu, poly, poly->v[0], poly->v[1], poly->v[2]);
|
|
}
|
|
}
|
|
|
|
#define GL_CLAMPI(v, d, u) (((v) <= (d)) ? (d) : (((v) >= (u)) ? (u) : (v)))
|
|
#define GL_SE10(v) ((int16_t)((v) << 5) >> 5)
|
|
|
|
static void gl_draw_rect(psx_gpu_backend_t* be, psx_gpu_t* gpu, const rect_data_t* in) {
|
|
hw_gl_t* g = gl_self(be);
|
|
rect_data_t data = *in;
|
|
uint16_t width = 0, height = 0;
|
|
uint16_t flags = 0;
|
|
uint16_t mode;
|
|
uint16_t texp;
|
|
int x0, y0, x1, y1;
|
|
|
|
if (g->failed)
|
|
return;
|
|
|
|
/* Keep the shader's blend rounding in step with psx_gpu_modulate_channel(). Re-read
|
|
per primitive rather than cached at create(): one predictable store, and it can never
|
|
be stale if the accuracy flags are set after the backend attaches. */
|
|
g_opt_tex_trunc = (psx_gpu_accuracy_flags(gpu) & PSX_GPU_ACCURACY_TEX_MODULATE) ? 1 : 0;
|
|
g_opt_mask_texel = psx_gpu_mask_from_texel(gpu) ? 1 : 0;
|
|
|
|
/* Texture replacement, decided by gpu_poly()/gpu_rect() BEFORE this hook ran, so the
|
|
shader and the two CPU rasterizers are answering the same question. Returns after one
|
|
branch when nothing is bound, which is always unless the feature is on. psx/texrep.h. */
|
|
gl_repl_resolve(g, gpu);
|
|
|
|
|
|
switch ((data.attrib >> 3) & 3) {
|
|
case RS_VARIABLE: width = data.width; height = data.height; break;
|
|
case RS_1X1: width = 1; height = 1; break;
|
|
case RS_8X8: width = 8; height = 8; break;
|
|
case RS_16X16: width = 16; height = 16; break;
|
|
}
|
|
|
|
/* gpu.c:498-510 — offset, re-sign-extend to 11 bits, then clamp the box to +-1024. The
|
|
double SE10 is a real behavioural detail, not redundancy. */
|
|
data.v0.x += (int16_t)gpu->off_x;
|
|
data.v0.y += (int16_t)gpu->off_y;
|
|
data.v0.x = GL_SE10(data.v0.x);
|
|
data.v0.y = GL_SE10(data.v0.y);
|
|
|
|
x1 = data.v0.x + width;
|
|
y1 = data.v0.y + height;
|
|
x1 = GL_CLAMPI(x1, -1024, 1024);
|
|
y1 = GL_CLAMPI(y1, -1024, 1024);
|
|
x0 = GL_CLAMPI((int)data.v0.x, -1024, 1024);
|
|
y0 = GL_CLAMPI((int)data.v0.y, -1024, 1024);
|
|
|
|
if (data.attrib & RA_RAW) flags |= GLF_RAW;
|
|
if (data.attrib & RA_TRANSP) flags |= GLF_TRANSP;
|
|
if (data.attrib & RA_TEXTURED) flags |= GLF_TEXTURED;
|
|
/* Sprites honour GP0(E6) exactly as polygons do — gpu.c:1387 and gpu_hw_rt.c:384 resolve
|
|
it from the same helpers in gpu_render_rect too. */
|
|
if (psx_gpu_mask_check(gpu)) flags |= GLF_MASK_CHECK;
|
|
if (psx_gpu_mask_set(gpu)) flags |= GLF_MASK_SET;
|
|
|
|
/* Sprites read the persistent texpage a previous textured POLYGON latched (gpu.c:536),
|
|
so it is rebuilt here into the same 16-bit word layout the shader decodes. */
|
|
texp = (uint16_t)(((gpu->texp_x >> 6) & 0xf) | ((gpu->texp_y >> 4) & 0x10) |
|
|
((gpu->texp_d & 3) << 7));
|
|
mode = (uint16_t)((gpu->gpustat >> 5) & 3);
|
|
|
|
/* Split for the same reason as gl_triangle() — the mark has to survive note_sample. */
|
|
gl_write_flush(g, x0, y0, x1 - x0, y1 - y0);
|
|
|
|
if (!g->dbg_defer_dirty)
|
|
gl_mark_drawn(g, x0, y0, x1 - x0, y1 - y0);
|
|
|
|
if (data.attrib & RA_TEXTURED)
|
|
gl_note_sample(g, texp, data.clut);
|
|
|
|
gl_emit_quad(g, x0, y0, x1, y1, data.v0.c, data.v0.tx, data.v0.ty,
|
|
texp, data.clut, flags, mode);
|
|
|
|
if (g->dbg_defer_dirty)
|
|
gl_mark_drawn(g, x0, y0, x1 - x0, y1 - y0);
|
|
}
|
|
|
|
/*
|
|
Lines. gpu_render_flat_line (gpu.c:704) is an integer Bresenham that writes raw BGR555
|
|
with no dithering, no blending and no modulation. Rather than approximate it with a
|
|
widened quad — which the backend admits is "genuinely fiddly" and only
|
|
approximately right — the same Bresenham runs here and emits one S x S block per plotted
|
|
pixel. That is exact by construction, and lines are rare enough (wireframe debug output
|
|
and a few racing HUDs) that the vertex cost does not matter.
|
|
*/
|
|
static void gl_plot(hw_gl_t* g, int x, int y, uint32_t color24) {
|
|
if ((x < 0) || (y < 0) || (x >= 1024) || (y >= 512))
|
|
return;
|
|
|
|
gl_emit_quad(g, x, y, x + 1, y + 1, color24, 0, 0, 0, 0, 0, 0);
|
|
}
|
|
|
|
static void gl_line_low(hw_gl_t* g, int x0, int y0, int x1, int y1, uint32_t color24) {
|
|
int dx = x1 - x0;
|
|
int dy = y1 - y0;
|
|
int yi = 1;
|
|
int d, y, x;
|
|
|
|
if (dy < 0) { yi = -1; dy = -dy; }
|
|
|
|
d = (2 * dy) - dx;
|
|
y = y0;
|
|
|
|
for (x = x0; x < x1; x++) {
|
|
gl_plot(g, x, y, color24);
|
|
|
|
if (d > 0) {
|
|
y += yi;
|
|
d += 2 * (dy - dx);
|
|
} else {
|
|
d += 2 * dy;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void gl_line_high(hw_gl_t* g, int x0, int y0, int x1, int y1, uint32_t color24) {
|
|
int dx = x1 - x0;
|
|
int dy = y1 - y0;
|
|
int xi = 1;
|
|
int d, x, y;
|
|
|
|
if (dx < 0) { xi = -1; dx = -dx; }
|
|
|
|
d = (2 * dx) - dy;
|
|
x = x0;
|
|
|
|
for (y = y0; y < y1; y++) {
|
|
gl_plot(g, x, y, color24);
|
|
|
|
if (d > 0) {
|
|
x += xi;
|
|
d += 2 * (dx - dy);
|
|
} else {
|
|
d += 2 * dx;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void gl_draw_line(psx_gpu_backend_t* be, psx_gpu_t* gpu,
|
|
const vertex_t* pv0, const vertex_t* pv1, uint16_t color_bgr555) {
|
|
hw_gl_t* g = gl_self(be);
|
|
int x0 = pv0->x + gpu->off_x;
|
|
int y0 = pv0->y + gpu->off_y;
|
|
int x1 = pv1->x + gpu->off_x;
|
|
int y1 = pv1->y + gpu->off_y;
|
|
/* The shader re-truncates to 5 bits, so the BGR555 the core already packed is expanded
|
|
back to the 0x00BBGGRR the vertex format carries. */
|
|
uint32_t color24 = (uint32_t)((color_bgr555 & 0x1f) << 3) |
|
|
(uint32_t)((((color_bgr555 >> 5) & 0x1f) << 3) << 8) |
|
|
(uint32_t)((((color_bgr555 >> 10) & 0x1f) << 3) << 16);
|
|
int lo_x, lo_y, hi_x, hi_y;
|
|
|
|
if (g->failed)
|
|
return;
|
|
|
|
/* Lines are never textured, so gpu_line() never binds a replacement; this only makes
|
|
sure the previous primitive's a_repl cannot leak into these vertices. After the
|
|
failed check, like the poly and rect hooks. */
|
|
gl_repl_resolve(g, gpu);
|
|
|
|
lo_x = x0 < x1 ? x0 : x1;
|
|
hi_x = x0 < x1 ? x1 : x0;
|
|
lo_y = y0 < y1 ? y0 : y1;
|
|
hi_y = y0 < y1 ? y1 : y0;
|
|
/* Not gl_write_hazard(): a LINE is a rasterizer write, so under gpu_own the render
|
|
target owns the result and the mark belongs in gpu_dirty. */
|
|
gl_write_flush(g, lo_x, lo_y, hi_x - lo_x + 1, hi_y - lo_y + 1);
|
|
gl_mark_drawn(g, lo_x, lo_y, hi_x - lo_x + 1, hi_y - lo_y + 1);
|
|
|
|
if (abs(y1 - y0) < abs(x1 - x0)) {
|
|
if (x0 > x1) gl_line_low(g, x1, y1, x0, y0, color24);
|
|
else gl_line_low(g, x0, y0, x1, y1, color24);
|
|
} else {
|
|
if (y0 > y1) gl_line_high(g, x1, y1, x0, y0, color24);
|
|
else gl_line_high(g, x0, y0, x1, y1, color24);
|
|
}
|
|
}
|
|
|
|
/* ---- VRAM transfers ---------------------------------------------------------------------- */
|
|
|
|
/* Defined below, next to the scanout resolve they share a shader with. */
|
|
static int gl_readback_rect(hw_gl_t* g, int x, int y, int w, int h,
|
|
uint16_t* dst, uint32_t dst_stride_px);
|
|
static void gl_vram_diff(hw_gl_t* g, const char* when);
|
|
|
|
static void gl_fill_vram(psx_gpu_backend_t* be, uint32_t x, uint32_t y,
|
|
uint32_t w, uint32_t h, uint16_t color) {
|
|
hw_gl_t* g = gl_self(be);
|
|
|
|
if (g->failed || !w || !h)
|
|
return;
|
|
|
|
gl_write_hazard(g, (int)x, (int)y, (int)w, (int)h);
|
|
gl_flush(g);
|
|
gl_bind_rt(g);
|
|
|
|
/* GP0(02) ignores the drawing area and the mask bit (gpu.c:1930-1935), so this is a
|
|
scissored clear rather than a scissored draw. */
|
|
g->gl.Enable(GL_SCISSOR_TEST);
|
|
g->gl.Scissor((GLint)x * g->scale, (GLint)y * g->scale,
|
|
(GLsizei)w * g->scale, (GLsizei)h * g->scale);
|
|
g->gl.ClearColor((GLfloat)((color & 0x1f) << 3) / 255.0f,
|
|
(GLfloat)(((color >> 5) & 0x1f) << 3) / 255.0f,
|
|
(GLfloat)(((color >> 10) & 0x1f) << 3) / 255.0f,
|
|
0.0f);
|
|
g->gl.Clear(GL_COLOR_BUFFER_BIT);
|
|
}
|
|
|
|
static int gl_ensure_scratch(hw_gl_t* g, int w, int h) {
|
|
if ((g->scratch_tex != 0) && (g->scratch_w >= w) && (g->scratch_h >= h))
|
|
return 1;
|
|
|
|
if (!g->scratch_tex) {
|
|
g->gl.GenTextures(1, &g->scratch_tex);
|
|
g->gl.GenFramebuffers(1, &g->scratch_fbo);
|
|
}
|
|
|
|
if (!g->scratch_tex || !g->scratch_fbo)
|
|
return 0;
|
|
|
|
g->scratch_w = w > g->scratch_w ? w : g->scratch_w;
|
|
g->scratch_h = h > g->scratch_h ? h : g->scratch_h;
|
|
|
|
g->gl.ActiveTexture(GL_TEXTURE0);
|
|
g->gl.BindTexture(GL_TEXTURE_2D, g->scratch_tex);
|
|
g->gl.TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, g->scratch_w, g->scratch_h, 0, GL_RGBA,
|
|
GL_UNSIGNED_BYTE, NULL);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
g->gl.BindFramebuffer(GL_FRAMEBUFFER, g->scratch_fbo);
|
|
g->gl.FramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
|
g->scratch_tex, 0);
|
|
g->rt_bound = 0;
|
|
|
|
return g->gl.CheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
|
}
|
|
|
|
static void gl_copy_vram(psx_gpu_backend_t* be, uint32_t sx, uint32_t sy,
|
|
uint32_t dx, uint32_t dy, uint32_t w, uint32_t h) {
|
|
hw_gl_t* g = gl_self(be);
|
|
int s = g->scale;
|
|
|
|
if (g->failed || !w || !h)
|
|
return;
|
|
|
|
if ((sx + w > 1024) || (sy + h > 512) || (dx + w > 1024) || (dy + h > 512)) {
|
|
/* gpu.c:1978-1982 drops out-of-range texels one at a time rather than wrapping.
|
|
Clamping the rectangle is the same result for every in-range texel, which is all
|
|
the software path keeps either. */
|
|
uint32_t maxw = 1024 - (sx > dx ? sx : dx);
|
|
uint32_t maxh = 512 - (sy > dy ? sy : dy);
|
|
|
|
if ((sx >= 1024) || (dx >= 1024) || (sy >= 512) || (dy >= 512))
|
|
return;
|
|
|
|
if (w > maxw) w = maxw;
|
|
if (h > maxh) h = maxh;
|
|
|
|
if (!w || !h)
|
|
return;
|
|
}
|
|
|
|
gl_write_hazard(g, (int)dx, (int)dy, (int)w, (int)h);
|
|
|
|
/*
|
|
row 4, and the condition on `src` is NOT an optimisation — it is the whole
|
|
correctness of the rule.
|
|
|
|
gpu.c copies host VRAM -> host VRAM right after this returns. If the source was
|
|
GPU-only, the host destination is stale and the render target's is right, so the
|
|
destination has to become gpu_dirty. If the source was NOT GPU-only, both copies
|
|
agree and host VRAM is the copy to prefer, because taking it through the render
|
|
target is LOSSY: RGBA8 has no bit 15, and Crash animates 8bpp texture pages with
|
|
GP0(80), where bit 15 is the top bit of a palette index. Marking unconditionally
|
|
cost 0.53 percentage points of 1x parity (0.4708 % -> 0.9986 %, ) for exactly
|
|
that reason.
|
|
*/
|
|
if (g->gpu_own && tiles_intersects(&g->gpu_dirty, (int)sx, (int)sy, (int)w, (int)h))
|
|
gl_mark_gpu_dirty(g, (int)dx, (int)dy, (int)w, (int)h);
|
|
|
|
gl_flush(g);
|
|
|
|
/* GL forbids a blit whose source and destination regions overlap inside one FBO, so the
|
|
copy bounces through a scratch target. Copies are rare (scrolling backgrounds and
|
|
screen effects) and this keeps the UPSCALED content, which reading back from host
|
|
VRAM would not. */
|
|
if (!gl_ensure_scratch(g, (int)w * s, (int)h * s)) {
|
|
gl_status("GP0(80) scratch target unavailable; disabling the GL rasterizer");
|
|
g->failed = 1;
|
|
return;
|
|
}
|
|
|
|
g->gl.Disable(GL_SCISSOR_TEST);
|
|
g->gl.BindFramebuffer(GL_READ_FRAMEBUFFER, g->rt_fbo);
|
|
g->gl.BindFramebuffer(GL_DRAW_FRAMEBUFFER, g->scratch_fbo);
|
|
g->gl.BlitFramebuffer((GLint)sx * s, (GLint)sy * s,
|
|
(GLint)(sx + w) * s, (GLint)(sy + h) * s,
|
|
0, 0, (GLint)w * s, (GLint)h * s,
|
|
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
|
g->gl.BindFramebuffer(GL_READ_FRAMEBUFFER, g->scratch_fbo);
|
|
g->gl.BindFramebuffer(GL_DRAW_FRAMEBUFFER, g->rt_fbo);
|
|
g->gl.BlitFramebuffer(0, 0, (GLint)w * s, (GLint)h * s,
|
|
(GLint)dx * s, (GLint)dy * s,
|
|
(GLint)(dx + w) * s, (GLint)(dy + h) * s,
|
|
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
|
g->gl.BindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
g->rt_bound = 0;
|
|
}
|
|
|
|
/* Replicates a native VRAM rectangle into the upscaled target. Host VRAM already holds the
|
|
data by the time this runs (gpu.c:1382-1386), so the vram texture is refreshed first and
|
|
the render target is then filled from it — one code path for GP0(A0), the initial seed
|
|
and the load-state re-seed. */
|
|
static void gl_blit_from_vram(hw_gl_t* g, int x, int y, int w, int h) {
|
|
gl_flush(g);
|
|
gl_bind_rt(g);
|
|
|
|
g->gl.Disable(GL_SCISSOR_TEST);
|
|
/* A PLAIN WRITE, stated rather than implied. It used to be one by arithmetic — alpha 0
|
|
under (ONE, SRC_ALPHA) — but the upload now writes bit 15 into alpha, and that value
|
|
must land unmodified rather than become a blend factor. Disabling the unit is the same
|
|
result for RGB in both modes; gl_flush() re-establishes the blend state it wants. */
|
|
g->gl.Disable(GL_BLEND);
|
|
g->gl.UseProgram(g->prog_xfer);
|
|
g->gl.Uniform4f(g->u_xfer_rect, (GLfloat)x, (GLfloat)y, (GLfloat)w, (GLfloat)h);
|
|
g->gl.Uniform2f(g->u_xfer_rt_size, (GLfloat)g->rt_w, (GLfloat)g->rt_h);
|
|
g->gl.Uniform1f(g->u_xfer_scale, (GLfloat)g->scale);
|
|
g->gl.Uniform1i(g->u_xfer_vram, 0);
|
|
g->gl.ActiveTexture(GL_TEXTURE0);
|
|
g->gl.BindTexture(GL_TEXTURE_2D, g->vram_tex);
|
|
g->gl.BindVertexArray(g->quad_vao);
|
|
g->gl.DrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
g->stat_draws++;
|
|
}
|
|
|
|
/* One non-wrapping rectangle of host VRAM, into the vram texture and then the render
|
|
target. Split out because GP0(A0)'s wrap case needs it up to four times. */
|
|
static void gl_upload_rect(hw_gl_t* g, int x, int y, int w, int h) {
|
|
if ((w <= 0) || (h <= 0))
|
|
return;
|
|
|
|
gl_write_hazard(g, x, y, w, h);
|
|
gl_flush(g);
|
|
gl_sync_vram(g, x, y, w, h);
|
|
gl_blit_from_vram(g, x, y, w, h);
|
|
}
|
|
|
|
static void gl_upload_vram(psx_gpu_backend_t* be, uint32_t x, uint32_t y,
|
|
uint32_t w, uint32_t h, const uint16_t* src,
|
|
uint32_t src_stride_px) {
|
|
hw_gl_t* g = gl_self(be);
|
|
|
|
(void)src;
|
|
(void)src_stride_px;
|
|
|
|
if (g->failed || !w || !h)
|
|
return;
|
|
|
|
if ((x + w > 1024) || (y + h > 512)) {
|
|
/*
|
|
GP0(A0) wraps per halfword at 1024/512 (gpu.c:1348-1349), so the destination is
|
|
not one rectangle — it is up to four, one per corner the transfer wraps into.
|
|
|
|
The old handling re-seeded the WHOLE surface from host VRAM, which is correct
|
|
only while the software shadow keeps host VRAM authoritative. Without the shadow
|
|
it would upload 1 MB of stale host VRAM over the render target and erase every
|
|
pixel the rasterizer had drawn — a wrapping 32x32 texture upload would blank the
|
|
frame. The wrapped rectangle is enumerated instead, which is correct in both
|
|
worlds and cheaper in each.
|
|
*/
|
|
int xs[2], ws[2], ys[2], hs[2];
|
|
int nx = 1, ny = 1, ix, iy;
|
|
|
|
if (x >= 1024) x &= 0x3ff;
|
|
if (y >= 512) y &= 0x1ff;
|
|
|
|
if (w > 1024) w = 1024;
|
|
if (h > 512) h = 512;
|
|
|
|
xs[0] = (int)x; ws[0] = (int)w;
|
|
ys[0] = (int)y; hs[0] = (int)h;
|
|
|
|
if (x + w > 1024) {
|
|
ws[0] = 1024 - (int)x;
|
|
xs[1] = 0;
|
|
ws[1] = (int)w - ws[0];
|
|
nx = 2;
|
|
}
|
|
|
|
if (y + h > 512) {
|
|
hs[0] = 512 - (int)y;
|
|
ys[1] = 0;
|
|
hs[1] = (int)h - hs[0];
|
|
ny = 2;
|
|
}
|
|
|
|
for (iy = 0; iy < ny; iy++)
|
|
for (ix = 0; ix < nx; ix++)
|
|
gl_upload_rect(g, xs[ix], ys[iy], ws[ix], hs[iy]);
|
|
|
|
return;
|
|
}
|
|
|
|
gl_upload_rect(g, (int)x, (int)y, (int)w, (int)h);
|
|
|
|
/* A whole-surface upload is the load-state re-seed (gpu.c:2574): host VRAM has just
|
|
been replaced wholesale and the render target now mirrors it exactly, so nothing is
|
|
GPU-only any more. Leaving the map set would only cost redundant resolves, but a
|
|
state load is exactly the moment to have it be empty. */
|
|
if (!x && !y && (w == 1024) && (h == 512)) {
|
|
tiles_clear(&g->gpu_dirty);
|
|
g->any_gpu_dirty = 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
GP0(C0), the backend — the only trigger that forces a GPU->CPU transfer,
|
|
and therefore the one ladder is built around.
|
|
|
|
While PSX_GPU_BACKEND_SOFTWARE_SHADOW is set there is nothing to fetch: gpu->vram is
|
|
authoritative, the GPUREAD drain already has correct data and there is no stall. The
|
|
traffic is measured either way, because that measurement is what decides whether this game
|
|
can live without the shadow at all.
|
|
*/
|
|
static void gl_download_vram(psx_gpu_backend_t* be, uint32_t x, uint32_t y,
|
|
uint32_t w, uint32_t h, uint16_t* dst, uint32_t dst_stride_px) {
|
|
hw_gl_t* g = gl_self(be);
|
|
const uint64_t bytes = (uint64_t)w * (uint64_t)h * 2u;
|
|
|
|
g->stat_readback_bytes += bytes;
|
|
g->c0_frame_bytes += (uint32_t)((bytes > 0xffffffffu) ? 0xffffffffu : bytes);
|
|
|
|
if (g->shadow || g->failed)
|
|
return;
|
|
|
|
if (!dst || !dst_stride_px)
|
|
return;
|
|
|
|
gl_readback_rect(g, (int)x, (int)y, (int)w, (int)h, dst, dst_stride_px);
|
|
}
|
|
|
|
/* ---- frame / scanout ---------------------------------------------------------------------- */
|
|
|
|
#define GL_STATS_PERIOD 600
|
|
|
|
/* averaging window, in frames. Must match the size of hw_gl_t::c0_window. */
|
|
#define kC0Window ((int)(sizeof(((hw_gl_t*)0)->c0_window) / sizeof(uint32_t)))
|
|
|
|
/* threshold: 256 KB/frame, "a quarter of VRAM". */
|
|
#define kC0LimitDefault ((uint64_t)256 * 1024)
|
|
|
|
/* `hwgl_vram_diff` period, in frames. 600 is ten seconds at 60 fps, which is short enough
|
|
that a session walks through several distinct scenes and long enough that the 1 MB
|
|
glReadPixels each one costs does not itself change what is being measured. */
|
|
#define kVramDiffPeriod 600
|
|
|
|
static void gl_end_frame(psx_gpu_backend_t* be, psx_gpu_t* gpu) {
|
|
hw_gl_t* g = gl_self(be);
|
|
GLenum err;
|
|
|
|
(void)gpu;
|
|
|
|
if (g->failed)
|
|
return;
|
|
|
|
gl_flush(g);
|
|
|
|
/* Drain the queue, not just the head. A single error can come from anywhere in the
|
|
process — including the present path, which shares this context — so one is not
|
|
evidence that THIS backend is broken. A run of them is, and a broken pipeline
|
|
silently renders nothing, which is the one thing this must never present as
|
|
"working". */
|
|
err = GL_NO_ERROR;
|
|
|
|
while (g->gl.GetError() != GL_NO_ERROR)
|
|
err = 1;
|
|
|
|
if (err != GL_NO_ERROR) {
|
|
g->error_streak++;
|
|
|
|
if (g->error_streak >= 30) {
|
|
gl_status("GL errors on %d consecutive frames (last at frame %llu); disabling "
|
|
"the GL rasterizer",
|
|
g->error_streak, (unsigned long long)g->frames);
|
|
g->failed = 1;
|
|
gl_release(g);
|
|
return;
|
|
}
|
|
} else {
|
|
g->error_streak = 0;
|
|
}
|
|
|
|
g->frames++;
|
|
|
|
if ((g->frames % GL_STATS_PERIOD) == 0) {
|
|
psxe_diag_logf("hwgl",
|
|
"frame=%llu scale=%d draws/f=%.1f ranges/f=%.1f prims/f=%.1f "
|
|
"vramsync/f=%.2f syncpx/f=%.0f c0bytes=%llu c0avg=%llu "
|
|
"reads/f=%.2f readpx/f=%.0f gres/f=%.2f grespx/f=%.0f "
|
|
"zerocopy=%u/%d",
|
|
(unsigned long long)g->frames, g->scale,
|
|
(double)g->stat_draws / GL_STATS_PERIOD,
|
|
(double)g->stat_ranges / GL_STATS_PERIOD,
|
|
(double)g->stat_prims / GL_STATS_PERIOD,
|
|
(double)g->stat_syncs / GL_STATS_PERIOD,
|
|
(double)g->stat_sync_px / GL_STATS_PERIOD,
|
|
(unsigned long long)g->stat_readback_bytes,
|
|
(unsigned long long)(g->c0_window_sum / kC0Window),
|
|
(double)g->stat_reads / GL_STATS_PERIOD,
|
|
(double)g->stat_read_px / GL_STATS_PERIOD,
|
|
(double)g->stat_gres / GL_STATS_PERIOD,
|
|
(double)g->stat_gres_px / GL_STATS_PERIOD,
|
|
g->stat_adopted, GL_STATS_PERIOD);
|
|
g->stat_draws = 0;
|
|
g->stat_ranges = 0;
|
|
g->stat_prims = 0;
|
|
g->stat_syncs = 0;
|
|
g->stat_sync_px = 0;
|
|
g->stat_adopted = 0;
|
|
g->stat_reads = 0;
|
|
g->stat_read_px = 0;
|
|
g->stat_gres = 0;
|
|
g->stat_gres_px = 0;
|
|
}
|
|
|
|
/* Periodic whole-VRAM diff. AFTER gl_flush() above, so the render target holds a
|
|
complete frame, and gated on the shadow because the shadow is the reference. */
|
|
if (g->dbg_vram_diff && g->shadow && ((g->frames % kVramDiffPeriod) == 0))
|
|
gl_vram_diff(g, "periodic");
|
|
|
|
/*
|
|
automatic downgrade. The window is a plain ring so the average tracks the last
|
|
60 frames rather than a whole session — a game that reads VRAM hard for two seconds
|
|
during a transition and then stops must NOT strand the session on the CPU, and a
|
|
session-long total cannot tell those two apart.
|
|
|
|
Note the ordering: the trigger runs at the END of the frame, after gl_flush(), so the
|
|
render target holds a complete frame when gl_seed_host_vram() reads it.
|
|
*/
|
|
{
|
|
const uint32_t frame_bytes = g->c0_frame_bytes;
|
|
uint64_t avg;
|
|
|
|
g->c0_frame_bytes = 0;
|
|
g->c0_window_sum -= g->c0_window[g->c0_window_idx];
|
|
g->c0_window[g->c0_window_idx] = frame_bytes;
|
|
g->c0_window_sum += frame_bytes;
|
|
g->c0_window_idx = (g->c0_window_idx + 1) % kC0Window;
|
|
|
|
avg = g->c0_window_sum / kC0Window;
|
|
|
|
/* `> 0` matters: with hwgl_c0_trip the limit is 0, and without it a session that
|
|
never touches GP0(C0) would satisfy `0 >= 0` on its very first frame. */
|
|
if (!g->downgraded && (g->c0_window_sum > 0) && (avg >= g->c0_limit)) {
|
|
gl_downgrade(g, "GP0(C0) readback traffic over the 60-frame limit");
|
|
return;
|
|
}
|
|
|
|
if (!g->downgraded && g->dbg_force_downgrade && (g->frames >= 900)) {
|
|
gl_downgrade(g, "hwgl_force_downgrade marker (deliberate test of the ladder)");
|
|
return;
|
|
}
|
|
}
|
|
|
|
gl_release(g);
|
|
}
|
|
|
|
static int gl_display_width(const psx_gpu_t* gpu) {
|
|
static const int kHres[4] = {256, 320, 512, 640};
|
|
int w = (gpu->display_mode & 0x40) ? 368 : kHres[gpu->display_mode & 3];
|
|
|
|
return (w == 368) ? 384 : w;
|
|
}
|
|
|
|
static int gl_display_height(const psx_gpu_t* gpu) {
|
|
int disp;
|
|
|
|
if (gpu->display_mode & 0x4)
|
|
return 480;
|
|
|
|
disp = (int)gpu->disp_y2 - (int)gpu->disp_y1;
|
|
|
|
return (disp < (255 - 16)) ? disp : 240;
|
|
}
|
|
|
|
/* The 2-bytes-per-pixel readback is only legal if the implementation offers it; ES 3.0
|
|
guarantees nothing beyond RGBA/UNSIGNED_BYTE. Probing beats assuming. Queried against
|
|
whichever RG8 FBO is bound — both of ours are RG8, so one answer serves both — and cached,
|
|
because it is a pipeline query and both call sites are on the hot path. */
|
|
static void gl_probe_read_format(hw_gl_t* g) {
|
|
GLint fmt = 0, type = 0;
|
|
|
|
if (g->read_probed)
|
|
return;
|
|
|
|
g->gl.GetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &fmt);
|
|
g->gl.GetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &type);
|
|
|
|
g->read_packed = ((GLenum)fmt == GL_RG) && ((GLenum)type == GL_UNSIGNED_BYTE);
|
|
g->read_format = g->read_packed ? GL_RG : GL_RGBA;
|
|
g->read_type = GL_UNSIGNED_BYTE;
|
|
g->read_probed = 1;
|
|
}
|
|
|
|
static int gl_ensure_resolve(hw_gl_t* g, int w, int h) {
|
|
if ((g->resolve_tex != 0) && (g->resolve_w == w) && (g->resolve_h == h))
|
|
return 1;
|
|
|
|
if (!g->resolve_tex) {
|
|
g->gl.GenTextures(1, &g->resolve_tex);
|
|
g->gl.GenFramebuffers(1, &g->resolve_fbo);
|
|
}
|
|
|
|
if (!g->resolve_tex || !g->resolve_fbo)
|
|
return 0;
|
|
|
|
g->resolve_w = w;
|
|
g->resolve_h = h;
|
|
|
|
g->gl.ActiveTexture(GL_TEXTURE0);
|
|
g->gl.BindTexture(GL_TEXTURE_2D, g->resolve_tex);
|
|
g->gl.TexImage2D(GL_TEXTURE_2D, 0, GL_RG8, w, h, 0, GL_RG, GL_UNSIGNED_BYTE, NULL);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
g->gl.BindFramebuffer(GL_FRAMEBUFFER, g->resolve_fbo);
|
|
g->gl.FramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
|
g->resolve_tex, 0);
|
|
g->rt_bound = 0;
|
|
|
|
if (g->gl.CheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
|
return 0;
|
|
|
|
gl_probe_read_format(g);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* ---- GPU -> host VRAM readback (the backend) ---------------------------- */
|
|
|
|
/* An RG8 target at NATIVE VRAM size, i.e. one texel per PlayStation halfword. Separate from
|
|
resolve_tex on purpose: that one is sized to the display region every frame and sharing it
|
|
would make a GP0(C0) of a texture page thrash the scanout's allocation. 1 MB at worst. */
|
|
static int gl_ensure_xfer(hw_gl_t* g, int w, int h) {
|
|
if ((g->xfer_tex != 0) && (g->xfer_w >= w) && (g->xfer_h >= h))
|
|
return 1;
|
|
|
|
if (!g->xfer_tex) {
|
|
g->gl.GenTextures(1, &g->xfer_tex);
|
|
g->gl.GenFramebuffers(1, &g->xfer_fbo);
|
|
}
|
|
|
|
if (!g->xfer_tex || !g->xfer_fbo)
|
|
return 0;
|
|
|
|
g->xfer_w = w > g->xfer_w ? w : g->xfer_w;
|
|
g->xfer_h = h > g->xfer_h ? h : g->xfer_h;
|
|
|
|
g->gl.ActiveTexture(GL_TEXTURE0);
|
|
g->gl.BindTexture(GL_TEXTURE_2D, g->xfer_tex);
|
|
g->gl.TexImage2D(GL_TEXTURE_2D, 0, GL_RG8, g->xfer_w, g->xfer_h, 0, GL_RG,
|
|
GL_UNSIGNED_BYTE, NULL);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
g->gl.BindFramebuffer(GL_FRAMEBUFFER, g->xfer_fbo);
|
|
g->gl.FramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
|
g->xfer_tex, 0);
|
|
g->rt_bound = 0;
|
|
|
|
if (g->gl.CheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
|
return 0;
|
|
|
|
gl_probe_read_format(g);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
Reads a NATIVE-coordinate rectangle of the render target back into host memory as packed
|
|
BGR555, downsampling by S on the GPU. This is "the stall": it flushes the batch and
|
|
then does a glReadPixels, which on a tiler is a full pipeline sync. Everything above it is
|
|
written so that it happens as rarely as possible.
|
|
|
|
dst is native VRAM layout; dst_stride_px is in halfwords. Returns 0 without touching dst
|
|
if anything went wrong, so a caller can leave host VRAM alone rather than corrupt it.
|
|
*/
|
|
static int gl_readback_rect(hw_gl_t* g, int x, int y, int w, int h,
|
|
uint16_t* dst, uint32_t dst_stride_px) {
|
|
size_t need;
|
|
int row;
|
|
|
|
if (g->failed || !dst || (w <= 0) || (h <= 0))
|
|
return 0;
|
|
|
|
if (x < 0) x = 0;
|
|
if (y < 0) y = 0;
|
|
if (x + w > 1024) w = 1024 - x;
|
|
if (y + h > 512) h = 512 - y;
|
|
|
|
if ((w <= 0) || (h <= 0))
|
|
return 0;
|
|
|
|
gl_flush(g);
|
|
|
|
if (!gl_ensure_xfer(g, w, h)) {
|
|
gl_status("VRAM readback target unavailable; disabling the GL rasterizer");
|
|
g->failed = 1;
|
|
gl_release(g);
|
|
return 0;
|
|
}
|
|
|
|
need = (size_t)w * (size_t)h * (g->read_packed ? 2u : 4u);
|
|
|
|
if (need > g->native_rb_cap) {
|
|
uint8_t* grown = (uint8_t*)realloc(g->native_rb, need);
|
|
|
|
if (!grown) {
|
|
gl_status("VRAM readback staging allocation of %zu bytes failed", need);
|
|
g->failed = 1;
|
|
gl_release(g);
|
|
return 0;
|
|
}
|
|
|
|
g->native_rb = grown;
|
|
g->native_rb_cap = need;
|
|
}
|
|
|
|
g->gl.BindFramebuffer(GL_FRAMEBUFFER, g->xfer_fbo);
|
|
g->rt_bound = 0;
|
|
g->gl.Viewport(0, 0, w, h);
|
|
g->gl.Disable(GL_SCISSOR_TEST);
|
|
g->gl.Disable(GL_BLEND);
|
|
g->gl.UseProgram(g->prog_resolve);
|
|
g->gl.Uniform1i(g->u_res_rt, 0);
|
|
/* + S/2 picks the centre-ish sub-texel of each native texel. At S == 1 that is +0, so the
|
|
1x path is exactly the scanout path and the parity gate keeps measuring what it did. */
|
|
g->gl.Uniform2i(g->u_res_origin, (GLint)(x * g->scale + g->scale / 2),
|
|
(GLint)(y * g->scale + g->scale / 2));
|
|
g->gl.Uniform2i(g->u_res_limit, g->rt_w - 1, g->rt_h - 1);
|
|
g->gl.Uniform1i(g->u_res_step, g->scale);
|
|
g->gl.Uniform1i(g->u_res_box, 1); /* native readback never downsamples a block */
|
|
/* Host VRAM is 16-bit and bit 15 is part of it: GP0(C0), downgrade seed and the
|
|
whole-VRAM diff all want the mask bit, not a 15-bit approximation of the pixel. */
|
|
g->gl.Uniform1i(g->u_res_mask, g->mask_mode);
|
|
g->gl.ActiveTexture(GL_TEXTURE0);
|
|
g->gl.BindTexture(GL_TEXTURE_2D, g->rt_tex);
|
|
g->gl.BindVertexArray(g->quad_vao);
|
|
g->gl.DrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
|
|
g->gl.PixelStorei(GL_PACK_ALIGNMENT, g->read_packed ? 2 : 4);
|
|
g->gl.ReadPixels(0, 0, w, h, g->read_format, g->read_type, g->native_rb);
|
|
|
|
if (!g->read_packed) {
|
|
const uint8_t* src = g->native_rb;
|
|
uint8_t* pack = g->native_rb;
|
|
size_t i, count = (size_t)w * (size_t)h;
|
|
|
|
for (i = 0; i < count; i++) {
|
|
pack[0] = src[0];
|
|
pack[1] = src[1];
|
|
pack += 2;
|
|
src += 4;
|
|
}
|
|
}
|
|
|
|
for (row = 0; row < h; row++) {
|
|
memcpy(dst + (size_t)(y + row) * dst_stride_px + x,
|
|
g->native_rb + (size_t)row * (size_t)w * 2u,
|
|
(size_t)w * 2u);
|
|
}
|
|
|
|
gl_release(g);
|
|
g->stat_reads++;
|
|
g->stat_read_px += (uint32_t)(w * h);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
"downloading vram_rt once to seed gpu->vram" — the step that makes the downgrade
|
|
transparent instead of a visible glitch, because whatever rasterizer takes over next reads
|
|
host VRAM and would otherwise inherit whatever was last there.
|
|
|
|
With the software shadow still installed gpu->vram is ALREADY authoritative and is the
|
|
more accurate of the two copies (it has not been through the render target's 8-bit blend
|
|
precision, ), so overwriting it would be a downgrade in the literal sense. The read is
|
|
still performed, into a scratch buffer, and the two copies are DIFFED — which turns the
|
|
seed path from untested code into a whole-VRAM parity measurement, strictly stronger than
|
|
gl_parity_check's display window, and exactly the number that says whether dropping the
|
|
shadow is safe. When the shadow goes, the same read lands in gpu->vram instead.
|
|
*/
|
|
|
|
/*
|
|
The measurement half of the seed, split out so it can be taken WITHOUT walking the
|
|
ladder. quoted one 0/524288 at frame 256, which was real, and a second copy that
|
|
landed on a black CD-load screen with softnonzero=0 and compared zeros to zeros. One
|
|
sample of a number this load-bearing is not enough and a vacuous sample is worse than
|
|
none, so `hwgl_vram_diff` repeats it on a period and every line carries its own frame
|
|
number and its own softnonzero — the sample is self-describing, so a busy frame can be
|
|
told from a black one in the log rather than by assertion.
|
|
|
|
Only meaningful while the shadow is on: the shadow IS the reference.
|
|
*/
|
|
static void gl_vram_diff(hw_gl_t* g, const char* when) {
|
|
uint16_t* scratch;
|
|
uint64_t diff = 0, nonzero = 0, gpunonzero = 0;
|
|
uint64_t blank = 0, extra = 0, near_ = 0, far_ = 0;
|
|
uint16_t fx[4], fy[4], fg[4], fw[4];
|
|
int fn = 0;
|
|
size_t i;
|
|
|
|
scratch = (uint16_t*)malloc(1024u * 512u * sizeof(uint16_t));
|
|
|
|
if (!scratch) {
|
|
psxe_diag_logf("hwgl", "VRAM diff (%s) skipped: out of memory", when);
|
|
return;
|
|
}
|
|
|
|
if (gl_readback_rect(g, 0, 0, 1024, 512, scratch, 1024)) {
|
|
for (i = 0; i < 1024u * 512u; i++) {
|
|
const uint16_t a = (uint16_t)(scratch[i] & 0x7fff);
|
|
const uint16_t b = (uint16_t)(g->gpu->vram[i] & 0x7fff);
|
|
|
|
if (b) nonzero++;
|
|
if (a) gpunonzero++;
|
|
|
|
if (a == b)
|
|
continue;
|
|
|
|
diff++;
|
|
|
|
/* The same buckets gl_parity_check() uses, and for the same reason: a raw count
|
|
is not actionable. "all within one 5-bit step" is documented
|
|
multi-blend precision divergence and is fine; a FAR count is the rasterization
|
|
itself disagreeing and is not. */
|
|
if (!a) {
|
|
blank++;
|
|
} else if (!b) {
|
|
extra++;
|
|
} else {
|
|
const int dr = (int)(a & 31u) - (int)(b & 31u);
|
|
const int dg = (int)((a >> 5) & 31u) - (int)((b >> 5) & 31u);
|
|
const int db = (int)((a >> 10) & 31u) - (int)((b >> 10) & 31u);
|
|
|
|
if ((dr >= -1) && (dr <= 1) && (dg >= -1) && (dg <= 1) &&
|
|
(db >= -1) && (db <= 1)) {
|
|
near_++;
|
|
} else {
|
|
if (fn < 4) {
|
|
fx[fn] = (uint16_t)(i & 1023u);
|
|
fy[fn] = (uint16_t)(i >> 10);
|
|
fg[fn] = a;
|
|
fw[fn] = b;
|
|
fn++;
|
|
}
|
|
|
|
far_++;
|
|
}
|
|
}
|
|
}
|
|
|
|
psxe_diag_logf("hwgl",
|
|
"whole-VRAM readback vs software shadow (%s, frame %llu): %llu/%u "
|
|
"texels differ (%.4f%%), softnonzero=%llu gpunonzero=%llu "
|
|
"blank=%llu extra=%llu near=%llu far=%llu",
|
|
when, (unsigned long long)g->frames,
|
|
(unsigned long long)diff, 1024u * 512u,
|
|
100.0 * (double)diff / (double)(1024u * 512u),
|
|
(unsigned long long)nonzero, (unsigned long long)gpunonzero,
|
|
(unsigned long long)blank, (unsigned long long)extra,
|
|
(unsigned long long)near_, (unsigned long long)far_);
|
|
|
|
if (fn) {
|
|
psxe_diag_logf("hwgl",
|
|
"whole-VRAM far samples [(%u,%u) %04x/%04x (%u,%u) %04x/%04x "
|
|
"(%u,%u) %04x/%04x (%u,%u) %04x/%04x] n=%d",
|
|
fx[0], fy[0], fg[0], fw[0],
|
|
fn > 1 ? fx[1] : 0, fn > 1 ? fy[1] : 0,
|
|
fn > 1 ? fg[1] : 0, fn > 1 ? fw[1] : 0,
|
|
fn > 2 ? fx[2] : 0, fn > 2 ? fy[2] : 0,
|
|
fn > 2 ? fg[2] : 0, fn > 2 ? fw[2] : 0,
|
|
fn > 3 ? fx[3] : 0, fn > 3 ? fy[3] : 0,
|
|
fn > 3 ? fg[3] : 0, fn > 3 ? fw[3] : 0,
|
|
fn);
|
|
}
|
|
} else {
|
|
psxe_diag_logf("hwgl", "whole-VRAM readback FAILED during the %s diff", when);
|
|
}
|
|
|
|
free(scratch);
|
|
}
|
|
|
|
static void gl_seed_host_vram(hw_gl_t* g) {
|
|
if (!g->shadow) {
|
|
if (!gl_readback_rect(g, 0, 0, 1024, 512, g->gpu->vram, 1024))
|
|
psxe_diag_logf("hwgl", "VRAM seed FAILED; host VRAM is whatever it was");
|
|
else
|
|
psxe_diag_logf("hwgl", "host VRAM seeded from the render target (1024x512)");
|
|
|
|
return;
|
|
}
|
|
|
|
gl_vram_diff(g, "downgrade seed");
|
|
}
|
|
|
|
/*
|
|
option 1, the whole ladder. Loud by contract: "explicitly and logged, never
|
|
silently". gl_status() is what frontend/main.cpp's checkRasterizerHealth() prints when it
|
|
notices `failed`, so the reason reaches the user-visible log as well as the diag file.
|
|
|
|
Setting `failed` is the handover. checkRasterizerHealth() (main.cpp) then drops the
|
|
adopted scanout texture, detaches the backend, destroys it and installs the CPU
|
|
internal-resolution rasterizer — which is immune to this failure mode by construction,
|
|
since it keeps the software shadow and therefore never reads VRAM back at all — or the
|
|
plain software rasterizer if that also declines. That path already existed for GL errors;
|
|
this is the first thing that makes it fire for a reason other than a broken driver.
|
|
*/
|
|
static void gl_downgrade(hw_gl_t* g, const char* reason) {
|
|
if (g->downgraded)
|
|
return;
|
|
|
|
g->downgraded = 1;
|
|
|
|
psxe_diag_logf("hwgl",
|
|
"AUTOMATIC DOWNGRADE at frame %llu: %s. GP0(C0) total=%llu bytes, "
|
|
"window=%llu bytes over %d frames (limit %llu bytes/frame). "
|
|
"the backend.",
|
|
(unsigned long long)g->frames, reason,
|
|
(unsigned long long)g->stat_readback_bytes,
|
|
(unsigned long long)g->c0_window_sum,
|
|
(int)(sizeof(g->c0_window) / sizeof(g->c0_window[0])),
|
|
(unsigned long long)g->c0_limit);
|
|
|
|
gl_seed_host_vram(g);
|
|
|
|
/* gl_status() last, so the string frontend/main.cpp echoes through log_error() is this
|
|
one and not a stale "GL rasterizer up". Reason-led rather than cause-led: the same
|
|
ladder is walked by the deliberate test markers, and a message that asserted "this game
|
|
reads VRAM back too fast" would then be a lie in the log. */
|
|
gl_status("automatic downgrade (%s); handing the session to a CPU rasterizer", reason);
|
|
|
|
g->failed = 1;
|
|
gl_release(g);
|
|
}
|
|
|
|
/*
|
|
The 1x parity gate, on device.
|
|
|
|
tests/gpu_renderer_parity.c cannot reach this backend: it runs on the host, where there
|
|
is no GL context, so it proves things about the software and CPU rasterizers only. The
|
|
requirement that 1x be pixel-identical therefore has to be MEASURED at runtime or it is
|
|
just an assertion.
|
|
|
|
This is nearly free because it reuses the scanout readback that has already happened:
|
|
at scale 1 the packed BGR555 the GPU just produced is the same shape as the region of
|
|
gpu->vram the software shadow wrote, so the comparison is one pass over the visible
|
|
framebuffer. It runs for the first few hundred frames of a 1x session and then stops,
|
|
reporting the mismatch rate and the first disagreement it saw.
|
|
|
|
Bit 15 is masked on both sides UNLESS the mask bit is on, in which case it is compared
|
|
like every other bit and disagreements land in their own `mask` bucket. That comparison is
|
|
the standing GL-vs-CPU check for : a GL mask stage that silently disagreed with
|
|
psx/dev/gpu.c — a framebuffer fetch returning stale destination alpha, a flag that never
|
|
reaches the shader, an inverted test — shows up here as a large `mask` count against an
|
|
otherwise clean frame. With the mask bit off the render target has no mask channel at all
|
|
and bit 15 is a known, deliberate difference rather than a parity failure.
|
|
*/
|
|
static void gl_parity_check(hw_gl_t* g, uint32_t disp_x, uint32_t disp_y, int w, int h) {
|
|
const uint16_t* got = (const uint16_t*)g->readback;
|
|
const uint16_t* want;
|
|
const uint16_t keep = g->mask_mode ? 0xffffu : 0x7fffu;
|
|
uint64_t frame_diff;
|
|
int y, x;
|
|
|
|
/* Three windows, not one, and each is reported and then RESET. The first 600 frames of
|
|
any disc are the BIOS and the boot logos — under 10 primitives a frame — so a clean
|
|
number there says almost nothing about a rasterizer. The counters have to survive
|
|
into the frames where prims/f is in the hundreds before "1x is pixel-identical"
|
|
means anything. */
|
|
if (g->parity_done || (w <= 0) || (h <= 0))
|
|
return;
|
|
|
|
if ((disp_x + (uint32_t)w > 1024) || (disp_y + (uint32_t)h > 512))
|
|
return;
|
|
|
|
want = g->gpu->vram + disp_x + (size_t)disp_y * 1024;
|
|
|
|
frame_diff = g->parity_diff;
|
|
g->parity_frames++;
|
|
|
|
/* One frame of both surfaces, straight to disk, when the marker file is present. A
|
|
difference map is worth more than any number of buckets once the buckets stop
|
|
narrowing it down: it shows immediately whether the residue is edges, one region, a
|
|
whole primitive class or an offset. */
|
|
if (g->dbg_dump && (g->frames == 3000)) {
|
|
char path[1024];
|
|
FILE* f;
|
|
|
|
g->dbg_dump = 0;
|
|
|
|
if (gl_debug_path(path, sizeof(path), "parity_gpu.bin")) {
|
|
f = fopen(path, "wb");
|
|
|
|
if (f) {
|
|
fwrite(got, 2u, (size_t)w * (size_t)h, f);
|
|
fclose(f);
|
|
}
|
|
}
|
|
|
|
if (gl_debug_path(path, sizeof(path), "parity_soft.bin")) {
|
|
f = fopen(path, "wb");
|
|
|
|
if (f) {
|
|
for (y = 0; y < h; y++)
|
|
fwrite(want + (size_t)y * 1024, 2u, (size_t)w, f);
|
|
|
|
fclose(f);
|
|
}
|
|
}
|
|
|
|
psxe_diag_logf("hwgl", "1x parity dump frame=%llu %dx%d disp=(%u,%u)",
|
|
(unsigned long long)g->frames, w, h, disp_x, disp_y);
|
|
}
|
|
|
|
for (y = 0; y < h; y++) {
|
|
for (x = 0; x < w; x++) {
|
|
uint16_t a = (uint16_t)(got[(size_t)y * w + x] & keep);
|
|
uint16_t b = (uint16_t)(want[(size_t)y * 1024 + x] & keep);
|
|
|
|
g->parity_pixels++;
|
|
|
|
if (a == b)
|
|
continue;
|
|
|
|
g->parity_diff++;
|
|
|
|
/* Bit 15 only, i.e. the two agree about the colour and disagree about the mask.
|
|
Bucketed FIRST and separately: it is a different failure from any of the three
|
|
below, and folding it into `far` (which compares 5-bit channels) would report a
|
|
mask disagreement as a colour disagreement and send the next reader to the
|
|
blend arithmetic. */
|
|
if (((a ^ b) & 0x8000u) != 0u) {
|
|
g->parity_mask++;
|
|
|
|
if ((uint16_t)(a & 0x7fffu) == (uint16_t)(b & 0x7fffu))
|
|
continue;
|
|
}
|
|
|
|
a = (uint16_t)(a & 0x7fffu);
|
|
b = (uint16_t)(b & 0x7fffu);
|
|
|
|
/* Neighbour test, left and up only — those two are already known, and a cluster
|
|
is symmetric so counting one side of each axis finds it just as reliably. */
|
|
if (x > 0) {
|
|
const uint16_t la = (uint16_t)(got[(size_t)y * w + (x - 1)] & 0x7fff);
|
|
const uint16_t lb = (uint16_t)(want[(size_t)y * 1024 + (x - 1)] & 0x7fff);
|
|
|
|
if (la != lb)
|
|
g->parity_clustered++;
|
|
} else if (y > 0) {
|
|
const uint16_t ua = (uint16_t)(got[(size_t)(y - 1) * w + x] & 0x7fff);
|
|
const uint16_t ub = (uint16_t)(want[(size_t)(y - 1) * 1024 + x] & 0x7fff);
|
|
|
|
if (ua != ub)
|
|
g->parity_clustered++;
|
|
}
|
|
|
|
/* Bucket it. The channel deltas are computed in 5-bit space because that is the
|
|
space the software rasterizer works in, so "within one step" here means the
|
|
two answers straddle a single integer rounding boundary. */
|
|
if (!a) {
|
|
if (g->parity_blank_n < 4) {
|
|
const int n = g->parity_blank_n++;
|
|
|
|
g->parity_blank_x[n] = (uint16_t)(disp_x + x);
|
|
g->parity_blank_y[n] = (uint16_t)(disp_y + y);
|
|
g->parity_blank_want[n] = b;
|
|
}
|
|
|
|
g->parity_blank++;
|
|
} else if (!b) {
|
|
g->parity_extra++;
|
|
} else {
|
|
const int dr = (int)(a & 31u) - (int)(b & 31u);
|
|
const int dg = (int)((a >> 5) & 31u) - (int)((b >> 5) & 31u);
|
|
const int db = (int)((a >> 10) & 31u) - (int)((b >> 10) & 31u);
|
|
|
|
if ((dr >= -1) && (dr <= 1) && (dg >= -1) && (dg <= 1) &&
|
|
(db >= -1) && (db <= 1)) {
|
|
g->parity_near++;
|
|
} else {
|
|
if (g->parity_far_n < 4) {
|
|
const int n = g->parity_far_n++;
|
|
|
|
g->parity_far_x[n] = (uint16_t)(disp_x + x);
|
|
g->parity_far_y[n] = (uint16_t)(disp_y + y);
|
|
g->parity_far_got[n] = a;
|
|
g->parity_far_want[n] = b;
|
|
}
|
|
|
|
g->parity_far++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
frame_diff = g->parity_diff - frame_diff;
|
|
|
|
if (!frame_diff)
|
|
g->parity_clean_frames++;
|
|
else if (frame_diff > g->parity_worst_frame)
|
|
g->parity_worst_frame = frame_diff;
|
|
|
|
if ((g->frames == 600) || (g->frames == 1800) || (g->frames == 3600)) {
|
|
psxe_diag_logf("hwgl",
|
|
"1x parity scanouts=%llu clean=%llu worst_scanout=%llu bbox=%d "
|
|
"defer=%d",
|
|
(unsigned long long)g->parity_frames,
|
|
(unsigned long long)g->parity_clean_frames,
|
|
(unsigned long long)g->parity_worst_frame,
|
|
g->dbg_tri_bbox, g->dbg_defer_dirty);
|
|
|
|
psxe_diag_logf("hwgl",
|
|
"1x parity frame=%llu pixels=%llu differing=%llu (%.4f%%) "
|
|
"blank=%llu extra=%llu near=%llu far=%llu clustered=%llu "
|
|
"maskbit=%llu (%s)",
|
|
(unsigned long long)g->frames,
|
|
(unsigned long long)g->parity_pixels,
|
|
(unsigned long long)g->parity_diff,
|
|
g->parity_pixels ? (100.0 * (double)g->parity_diff /
|
|
(double)g->parity_pixels) : 0.0,
|
|
(unsigned long long)g->parity_blank,
|
|
(unsigned long long)g->parity_extra,
|
|
(unsigned long long)g->parity_near,
|
|
(unsigned long long)g->parity_far,
|
|
(unsigned long long)g->parity_clustered,
|
|
(unsigned long long)g->parity_mask,
|
|
g->mask_mode ? "compared" : "not compared, mask bit off");
|
|
|
|
psxe_diag_logf("hwgl",
|
|
"1x parity samples blank[(%u,%u)=%04x (%u,%u)=%04x (%u,%u)=%04x "
|
|
"(%u,%u)=%04x] n=%d",
|
|
g->parity_blank_x[0], g->parity_blank_y[0], g->parity_blank_want[0],
|
|
g->parity_blank_x[1], g->parity_blank_y[1], g->parity_blank_want[1],
|
|
g->parity_blank_x[2], g->parity_blank_y[2], g->parity_blank_want[2],
|
|
g->parity_blank_x[3], g->parity_blank_y[3], g->parity_blank_want[3],
|
|
g->parity_blank_n);
|
|
|
|
psxe_diag_logf("hwgl",
|
|
"1x parity samples far[(%u,%u) %04x/%04x (%u,%u) %04x/%04x "
|
|
"(%u,%u) %04x/%04x (%u,%u) %04x/%04x] n=%d",
|
|
g->parity_far_x[0], g->parity_far_y[0],
|
|
g->parity_far_got[0], g->parity_far_want[0],
|
|
g->parity_far_x[1], g->parity_far_y[1],
|
|
g->parity_far_got[1], g->parity_far_want[1],
|
|
g->parity_far_x[2], g->parity_far_y[2],
|
|
g->parity_far_got[2], g->parity_far_want[2],
|
|
g->parity_far_x[3], g->parity_far_y[3],
|
|
g->parity_far_got[3], g->parity_far_want[3],
|
|
g->parity_far_n);
|
|
|
|
/* Reset so the next window is attributable on its own rather than dominated by
|
|
whatever the previous one accumulated. */
|
|
g->parity_pixels = 0;
|
|
g->parity_diff = 0;
|
|
g->parity_blank = 0;
|
|
g->parity_extra = 0;
|
|
g->parity_near = 0;
|
|
g->parity_far = 0;
|
|
g->parity_mask = 0;
|
|
g->parity_clustered = 0;
|
|
g->parity_worst_frame = 0;
|
|
g->parity_clean_frames = 0;
|
|
g->parity_frames = 0;
|
|
g->parity_blank_n = 0;
|
|
g->parity_far_n = 0;
|
|
memset(g->parity_blank_x, 0, sizeof(g->parity_blank_x));
|
|
memset(g->parity_blank_y, 0, sizeof(g->parity_blank_y));
|
|
memset(g->parity_blank_want, 0, sizeof(g->parity_blank_want));
|
|
memset(g->parity_far_x, 0, sizeof(g->parity_far_x));
|
|
memset(g->parity_far_y, 0, sizeof(g->parity_far_y));
|
|
memset(g->parity_far_got, 0, sizeof(g->parity_far_got));
|
|
memset(g->parity_far_want, 0, sizeof(g->parity_far_want));
|
|
|
|
if (g->frames == 3600)
|
|
g->parity_done = 1;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Scanout, in two halves.
|
|
|
|
gl_scanout_resolve() packs the display region back down to BGR555 ON THE GPU, into
|
|
resolve_tex. That texture is byte-for-byte what the CPU upload path hands the present
|
|
layer — frontend/render_gl.cpp's ResolveFormat() maps SDL_PIXELFORMAT_BGR555 onto GL_RG8
|
|
holding packed 555, which is exactly this target's format — so there are two ways out of
|
|
it, and only one of them moves any bytes:
|
|
|
|
* armsx_hw_gl_present_texture() hands the TEXTURE to the present layer through
|
|
armsx_renderer_adopt_gl_texture(). The pixels never leave the GPU. This is the
|
|
brokered presentation seam, removing both S^2 scanout
|
|
terms (the readback and the re-upload) plus the per-frame pipeline sync.
|
|
* gl_display_buffer() reads it back at 2 bytes per pixel, for every caller that
|
|
genuinely needs host pixels: a non-GL present backend, the screenshot path, and the
|
|
1x parity gate. On a tiler that readback is a full pipeline sync, which is the whole
|
|
reason the first path exists.
|
|
|
|
On success the resolve FBO is LEFT BOUND: a caller that wants the pixels reads them
|
|
immediately, and every caller must gl_release() afterwards or present renders into our
|
|
FBO and the screen goes black (see gl_bind_rt's comment).
|
|
*/
|
|
/*
|
|
[video] downsample — the effective box factor, which is NOT simply the requested one.
|
|
|
|
The factor has to DIVIDE the internal scale or the blocks straddle native pixel edges and
|
|
the result shimmers instead of resolving. So the request is a ceiling: take the largest
|
|
divisor of the scale that is <= it. At 1x nothing divides usefully and the answer is 1
|
|
(off), which is why the setting says "hardware rasterizer, above 1x" in the UI rather than
|
|
silently doing nothing.
|
|
|
|
Returns 1 when downsampling is off, and 1 is the value that keeps every uniform below at
|
|
exactly what it was before this feature existed.
|
|
*/
|
|
static int gl_downsample_factor(const hw_gl_t* g) {
|
|
int want = g_opt_downsample;
|
|
int f;
|
|
|
|
if (want < 2)
|
|
return 1;
|
|
|
|
if (want > g->scale)
|
|
want = g->scale;
|
|
|
|
for (f = want; f >= 2; f--) {
|
|
if ((g->scale % f) == 0)
|
|
return f;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* The multiplier the PRESENTED image carries over native, i.e. the internal scale after
|
|
downsampling. Everything outside this file that asks "how much bigger is the scanout"
|
|
must use this and not g->scale — the render target is still g->scale, the picture is not. */
|
|
static int gl_present_scale(const hw_gl_t* g) {
|
|
return g->scale / gl_downsample_factor(g);
|
|
}
|
|
|
|
static int gl_scanout_resolve(hw_gl_t* g, uint32_t disp_x, uint32_t disp_y,
|
|
int* out_w, int* out_h) {
|
|
int w, h, sw, sh, box;
|
|
|
|
if (g->failed)
|
|
return 0;
|
|
|
|
w = gl_display_width(g->gpu);
|
|
h = gl_display_height(g->gpu);
|
|
|
|
if ((w <= 0) || (h <= 0))
|
|
return 0;
|
|
|
|
/* Deliberately NOT clamped against the VRAM edge. frontend/main.cpp sizes its upload
|
|
from psx_get_display_width/height and multiplies by the scale, so a buffer narrower
|
|
than that would be read past the end. The resolve shader clamps its texelFetch
|
|
instead, which edge-replicates rather than reading out of bounds. */
|
|
/* Downsampling shrinks the RESOLVE TARGET, not the render target: the frame is still
|
|
rasterized at g->scale and each output texel box-averages a box x box block of it. */
|
|
box = gl_downsample_factor(g);
|
|
sw = (w * g->scale) / box;
|
|
sh = (h * g->scale) / box;
|
|
|
|
gl_flush(g);
|
|
|
|
if (!gl_ensure_resolve(g, sw, sh)) {
|
|
gl_status("scanout resolve target unavailable; disabling the GL rasterizer");
|
|
g->failed = 1;
|
|
gl_release(g);
|
|
return 0;
|
|
}
|
|
|
|
g->gl.BindFramebuffer(GL_FRAMEBUFFER, g->resolve_fbo);
|
|
g->rt_bound = 0;
|
|
g->gl.Viewport(0, 0, sw, sh);
|
|
g->gl.Disable(GL_SCISSOR_TEST);
|
|
g->gl.Disable(GL_BLEND);
|
|
g->gl.UseProgram(g->prog_resolve);
|
|
g->gl.Uniform1i(g->u_res_rt, 0);
|
|
g->gl.Uniform2i(g->u_res_origin, (GLint)disp_x * g->scale, (GLint)disp_y * g->scale);
|
|
g->gl.Uniform2i(g->u_res_limit, g->rt_w - 1, g->rt_h - 1);
|
|
/* box == 1 restores the original pair exactly: step 1 is one output texel per RT texel
|
|
and box 1 takes the single-tap branch in kResolveFS. */
|
|
g->gl.Uniform1i(g->u_res_step, box);
|
|
g->gl.Uniform1i(g->u_res_box, box);
|
|
/* The scanout carries bit 15 too, which costs nothing (both present paths decode BGR555
|
|
and ignore it — render_gl.cpp's armsx_decode masks it off, render_vk.cpp's
|
|
Convert555Rows never reads it) and is what lets gl_parity_check() compare the mask bit
|
|
against the software shadow instead of masking it off on both sides. */
|
|
g->gl.Uniform1i(g->u_res_mask, g->mask_mode);
|
|
g->gl.ActiveTexture(GL_TEXTURE0);
|
|
g->gl.BindTexture(GL_TEXTURE_2D, g->rt_tex);
|
|
g->gl.BindVertexArray(g->quad_vao);
|
|
g->gl.DrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
|
|
if (out_w) *out_w = w;
|
|
if (out_h) *out_h = h;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* Pulls the resolved region into g->readback as packed BGR555. Requires the resolve FBO to
|
|
be bound, i.e. gl_scanout_resolve() must have just returned 1. */
|
|
static int gl_scanout_read(hw_gl_t* g, int sw, int sh) {
|
|
size_t need = (size_t)sw * (size_t)sh * (g->read_packed ? 2u : 4u);
|
|
|
|
if (need > g->readback_cap) {
|
|
uint8_t* grown = (uint8_t*)realloc(g->readback, need);
|
|
|
|
if (!grown) {
|
|
gl_status("scanout staging allocation of %zu bytes failed", need);
|
|
g->failed = 1;
|
|
gl_release(g);
|
|
return 0;
|
|
}
|
|
|
|
g->readback = grown;
|
|
g->readback_cap = need;
|
|
}
|
|
|
|
g->gl.PixelStorei(GL_PACK_ALIGNMENT, g->read_packed ? 2 : 4);
|
|
g->gl.ReadPixels(0, 0, sw, sh, g->read_format, g->read_type, g->readback);
|
|
|
|
if (!g->read_packed) {
|
|
/* Fallback: no 2-byte read format was offered, so gather the two bytes the resolve
|
|
pass already packed into R and G out of the RGBA the implementation insisted on.
|
|
In place, forwards — dst always trails src. */
|
|
const uint8_t* src = g->readback;
|
|
uint8_t* dst = g->readback;
|
|
size_t i;
|
|
size_t count = (size_t)sw * (size_t)sh;
|
|
|
|
for (i = 0; i < count; i++) {
|
|
dst[0] = src[0];
|
|
dst[1] = src[1];
|
|
dst += 2;
|
|
src += 4;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
UPSCALED parity — the gate the 1x one structurally cannot be.
|
|
|
|
gl_parity_check() only ever runs at scale 1, so it reads 13/13 green while the renderer has
|
|
visible seams at 3x. That is not an oversight in it; a 1x check compares one GPU pixel to one
|
|
software pixel and there is simply nowhere for an upscale-only defect to show. Every
|
|
hypothesis about the seams so far has been argued from reading the shader, and four of them
|
|
were wrong, so this measures instead.
|
|
|
|
For each NATIVE pixel it looks at the whole scale x scale block the GPU produced and sorts it:
|
|
|
|
clean - every subpixel equals the software shadow. Nothing to explain.
|
|
edge - subpixels disagree with each other, but each one matches the software value at
|
|
this pixel or at one of its eight neighbours. That is what a higher-resolution
|
|
rasterisation of a polygon boundary is SUPPOSED to look like, and is not a bug.
|
|
seam - at least one subpixel matches nothing in the 3x3 software neighbourhood. It is
|
|
not a sharper edge and it is not the shadow's colour: it is a pixel the GPU
|
|
invented, which is exactly what the reported seams look like.
|
|
|
|
The seam bucket is the whole point. It separates "upscaling reveals the console's own texture
|
|
edges" from "the rasteriser is producing wrong pixels", which is the question four rounds of
|
|
shader-reading could not answer. First few offenders are logged with coordinates and values.
|
|
|
|
Armed by the `hwgl_upscale_parity` marker, so it costs nothing in a normal session.
|
|
*/
|
|
static void gl_parity_check_upscaled(hw_gl_t* g, uint32_t disp_x, uint32_t disp_y, int w, int h) {
|
|
const uint16_t* got = (const uint16_t*)g->readback;
|
|
const uint16_t* want;
|
|
const int S = g->scale;
|
|
const int stride = w * S;
|
|
uint64_t close = 0, off = 0, bad = 0, bad_uniform = 0;
|
|
uint64_t worst_dev = 0;
|
|
int worst_x = -1, worst_y = -1;
|
|
int reported = 0;
|
|
int y, x;
|
|
|
|
if (!g->dbg_upscale_parity || (S <= 1) || (w <= 0) || (h <= 0))
|
|
return;
|
|
|
|
if ((disp_x + (uint32_t)w > 1024) || (disp_y + (uint32_t)h > 512))
|
|
return;
|
|
|
|
/* Boot frames prove nothing: with almost no geometry there are no boundaries to get wrong. */
|
|
if (g->frames < 3000)
|
|
return;
|
|
|
|
want = g->gpu->vram + disp_x + (size_t)disp_y * 1024;
|
|
|
|
for (y = 0; y < h; y++) {
|
|
for (x = 0; x < w; x++) {
|
|
const uint16_t ref = want[x + (size_t)y * 1024] & 0x7fff;
|
|
const int rr = ref & 31, rg = (ref >> 5) & 31, rb = (ref >> 10) & 31;
|
|
int sr = 0, sg = 0, sb = 0;
|
|
int sy, sx;
|
|
int dr, dg, db, dev;
|
|
int block_uniform;
|
|
|
|
{
|
|
const uint16_t f0 = got[(size_t)(y * S) * stride + x * S] & 0x7fff;
|
|
|
|
block_uniform = 1;
|
|
|
|
for (sy = 0; sy < S; sy++) {
|
|
for (sx = 0; sx < S; sx++) {
|
|
const uint16_t sub =
|
|
got[(size_t)(y * S + sy) * stride + (x * S + sx)] & 0x7fff;
|
|
|
|
if (sub != f0)
|
|
block_uniform = 0;
|
|
|
|
sr += sub & 31;
|
|
sg += (sub >> 5) & 31;
|
|
sb += (sub >> 10) & 31;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Box-average the block back down to one pixel and compare with the shadow.
|
|
A correct upscaled render of a flat area averages to exactly the software value;
|
|
a correct EDGE averages to something between the two sides, so a small deviation
|
|
is expected and fine. Only a large one means the GPU drew something the software
|
|
rasteriser never would — which is the actual definition of the bug being chased.
|
|
(The previous version compared each subpixel against the 3x3 software
|
|
neighbourhood and called anything unmatched a seam. That counted legitimate
|
|
Gouraud interpolation — which by design produces colours absent from the 1x
|
|
image — as failures, and reported 17% on a picture that is mostly correct.) */
|
|
sr /= (S * S); sg /= (S * S); sb /= (S * S);
|
|
|
|
dr = sr > rr ? sr - rr : rr - sr;
|
|
dg = sg > rg ? sg - rg : rg - sg;
|
|
db = sb > rb ? sb - rb : rb - sb;
|
|
dev = dr > dg ? dr : dg;
|
|
dev = dev > db ? dev : db;
|
|
|
|
if ((uint64_t)dev > worst_dev) {
|
|
worst_dev = (uint64_t)dev;
|
|
worst_x = x;
|
|
worst_y = y;
|
|
}
|
|
|
|
if (dev <= 1) {
|
|
close++;
|
|
} else if (dev <= 4) {
|
|
off++;
|
|
} else {
|
|
bad++;
|
|
|
|
/* UNIFORM + darker is the decisive split. A flat block that averages darker than
|
|
software cannot be a coverage or edge problem — every subpixel agreed, so the
|
|
rasteriser covered the pixel exactly and still produced the wrong colour. That
|
|
is blending or quantisation. A non-uniform block is an edge, where some
|
|
deviation is legitimate. */
|
|
if (block_uniform)
|
|
bad_uniform++;
|
|
|
|
if (reported < 8) {
|
|
psxe_diag_logf("hwgl",
|
|
"upscale bad: native=(%d,%d) avg=(%d,%d,%d) want=(%d,%d,%d) "
|
|
"dev=%d",
|
|
x, y, sr, sg, sb, rr, rg, rb, dev);
|
|
reported++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
psxe_diag_logf("hwgl",
|
|
"upscale parity frame=%llu scale=%d pixels=%d close=%llu off=%llu bad=%llu "
|
|
"(%.4f%% bad) worst=%llu at (%d,%d)",
|
|
(unsigned long long)g->frames, S, w * h,
|
|
(unsigned long long)close, (unsigned long long)off,
|
|
(unsigned long long)bad,
|
|
(w * h) ? (100.0 * (double)bad / (double)(w * h)) : 0.0,
|
|
(unsigned long long)worst_dev, worst_x, worst_y);
|
|
|
|
psxe_diag_logf("hwgl",
|
|
"upscale bad split: uniform=%llu (flat blocks, so blending/quantisation) "
|
|
"non_uniform=%llu (edges)",
|
|
(unsigned long long)bad_uniform,
|
|
(unsigned long long)(bad - bad_uniform));
|
|
|
|
g->dbg_upscale_parity = 0;
|
|
}
|
|
|
|
static const void* gl_display_buffer(psx_gpu_backend_t* be, uint32_t disp_x, uint32_t disp_y,
|
|
uint32_t* out_stride_bytes) {
|
|
hw_gl_t* g = gl_self(be);
|
|
int w = 0, h = 0;
|
|
const int ps = gl_present_scale(g);
|
|
|
|
if (!gl_scanout_resolve(g, disp_x, disp_y, &w, &h))
|
|
return NULL;
|
|
|
|
if (!gl_scanout_read(g, w * ps, h * ps))
|
|
return NULL;
|
|
|
|
gl_release(g);
|
|
|
|
/* Both parity harnesses read g->readback assuming a stride of w * g->scale and compare
|
|
against the software shadow at that granularity. Downsampling makes the buffer
|
|
w * ps wide and deliberately no longer equal to the shadow, so the harnesses are
|
|
skipped rather than fed a buffer they would misread. ps == g->scale when the feature
|
|
is off, which is every shipping configuration and every gate run. */
|
|
if (ps == g->scale) {
|
|
if (g->scale == 1)
|
|
gl_parity_check(g, disp_x, disp_y, w, h);
|
|
else
|
|
gl_parity_check_upscaled(g, disp_x, disp_y, w, h);
|
|
}
|
|
|
|
if (out_stride_bytes)
|
|
*out_stride_bytes = (uint32_t)(w * ps * 2);
|
|
|
|
return g->readback;
|
|
}
|
|
|
|
/*
|
|
The brokered seam, caller side. the backend specifies the renderer side
|
|
(armsx_renderer_adopt_gl_texture); this is the half that decides when it is legal.
|
|
|
|
Returns 1 only when the present layer has adopted the resolved texture, in which case the
|
|
frontend MUST skip armsx_renderer_upload_frame() for this frame. Every 0 is a working
|
|
fallback to display_buffer(), never an error: the present backend is user-selectable at
|
|
runtime and the pixels are always still available through the readback path.
|
|
|
|
The refusals, and why each one has to be here rather than in the present layer:
|
|
|
|
* owns_context — when the presentation backend is not OpenGL this backend made its own
|
|
EGL context, so resolve_tex is a name in OUR namespace and means nothing (or worse,
|
|
something else) in the presenter's. The present layer cannot detect this; we can.
|
|
* gpustat bit 23 — display disabled. psx_gpu_get_display_surface() hands back native
|
|
VRAM in that case (gpu.c:2351), so adopting would put a stale frame on screen.
|
|
* adopt_disabled — sticky after the first refusal from the present layer, so a Vulkan
|
|
or SDL present path costs exactly one wasted resolve rather than one per frame.
|
|
*/
|
|
int armsx_hw_gl_present_texture(psx_gpu_backend_t* be, struct armsx_renderer* renderer) {
|
|
hw_gl_t* g;
|
|
int w = 0, h = 0, sw, sh;
|
|
|
|
if (!be || !renderer)
|
|
return 0;
|
|
|
|
g = gl_self(be);
|
|
|
|
if (g->failed || !g->have_context || g->owns_context || g->adopt_disabled)
|
|
return 0;
|
|
|
|
if (g->gpu->gpustat & 0x800000)
|
|
return 0;
|
|
|
|
if (!gl_scanout_resolve(g, g->gpu->disp_x, g->gpu->disp_y, &w, &h))
|
|
return 0;
|
|
|
|
/* The adopted texture is the RESOLVE target, so its size follows the presented scale,
|
|
not the render-target scale — they differ only while downsampling is on. */
|
|
sw = w * gl_present_scale(g);
|
|
sh = h * gl_present_scale(g);
|
|
|
|
/*
|
|
The 1x parity gate outlives the readback it used to ride on. 1x being pixel-identical
|
|
is the gate for every scale above it, and a texture that bypasses the readback is
|
|
exactly the kind of change that can break it silently, so while the gate is still
|
|
running (it stops itself at frame 3600, and only ever runs at scale 1) the readback
|
|
is kept — costing precisely what today's build costs — and only then does the seam
|
|
become free. the backend's warning about g->readback.
|
|
*/
|
|
if ((g->scale == 1) && !g->parity_done) {
|
|
if (gl_scanout_read(g, sw, sh)) {
|
|
gl_release(g);
|
|
gl_parity_check(g, g->gpu->disp_x, g->gpu->disp_y, w, h);
|
|
} else {
|
|
return 0; /* gl_scanout_read() already released and failed the backend */
|
|
}
|
|
} else {
|
|
gl_release(g);
|
|
}
|
|
|
|
if (!armsx_renderer_adopt_gl_texture(renderer, (unsigned int)g->resolve_tex, sw, sh,
|
|
SDL_PIXELFORMAT_BGR555)) {
|
|
g->adopt_disabled = 1;
|
|
gl_status("present layer will not adopt a GL texture; staying on the readback path");
|
|
return 0;
|
|
}
|
|
|
|
if (!g->adopt_logged) {
|
|
g->adopt_logged = 1;
|
|
psxe_diag_logf("hwgl", "zero-copy scanout: presenting resolve texture %u (%dx%d) "
|
|
"directly, no glReadPixels", (unsigned)g->resolve_tex, sw, sh);
|
|
}
|
|
|
|
g->stat_adopted++;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* What the core and the frontend mean by "internal-resolution multiplier in use" is the
|
|
multiplier of the image they are handed, which downsampling reduces. main.cpp multiplies
|
|
the native display size by exactly this to size its upload, so returning g->scale here
|
|
with downsampling on would size the texture for pixels that no longer exist. */
|
|
static int gl_resolution_scale(psx_gpu_backend_t* be) {
|
|
return gl_present_scale(gl_self(be));
|
|
}
|
|
|
|
int armsx_hw_gl_failed(const psx_gpu_backend_t* backend) {
|
|
return backend ? ((const hw_gl_t*)backend)->failed : 0;
|
|
}
|
|
|
|
/* ---- entry-point loading -------------------------------------------------------------------- */
|
|
|
|
/*
|
|
Provider selection.
|
|
|
|
When the presentation backend IS OpenGL we must bind to the same GLES provider
|
|
frontend/render_gl.cpp bound, or we get system-GLES function pointers operating on an
|
|
ANGLE context. armsx_render_active_name() reports what actually survived that backend's
|
|
fallback ladder — never what was requested — so it is the authoritative source. Its
|
|
documented shapes are "OpenGL ES (ANGLE)" and "OpenGL ES (system)"; matching on the
|
|
prefix rather than a substring keeps "SDL accelerated (opengl)" from looking like a hit.
|
|
|
|
When it is NOT OpenGL — Vulkan is the present path confirmed working on the test device
|
|
— there is no context to borrow, and the system EGL is used to make our own. That is
|
|
sound because this backend's output leaves as PIXELS (the scanout resolve reads back to
|
|
the host), not as a shared GPU texture, so it does not need to live in the presenter's
|
|
context at all. It costs one extra context; it buys the GPU rasterizer working under
|
|
every presentation backend instead of exactly one.
|
|
*/
|
|
static void* gl_open_provider(int* out_is_angle, int* out_present_is_gl) {
|
|
char name[128];
|
|
void* lib = NULL;
|
|
|
|
name[0] = '\0';
|
|
armsx_render_active_name(name, (int)sizeof(name));
|
|
|
|
*out_present_is_gl = (strncmp(name, "OpenGL ES", 9) == 0);
|
|
*out_is_angle = *out_present_is_gl && (strstr(name, "ANGLE") != NULL);
|
|
|
|
if (*out_is_angle) {
|
|
lib = dlopen("libGLESv2_angle.so", RTLD_NOW | RTLD_LOCAL);
|
|
} else {
|
|
lib = dlopen("libGLESv3.so", RTLD_NOW | RTLD_LOCAL);
|
|
|
|
if (!lib)
|
|
lib = dlopen("libGLESv2.so", RTLD_NOW | RTLD_LOCAL);
|
|
}
|
|
|
|
if (!lib) {
|
|
const char* err = dlerror();
|
|
|
|
gl_status("dlopen of the %s GLES library failed: %s",
|
|
*out_is_angle ? "ANGLE" : "system", err ? err : "(no error)");
|
|
}
|
|
|
|
return lib;
|
|
}
|
|
|
|
/* Creates a 1x1 pbuffer context so the rasterizer has somewhere to run when the present
|
|
path is not OpenGL. Returns 0 and logs on any failure; the caller then declines. */
|
|
static int gl_make_own_context(hw_gl_t* g, int is_angle) {
|
|
EGLDisplay (*GetDisplay)(EGLNativeDisplayType);
|
|
EGLBoolean (*Initialize)(EGLDisplay, EGLint*, EGLint*);
|
|
EGLBoolean (*ChooseConfig)(EGLDisplay, const EGLint*, EGLConfig*, EGLint, EGLint*);
|
|
EGLContext (*CreateContext)(EGLDisplay, EGLConfig, EGLContext, const EGLint*);
|
|
EGLSurface (*CreatePbufferSurface)(EGLDisplay, EGLConfig, const EGLint*);
|
|
EGLBoolean (*MakeCurrent)(EGLDisplay, EGLSurface, EGLSurface, EGLContext);
|
|
EGLBoolean (*BindAPI)(EGLenum);
|
|
EGLint (*GetError)(void);
|
|
void* lib;
|
|
EGLConfig config;
|
|
EGLint count = 0;
|
|
EGLint major = 0, minor = 0;
|
|
|
|
static const EGLint kConfig[] = {
|
|
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
|
|
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT_KHR,
|
|
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8,
|
|
EGL_DEPTH_SIZE, 0, EGL_STENCIL_SIZE, 0,
|
|
EGL_NONE
|
|
};
|
|
static const EGLint kPbuffer[] = { EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE };
|
|
static const EGLint kContext[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE };
|
|
|
|
lib = dlopen(is_angle ? "libEGL_angle.so" : "libEGL.so", RTLD_NOW | RTLD_LOCAL);
|
|
|
|
if (!lib) {
|
|
const char* err = dlerror();
|
|
|
|
gl_status("dlopen of libEGL failed: %s", err ? err : "(no error)");
|
|
return 0;
|
|
}
|
|
|
|
*(void**)&GetDisplay = dlsym(lib, "eglGetDisplay");
|
|
*(void**)&Initialize = dlsym(lib, "eglInitialize");
|
|
*(void**)&ChooseConfig = dlsym(lib, "eglChooseConfig");
|
|
*(void**)&CreateContext = dlsym(lib, "eglCreateContext");
|
|
*(void**)&CreatePbufferSurface = dlsym(lib, "eglCreatePbufferSurface");
|
|
*(void**)&MakeCurrent = dlsym(lib, "eglMakeCurrent");
|
|
*(void**)&BindAPI = dlsym(lib, "eglBindAPI");
|
|
*(void**)&GetError = dlsym(lib, "eglGetError");
|
|
|
|
if (!GetDisplay || !Initialize || !ChooseConfig || !CreateContext ||
|
|
!CreatePbufferSurface || !MakeCurrent || !GetError) {
|
|
gl_status("libEGL is missing an entry point the GL rasterizer needs");
|
|
dlclose(lib);
|
|
return 0;
|
|
}
|
|
|
|
g->egl_display = GetDisplay(EGL_DEFAULT_DISPLAY);
|
|
|
|
if (g->egl_display == EGL_NO_DISPLAY) {
|
|
gl_status("eglGetDisplay failed (0x%04x)", (unsigned)GetError());
|
|
dlclose(lib);
|
|
return 0;
|
|
}
|
|
|
|
/* eglInitialize on an already-initialised display is a no-op that bumps nothing we
|
|
own, which is why this deliberately never calls eglTerminate: the presentation
|
|
backend may be using the same display. */
|
|
if (!Initialize(g->egl_display, &major, &minor)) {
|
|
gl_status("eglInitialize failed (0x%04x)", (unsigned)GetError());
|
|
dlclose(lib);
|
|
return 0;
|
|
}
|
|
|
|
if (BindAPI)
|
|
BindAPI(EGL_OPENGL_ES_API);
|
|
|
|
if (!ChooseConfig(g->egl_display, kConfig, &config, 1, &count) || (count < 1)) {
|
|
gl_status("no ES3 pbuffer config available (0x%04x)", (unsigned)GetError());
|
|
dlclose(lib);
|
|
return 0;
|
|
}
|
|
|
|
g->egl_context = CreateContext(g->egl_display, config, EGL_NO_CONTEXT, kContext);
|
|
|
|
if (g->egl_context == EGL_NO_CONTEXT) {
|
|
gl_status("eglCreateContext failed (0x%04x)", (unsigned)GetError());
|
|
dlclose(lib);
|
|
return 0;
|
|
}
|
|
|
|
g->egl_surface = CreatePbufferSurface(g->egl_display, config, kPbuffer);
|
|
|
|
if (g->egl_surface == EGL_NO_SURFACE) {
|
|
gl_status("eglCreatePbufferSurface failed (0x%04x)", (unsigned)GetError());
|
|
dlclose(lib);
|
|
return 0;
|
|
}
|
|
|
|
if (!MakeCurrent(g->egl_display, g->egl_surface, g->egl_surface, g->egl_context)) {
|
|
gl_status("eglMakeCurrent on the rasterizer's own context failed (0x%04x)",
|
|
(unsigned)GetError());
|
|
dlclose(lib);
|
|
return 0;
|
|
}
|
|
|
|
g->egl_library = lib;
|
|
g->owns_context = 1;
|
|
|
|
return 1;
|
|
}
|
|
|
|
#define GL_LOAD(field, symbol) \
|
|
do { \
|
|
*(void**)&g->gl.field = dlsym(lib, symbol); \
|
|
if (!g->gl.field) { \
|
|
gl_status("GLES entry point %s is missing; the GL rasterizer cannot " \
|
|
"run on this driver", symbol); \
|
|
return 0; \
|
|
} \
|
|
} while (0)
|
|
|
|
static int gl_load_api(hw_gl_t* g, void* lib) {
|
|
GL_LOAD(ActiveTexture, "glActiveTexture");
|
|
GL_LOAD(AttachShader, "glAttachShader");
|
|
GL_LOAD(BindAttribLocation, "glBindAttribLocation");
|
|
GL_LOAD(BindBuffer, "glBindBuffer");
|
|
GL_LOAD(BindFramebuffer, "glBindFramebuffer");
|
|
GL_LOAD(BindTexture, "glBindTexture");
|
|
GL_LOAD(BindVertexArray, "glBindVertexArray");
|
|
GL_LOAD(BlendEquation, "glBlendEquation");
|
|
GL_LOAD(BlendFunc, "glBlendFunc");
|
|
GL_LOAD(BlitFramebuffer, "glBlitFramebuffer");
|
|
GL_LOAD(BufferData, "glBufferData");
|
|
GL_LOAD(CheckFramebufferStatus, "glCheckFramebufferStatus");
|
|
GL_LOAD(Clear, "glClear");
|
|
GL_LOAD(ClearColor, "glClearColor");
|
|
GL_LOAD(ColorMask, "glColorMask");
|
|
GL_LOAD(CompileShader, "glCompileShader");
|
|
GL_LOAD(CreateProgram, "glCreateProgram");
|
|
GL_LOAD(CreateShader, "glCreateShader");
|
|
GL_LOAD(DeleteBuffers, "glDeleteBuffers");
|
|
GL_LOAD(DeleteFramebuffers, "glDeleteFramebuffers");
|
|
GL_LOAD(DeleteProgram, "glDeleteProgram");
|
|
GL_LOAD(DeleteShader, "glDeleteShader");
|
|
GL_LOAD(DeleteTextures, "glDeleteTextures");
|
|
GL_LOAD(DeleteVertexArrays, "glDeleteVertexArrays");
|
|
GL_LOAD(Disable, "glDisable");
|
|
GL_LOAD(DrawArrays, "glDrawArrays");
|
|
GL_LOAD(Enable, "glEnable");
|
|
GL_LOAD(EnableVertexAttribArray, "glEnableVertexAttribArray");
|
|
GL_LOAD(Finish, "glFinish");
|
|
GL_LOAD(FramebufferTexture2D_, "glFramebufferTexture2D");
|
|
GL_LOAD(GenBuffers, "glGenBuffers");
|
|
GL_LOAD(GenFramebuffers, "glGenFramebuffers");
|
|
GL_LOAD(GenTextures, "glGenTextures");
|
|
GL_LOAD(GenVertexArrays, "glGenVertexArrays");
|
|
GL_LOAD(GetError, "glGetError");
|
|
GL_LOAD(GetIntegerv, "glGetIntegerv");
|
|
GL_LOAD(GetProgramInfoLog, "glGetProgramInfoLog");
|
|
GL_LOAD(GetProgramiv, "glGetProgramiv");
|
|
GL_LOAD(GetShaderInfoLog, "glGetShaderInfoLog");
|
|
GL_LOAD(GetShaderiv, "glGetShaderiv");
|
|
GL_LOAD(GetString, "glGetString");
|
|
GL_LOAD(GetUniformLocation, "glGetUniformLocation");
|
|
GL_LOAD(LinkProgram, "glLinkProgram");
|
|
GL_LOAD(PixelStorei, "glPixelStorei");
|
|
GL_LOAD(ReadPixels, "glReadPixels");
|
|
GL_LOAD(Scissor, "glScissor");
|
|
GL_LOAD(ShaderSource, "glShaderSource");
|
|
GL_LOAD(TexImage2D, "glTexImage2D");
|
|
GL_LOAD(TexParameteri, "glTexParameteri");
|
|
GL_LOAD(TexSubImage2D, "glTexSubImage2D");
|
|
GL_LOAD(Uniform1f, "glUniform1f");
|
|
GL_LOAD(Uniform1i, "glUniform1i");
|
|
GL_LOAD(Uniform1iv, "glUniform1iv");
|
|
GL_LOAD(Uniform2f, "glUniform2f");
|
|
GL_LOAD(Uniform2i, "glUniform2i");
|
|
GL_LOAD(Uniform4f, "glUniform4f");
|
|
GL_LOAD(UseProgram, "glUseProgram");
|
|
GL_LOAD(VertexAttribIPointer, "glVertexAttribIPointer");
|
|
GL_LOAD(VertexAttribPointer, "glVertexAttribPointer");
|
|
GL_LOAD(Viewport, "glViewport");
|
|
|
|
return 1;
|
|
}
|
|
|
|
#undef GL_LOAD
|
|
|
|
/* ---- creation ------------------------------------------------------------------------------- */
|
|
|
|
static void gl_setup_attribs(hw_gl_t* g) {
|
|
const GLsizei stride = (GLsizei)sizeof(gl_vertex_t);
|
|
|
|
g->gl.BindVertexArray(g->vao);
|
|
g->gl.BindBuffer(GL_ARRAY_BUFFER, g->vbo);
|
|
|
|
g->gl.EnableVertexAttribArray(0);
|
|
g->gl.VertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, stride,
|
|
(const void*)(size_t)offsetof(gl_vertex_t, pos));
|
|
g->gl.EnableVertexAttribArray(1);
|
|
g->gl.VertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, stride,
|
|
(const void*)(size_t)offsetof(gl_vertex_t, tri0));
|
|
g->gl.EnableVertexAttribArray(2);
|
|
g->gl.VertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, stride,
|
|
(const void*)(size_t)offsetof(gl_vertex_t, tri1));
|
|
g->gl.EnableVertexAttribArray(3);
|
|
g->gl.VertexAttribIPointer(3, 4, GL_UNSIGNED_BYTE, stride,
|
|
(const void*)(size_t)offsetof(gl_vertex_t, col0));
|
|
g->gl.EnableVertexAttribArray(4);
|
|
g->gl.VertexAttribIPointer(4, 4, GL_UNSIGNED_BYTE, stride,
|
|
(const void*)(size_t)offsetof(gl_vertex_t, col1));
|
|
g->gl.EnableVertexAttribArray(5);
|
|
g->gl.VertexAttribIPointer(5, 4, GL_UNSIGNED_BYTE, stride,
|
|
(const void*)(size_t)offsetof(gl_vertex_t, col2));
|
|
g->gl.EnableVertexAttribArray(6);
|
|
g->gl.VertexAttribIPointer(6, 4, GL_UNSIGNED_SHORT, stride,
|
|
(const void*)(size_t)offsetof(gl_vertex_t, uv01));
|
|
g->gl.EnableVertexAttribArray(7);
|
|
g->gl.VertexAttribIPointer(7, 4, GL_UNSIGNED_SHORT, stride,
|
|
(const void*)(size_t)offsetof(gl_vertex_t, uv2));
|
|
g->gl.EnableVertexAttribArray(8);
|
|
g->gl.VertexAttribIPointer(8, 4, GL_UNSIGNED_SHORT, stride,
|
|
(const void*)(size_t)offsetof(gl_vertex_t, misc));
|
|
g->gl.EnableVertexAttribArray(9);
|
|
g->gl.VertexAttribIPointer(9, 4, GL_UNSIGNED_BYTE, stride,
|
|
(const void*)(size_t)offsetof(gl_vertex_t, texwin));
|
|
g->gl.EnableVertexAttribArray(10);
|
|
g->gl.VertexAttribPointer(10, 3, GL_FLOAT, GL_FALSE, stride,
|
|
(const void*)(size_t)offsetof(gl_vertex_t, triw));
|
|
/* Texture replacement; zero on every vertex unless a pack matched. psx/texrep.h. */
|
|
g->gl.EnableVertexAttribArray(11);
|
|
g->gl.VertexAttribIPointer(11, 4, GL_UNSIGNED_INT, stride,
|
|
(const void*)(size_t)offsetof(gl_vertex_t, repl));
|
|
|
|
g->gl.BindVertexArray(0);
|
|
}
|
|
|
|
/*
|
|
The mask bit, PROVEN on this device before the backend is handed over.
|
|
|
|
Comparing rasterizers against each other is blind when both share the same error, and the
|
|
GL path has a second failure mode the
|
|
CPU ones do not: framebuffer fetch is a driver capability that can be advertised and then
|
|
return stale or zero destination colour (see armsx_gpu_profile_t::fbfetch_gl — MediaTek
|
|
Mali and ANGLE are the known cases). The mask CHECK is one `if` on that value, so a driver
|
|
that lies about it silently un-implements the whole feature and puts Silent Hill's box
|
|
back around the player — which nothing else in this file would notice.
|
|
|
|
So it is measured, once, at attach, through the real draw path:
|
|
|
|
1. a "set mask" sprite is drawn, so the destination carries bit 15 = 1;
|
|
2. a second, differently coloured sprite is drawn over it with "check mask" on. It must
|
|
be REJECTED — the pixel keeps colour 1, with its mask bit intact;
|
|
3. the CONTROL: the same overdraw one pixel to the right with "check mask" off, which
|
|
must land. Without it the test would also pass on a backend that had simply stopped
|
|
drawing anything.
|
|
|
|
Read back through the ordinary resolve path, so it also proves that bit 15 survives the
|
|
render target -> host round trip that GP0(C0) and the downgrade seed depend on.
|
|
|
|
Runs before the initial whole-surface seed, so the four pixels it dirties are overwritten
|
|
a moment later and never reach a frame. Returns 0 to REFUSE the attach: the software
|
|
rasterizer is right, and a GL path that reintroduces the mask-check bug is not an acceptable
|
|
fallback for it.
|
|
*/
|
|
static int gl_mask_selftest(hw_gl_t* g) {
|
|
psx_gpu_t* gpu = g->gpu;
|
|
const uint32_t save_x1 = gpu->draw_x1, save_y1 = gpu->draw_y1;
|
|
const uint32_t save_x2 = gpu->draw_x2, save_y2 = gpu->draw_y2;
|
|
/* VRAM (0,0), because gl_readback_rect() writes at dst[y * stride + x] — a native-VRAM
|
|
layout — so the origin is the one place a two-halfword destination buffer is enough. */
|
|
const int px = 0, py = 0;
|
|
const uint32_t kFirst = 0x0000f8u; /* red -> BGR555 0x001f */
|
|
const uint32_t kSecond = 0xf80000u; /* blue -> BGR555 0x7c00 */
|
|
uint16_t probe[4];
|
|
int ok;
|
|
|
|
gpu->draw_x1 = 0;
|
|
gpu->draw_y1 = 0;
|
|
gpu->draw_x2 = 1023;
|
|
gpu->draw_y2 = 511;
|
|
|
|
/* Step 1. Untextured, opaque, "set mask while drawing". */
|
|
gl_emit_quad(g, px, py, px + 2, py + 1, kFirst, 0, 0, 0, 0, GLF_MASK_SET, 0);
|
|
gl_flush(g);
|
|
|
|
/* Steps 2 and 3, in one batch on purpose: the check must see the write from step 1
|
|
through the same framebuffer-fetch read a real frame uses, not through a flush. */
|
|
gl_emit_quad(g, px, py, px + 1, py + 1, kSecond, 0, 0, 0, 0, GLF_MASK_CHECK, 0);
|
|
gl_emit_quad(g, px + 1, py, px + 2, py + 1, kSecond, 0, 0, 0, 0, 0, 0);
|
|
gl_flush(g);
|
|
|
|
memset(probe, 0, sizeof(probe));
|
|
|
|
if (!gl_readback_rect(g, px, py, 2, 1, probe, 2)) {
|
|
gpu->draw_x1 = save_x1; gpu->draw_y1 = save_y1;
|
|
gpu->draw_x2 = save_x2; gpu->draw_y2 = save_y2;
|
|
gl_status("mask-bit self-test could not read the render target back");
|
|
return 0;
|
|
}
|
|
|
|
gpu->draw_x1 = save_x1; gpu->draw_y1 = save_y1;
|
|
gpu->draw_x2 = save_x2; gpu->draw_y2 = save_y2;
|
|
|
|
/* Checked pixel: colour 1 survived AND kept its mask bit. Control: colour 2 landed AND
|
|
its own mask bit is CLEAR — which is the other half of control, since a
|
|
backend that simply forced bit 15 on every write would pass the first half. */
|
|
ok = (probe[0] == (uint16_t)(0x001f | 0x8000)) && (probe[1] == 0x7c00u);
|
|
|
|
if (!ok) {
|
|
gl_status("mask-bit self-test failed (checked=%04x want=801f, control=%04x want=7c00); "
|
|
"using the CPU rasterizer", probe[0], probe[1]);
|
|
return 0;
|
|
}
|
|
|
|
psxe_diag_logf("hwgl", "mask-bit self-test passed: checked=%04x control=%04x "
|
|
"(GP0(E6) set+check honoured, bit 15 survives the resolve)",
|
|
probe[0], probe[1]);
|
|
|
|
return 1;
|
|
}
|
|
|
|
psx_gpu_backend_t* armsx_hw_gl_create(psx_gpu_t* gpu, int scale) {
|
|
hw_gl_t* g;
|
|
void* lib;
|
|
int is_angle = 0;
|
|
int present_is_gl = 0;
|
|
GLint max_texture = 0;
|
|
const unsigned char* version;
|
|
|
|
if (!gpu || !gpu->vram) {
|
|
gl_status("no GPU/VRAM to attach to");
|
|
return NULL;
|
|
}
|
|
|
|
lib = gl_open_provider(&is_angle, &present_is_gl);
|
|
|
|
if (!lib)
|
|
return NULL;
|
|
|
|
g = (hw_gl_t*)calloc(1, sizeof(hw_gl_t));
|
|
|
|
if (!g) {
|
|
gl_status("out of memory");
|
|
dlclose(lib);
|
|
return NULL;
|
|
}
|
|
|
|
g->gpu = gpu;
|
|
g->gles_library = lib;
|
|
|
|
/* Default ON = the shipping behaviour; the markers only exist to reproduce the pre-fix
|
|
build as a control on the same binary. the backend. */
|
|
g->dbg_tri_bbox = !gl_debug_marker("hwgl_no_bbox");
|
|
g->dbg_upscale_parity = gl_debug_marker("hwgl_upscale_parity");
|
|
g->dbg_paint = gl_debug_marker("hwgl_paint_reject");
|
|
g->dbg_defer_dirty = !gl_debug_marker("hwgl_no_defer");
|
|
g->dbg_dump = gl_debug_marker("hwgl_dump");
|
|
|
|
/* `hwgl_no_adopt` forces the readback scanout path even when the seam is available, so
|
|
the zero-copy present can be A/B'd against its own binary on the same scene — which is
|
|
the only way numbers are comparable at all ( methodology). */
|
|
g->adopt_disabled = gl_debug_marker("hwgl_no_adopt");
|
|
|
|
/* two test markers. See the hw_gl_t comment: the trigger cannot fire on its own
|
|
while the software shadow makes GP0(C0) free, so it has to be armed deliberately. */
|
|
g->dbg_c0_trip = gl_debug_marker("hwgl_c0_trip");
|
|
g->dbg_force_downgrade = gl_debug_marker("hwgl_force_downgrade");
|
|
g->c0_limit = g->dbg_c0_trip ? 0u : kC0LimitDefault;
|
|
|
|
/* row 1 and its measurement, both off by default while the shadow is on. */
|
|
g->dbg_gpu_resolve = gl_debug_marker("hwgl_gpu_resolve");
|
|
g->dbg_vram_diff = gl_debug_marker("hwgl_vram_diff");
|
|
g->dbg_geom = gl_debug_marker("hwgl_geom");
|
|
g->geom_left = 8;
|
|
g->geom_draw_left = 12;
|
|
|
|
if (!gl_load_api(g, lib)) {
|
|
free(g);
|
|
dlclose(lib);
|
|
return NULL;
|
|
}
|
|
|
|
/* glGetString only returns NULL when no context is current on this thread, which
|
|
answers the whole availability question in one call. */
|
|
version = g->gl.GetString(GL_VERSION);
|
|
|
|
if (!version) {
|
|
if (!gl_make_own_context(g, is_angle)) {
|
|
armsx_hw_gl_destroy(&g->base);
|
|
return NULL;
|
|
}
|
|
|
|
version = g->gl.GetString(GL_VERSION);
|
|
|
|
if (!version) {
|
|
gl_status("made an EGL context but no GL is current on it; staying off the GL "
|
|
"rasterizer");
|
|
armsx_hw_gl_destroy(&g->base);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
g->have_context = 1;
|
|
|
|
/* The presentation backend normally records these strings first. SDL acceleration does
|
|
not use render_gl.cpp, however, and the rasterizer may also own an independent pbuffer
|
|
context. Record the context we are actually about to compile against before consulting
|
|
any per-driver gate below. This is particularly important on MediaTek Mali: the host hint
|
|
marks the SoC and these strings supply the Mali identity, together disabling the known-bad
|
|
framebuffer-fetch path instead of leaving an "unknown" profile to opt into it. */
|
|
{
|
|
const unsigned char* vendor = g->gl.GetString(GL_VENDOR);
|
|
const unsigned char* renderer = g->gl.GetString(GL_RENDERER);
|
|
armsx_gpu_profile_note_gl((const char*)vendor, (const char*)renderer,
|
|
(const char*)version);
|
|
}
|
|
|
|
/*
|
|
The mask bit, and the one capability it needs. the backend.
|
|
|
|
planned this as stencil work. It is not: the mask CHECK is a read-modify-write
|
|
against the destination, and the only mechanism in GLES that gives a fragment shader
|
|
the destination is framebuffer fetch. (Stencil can express the check, but its
|
|
reference value is per-DRAW while "set mask" varies per FRAGMENT, and nothing in
|
|
GLES 3.0 can read a stencil buffer back into vram_tex or into GP0(C0) — the resolve
|
|
and readback paths would have no way to carry bit 15. Alpha can carry it everywhere;
|
|
stencil cannot leave the FBO.)
|
|
|
|
So: with the extension, implement it properly. Without it, decline — and say exactly
|
|
why, because "the toggle does nothing" is what this cost the user once already. The
|
|
profile veto matters as much as the extension string: MediaTek Mali advertises
|
|
framebuffer fetch and returns zero or stale destination colour, and under ANGLE it has
|
|
been seen to crash the compiler outright (armsx_gpu_profile_t::fbfetch_gl). Either way
|
|
the mask CHECK would silently do nothing.
|
|
*/
|
|
if (psx_gpu_accuracy_flags(gpu) & PSX_GPU_ACCURACY_MASK_BIT) {
|
|
const armsx_gpu_profile_t* profile = armsx_gpu_profile_get();
|
|
const int have_ext = gl_has_extension(g, "GL_EXT_shader_framebuffer_fetch");
|
|
const int trusted = profile ? profile->fbfetch_gl : 1;
|
|
|
|
/* The framebuffer-fetch mask path is NEW and unverified on device: it landed the same
|
|
evening a regression appeared where the BIOS could not draw its own text, on an
|
|
Adreno 740 that reports have_ext=yes/trusted=yes and therefore took this branch.
|
|
Declining is the behaviour every confirmed fix was validated against (
|
|
mask-from-texel lives in the CPU rasterizer), so the safe default is to keep
|
|
declining until the GL path is proven against a real boot. Set ARMSX_GL_MASK_BIT=1
|
|
to opt in and test it.
|
|
|
|
The decision itself lives in armsx_hw_gl_mask_bit_supported() (gpu_hw_gl.h) so a
|
|
host test can pin it without a GL context — see tests/gpu_rasterizer_select.c. */
|
|
const int allow_gl_mask = armsx_hw_gl_mask_bit_opt_in();
|
|
|
|
if (armsx_hw_gl_mask_bit_supported(have_ext, trusted, is_angle, allow_gl_mask)) {
|
|
g->mask_mode = 1;
|
|
} else {
|
|
gl_status("accurate mask bit needs GL_EXT_shader_framebuffer_fetch (present=%s, "
|
|
"driver trusted=%s, angle=%s, opt-in=%s); using the CPU rasterizer, "
|
|
"which implements it",
|
|
have_ext ? "yes" : "no", trusted ? "yes" : "no", is_angle ? "yes" : "no",
|
|
allow_gl_mask ? "yes" : "no");
|
|
armsx_hw_gl_destroy(&g->base);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
g->gl.GetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture);
|
|
|
|
if (max_texture < 1024) {
|
|
gl_status("GL_MAX_TEXTURE_SIZE is %d; VRAM needs 1024", (int)max_texture);
|
|
armsx_hw_gl_destroy(&g->base);
|
|
return NULL;
|
|
}
|
|
|
|
if (scale < 1) scale = 1;
|
|
if (scale > ARMSX_HW_GL_MAX_SCALE) scale = ARMSX_HW_GL_MAX_SCALE;
|
|
|
|
while ((scale > 1) && ((1024 * scale) > max_texture))
|
|
scale--;
|
|
|
|
g->scale = scale;
|
|
g->rt_w = 1024 * scale;
|
|
g->rt_h = 512 * scale;
|
|
|
|
g->base.impl = g;
|
|
/* The shadow stays on deliberately — see the header. It is what makes GP0(C0) free and
|
|
turns two-way coherency into a one-way upload. */
|
|
/*
|
|
`hwgl_no_shadow` is step 3 — g->shadow = 0 — behind a marker instead of in the
|
|
default, and it exists for ONE reason: the economic case for the whole of is the
|
|
claim that most of the ~9 ms `emu` phase is the software shadow, and that claim
|
|
cannot be tested without turning the shadow off.
|
|
|
|
It is NOT the flip. row 1 is measurably not correct yet (: arming
|
|
`hwgl_gpu_resolve` moves the 1x parity window from 0.4708 % to 0.9986 %), so a
|
|
session run this way renders a picture that may be wrong and MUST NOT be used to
|
|
save a state — psx_gpu_save_state() serialises gpu->vram, which nothing refreshes
|
|
from the render target. The TIMING is valid regardless: the same primitives are
|
|
still submitted to the same backend, and the only thing that stops happening is the
|
|
software rasterizer.
|
|
*/
|
|
g->base.flags = gl_debug_marker("hwgl_no_shadow") ? 0u
|
|
: PSX_GPU_BACKEND_SOFTWARE_SHADOW;
|
|
g->shadow = (g->base.flags & PSX_GPU_BACKEND_SOFTWARE_SHADOW) ? 1 : 0;
|
|
/* Without the shadow the render target is the only copy of anything the rasterizer drew,
|
|
so row 1 is mandatory. `hwgl_gpu_resolve` turns it on while the shadow is still
|
|
there, which is how it gets an oracle. */
|
|
g->gpu_own = !g->shadow || g->dbg_gpu_resolve;
|
|
g->base.destroy = NULL; /* owned by armsx_hw_gl_destroy(), not by the core */
|
|
g->base.draw_poly = gl_draw_poly;
|
|
g->base.draw_rect = gl_draw_rect;
|
|
g->base.draw_line = gl_draw_line;
|
|
g->base.fill_vram = gl_fill_vram;
|
|
g->base.copy_vram = gl_copy_vram;
|
|
g->base.upload_vram = gl_upload_vram;
|
|
g->base.download_vram = gl_download_vram;
|
|
g->base.end_frame = gl_end_frame;
|
|
g->base.display_buffer = gl_display_buffer;
|
|
g->base.resolution_scale = gl_resolution_scale;
|
|
|
|
/* Programs. The draw one is assembled rather than a literal, because its mask stage is
|
|
compiled in or out and comes from psx/dev/gpu.h — see gl_build_draw_fs(). */
|
|
{
|
|
char* draw_fs = gl_build_draw_fs(g->mask_mode);
|
|
|
|
if (!draw_fs) {
|
|
gl_status("out of memory assembling the draw shader");
|
|
armsx_hw_gl_destroy(&g->base);
|
|
return NULL;
|
|
}
|
|
|
|
g->prog_draw = gl_link(g, kDrawVS, draw_fs, kDrawAttribs, 10, "draw");
|
|
free(draw_fs);
|
|
}
|
|
|
|
g->prog_xfer = gl_link(g, kXferVS, kXferFS, NULL, 0, "xfer");
|
|
g->prog_resolve = gl_link(g, kResolveVS, kResolveFS, NULL, 0, "resolve");
|
|
g->prog_gres = gl_link(g, kResolveVS, kGResFS, NULL, 0, "gres");
|
|
|
|
if (!g->prog_draw || !g->prog_xfer || !g->prog_resolve || !g->prog_gres) {
|
|
/* A driver that advertises the extension and then rejects the `inout` output is a
|
|
real failure mode (ANGLE); name it, without rewriting g_status — which already
|
|
carries the compiler's own message and is what the frontend logs. */
|
|
if (!g->prog_draw && g->mask_mode)
|
|
psxe_diag_logf("hwgl", "the draw shader that failed above is the mask-bit variant "
|
|
"(GL_EXT_shader_framebuffer_fetch); the driver advertised "
|
|
"the extension and then rejected it");
|
|
|
|
armsx_hw_gl_destroy(&g->base);
|
|
return NULL;
|
|
}
|
|
|
|
g->u_draw_rt_size = g->gl.GetUniformLocation(g->prog_draw, "u_rt_size");
|
|
g->u_draw_scale = g->gl.GetUniformLocation(g->prog_draw, "u_scale");
|
|
g->u_draw_vram = g->gl.GetUniformLocation(g->prog_draw, "u_vram");
|
|
g->u_draw_dither = g->gl.GetUniformLocation(g->prog_draw, "u_dither");
|
|
g->u_draw_dither_on = g->gl.GetUniformLocation(g->prog_draw, "u_dither_on");
|
|
g->u_draw_stp_pass = g->gl.GetUniformLocation(g->prog_draw, "u_stp_pass");
|
|
g->u_draw_paint = g->gl.GetUniformLocation(g->prog_draw, "u_paint");
|
|
g->u_draw_filter = g->gl.GetUniformLocation(g->prog_draw, "u_filter");
|
|
g->u_draw_repl = g->gl.GetUniformLocation(g->prog_draw, "u_repl");
|
|
g->u_draw_tex_trunc = g->gl.GetUniformLocation(g->prog_draw, "u_tex_trunc");
|
|
g->u_draw_mask_texel = g->gl.GetUniformLocation(g->prog_draw, "u_mask_texel");
|
|
g->u_xfer_rect = g->gl.GetUniformLocation(g->prog_xfer, "u_rect");
|
|
g->u_xfer_rt_size = g->gl.GetUniformLocation(g->prog_xfer, "u_rt_size");
|
|
g->u_xfer_scale = g->gl.GetUniformLocation(g->prog_xfer, "u_scale");
|
|
g->u_xfer_vram = g->gl.GetUniformLocation(g->prog_xfer, "u_vram");
|
|
g->u_res_rt = g->gl.GetUniformLocation(g->prog_resolve, "u_rt");
|
|
g->u_res_origin = g->gl.GetUniformLocation(g->prog_resolve, "u_origin");
|
|
g->u_res_limit = g->gl.GetUniformLocation(g->prog_resolve, "u_limit");
|
|
g->u_res_step = g->gl.GetUniformLocation(g->prog_resolve, "u_step");
|
|
g->u_res_box = g->gl.GetUniformLocation(g->prog_resolve, "u_box");
|
|
g->u_res_mask = g->gl.GetUniformLocation(g->prog_resolve, "u_mask");
|
|
g->u_gres_rt = g->gl.GetUniformLocation(g->prog_gres, "u_rt");
|
|
g->u_gres_limit = g->gl.GetUniformLocation(g->prog_gres, "u_limit");
|
|
g->u_gres_step = g->gl.GetUniformLocation(g->prog_gres, "u_step");
|
|
g->u_gres_half = g->gl.GetUniformLocation(g->prog_gres, "u_half");
|
|
g->u_gres_mask = g->gl.GetUniformLocation(g->prog_gres, "u_mask");
|
|
|
|
/* Render target. */
|
|
g->gl.GenTextures(1, &g->rt_tex);
|
|
g->gl.ActiveTexture(GL_TEXTURE0);
|
|
g->gl.BindTexture(GL_TEXTURE_2D, g->rt_tex);
|
|
g->gl.TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, g->rt_w, g->rt_h, 0, GL_RGBA,
|
|
GL_UNSIGNED_BYTE, NULL);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
|
|
g->gl.GenFramebuffers(1, &g->rt_fbo);
|
|
g->gl.BindFramebuffer(GL_FRAMEBUFFER, g->rt_fbo);
|
|
g->gl.FramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
|
g->rt_tex, 0);
|
|
|
|
if (g->gl.CheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
|
gl_status("render target %dx%d is not framebuffer-complete", g->rt_w, g->rt_h);
|
|
g->gl.BindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
armsx_hw_gl_destroy(&g->base);
|
|
return NULL;
|
|
}
|
|
|
|
/* Native-resolution VRAM mirror. R16UI so the shader does exact integer bit extraction
|
|
with no float round trip — RGBA5551 sampled as normalized float could not index a
|
|
CLUT exactly (the backend). */
|
|
g->gl.GenTextures(1, &g->vram_tex);
|
|
g->gl.BindTexture(GL_TEXTURE_2D, g->vram_tex);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
g->gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
g->gl.PixelStorei(GL_UNPACK_ALIGNMENT, 2);
|
|
g->gl.PixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
|
g->gl.TexImage2D(GL_TEXTURE_2D, 0, GL_R16UI, 1024, 512, 0, GL_RED_INTEGER,
|
|
GL_UNSIGNED_SHORT, gpu->vram);
|
|
|
|
g->gl.GenBuffers(1, &g->vbo);
|
|
g->gl.GenVertexArrays(1, &g->vao);
|
|
g->gl.GenVertexArrays(1, &g->quad_vao);
|
|
gl_setup_attribs(g);
|
|
|
|
/* Dither kernel: a plain uniform int[16] rather than a texture. Indexed by the NATIVE
|
|
coordinate in the shader, so the pattern is scale-invariant. */
|
|
g->gl.UseProgram(g->prog_draw);
|
|
g->gl.Uniform1iv(g->u_draw_dither, 16, g_psx_gpu_dither_kernel);
|
|
g->gl.Uniform1i(g->u_draw_dither_on, 1);
|
|
g->gl.Uniform1i(g->u_draw_stp_pass, 0);
|
|
g->gl.Uniform1i(g->u_draw_paint, g->dbg_paint);
|
|
g->gl.Uniform1i(g->u_draw_mask_texel, g->mask_mode);
|
|
|
|
/* Prove the mask bit on THIS driver before anything depends on it — and do it before the
|
|
seed below, which overwrites every pixel the probe touched. Skipped only under
|
|
`hwgl_paint_reject`, which replaces every fragment's colour with a coverage marker and
|
|
would fail the probe by design. */
|
|
g->rt_bound = 0;
|
|
|
|
if (g->mask_mode && !g->dbg_paint && !gl_mask_selftest(g)) {
|
|
armsx_hw_gl_destroy(&g->base);
|
|
return NULL;
|
|
}
|
|
|
|
/* Seed the render target from whatever VRAM already holds — a backend installed at boot
|
|
sees a cleared surface, one installed after a load-state sees the restored one. */
|
|
g->rt_bound = 0;
|
|
gl_blit_from_vram(g, 0, 0, 1024, 512);
|
|
gl_release(g);
|
|
|
|
if (g->gl.GetError() != GL_NO_ERROR) {
|
|
gl_status("GL errors during setup; refusing to install the GL rasterizer");
|
|
armsx_hw_gl_destroy(&g->base);
|
|
return NULL;
|
|
}
|
|
|
|
/* Every state this line reports is settings- or marker-driven, and records that a
|
|
pushed settings.toml does not reliably survive to the core. This line is the proof that
|
|
what is running is what was asked for; nothing downstream should be believed without
|
|
it. */
|
|
gl_status("GL rasterizer up: %s, %s context, scale=%dx, target=%dx%d, mask=%s, "
|
|
"zerocopy=%s, shadow=%s, c0limit=%llu/frame%s",
|
|
is_angle ? "ANGLE" : "system GLES",
|
|
g->owns_context ? "own EGL" : (present_is_gl ? "shared with present"
|
|
: "pre-existing"),
|
|
g->scale, g->rt_w, g->rt_h,
|
|
g->mask_mode ? "on (fbfetch)" : "off",
|
|
g->adopt_disabled ? "off (hwgl_no_adopt)"
|
|
: (g->owns_context ? "unavailable (own context)" : "armed"),
|
|
g->shadow ? "on" : "off",
|
|
(unsigned long long)g->c0_limit,
|
|
g->dbg_force_downgrade ? " forcedowngrade=frame900"
|
|
: (g->dbg_c0_trip ? " (hwgl_c0_trip)" : ""));
|
|
/* Separate line rather than more fields on the one above: gl_status() has a 192-byte
|
|
buffer and silently truncates, which would take the shadow= field with it. */
|
|
psxe_diag_logf("hwgl", "coherency: gpuown=%s%s vramdiff=%s max_texture=%d",
|
|
g->gpu_own ? "on" : "off",
|
|
(g->gpu_own && g->shadow) ? " (hwgl_gpu_resolve, shadow still on)" : "",
|
|
g->dbg_vram_diff ? "every 600 frames" : "off", (int)max_texture);
|
|
psxe_diag_logf("hwgl", "GL_VERSION=%s renderer=%s", (const char*)version,
|
|
(const char*)(g->gl.GetString(GL_RENDERER)
|
|
? g->gl.GetString(GL_RENDERER)
|
|
: (const unsigned char*)"?"));
|
|
|
|
return &g->base;
|
|
}
|
|
|
|
void armsx_hw_gl_destroy(psx_gpu_backend_t* backend) {
|
|
hw_gl_t* g = (hw_gl_t*)backend;
|
|
|
|
if (!g)
|
|
return;
|
|
|
|
if (!g->have_context)
|
|
goto release_libraries;
|
|
|
|
g->gl.BindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
|
|
if (g->gl.DeleteTextures) {
|
|
if (g->rt_tex) g->gl.DeleteTextures(1, &g->rt_tex);
|
|
if (g->vram_tex) g->gl.DeleteTextures(1, &g->vram_tex);
|
|
if (g->repl_tex) g->gl.DeleteTextures(1, &g->repl_tex);
|
|
if (g->scratch_tex) g->gl.DeleteTextures(1, &g->scratch_tex);
|
|
if (g->resolve_tex) g->gl.DeleteTextures(1, &g->resolve_tex);
|
|
if (g->xfer_tex) g->gl.DeleteTextures(1, &g->xfer_tex);
|
|
}
|
|
|
|
if (g->gl.DeleteFramebuffers) {
|
|
if (g->rt_fbo) g->gl.DeleteFramebuffers(1, &g->rt_fbo);
|
|
if (g->vram_fbo) g->gl.DeleteFramebuffers(1, &g->vram_fbo);
|
|
if (g->scratch_fbo) g->gl.DeleteFramebuffers(1, &g->scratch_fbo);
|
|
if (g->resolve_fbo) g->gl.DeleteFramebuffers(1, &g->resolve_fbo);
|
|
if (g->xfer_fbo) g->gl.DeleteFramebuffers(1, &g->xfer_fbo);
|
|
}
|
|
|
|
if (g->gl.DeleteBuffers && g->vbo)
|
|
g->gl.DeleteBuffers(1, &g->vbo);
|
|
|
|
if (g->gl.DeleteVertexArrays) {
|
|
if (g->vao) g->gl.DeleteVertexArrays(1, &g->vao);
|
|
if (g->quad_vao) g->gl.DeleteVertexArrays(1, &g->quad_vao);
|
|
}
|
|
|
|
if (g->gl.DeleteProgram) {
|
|
if (g->prog_draw) g->gl.DeleteProgram(g->prog_draw);
|
|
if (g->prog_xfer) g->gl.DeleteProgram(g->prog_xfer);
|
|
if (g->prog_resolve) g->gl.DeleteProgram(g->prog_resolve);
|
|
if (g->prog_gres) g->gl.DeleteProgram(g->prog_gres);
|
|
}
|
|
|
|
release_libraries:
|
|
|
|
if (g->owns_context && g->egl_library) {
|
|
EGLBoolean (*MakeCurrent)(EGLDisplay, EGLSurface, EGLSurface, EGLContext);
|
|
EGLBoolean (*DestroyContext)(EGLDisplay, EGLContext);
|
|
EGLBoolean (*DestroySurface)(EGLDisplay, EGLSurface);
|
|
|
|
*(void**)&MakeCurrent = dlsym(g->egl_library, "eglMakeCurrent");
|
|
*(void**)&DestroyContext = dlsym(g->egl_library, "eglDestroyContext");
|
|
*(void**)&DestroySurface = dlsym(g->egl_library, "eglDestroySurface");
|
|
|
|
if (MakeCurrent)
|
|
MakeCurrent(g->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
|
|
|
if (DestroySurface && (g->egl_surface != EGL_NO_SURFACE))
|
|
DestroySurface(g->egl_display, g->egl_surface);
|
|
|
|
if (DestroyContext && (g->egl_context != EGL_NO_CONTEXT))
|
|
DestroyContext(g->egl_display, g->egl_context);
|
|
|
|
/* No eglTerminate: the presentation backend may share this display. */
|
|
}
|
|
|
|
free(g->verts);
|
|
free(g->ranges);
|
|
free(g->readback);
|
|
free(g->native_rb);
|
|
|
|
if (g->egl_library)
|
|
dlclose(g->egl_library);
|
|
|
|
if (g->gles_library)
|
|
dlclose(g->gles_library);
|
|
|
|
free(g);
|
|
}
|
|
|
|
#else /* !ARMSX_HW_GL_BUILD */
|
|
|
|
#ifdef USE_HARDWARE
|
|
|
|
psx_gpu_backend_t* armsx_hw_gl_create(psx_gpu_t* gpu, int scale) {
|
|
(void)gpu;
|
|
(void)scale;
|
|
return NULL;
|
|
}
|
|
|
|
void armsx_hw_gl_destroy(psx_gpu_backend_t* backend) {
|
|
(void)backend;
|
|
}
|
|
|
|
/* Accepted and discarded on platforms without the GLES backend, so the frontend can push
|
|
settings unconditionally instead of guarding every call site. */
|
|
void armsx_hw_gl_set_video_options(int texture_filter, int downsample, int line_detect) {
|
|
(void)texture_filter;
|
|
(void)downsample;
|
|
(void)line_detect;
|
|
}
|
|
|
|
int armsx_hw_gl_failed(const psx_gpu_backend_t* backend) {
|
|
(void)backend;
|
|
return 0;
|
|
}
|
|
|
|
const char* armsx_hw_gl_status(void) {
|
|
return "not built on this platform";
|
|
}
|
|
|
|
int armsx_hw_gl_present_texture(psx_gpu_backend_t* backend, struct armsx_renderer* renderer) {
|
|
(void)backend;
|
|
(void)renderer;
|
|
return 0;
|
|
}
|
|
|
|
#endif
|
|
|
|
#endif
|