mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Merge pull request #587 from RetroPortingToolKit/feat/gl-accurate-blending
GL: correct PS1 blending by default + blend/mask accuracy test
This commit is contained in:
@@ -486,15 +486,16 @@ between Play and TCP availability.
|
||||
|
||||
## Complete command index (generated)
|
||||
|
||||
**368 commands registered** — 355 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
**369 commands registered** — 356 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
|
||||
82 of 368 have prose above; **286 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
82 of 369 have prose above; **287 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
|
||||
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
|
||||
|
||||
| Command | Native | Beetle | Described above |
|
||||
|---|:--:|:--:|:--:|
|
||||
| `a0_history` | ✓ | | |
|
||||
| `accurate_blending` | ✓ | | |
|
||||
| `audio_events` | ✓ | ✓ | |
|
||||
| `audio_stats` | ✓ | ✓ | |
|
||||
| `audio_wav` | ✓ | ✓ | |
|
||||
|
||||
@@ -698,6 +698,34 @@ keep their exact texels, and `off` is the historical path. Cost is up to 64
|
||||
fetches per minified pixel. OpenGL only. `PSX_TEXTURE_LOD=0|1` and
|
||||
`PSX_ANISO=N` override them for one run; the `texture_lod` TCP command
|
||||
switches them live.
|
||||
### Accurate blending (`accurate_blending`)
|
||||
|
||||
```toml
|
||||
[video]
|
||||
accurate_blending = true # default: PS1 colour/blend correctness
|
||||
```
|
||||
|
||||
Accurate blending is enabled by default because it corrects PS1 rendering.
|
||||
`false` retains the historical path for diagnosis.
|
||||
|
||||
The OpenGL surface holds each 5-bit VRAM channel as an 8-bit value. Uploads,
|
||||
copies and untextured primitives store `k*8`, but fills and texels have
|
||||
always stored `k*255/31` (full-range white). Semi-transparent blending of two
|
||||
operands in different bases, truncated back to 5 bits when the game reads
|
||||
VRAM, is off by one step in places: B+F/4 over a fill, B/2+F/2 with an odd
|
||||
sum, B+F/4 of a texel. `true` makes every VRAM write use `k*8`, so GL's
|
||||
8-bit blend followed by the readback's `>>3` is exactly the PS1's 5-bit
|
||||
arithmetic in all four modes (B/2+F/2, B+F, B-F, B+F/4), with set-mask,
|
||||
mask check, texel STP and PGXP depth, at any internal scale and on drivers that quantise the blend constant to 8 bits (it uses 128/255 and 64/255, exact in unorm8, instead of 0.5 and 0.25) (checked against
|
||||
the formulas and the software rasterizer by `gl_blend_accuracy_test`). The
|
||||
visible cost: textured and filled whites present as 248 instead of 255, as
|
||||
uploaded images and untextured primitives already do. One divergence
|
||||
remains in both settings: a second semi-transparent layer blends with the
|
||||
first layer's full 8-bit result where the console would have truncated it to
|
||||
5 bits first (at most one 5-bit step). OpenGL only.
|
||||
`PSX_ACCURATE_BLENDING=0|1` overrides it for one run and the
|
||||
`accurate_blending` TCP command switches it live.
|
||||
|
||||
### Texture filtering (`texture_filtering`)
|
||||
|
||||
```toml
|
||||
|
||||
@@ -1 +1 @@
|
||||
fcb3412a5245142593c6d5cdaabfda1f5d58ff878e09b175fd8aa10dee1554db
|
||||
e7c8c626e955b89839c5dda1d9990857d87a0042a1cdc1071bbdc42b754a4146
|
||||
|
||||
@@ -836,6 +836,8 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
"[video] anisotropic_filtering must be 1..16: {}", n));
|
||||
rt.video_anisotropic_filtering = n;
|
||||
}
|
||||
if (video.contains("accurate_blending"))
|
||||
rt.video_accurate_blending = toml::find<bool>(video, "accurate_blending");
|
||||
if (video.contains("pgxp_depth_buffer"))
|
||||
rt.video_pgxp_depth_buffer = toml::find<bool>(video, "pgxp_depth_buffer");
|
||||
if (video.contains("pgxp_color_correction"))
|
||||
|
||||
@@ -574,6 +574,10 @@ struct RuntimeConfig {
|
||||
// default 1); only with texture_lod. OpenGL only.
|
||||
int video_texture_lod = 0;
|
||||
int video_anisotropic_filtering = 1;
|
||||
// accurate_blending: one 8-bit basis (k*8) for every VRAM write so GL
|
||||
// semi-transparency truncates exactly like the PS1's 5-bit blend.
|
||||
// Default true: PS1 colour/blend correctness. OpenGL only.
|
||||
bool video_accurate_blending = true;
|
||||
bool video_pgxp_color_correction = false;
|
||||
int video_pgxp_seam = 0;
|
||||
double video_pgxp_depth_threshold = 4096.0;
|
||||
|
||||
@@ -281,6 +281,19 @@ if(BUILD_TESTING)
|
||||
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-texture-lod-results")
|
||||
set_tests_properties(gl_texture_lod_test PROPERTIES
|
||||
LABELS "gpu;opengl;hardware" TIMEOUT 180 SKIP_RETURN_CODE 77)
|
||||
# PS1 semi-transparency modes and mask bit at 4x with PGXP depth and
|
||||
# the render thread, against the formulas and the software reference
|
||||
# ([video] accurate_blending on; off is measured and printed).
|
||||
add_test(NAME gl_blend_accuracy_test
|
||||
COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_render_thread.py" --single
|
||||
--fixture "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_blend_accuracy.c"
|
||||
--cc "${CMAKE_C_COMPILER}"
|
||||
--sdl-include "${_gl_scale_sdl_inc}"
|
||||
--sdl-library "$<TARGET_FILE:SDL3::SDL3>"
|
||||
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-blend-accuracy-results")
|
||||
set_tests_properties(gl_blend_accuracy_test PROPERTIES
|
||||
LABELS "gpu;opengl;hardware" TIMEOUT 180 SKIP_RETURN_CODE 77)
|
||||
# texture_filtering = "xbr": diagonals lose the staircase, straight
|
||||
# edges, flat areas, cutouts and 1:1 draws stay nearest; RT agrees.
|
||||
add_test(NAME gl_xbr_filter_test
|
||||
|
||||
@@ -297,6 +297,12 @@ void gl_renderer_set_pgxp_depth(int on);
|
||||
void gl_renderer_set_texture_lod(int mode, int aniso);
|
||||
int gl_renderer_texture_lod(void);
|
||||
int gl_renderer_anisotropy(void);
|
||||
/* [video] accurate_blending: store every VRAM write in the k*8/255 basis so
|
||||
* 8-bit GL blending truncated to 5 bits is exactly the PS1's semi-transparency
|
||||
* arithmetic (whites of fills and textures become 248 instead of 255, as
|
||||
* uploads and untextured primitives already are). On by default. */
|
||||
void gl_renderer_set_accurate_blending(int on);
|
||||
int gl_renderer_accurate_blending(void);
|
||||
int gl_renderer_get_pgxp_depth(void);
|
||||
void gl_renderer_set_pgxp_color_perspective(int on);
|
||||
int gl_renderer_get_pgxp_color_perspective(void);
|
||||
|
||||
@@ -8629,6 +8629,15 @@ static void handle_texture_lod(int id, const char *json)
|
||||
gl_renderer_texture_lod(), gl_renderer_anisotropy());
|
||||
}
|
||||
|
||||
/* accurate_blending: {"cmd":"accurate_blending"} reads, {"cmd":
|
||||
* "accurate_blending","on":0|1} sets [video] accurate_blending live. */
|
||||
static void handle_accurate_blending(int id, const char *json)
|
||||
{
|
||||
int on = json_get_int(json, "on", -1);
|
||||
if (on >= 0) gl_renderer_set_accurate_blending(on);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"on\":%d}", id, gl_renderer_accurate_blending());
|
||||
}
|
||||
|
||||
/* texture_filter: {"cmd":"texture_filter"} reads, {"cmd":"texture_filter",
|
||||
* "mode":0..3} sets it live (nearest, bilinear, stable world, xbr). */
|
||||
static void handle_texture_filter(int id, const char *json)
|
||||
@@ -15863,6 +15872,8 @@ static const CmdEntry s_commands[] = {
|
||||
{ "post_aa", handle_post_aa },
|
||||
{ "fmv_chroma", handle_fmv_chroma },
|
||||
{ "texture_lod", handle_texture_lod },
|
||||
{ "accurate_blending", handle_accurate_blending },
|
||||
|
||||
{ "texture_filter", handle_texture_filter },
|
||||
{ "gl_ws_ablate", handle_gl_ws_ablate },
|
||||
{ "gl_interp", handle_gl_interp },
|
||||
|
||||
@@ -902,6 +902,23 @@ static int s_tex_filter = 0;
|
||||
/* [video] texture_lod (0 off, 1 mipmap emulation) and anisotropic_filtering
|
||||
* (taps along the footprint's major axis, 1..16). */
|
||||
static int s_tex_lod = 0, s_tex_aniso = 1;
|
||||
/* [video] accurate_blending. The FBO holds 5-bit channels as 8-bit values:
|
||||
* uploads, copies and untextured primitives already store k*8, but fills and
|
||||
* texels have stored k*255/31 (full-range white). Blending two operands in
|
||||
* different bases and truncating to 5 bits at readback is off by one in
|
||||
* places (e.g. B+F/4, B/2+F/2 with odd sums). On, every VRAM write uses
|
||||
* k*8, so GL's 8-bit blend followed by >>3 is exactly the PS1's 5-bit
|
||||
* arithmetic. On by default; off is the historical diagnostic path. */
|
||||
static int s_accurate_blend = 1;
|
||||
static void c5_to_float(uint16_t c, float *r, float *g, float *b) {
|
||||
if (s_accurate_blend) {
|
||||
*r = (float)((c & 0x1F) << 3) / 255.0f;
|
||||
*g = (float)(((c >> 5) & 0x1F) << 3) / 255.0f;
|
||||
*b = (float)(((c >> 10) & 0x1F) << 3) / 255.0f;
|
||||
} else {
|
||||
*r = (c & 0x1F) / 31.0f; *g = ((c >> 5) & 0x1F) / 31.0f; *b = ((c >> 10) & 0x1F) / 31.0f;
|
||||
}
|
||||
}
|
||||
/* Opaque textured draws carry the exact mask bit in FBO alpha. Keeping the
|
||||
* duplicate stencil copy current is deferred until mask checking is requested. */
|
||||
static int s_stencil_valid = 1;
|
||||
@@ -1955,8 +1972,10 @@ static const char *TEX_FS =
|
||||
" }\n"
|
||||
" return vram_at(v_tpage.x + u, v_tpage.y + v);\n"
|
||||
"}\n"
|
||||
"uniform int u_accurate; /* [video] accurate_blending: VRAM in the k*8/255 basis */\n"
|
||||
"vec3 col5(int raw){\n"
|
||||
" return vec3(float(raw & 31), float((raw >> 5) & 31), float((raw >> 10) & 31)) / 31.0;\n"
|
||||
" vec3 k = vec3(float(raw & 31), float((raw >> 5) & 31), float((raw >> 10) & 31));\n"
|
||||
" return u_accurate != 0 ? k * (8.0 / 255.0) : k / 31.0;\n"
|
||||
"}\n"
|
||||
"int stable_texel(ivec2 p){\n"
|
||||
" /* Clamp before wrap, so a footprint left of u=0 cannot pick up u=255. */\n"
|
||||
@@ -2287,9 +2306,18 @@ static void apply_psx_blend(int mode) {
|
||||
p_glBlendEquationSeparate((mode & 3) == 2 ? PSXGL_FUNC_REVERSE_SUBTRACT
|
||||
: PSXGL_FUNC_ADD,
|
||||
PSXGL_FUNC_ADD);
|
||||
/* [video] accurate_blending: drivers quantise the constant blend colour
|
||||
* to the target's 8-bit unorm (NVIDIA: 0.5 -> 127/255, 0.25 -> 63/255),
|
||||
* which puts B/2+F/2 and B+F/4 one 8-bit step under a k*8 boundary and
|
||||
* the >>3 readback one 5-bit step low. 128/255 and 64/255 are exact in
|
||||
* unorm8 and bias every result up by less than one 8-bit step, which
|
||||
* stays inside the PS1's truncation bucket for all 5-bit operands, so the
|
||||
* result is the same on drivers that blend in float and in fixed point. */
|
||||
const float half = s_accurate_blend ? 128.0f / 255.0f : 0.5f;
|
||||
const float quarter = s_accurate_blend ? 64.0f / 255.0f : 0.25f;
|
||||
switch (mode & 3) {
|
||||
case 0:
|
||||
p_glBlendColor(0.5f, 0.5f, 0.5f, 0.5f);
|
||||
p_glBlendColor(half, half, half, half);
|
||||
p_glBlendFuncSeparate(PSXGL_CONSTANT_ALPHA, PSXGL_CONSTANT_ALPHA, GL_ONE, GL_ZERO);
|
||||
break;
|
||||
case 1:
|
||||
@@ -2297,7 +2325,7 @@ static void apply_psx_blend(int mode) {
|
||||
p_glBlendFuncSeparate(GL_ONE, GL_ONE, GL_ONE, GL_ZERO);
|
||||
break;
|
||||
case 3:
|
||||
p_glBlendColor(0.25f, 0.25f, 0.25f, 0.25f);
|
||||
p_glBlendColor(quarter, quarter, quarter, quarter);
|
||||
p_glBlendFuncSeparate(PSXGL_CONSTANT_ALPHA, GL_ONE, GL_ONE, GL_ZERO);
|
||||
break;
|
||||
}
|
||||
@@ -4855,7 +4883,8 @@ static void gpu_fill(int x,int y,int w,int h,uint16_t c) {
|
||||
flush_flat_batch();
|
||||
flush_tex_batch();
|
||||
flush_cpu_upload();
|
||||
float r=(c&0x1F)/31.0f, g=((c>>5)&0x1F)/31.0f, b=((c>>10)&0x1F)/31.0f;
|
||||
float r, g, b;
|
||||
c5_to_float(c, &r, &g, &b);
|
||||
x &= VRAM_W - 1; y &= VRAM_H - 1;
|
||||
if (w > VRAM_W) w = VRAM_W;
|
||||
if (h > VRAM_H) h = VRAM_H;
|
||||
@@ -4988,6 +5017,21 @@ void gl_renderer_set_texture_lod(int mode, int aniso) {
|
||||
}
|
||||
int gl_renderer_texture_lod(void) { return s_tex_lod; }
|
||||
int gl_renderer_anisotropy(void) { return s_tex_aniso; }
|
||||
static GLint s_uAccurate = -1;
|
||||
static void accurate_apply_uniforms(void) {
|
||||
if (!s_tex_prog) return;
|
||||
GLint prev = 0; glGetIntegerv(GL_CURRENT_PROGRAM, &prev);
|
||||
if (s_uAccurate < 0) s_uAccurate = p_glGetUniformLocation(s_tex_prog, "u_accurate");
|
||||
p_glUseProgram(s_tex_prog); p_glUniform1i(s_uAccurate, s_accurate_blend);
|
||||
p_glUseProgram((GLuint)prev);
|
||||
}
|
||||
void gl_renderer_set_accurate_blending(int on) {
|
||||
GL_RT_SYNC("accurate_blending");
|
||||
if (s_ctx) { flush_flat_batch(); flush_tex_batch(); hiw_flush_queue(); }
|
||||
s_accurate_blend = on ? 1 : 0;
|
||||
if (s_ctx) accurate_apply_uniforms();
|
||||
}
|
||||
int gl_renderer_accurate_blending(void) { return s_accurate_blend; }
|
||||
static void glb_set_texture_filter(int b) { s_tex_filter = b >= 0 && b <= 3 ? b : 0; sw_set_texture_filter(b != 0); }
|
||||
static int glb_texture_filter(void) { return s_tex_filter; }
|
||||
|
||||
@@ -5853,6 +5897,8 @@ static int init_gpu_raster(void) {
|
||||
s_uFilter = p_glGetUniformLocation(s_tex_prog, "u_filter");
|
||||
s_uAniso = -1;
|
||||
lod_apply_uniforms();
|
||||
s_uAccurate = -1;
|
||||
accurate_apply_uniforms();
|
||||
s_uLimits = p_glGetUniformLocation(s_tex_prog, "u_limits");
|
||||
s_uHdTexture = p_glGetUniformLocation(s_tex_prog, "u_hd_texture");
|
||||
s_uBlitSrc = p_glGetUniformLocation(s_blit_prog, "u_src");
|
||||
@@ -6826,9 +6872,8 @@ static void glb_wide_clear(int base_x, int y, int h, uint16_t color) {
|
||||
if (y0 < 0) y0 = 0;
|
||||
if (y1 > H) y1 = H;
|
||||
if (y1 <= y0) return;
|
||||
float r = (color & 0x1F) / 31.0f;
|
||||
float g = ((color >> 5) & 0x1F) / 31.0f;
|
||||
float b = ((color >> 10) & 0x1F) / 31.0f;
|
||||
float r, g, b;
|
||||
c5_to_float(color, &r, &g, &b);
|
||||
float a = (color >> 15) & 1 ? 1.0f : 0.0f;
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, fbo);
|
||||
glViewport(0, 0, g_wide_w * s_out_scale, H);
|
||||
@@ -6866,9 +6911,8 @@ static void glb_wide_clear_margins(int base_x, int y, int h, uint16_t color, int
|
||||
s_cw_wide_ms += cw_ms() - t0;
|
||||
return;
|
||||
}
|
||||
float r = (color & 0x1F) / 31.0f;
|
||||
float g = ((color >> 5) & 0x1F) / 31.0f;
|
||||
float b = ((color >> 10) & 0x1F) / 31.0f;
|
||||
float r, g, b;
|
||||
c5_to_float(color, &r, &g, &b);
|
||||
float a = (color >> 15) & 1 ? 1.0f : 0.0f;
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, fbo);
|
||||
glViewport(0, 0, W, H);
|
||||
|
||||
@@ -1286,6 +1286,7 @@ static int g_video_pgxp_depth_buffer = 0;
|
||||
static int g_video_fmv_chroma = 0; /* [video] fmv_chroma_smoothing / PSX_FMV_CHROMA */
|
||||
/* [video] texture_lod / anisotropic_filtering; PSX_TEXTURE_LOD / PSX_ANISO. */
|
||||
static int g_video_texture_lod = 0, g_video_aniso = 1;
|
||||
static int g_video_accurate_blending = 1; /* [video] accurate_blending / PSX_ACCURATE_BLENDING */
|
||||
static int g_video_pgxp_color_correction = 0;
|
||||
static int g_video_pgxp_seam = 0;
|
||||
static float g_video_pgxp_depth_threshold = 4096.0f;
|
||||
@@ -16106,6 +16107,7 @@ int main(int argc, char** argv) {
|
||||
g_video_fmv_chroma = gc.runtime.video_fmv_chroma_smoothing ? 1 : 0;
|
||||
g_video_texture_lod = gc.runtime.video_texture_lod;
|
||||
g_video_aniso = gc.runtime.video_anisotropic_filtering;
|
||||
g_video_accurate_blending = gc.runtime.video_accurate_blending ? 1 : 0;
|
||||
g_video_pgxp_color_correction = gc.runtime.video_pgxp_color_correction ? 1 : 0;
|
||||
g_video_pgxp_seam = gc.runtime.video_pgxp_seam;
|
||||
g_video_pgxp_depth_threshold = (float)gc.runtime.video_pgxp_depth_threshold;
|
||||
@@ -18517,6 +18519,8 @@ session_reboot:
|
||||
if (const char* e = std::getenv("PSX_TEXTURE_LOD")) g_video_texture_lod = std::atoi(e);
|
||||
if (const char* e = std::getenv("PSX_ANISO")) g_video_aniso = std::atoi(e);
|
||||
gl_renderer_set_texture_lod(g_video_texture_lod, g_video_aniso);
|
||||
if (const char* e = std::getenv("PSX_ACCURATE_BLENDING")) g_video_accurate_blending = (*e && *e != '0');
|
||||
gl_renderer_set_accurate_blending(g_video_accurate_blending);
|
||||
gl_renderer_set_pgxp_color_perspective(g_video_pgxp_color_correction);
|
||||
gl_renderer_set_pgxp_seam(g_video_pgxp_seam);
|
||||
if (const char* e = std::getenv("PSX_PGXP_DEPTH_THRESHOLD")) g_video_pgxp_depth_threshold = (float)std::atof(e);
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
/* PS1 semi-transparency and mask-bit accuracy of the OpenGL renderer at
|
||||
* internal resolution with PGXP depth, on a real, hidden OpenGL context.
|
||||
*
|
||||
* The same scene goes through the renderer facade twice: once on the
|
||||
* software rasterizer (the reference) and once on OpenGL at 4x internal
|
||||
* resolution, with PGXP depth off and on, directly and on the render thread.
|
||||
* Every mode (0: B/2+F/2, 1: B+F, 2: B-F, 3: B+F/4) is drawn over a ramp of
|
||||
* background colours, as flat rects, depth-tagged flat triangles, raw
|
||||
* textured rects whose texels mix STP=1 (blended), STP=0 (opaque) and 0
|
||||
* (transparent), and modulated textured triangles; then the mask bit: rects
|
||||
* drawn with set-mask, and semi-transparent draws over them with mask check.
|
||||
*
|
||||
* With [video] accurate_blending (every VRAM write in one 8-bit basis) the
|
||||
* following must hold; with it off (the historical bases) the mismatch count
|
||||
* is printed for the record.
|
||||
* Checked: the native VRAM a game reads back equals the software reference
|
||||
* exactly in every configuration, and the flat rects also equal the PS1
|
||||
* formula computed here (an oracle independent of both renderers): per 5-bit
|
||||
* channel, mode 0 = (B+F)>>1, 1 = min(B+F,31), 2 = max(B-F,0),
|
||||
* 3 = min(B+(F>>2),31); mask bit = set-mask OR source STP; a masked
|
||||
* destination is never written while check is on.
|
||||
* Original source-owned scene; no retail payload. */
|
||||
#define main rt_fixture_main
|
||||
#include "test_gl_render_thread.c"
|
||||
#undef main
|
||||
|
||||
#define TP 0x0108 /* 15-bit page at (512,0) */
|
||||
static const uint16_t BG[8] = {0x0000, 0x7fff, 0x4210, 0x001f, 0x03e0, 0x7c00, 0x1ce7, 0x6318};
|
||||
static const uint16_t FG[4] = {0x7fff, 0x2d6b, 0x4210, 0x0c63};
|
||||
|
||||
static void page(void) {
|
||||
static uint16_t pg[16 * 16];
|
||||
for (int y = 0; y < 16; y++)
|
||||
for (int x = 0; x < 16; x++) {
|
||||
uint16_t c = (uint16_t)(0x0421 * (1 + ((x + 2 * y) % 30)));
|
||||
int k = (x + y) % 4;
|
||||
pg[y * 16 + x] = k == 0 ? 0 : k == 1 ? (uint16_t)(c | 0x8000) : c;
|
||||
}
|
||||
cpu_upload(512, 0, 16, 16, pg);
|
||||
}
|
||||
static void scene(int depth) {
|
||||
gr_set_draw_area(0, 0, 319, 239);
|
||||
gr_set_draw_offset(0, 0);
|
||||
mask_bits(0, 0);
|
||||
gr_set_semi_transparency(0, 0);
|
||||
for (int i = 0; i < 8; i++) gr_fill_rect(0, i * 30, 320, 30, BG[i]);
|
||||
for (int m = 0; m < 4; m++) {
|
||||
int x = m * 80;
|
||||
gr_set_semi_transparency(1, m);
|
||||
gr_draw_flat_rect(x, 0, 16, 240, FG[m]);
|
||||
if (depth) gr_set_depth_triangle(1, 700.0f + m, 700.0f + m, 700.0f + m);
|
||||
gr_draw_flat_triangle(x + 18, 2, x + 34, 2, x + 18, 236, FG[(m + 1) & 3]);
|
||||
gr_set_color_modulation(128, 128, 128, 1);
|
||||
for (int y = 0; y < 240; y += 16) gr_draw_textured_rect(x + 36, y, 16, 16, 0, 0, 0, 0, TP);
|
||||
gr_set_color_modulation(128, 128, 128, 0);
|
||||
if (depth) gr_set_depth_triangle(1, 650.0f, 650.0f, 650.0f);
|
||||
gr_draw_shaded_textured_triangle(x + 54, 0, 0, 0, 0x6080a0, x + 78, 0, 15, 0, 0xa08060,
|
||||
x + 54, 239, 0, 15, 0x806080, 0, 0, TP, 1);
|
||||
}
|
||||
/* Mask: set-mask rects, then every mode over them with check on. */
|
||||
gr_set_semi_transparency(0, 0);
|
||||
mask_bits(1, 0);
|
||||
for (int m = 0; m < 4; m++) gr_draw_flat_rect(m * 80 + 4, 100, 8, 8, 0x1234);
|
||||
mask_bits(0, 1);
|
||||
for (int m = 0; m < 4; m++) {
|
||||
gr_set_semi_transparency(1, m);
|
||||
gr_draw_flat_rect(m * 80, 96, 16, 16, 0x7fff);
|
||||
}
|
||||
mask_bits(1, 0); /* set-mask with semi: result carries bit 15 */
|
||||
for (int m = 0; m < 4; m++) {
|
||||
gr_set_semi_transparency(1, m);
|
||||
gr_draw_flat_rect(m * 80, 200, 16, 8, FG[m]);
|
||||
}
|
||||
mask_bits(0, 0);
|
||||
gr_set_semi_transparency(0, 0);
|
||||
gl_renderer_render_thread_frame_boundary();
|
||||
}
|
||||
static int ch(uint16_t p, int c) { return (p >> (5 * c)) & 31; }
|
||||
static uint16_t blend(uint16_t b, uint16_t f, int m) {
|
||||
uint16_t o = 0;
|
||||
for (int c = 0; c < 3; c++) {
|
||||
int B = ch(b, c), F = ch(f, c), r;
|
||||
if (m == 0) r = (B + F) >> 1;
|
||||
else if (m == 1) r = B + F > 31 ? 31 : B + F;
|
||||
else if (m == 2) r = B - F < 0 ? 0 : B - F;
|
||||
else r = B + (F >> 2) > 31 ? 31 : B + (F >> 2);
|
||||
o |= (uint16_t)(r << (5 * c));
|
||||
}
|
||||
return o;
|
||||
}
|
||||
static int near1(uint16_t a, uint16_t b) {
|
||||
for (int c = 0; c < 3; c++) if (abs(ch(a, c) - ch(b, c)) > 1) return 0;
|
||||
return (a & 0x8000) == (b & 0x8000);
|
||||
}
|
||||
/* Single-layer blends must be exact. Rows 96..111 and 200..207 stack a
|
||||
* second semi-transparent layer on a blended one: the hardware truncates the
|
||||
* first result to 5 bits before the second blend, while the 8-bit surface
|
||||
* keeps the first result's extra precision (a fixed-function blend cannot
|
||||
* truncate), so those rows may differ by one 5-bit step per channel. */
|
||||
static long oracle_errors(const uint16_t *v) {
|
||||
long bad = 0;
|
||||
for (int m = 0; m < 4; m++)
|
||||
for (int y = 0; y < 240; y++)
|
||||
for (int x = m * 80; x < m * 80 + 16; x++) {
|
||||
uint16_t got = v[y * 1024 + x], want;
|
||||
int stacked = 0;
|
||||
if (y >= 96 && y < 112) { /* masked square kept, rest blended white */
|
||||
int masked = x >= m * 80 + 4 && x < m * 80 + 12 && y >= 100 && y < 108;
|
||||
uint16_t under = blend(BG[y / 30], FG[m], m);
|
||||
want = masked ? 0x9234 : blend(under, 0x7fff, m);
|
||||
stacked = !masked;
|
||||
} else if (y >= 200 && y < 208) {
|
||||
want = (uint16_t)(blend(blend(BG[y / 30], FG[m], m), FG[m], m) | 0x8000);
|
||||
stacked = 1;
|
||||
} else want = blend(BG[y / 30], FG[m], m);
|
||||
const int ok = stacked ? near1(got, want) : got == want;
|
||||
if (!ok && getenv("BLEND_VERBOSE") && bad < 12)
|
||||
printf(" oracle (%d,%d) got=%04x want=%04x\n", x, y, got, want);
|
||||
bad += !ok;
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
/* Flat triangles: the GL and software edges differ by a pixel (documented
|
||||
* coverage rules), so only the interior, one pixel in from every edge, is
|
||||
* checked, against the formula. */
|
||||
static long oracle_tri_errors(const uint16_t *v) {
|
||||
long bad = 0;
|
||||
for (int m = 0; m < 4; m++)
|
||||
for (int y = 4; y < 234; y++) {
|
||||
if ((y >= 96 && y < 112) || (y >= 200 && y < 208)) continue;
|
||||
const double span = 16.0 * (1.0 - (y - 2) / 234.0);
|
||||
for (int lx = 1; lx + 1.5 < span; lx++) {
|
||||
const uint16_t got = v[y * 1024 + m * 80 + 18 + lx];
|
||||
bad += got != blend(BG[y / 30], FG[(m + 1) & 3], m);
|
||||
}
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
/* The software rasterizer drops a texel's STP bit instead of writing it to
|
||||
* bit 15 (the PS1 writes texel STP OR set-mask), so bit 15 is compared
|
||||
* against the oracle on its own (stp_errors) and masked here. */
|
||||
/* Rects against the software reference: exact where a single layer is drawn
|
||||
* from 5-bit operands (flat and raw-textured rects), within one step in the
|
||||
* stacked rows. */
|
||||
static long diff(const uint16_t *a, const uint16_t *b) {
|
||||
long n = 0;
|
||||
for (int y = 0; y < 240; y++) for (int x = 0; x < 320; x++) {
|
||||
const uint16_t p = a[y * 1024 + x] | 0x8000, q = b[y * 1024 + x] | 0x8000;
|
||||
const int loose = (y >= 96 && y < 112) || (y >= 200 && y < 208) || (x % 80) >= 52;
|
||||
if ((x % 80) >= 16 && ((x % 80) < 36 || (x % 80) >= 52))
|
||||
continue; /* triangles: edges differ by a pixel; see oracle_tri_errors */
|
||||
n += loose ? !near1(p, q) : p != q;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
/* Raw textured rects: bit 15 of each written pixel is its texel's STP. */
|
||||
static long stp_errors(const uint16_t *v) {
|
||||
long bad = 0;
|
||||
for (int m = 0; m < 4; m++)
|
||||
for (int y = 0; y < 240; y++) {
|
||||
if (y >= 96 && y < 112) continue;
|
||||
for (int tx = 0; tx < 16; tx++) {
|
||||
int k = (tx + (y & 15)) % 4;
|
||||
if (k == 0) continue; /* transparent texel: background stays */
|
||||
bad += ((v[y * 1024 + m * 80 + 36 + tx] >> 15) & 1) != (k == 1);
|
||||
}
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
if (SDL_Init(SDL_INIT_VIDEO) != 0) { fprintf(stderr, "SKIP no video\n"); return 77; }
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
#if defined(__APPLE__)
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
|
||||
#endif
|
||||
static uint16_t ref[1024 * 512], out[1024 * 512];
|
||||
/* Software reference. */
|
||||
gr_set_backend(GR_BACKEND_SOFTWARE);
|
||||
gr_init(vram);
|
||||
page();
|
||||
scene(0);
|
||||
gr_vram_transfer_out(0, 0, 1024, 512, ref);
|
||||
const long ref_oracle = oracle_errors(ref);
|
||||
printf("software reference: oracle mismatches=%ld\n", ref_oracle);
|
||||
check(ref_oracle == 0, "software reference matches the PS1 formulas");
|
||||
|
||||
SDL_Window *win = SDL_CreateWindow("Blend accuracy hidden test", 0, 0, 320, 240,
|
||||
SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN);
|
||||
if (!win) { fprintf(stderr, "SKIP no window\n"); return 77; }
|
||||
memset(vram, 0, sizeof vram);
|
||||
gr_set_backend(GR_BACKEND_OPENGL);
|
||||
gr_init(vram);
|
||||
gr_set_scale(4);
|
||||
gl_renderer_set_swap_interval(0);
|
||||
if (!gl_renderer_init_context(win)) { fprintf(stderr, "SKIP no GL context\n"); return 77; }
|
||||
page();
|
||||
/* No caller opt-in: default rendering must match the console. */
|
||||
check(gl_renderer_accurate_blending() == 1, "correct blending is the default");
|
||||
scene(0);
|
||||
gr_vram_transfer_out(0, 0, 1024, 512, out);
|
||||
check(oracle_errors(out) == 0, "default GL rendering matches the PS1 formulas");
|
||||
check(diff(out, ref) == 0, "default GL VRAM matches the software reference");
|
||||
/* Historical bases: measured for diagnosis, not required. */
|
||||
gl_renderer_set_accurate_blending(0);
|
||||
scene(0);
|
||||
gr_vram_transfer_out(0, 0, 1024, 512, out);
|
||||
printf("gl 4x accurate_blending=0: oracle mismatches=%ld (historical bases)\n", oracle_errors(out));
|
||||
gl_renderer_set_accurate_blending(1);
|
||||
for (int threaded = 0; threaded < 2; threaded++)
|
||||
for (int depth = 0; depth < 2; depth++) {
|
||||
if (threaded && depth == 0) check(gl_renderer_render_thread_start(2) == 1, "render thread started");
|
||||
gl_renderer_set_pgxp_depth(depth);
|
||||
scene(depth);
|
||||
if (threaded) gl_renderer_render_thread_sync("blend");
|
||||
gr_vram_transfer_out(0, 0, 1024, 512, out);
|
||||
const long d = diff(out, ref), o = oracle_errors(out);
|
||||
printf("gl 4x pgxp_depth=%d render_thread=%d: differs from software=%ld oracle mismatches=%ld\n",
|
||||
depth, threaded, d, o);
|
||||
check(o == 0, "GL matches the PS1 blend/mask formulas");
|
||||
check(oracle_tri_errors(out) == 0, "depth-tested triangle interiors match the formulas");
|
||||
check(stp_errors(out) == 0, "textured pixels carry their texel's STP in bit 15");
|
||||
check(d == 0, "GL native VRAM equals the software reference");
|
||||
if (d) {
|
||||
int shown = 0;
|
||||
for (int y = 0; y < 240 && shown < 8; y++) for (int x = 0; x < 320 && shown < 8; x++)
|
||||
if ((out[y * 1024 + x] | 0x8000) != (ref[y * 1024 + x] | 0x8000) && (x % 80) < 52 &&
|
||||
((x % 80) < 16 || (x % 80) >= 36) &&
|
||||
!((y >= 96 && y < 112) || (y >= 200 && y < 208))) {
|
||||
printf(" (%d,%d) gl=%04x sw=%04x\n", x, y, out[y * 1024 + x], ref[y * 1024 + x]);
|
||||
shown++;
|
||||
}
|
||||
}
|
||||
}
|
||||
gl_renderer_render_thread_stop();
|
||||
gl_renderer_set_pgxp_depth(0);
|
||||
gl_renderer_set_accurate_blending(0);
|
||||
check(glGetError() == GL_NO_ERROR, "GL errors");
|
||||
printf("checks=%d failures=%d\n", checks, failures);
|
||||
gl_renderer_shutdown(); SDL_DestroyWindow(win); SDL_Quit();
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user