mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
GL: opt-in PS1 dithering ([video] dithering = off|on|scaled)
The OpenGL renderer has always drawn shaded and texture-modulated pixels at 8 bits per channel with no dither pattern (true colour). That stays the default. 'on' applies the PS1 4x4 offset matrix and 5-bit truncation per internal-resolution pixel; 'scaled' puts the pattern on the native grid, so it reads like the console at any scale and the native VRAM is the same at every internal resolution. GP0(E1h) bit 9 reaches the backend through a new optional facade hook (gr_set_dither) recorded for the render thread and Smooth motion redraws. With the mode off nothing flushes and no uniform changes. Flat, raw and other 5-bit-exact pixels are never touched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,8 @@ Columns: **N** = native, **D** = DuckStation oracle.
|
||||
| `present_shot_seq` | ✓ | | — | Completion counter for `present_shot`, plus `wrote` (1 = that completion produced a PNG). Sample before staging, poll until `seq` moves. Advances on success *and* failure, so the poll always terminates |
|
||||
| `fmv_chroma` | ✓ | | `on` (optional: 0/1) | Read or set `[video] fmv_chroma_smoothing` live. Replies `on` and `frames` (24-bit presents smoothed so far) |
|
||||
| `texture_lod` | ✓ | | `mode` (optional: 0 off, 1 mipmap), `aniso` (optional: 1..16) | Read or set `[video] texture_lod` / `anisotropic_filtering` live. Replies `mode`, `aniso` |
|
||||
| `accurate_blending` | ✓ | | `on` (optional: 0/1) | Read or set `[video] accurate_blending` live. Replies `on` |
|
||||
| `dithering` | ✓ | | `mode` (optional: 0 off, 1 on, 2 scaled) | Read or set `[video] dithering` live. Replies `mode` |
|
||||
| `post_aa` | ✓ | | `mode` (optional: 0 off, 1 fxaa, 2 fxaa_hq) | Read or set post-process anti-aliasing live (`[video] antialiasing_mode`). Replies `mode`, `passes` (filtered presents so far) and `gpu_us`: the mean pass time since the last query when the run has `PSX_POST_AA_TIME=N` (N passes per present between `glFinish` fences, a measurement mode), else 0 |
|
||||
| `hd_textures` | ✓ | | optional `replacements`, `dump` (0 or 1), `reload` (1) | HD pack root, switches, backend support, replacement count, matched/ready/applied draw counts, and queued dump count. Optional controls update the configured host pack on the emulation thread; no configured root is an error. Reload retains unchanged upload identities. OpenGL displays replacements; software/Vulkan retain original artwork. Pair with `present_shot` for visual evidence and native VRAM probes for architectural data |
|
||||
| `gl_interp` | ✓ | | — | OpenGL frame-rate presenter ([FRAME_RATE.md](FRAME_RATE.md)): enabled/suspended, host and target Hz, swaps, `source` (`vblank`/`flip`), `flip_period`, `captures` (new source frames) and `duplicates` (VBlanks that re-presented the same frame) |
|
||||
@@ -486,16 +488,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.
|
||||
84 of 369 have prose above; **285 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` | ✓ | | |
|
||||
| `accurate_blending` | ✓ | | ✓ |
|
||||
| `audio_events` | ✓ | ✓ | |
|
||||
| `audio_stats` | ✓ | ✓ | |
|
||||
| `audio_wav` | ✓ | ✓ | |
|
||||
@@ -574,6 +576,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `display_ring_aux` | ✓ | | |
|
||||
| `display_ring_get` | ✓ | | |
|
||||
| `display_ring_stats` | ✓ | | |
|
||||
| `dithering` | ✓ | | ✓ |
|
||||
| `dma_cdrom_history` | ✓ | | |
|
||||
| `dma_hold_ring` | ✓ | | ✓ |
|
||||
| `dma_hold_stats` | ✓ | | ✓ |
|
||||
|
||||
@@ -722,6 +722,31 @@ 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.
|
||||
### Dithering (`dithering`)
|
||||
|
||||
```toml
|
||||
[video]
|
||||
dithering = "off" # off (default, true colour) | on | scaled
|
||||
```
|
||||
|
||||
The PS1 GPU adds a 4x4 offset pattern to shaded and texture-modulated pixels
|
||||
before truncating them to 5 bits per channel, when the game sets GP0(E1h)
|
||||
bit 9. The OpenGL renderer has always drawn those pixels at 8 bits per channel
|
||||
with no pattern ("true colour"), and `off` keeps exactly that. `on` applies
|
||||
the PS1 pattern and 5-bit truncation per internal-resolution pixel (a fine
|
||||
grain that all but vanishes at high scales); `scaled` puts the pattern on the
|
||||
native pixel grid, so it looks like the console at any scale and the native
|
||||
VRAM a game reads back is the same at every internal resolution. Flat fills,
|
||||
flat polygons and raw textures are already 5-bit exact and are never touched.
|
||||
The pattern is applied to the source colour before semi-transparent blending;
|
||||
the console dithers the blended result at its final 8-to-5-bit write. Fixed-
|
||||
function GL blending cannot run a shader after the blend (GL 3.3 has no
|
||||
framebuffer fetch, and macOS offers none), so a dithered semi-transparent
|
||||
pixel can differ from the console by one 5-bit step; opaque pixels match.
|
||||
The dither output follows the surface basis, so it composes with
|
||||
`accurate_blending`.
|
||||
OpenGL only. `PSX_DITHERING=0|1|2` overrides it for one run and the
|
||||
`dithering` TCP command switches it live.
|
||||
|
||||
The runtime clamps N per backend:
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
83f82f8b2daed07e75765c7ae4adb78cdb1a9fd7396fb879089c2352c8dc78a6
|
||||
2e0085fd713b394d38308a403ef22811043ba6a60686ac6e697d4c016d106e19
|
||||
|
||||
@@ -837,6 +837,14 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
}
|
||||
if (video.contains("accurate_blending"))
|
||||
rt.video_accurate_blending = toml::find<bool>(video, "accurate_blending");
|
||||
if (video.contains("dithering")) {
|
||||
const auto mode = toml::find<std::string>(video, "dithering");
|
||||
if (mode == "off") rt.video_dithering = 0;
|
||||
else if (mode == "on") rt.video_dithering = 1;
|
||||
else if (mode == "scaled") rt.video_dithering = 2;
|
||||
else throw std::runtime_error(fmt::format(
|
||||
"[video] dithering must be \"off\", \"on\" or \"scaled\": {}", mode));
|
||||
}
|
||||
if (video.contains("pgxp_depth_buffer"))
|
||||
rt.video_pgxp_depth_buffer = toml::find<bool>(video, "pgxp_depth_buffer");
|
||||
if (video.contains("pgxp_color_correction"))
|
||||
|
||||
@@ -577,6 +577,11 @@ struct RuntimeConfig {
|
||||
// semi-transparency truncates exactly like the PS1's 5-bit blend.
|
||||
// Default false (historical: fills/texels full-range). OpenGL only.
|
||||
bool video_accurate_blending = false;
|
||||
// dithering: PS1 dither pattern on shaded/modulated draws while the
|
||||
// guest enables it (GP0(E1h) bit 9). 0 "off" = true colour (default),
|
||||
// 1 "on" = pattern per internal-resolution pixel, 2 "scaled" = pattern
|
||||
// on the native pixel grid (the PS1 look at any scale). OpenGL only.
|
||||
int video_dithering = 0;
|
||||
bool video_pgxp_color_correction = false;
|
||||
int video_pgxp_seam = 0;
|
||||
double video_pgxp_depth_threshold = 4096.0;
|
||||
|
||||
@@ -288,6 +288,19 @@ if(BUILD_TESTING)
|
||||
--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)
|
||||
# [video] dithering: off ignores the guest's dither bit (byte-identical
|
||||
# true colour); on/scaled add the PS1 pattern to shaded/modulated draws
|
||||
# only, through the facade, the render thread, PGXP depth and wide.
|
||||
add_test(NAME gl_dithering_test
|
||||
COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_render_thread.py" --single
|
||||
--fixture "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_dithering.c"
|
||||
--cc "${CMAKE_C_COMPILER}"
|
||||
--sdl-include "${_gl_scale_sdl_inc}"
|
||||
--sdl-library "$<TARGET_FILE:SDL3::SDL3>"
|
||||
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-dithering-results")
|
||||
set_tests_properties(gl_dithering_test PROPERTIES
|
||||
LABELS "gpu;opengl;hardware" TIMEOUT 180 SKIP_RETURN_CODE 77)
|
||||
unset(_gl_scale_sdl_inc)
|
||||
else()
|
||||
message(STATUS "gl_scale_invariance_test: not registered (needs macOS, Linux or MinGW, Python 3 and SDL3)")
|
||||
|
||||
@@ -303,6 +303,11 @@ int gl_renderer_anisotropy(void);
|
||||
* uploads and untextured primitives already are). Off by default. */
|
||||
void gl_renderer_set_accurate_blending(int on);
|
||||
int gl_renderer_accurate_blending(void);
|
||||
/* [video] dithering: 0 off (true colour, default), 1 PS1 dither pattern at
|
||||
* internal resolution, 2 pattern scaled to the native pixel grid. Applies to
|
||||
* shaded/modulated draws while the guest's GP0(E1h) dither bit is set. */
|
||||
void gl_renderer_set_dithering(int mode);
|
||||
int gl_renderer_dithering(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);
|
||||
|
||||
@@ -61,6 +61,9 @@ void gr_set_precise_triangle(int enabled,
|
||||
* perspective_texturing). q[i] is the normalized 1/z homogeneous weight at
|
||||
* vertex i. enabled == 0 restores the PS1's affine UV interpolation. */
|
||||
void gr_set_perspective_triangle(int enabled, float q0, float q1, float q2);
|
||||
/* GP0(E1h) bit 9 (dither enable). Only a backend with [video] dithering
|
||||
* implements it; the default renderers stay true colour. */
|
||||
void gr_set_dither(int enabled);
|
||||
/* PGXP depth for the NEXT triangle (docs/ENHANCEMENTS.md G1.14): the GTE SZ
|
||||
* of each vertex (1..65535), taken from validated dataflow shadows. Only a
|
||||
* backend with a PGXP depth buffer / perspective-correct colour uses it;
|
||||
@@ -167,6 +170,7 @@ typedef struct GpuRenderBackend {
|
||||
int32_t x1, int32_t y1,
|
||||
int32_t x2, int32_t y2);
|
||||
void (*set_perspective_triangle)(int enabled, float q0, float q1, float q2);
|
||||
void (*set_dither)(int enabled); /* optional: GP0(E1h) bit 9 */
|
||||
void (*fill_rect)(int x, int y, int w, int h, uint16_t color);
|
||||
void (*copy_rect)(int src_x, int src_y, int dst_x, int dst_y, int w, int h);
|
||||
void (*draw_flat_triangle)(int x0, int y0, int x1, int y1, int x2, int y2,
|
||||
|
||||
@@ -8638,6 +8638,15 @@ static void handle_accurate_blending(int id, const char *json)
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"on\":%d}", id, gl_renderer_accurate_blending());
|
||||
}
|
||||
|
||||
/* dithering: {"cmd":"dithering"} reads, {"cmd":"dithering","mode":0|1|2}
|
||||
* sets [video] dithering live (off, on, scaled). */
|
||||
static void handle_dithering(int id, const char *json)
|
||||
{
|
||||
int mode = json_get_int(json, "mode", -1);
|
||||
if (mode >= 0) gl_renderer_set_dithering(mode);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"mode\":%d}", id, gl_renderer_dithering());
|
||||
}
|
||||
|
||||
static void handle_frame_perf(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
@@ -15864,6 +15873,7 @@ static const CmdEntry s_commands[] = {
|
||||
{ "fmv_chroma", handle_fmv_chroma },
|
||||
{ "texture_lod", handle_texture_lod },
|
||||
{ "accurate_blending", handle_accurate_blending },
|
||||
{ "dithering", handle_dithering },
|
||||
{ "gl_ws_ablate", handle_gl_ws_ablate },
|
||||
{ "gl_interp", handle_gl_interp },
|
||||
{ "render_pass_stats", handle_render_pass_stats },
|
||||
|
||||
@@ -3789,6 +3789,7 @@ static void gpu_reset_state(int clear_vram) {
|
||||
semi_transparency = 0;
|
||||
texpage_colors = 0;
|
||||
dither_enabled = 0;
|
||||
gr_set_dither(0);
|
||||
draw_to_display = 0;
|
||||
set_mask_bit = 0;
|
||||
check_mask_bit = 0;
|
||||
@@ -5894,6 +5895,7 @@ static void gp0_exec_draw_mode(void) {
|
||||
semi_transparency = (param >> 5) & 3;
|
||||
texpage_colors = (param >> 7) & 3;
|
||||
dither_enabled = (param >> 9) & 1;
|
||||
gr_set_dither((int)dither_enabled);
|
||||
draw_to_display = (param >> 10) & 1;
|
||||
texture_disable = (param >> 11) & 1;
|
||||
/* Sync semi-transparency to SW renderer — actual blending is per-primitive */
|
||||
@@ -7864,6 +7866,10 @@ int gpu_snapshot_read(const uint8_t *p, uint32_t len) {
|
||||
* on a stale draw area after savestate load. */
|
||||
gr_set_draw_area((int)draw_area_left, (int)draw_area_top,
|
||||
(int)draw_area_right, (int)draw_area_bottom);
|
||||
/* GP0(E1h) bit 9 is restored with the registers; the renderer only
|
||||
* learns it from E1, so push it now (else dithering keeps the pre-load
|
||||
* setting until the game's next E1). */
|
||||
gr_set_dither((int)dither_enabled);
|
||||
/* A state saved mid-A0 resumes streaming its payload into the array. */
|
||||
gr_vram_upload_set_open(gp0_state == GP0_VRAM_WRITE);
|
||||
ws_nw_sync_target();
|
||||
@@ -8018,6 +8024,7 @@ void gpu_pass_checkpoint_restore(void) {
|
||||
(int)draw_area_right, (int)draw_area_bottom);
|
||||
gr_set_draw_offset(draw_offset_x, draw_offset_y);
|
||||
gr_set_mask_bits((int)set_mask_bit, (int)check_mask_bit);
|
||||
gr_set_dither((int)dither_enabled);
|
||||
ws_nw_sync_target();
|
||||
/* Target sync may classify the scene before render_pass.c restores RAM.
|
||||
* Do not retain history learned from the synthetic draw or that query. */
|
||||
|
||||
@@ -461,7 +461,7 @@ enum {
|
||||
RTH_WIDE_DISABLE, RTH_WIDE_CLEAR, RTH_WIDE_CLEAR_MARGINS, RTH_PROJ_TRI,
|
||||
RTH_WIDE_RECOVERY, RTH_INTERP_SUSPENDED, RTH_PRESENT_VRAM,
|
||||
RTH_PRESENT_WIDE, RTH_STATE, RTH_PEEK, RTH_RING_CAPTURE, RTH_DYN_STEP,
|
||||
RTH_FRAME, RTH_FG_SRC, RTH_DEPTH, RTH_HD_NOTE
|
||||
RTH_FRAME, RTH_FG_SRC, RTH_DEPTH, RTH_HD_NOTE, RTH_DITHER
|
||||
};
|
||||
/* GP0(A0) whose payload gpu.c is still streaming into its array (the
|
||||
* facade's vram_upload_open, emulation thread), tracked whatever the HD
|
||||
@@ -919,6 +919,11 @@ static void c5_to_float(uint16_t c, float *r, float *g, float *b) {
|
||||
*r = (c & 0x1F) / 31.0f; *g = ((c >> 5) & 0x1F) / 31.0f; *b = ((c >> 10) & 0x1F) / 31.0f;
|
||||
}
|
||||
}
|
||||
/* [video] dithering (see psx_dither in the shaders): 0 off = true colour (the
|
||||
* historical path), 1 the PS1 4x4 pattern at internal resolution, 2 scaled to
|
||||
* the native pixel grid. s_dither_bit is the last GP0(E1h) bit 9; the shaders
|
||||
* only ever see s_dither_mode while the guest has dithering enabled. */
|
||||
static int s_dither_mode = 0, s_dither_bit = 0, s_dither_live = 0;
|
||||
/* 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;
|
||||
@@ -1812,6 +1817,27 @@ static const char *INTERP_FS =
|
||||
* rect coverage exactly [x*S, (x+w)*S) at every scale AND makes the
|
||||
* top-left subpixel of each S*S block sample the exact PS1 value (which is
|
||||
* what the PACK pass reads back). */
|
||||
/* PS1 dithering ([video] dithering). The GPU adds the 4x4 offset matrix below
|
||||
* to a shaded or texture-modulated pixel before truncating it to 5 bits, when
|
||||
* GP0(E1h) bit 9 is set. Flat and raw-texture pixels are already 5-bit exact
|
||||
* and are left alone, which is what the hardware's shading/modulation rule
|
||||
* amounts to. u_dither 0 = off (true colour, the default and the historical
|
||||
* renderer), 1 = pattern per internal-resolution pixel, 2 = pattern scaled
|
||||
* to the native grid (v_npos is the fragment's native VRAM position). */
|
||||
#define PSX_DITHER_GLSL \
|
||||
"uniform int u_dither;\n" \
|
||||
"uniform int u_accurate; /* [video] accurate_blending: VRAM in the k*8/255 basis */\n" \
|
||||
"vec3 psx_dither(vec3 c, vec2 npos){\n" \
|
||||
" if (u_dither == 0) return c;\n" \
|
||||
" vec3 q8 = c * (255.0 / 8.0), q31 = c * 31.0;\n" \
|
||||
" if (all(lessThan(abs(q8 - floor(q8 + 0.5)), vec3(0.02))) ||\n" \
|
||||
" (u_accurate == 0 && all(lessThan(abs(q31 - floor(q31 + 0.5)), vec3(0.02))))) return c;\n" \
|
||||
" ivec2 p = (u_dither == 2 ? ivec2(floor(npos)) : ivec2(gl_FragCoord.xy)) & 3;\n" \
|
||||
" const float m[16] = float[16](-4.0,0.0,-3.0,1.0, 2.0,-2.0,3.0,-1.0,\n" \
|
||||
" -3.0,1.0,-4.0,0.0, 3.0,-1.0,2.0,-2.0);\n" \
|
||||
" vec3 k = clamp(floor((c * 255.0 + m[p.y * 4 + p.x]) / 8.0), 0.0, 31.0);\n" \
|
||||
" return u_accurate != 0 ? k * (8.0 / 255.0) : k / 31.0; /* the surface's basis */\n" \
|
||||
"}\n"
|
||||
static const char *GEO_VS =
|
||||
"#version 330\n"
|
||||
"layout(location=0) in vec2 a_pos;\n"
|
||||
@@ -1825,7 +1851,9 @@ static const char *GEO_VS =
|
||||
"noperspective out vec4 v_col;\n"
|
||||
"smooth out vec3 v_col_p; /* perspective-correct colour (PGXP, G1.14) */\n"
|
||||
"flat out int v_cp;\n"
|
||||
"noperspective out vec2 v_npos; /* native VRAM position (dithering) */\n"
|
||||
"void main(){\n"
|
||||
" v_npos = a_pos;\n"
|
||||
" /* a_col.a carries the mask bit (0/1) and, for a PGXP 3D vertex, its\n"
|
||||
" * GTE SZ: a = mask + 2*(sz + 65536*cp), cp = perspective colour. A\n"
|
||||
" * negative a is a depth-clear vertex (beyond every SZ, inside the far plane). */\n"
|
||||
@@ -1845,7 +1873,10 @@ static const char *GEO_VS =
|
||||
static const char *GEO_FS =
|
||||
"#version 330\n"
|
||||
"noperspective in vec4 v_col; smooth in vec3 v_col_p; flat in int v_cp; out vec4 frag;\n"
|
||||
"void main(){ frag = v_cp != 0 ? vec4(v_col_p, v_col.a) : v_col; }\n";
|
||||
"noperspective in vec2 v_npos;\n"
|
||||
PSX_DITHER_GLSL
|
||||
"void main(){ frag = v_cp != 0 ? vec4(v_col_p, v_col.a) : v_col;\n"
|
||||
" frag.rgb = psx_dither(frag.rgb, v_npos); }\n";
|
||||
|
||||
/* Textured prims: sample raw 1555 VRAM (integer), CLUT decode per depth,
|
||||
* texture window, optional bilinear, texel-0 discard, STP-split discard,
|
||||
@@ -1888,7 +1919,9 @@ static const char *TEX_VS =
|
||||
"flat out int v_twin;\n"
|
||||
"flat out vec4 v_hd_source; flat out int v_hd_mode;\n"
|
||||
"smooth out vec3 v_col_p; flat out int v_cp;\n"
|
||||
"noperspective out vec2 v_npos; /* native VRAM position (dithering) */\n"
|
||||
"void main(){ v_uv = a_uv; v_uv_p = a_uv; v_col = a_col; v_col_p = a_col.rgb;\n"
|
||||
" v_npos = a_pos;\n"
|
||||
" v_persp = (a_q > 0.0) ? 1 : 0;\n"
|
||||
" v_tpage = ivec2(a_tpage + 0.5); v_clut = ivec2(a_clut + 0.5);\n"
|
||||
" v_depth = int(a_depth + 0.5); v_raw = int(a_raw + 0.5);\n"
|
||||
@@ -1923,6 +1956,8 @@ static const char *TEX_FS =
|
||||
"noperspective in vec2 v_uv; noperspective in vec4 v_col;\n"
|
||||
"smooth in vec2 v_uv_p; flat in int v_persp;\n"
|
||||
"smooth in vec3 v_col_p; flat in int v_cp;\n"
|
||||
"noperspective in vec2 v_npos;\n"
|
||||
PSX_DITHER_GLSL
|
||||
"out vec4 frag; out vec4 blend_factor;\n"
|
||||
"flat in ivec2 v_tpage; /* texture page base, VRAM px */\n"
|
||||
"flat in ivec2 v_clut; /* CLUT base, VRAM px */\n"
|
||||
@@ -1972,7 +2007,6 @@ 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"
|
||||
" 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"
|
||||
@@ -2107,7 +2141,7 @@ static const char *TEX_FS =
|
||||
" if ((u_filter & 16) != 0 && v_hd_mode == 0) rgb = lod_sample(uv0, dx0, dy0, stp, rgb);\n"
|
||||
" if (u_semipass == 1 && stp == 1) discard;\n"
|
||||
" if (u_semipass == 2 && stp == 0) discard;\n"
|
||||
" if (v_raw == 0) rgb = clamp(rgb * (v_cp != 0 ? v_col_p : v_col.rgb) * 2.0, 0.0, 1.0);\n"
|
||||
" if (v_raw == 0) rgb = psx_dither(clamp(rgb * (v_cp != 0 ? v_col_p : v_col.rgb) * 2.0, 0.0, 1.0), v_npos);\n"
|
||||
" float dst_factor = 0.0;\n"
|
||||
" if (u_semimode == 4 && v_semi != 0 && stp != 0) {\n"
|
||||
" dst_factor = v_semi == 1 ? 0.5 : 1.0;\n"
|
||||
@@ -4970,12 +5004,15 @@ 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 GLint s_uAccurate = -1, s_geo_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");
|
||||
if (s_geo_uAccurate < 0 && s_geo_prog) s_geo_uAccurate = p_glGetUniformLocation(s_geo_prog, "u_accurate");
|
||||
p_glUseProgram(s_tex_prog); p_glUniform1i(s_uAccurate, s_accurate_blend);
|
||||
/* The dither snippet in the geometry program follows the same basis. */
|
||||
if (s_geo_prog) { p_glUseProgram(s_geo_prog); p_glUniform1i(s_geo_uAccurate, s_accurate_blend); }
|
||||
p_glUseProgram((GLuint)prev);
|
||||
}
|
||||
void gl_renderer_set_accurate_blending(int on) {
|
||||
@@ -4985,6 +5022,38 @@ void gl_renderer_set_accurate_blending(int on) {
|
||||
if (s_ctx) accurate_apply_uniforms();
|
||||
}
|
||||
int gl_renderer_accurate_blending(void) { return s_accurate_blend; }
|
||||
/* Dither uniforms live in the geometry and textured programs. The effective
|
||||
* value only changes between batches (glb_set_dither drains them first). */
|
||||
static GLint s_geo_uDither = -1, s_tex_uDither = -1;
|
||||
static void dither_apply_uniforms(void) {
|
||||
if (!s_geo_prog || !s_tex_prog) return;
|
||||
GLint prev = 0; glGetIntegerv(GL_CURRENT_PROGRAM, &prev);
|
||||
if (s_geo_uDither < 0) s_geo_uDither = p_glGetUniformLocation(s_geo_prog, "u_dither");
|
||||
if (s_tex_uDither < 0) s_tex_uDither = p_glGetUniformLocation(s_tex_prog, "u_dither");
|
||||
p_glUseProgram(s_geo_prog); p_glUniform1i(s_geo_uDither, s_dither_live);
|
||||
p_glUseProgram(s_tex_prog); p_glUniform1i(s_tex_uDither, s_dither_live);
|
||||
p_glUseProgram((GLuint)prev);
|
||||
}
|
||||
static void dither_update(void) {
|
||||
const int live = s_dither_bit ? s_dither_mode : 0;
|
||||
if (live == s_dither_live) return;
|
||||
/* Queued draws keep the state they were submitted under. */
|
||||
flush_flat_batch(); flush_tex_batch(); hiw_flush_queue();
|
||||
s_dither_live = live;
|
||||
dither_apply_uniforms();
|
||||
}
|
||||
/* GP0(E1h) bit 9. With [video] dithering off (the default) this only records
|
||||
* the bit; nothing is flushed and no uniform changes. */
|
||||
static void glb_set_dither(int on) {
|
||||
s_dither_bit = on ? 1 : 0;
|
||||
if (s_dither_mode || s_dither_live) dither_update();
|
||||
}
|
||||
void gl_renderer_set_dithering(int mode) {
|
||||
GL_RT_SYNC("dithering");
|
||||
s_dither_mode = (mode == 1 || mode == 2) ? mode : 0;
|
||||
if (s_ctx) dither_update();
|
||||
}
|
||||
int gl_renderer_dithering(void) { return s_dither_mode; }
|
||||
static void glb_set_texture_filter(int b) { s_tex_filter = b >= 0 && b <= 2 ? b : 0; sw_set_texture_filter(b != 0); }
|
||||
static int glb_texture_filter(void) { return s_tex_filter; }
|
||||
|
||||
@@ -5850,7 +5919,7 @@ static int init_gpu_raster(void) {
|
||||
s_uFilter = p_glGetUniformLocation(s_tex_prog, "u_filter");
|
||||
s_uAniso = -1;
|
||||
lod_apply_uniforms();
|
||||
s_uAccurate = -1;
|
||||
s_uAccurate = s_geo_uAccurate = -1;
|
||||
accurate_apply_uniforms();
|
||||
s_uLimits = p_glGetUniformLocation(s_tex_prog, "u_limits");
|
||||
s_uHdTexture = p_glGetUniformLocation(s_tex_prog, "u_hd_texture");
|
||||
@@ -5900,6 +5969,9 @@ static int init_gpu_raster(void) {
|
||||
s_shift_hr = shift;
|
||||
s_shift_hi = 0.5f / (float)s_out_scale - 1.0f / 64.0f;
|
||||
s_geo_uShift = p_glGetUniformLocation(s_geo_prog, "u_shift");
|
||||
s_geo_uDither = s_tex_uDither = -1;
|
||||
s_dither_live = s_dither_bit ? s_dither_mode : 0;
|
||||
dither_apply_uniforms();
|
||||
s_tex_uShift = p_glGetUniformLocation(s_tex_prog, "u_shift");
|
||||
s_geo_uZbias = p_glGetUniformLocation(s_geo_prog, "u_zbias");
|
||||
s_tex_uZbias = p_glGetUniformLocation(s_tex_prog, "u_zbias");
|
||||
@@ -10520,6 +10592,7 @@ static const GpuRenderBackend GL_BACKEND = {
|
||||
.set_texture_window = glb_set_texture_window, .set_color_modulation = glb_set_color_modulation,
|
||||
.set_precise_triangle = glb_set_precise_triangle,
|
||||
.set_perspective_triangle = glb_set_perspective_triangle,
|
||||
.set_dither = glb_set_dither,
|
||||
.fill_rect = glb_fill_rect, .copy_rect = glb_copy_rect,
|
||||
.draw_flat_triangle = glb_draw_flat_triangle, .draw_gouraud_triangle = glb_draw_gouraud_triangle,
|
||||
.draw_textured_triangle = glb_draw_textured_triangle,
|
||||
@@ -11177,7 +11250,7 @@ static int fg_raw_add(const FgRaw *r) {
|
||||
static int fg_op_kept(uint16_t op) {
|
||||
switch (op) {
|
||||
case RTH_SEMI: case RTH_MASK: case RTH_TWIN: case RTH_MOD: case RTH_PRECISE:
|
||||
case RTH_PERSP: case RTH_DEPTH: case RTH_FILL: case RTH_FLAT_TRI: case RTH_GOURAUD_TRI:
|
||||
case RTH_PERSP: case RTH_DEPTH: case RTH_DITHER: case RTH_FILL: case RTH_FLAT_TRI: case RTH_GOURAUD_TRI:
|
||||
case RTH_TEX_TRI: case RTH_SHADED_TEX_TRI: case RTH_FLAT_RECT: case RTH_TEX_RECT:
|
||||
case RTH_TEX_RECT_SCALED: case RTH_LINE: case RTH_SHADED_LINE: case RTH_AREA:
|
||||
case RTH_OFFSET: case RTH_WIDE_VIEW: case RTH_WIDE_TARGET: case RTH_WIDE_DISABLE:
|
||||
@@ -11648,6 +11721,7 @@ static void fg_replay(const FgList *a, const FgList *b, double t, GLuint gen_wid
|
||||
const int in = !s_fg_replay_objects_only && fg_area_meets(area, disp);
|
||||
switch (h.op) {
|
||||
case RTH_SEMI: glb_set_semi_transparency(v[0], v[1]); break;
|
||||
case RTH_DITHER: glb_set_dither(v[0]); break;
|
||||
case RTH_MASK: glb_set_mask_bits(v[0], v[1]); break;
|
||||
case RTH_TWIN: glb_set_texture_window((uint32_t)v[0]); break;
|
||||
case RTH_MOD: glb_set_color_modulation(v[0], v[1], v[2], v[3]); break;
|
||||
@@ -13128,6 +13202,8 @@ static int rtb_scale(void) {
|
||||
}
|
||||
static void rtb_set_texture_filter(int b) { GL_RT_SYNC("texture_filter"); glb_set_texture_filter(b); }
|
||||
static int rtb_texture_filter(void) { return s_tex_filter; }
|
||||
static void rtb_set_dither(int on) {
|
||||
RTH_DIRECT_OR(RTH_REC(RTH_DITHER, 0, on ? 1 : 0)); glb_set_dither(on); }
|
||||
static void rtb_set_semi_transparency(int e, int m) {
|
||||
RTH_DIRECT_OR(RTH_REC(RTH_SEMI, 0, e, m)); glb_set_semi_transparency(e, m); }
|
||||
static void rtb_set_mask_bits(int sb, int cb) {
|
||||
@@ -13336,6 +13412,7 @@ static const GpuRenderBackend GL_RT_BACKEND = {
|
||||
.set_texture_window = rtb_set_texture_window, .set_color_modulation = rtb_set_color_modulation,
|
||||
.set_precise_triangle = rtb_set_precise_triangle,
|
||||
.set_perspective_triangle = rtb_set_perspective_triangle,
|
||||
.set_dither = rtb_set_dither,
|
||||
.fill_rect = rtb_fill_rect, .copy_rect = rtb_copy_rect,
|
||||
.draw_flat_triangle = rtb_draw_flat_triangle, .draw_gouraud_triangle = rtb_draw_gouraud_triangle,
|
||||
.draw_textured_triangle = rtb_draw_textured_triangle,
|
||||
@@ -13374,6 +13451,7 @@ static void gl_rth_exec(void *user, const RtCmd *c, const void *payload) {
|
||||
if (s_fg_on) fg_capture(c, payload);
|
||||
switch (c->op) {
|
||||
case RTH_SEMI: glb_set_semi_transparency(v[0], v[1]); break;
|
||||
case RTH_DITHER: glb_set_dither(v[0]); break;
|
||||
case RTH_MASK: glb_set_mask_bits(v[0], v[1]); break;
|
||||
case RTH_TWIN: glb_set_texture_window((uint32_t)v[0]); break;
|
||||
case RTH_MOD: glb_set_color_modulation(v[0], v[1], v[2], v[3]); break;
|
||||
|
||||
@@ -177,6 +177,9 @@ void gr_set_perspective_triangle(int enabled, float q0, float q1, float q2) {
|
||||
if (g_b->set_perspective_triangle)
|
||||
g_b->set_perspective_triangle(enabled, q0, q1, q2);
|
||||
}
|
||||
void gr_set_dither(int enabled) {
|
||||
if (g_b->set_dither) g_b->set_dither(enabled);
|
||||
}
|
||||
void gr_set_depth_triangle(int enabled, float z0, float z1, float z2) {
|
||||
if (g_b->set_depth_triangle)
|
||||
g_b->set_depth_triangle(enabled, z0, z1, z2);
|
||||
|
||||
@@ -1284,6 +1284,7 @@ static int g_video_fmv_chroma = 0; /* [video] fmv_chroma_smoothing /
|
||||
/* [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 = 0; /* [video] accurate_blending / PSX_ACCURATE_BLENDING */
|
||||
static int g_video_dithering = 0; /* [video] dithering / PSX_DITHERING */
|
||||
static int g_video_pgxp_color_correction = 0;
|
||||
static int g_video_pgxp_seam = 0;
|
||||
static float g_video_pgxp_depth_threshold = 4096.0f;
|
||||
@@ -16070,6 +16071,7 @@ int main(int argc, char** argv) {
|
||||
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_dithering = gc.runtime.video_dithering;
|
||||
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;
|
||||
@@ -18463,6 +18465,8 @@ session_reboot:
|
||||
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);
|
||||
if (const char* e = std::getenv("PSX_DITHERING")) g_video_dithering = std::atoi(e);
|
||||
gl_renderer_set_dithering(g_video_dithering);
|
||||
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);
|
||||
|
||||
@@ -268,6 +268,7 @@ int gr_scale(void) { return 1; }
|
||||
void gr_set_texture_filter(int bilinear) { (void)bilinear; }
|
||||
int gr_texture_filter(void) { return 0; }
|
||||
void gr_set_semi_transparency(int enabled, int mode) { (void)enabled; (void)mode; }
|
||||
void gr_set_dither(int enabled) { (void)enabled; }
|
||||
void gr_set_mask_bits(int set_bit, int check_bit) { (void)set_bit; (void)check_bit; }
|
||||
void gr_set_texture_window(uint32_t raw) { (void)raw; }
|
||||
void gr_set_color_modulation(int r, int g, int b, int raw_texture) {
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/* [video] dithering on a real, hidden OpenGL context, driven through the
|
||||
* renderer facade (gr_*) the way gpu.c drives it.
|
||||
*
|
||||
* - off (the default): the GP0(E1h) dither bit changes nothing, so the
|
||||
* native VRAM and the frame at internal resolution are byte-identical
|
||||
* with the bit set or clear;
|
||||
* - on / scaled with the bit clear: identical to off;
|
||||
* - on / scaled with the bit set: a gouraud gradient and a modulated
|
||||
* texture gain the PS1 4x4 pattern (more 5-bit steps along a row, same
|
||||
* mean brightness), while flat fills, flat polygons and raw textures stay
|
||||
* exactly as they were;
|
||||
* - "scaled" puts the pattern on the native grid: the native VRAM at 4x
|
||||
* (stepped by dynamic resolution) equals the native VRAM at 1x;
|
||||
* - the render thread (records replayed later), PGXP depth and native-wide
|
||||
* margins give the same pixels as the direct path.
|
||||
* Original source-owned scene; no retail payload. */
|
||||
#define main rt_fixture_main
|
||||
#include "test_gl_render_thread.c"
|
||||
#undef main
|
||||
|
||||
#define DW 320
|
||||
#define DH 240
|
||||
static uint16_t nat[1024 * 512];
|
||||
static uint32_t hi[DW * 4 * DH * 4];
|
||||
static int k_test_scale = 4;
|
||||
|
||||
static void scene(int bit, int depth) {
|
||||
gr_set_draw_area(0, 0, DW - 1, DH - 1);
|
||||
gr_set_draw_offset(0, 0);
|
||||
gr_wide_set_target(0);
|
||||
mask_bits(0, 0);
|
||||
gr_set_semi_transparency(0, 0);
|
||||
gr_set_dither(0);
|
||||
gr_fill_rect(0, 0, DW, DH, 0x0000);
|
||||
gr_wide_clear(0, 0, DH, 0x0000);
|
||||
gr_set_dither(bit);
|
||||
/* A horizontal gouraud gradient, black to white (rows 0..63), reaching
|
||||
* into the left native-wide margin. */
|
||||
if (depth) gr_set_depth_triangle(1, 900.0f, 900.0f, 900.0f);
|
||||
gr_draw_gouraud_triangle(-40, 0, 0x0000, 256, 0, 0x7fff, -40, 64, 0x0000);
|
||||
if (depth) gr_set_depth_triangle(1, 900.0f, 900.0f, 900.0f);
|
||||
gr_draw_gouraud_triangle(256, 0, 0x7fff, 256, 64, 0x7fff, -40, 64, 0x0000);
|
||||
/* Flat polygon and flat rect (never dithered). */
|
||||
gr_draw_flat_rect(0, 80, 64, 32, 0x4210);
|
||||
gr_draw_flat_triangle(80, 80, 140, 80, 80, 112, 0x2d6b);
|
||||
/* Modulated and raw textured draws. */
|
||||
gr_set_color_modulation(128, 128, 128, 0);
|
||||
gr_draw_shaded_textured_triangle(160, 80, 0, 0, 0x404040, 300, 80, 63, 0, 0xa0a0a0,
|
||||
160, 200, 0, 63, 0x606060, 512, 256, 0x0008, 0);
|
||||
gr_set_color_modulation(128, 128, 128, 1);
|
||||
gr_draw_textured_rect(0, 130, 64, 64, 0, 0, 512, 256, 0x0008);
|
||||
gr_set_color_modulation(128, 128, 128, 0);
|
||||
gr_set_dither(0);
|
||||
gl_renderer_render_thread_frame_boundary();
|
||||
}
|
||||
|
||||
typedef struct { uint64_t native, hires, wide; } Shot;
|
||||
static Shot shot(void) {
|
||||
Shot s;
|
||||
gr_vram_transfer_out(0, 0, 1024, 512, nat);
|
||||
s.native = fnv(nat, sizeof nat, 0xcbf29ce484222325ull);
|
||||
int ow = 0, oh = 0, sc = gr_scale();
|
||||
int n = gl_renderer_read_display_hires(0, 0, DW, DH, hi, DW * sc * DH * sc, &ow, &oh);
|
||||
check(n == DW * sc * DH * sc, "hires readback");
|
||||
s.hires = fnv(hi, (size_t)n * 4, 0xcbf29ce484222325ull);
|
||||
static uint32_t wb[426 * 4 * 512 * 4];
|
||||
int gw = 0, gh = 0, got = gr_wide_dump_full(wb, 426 * sc * 512 * sc, &gw, &gh, 0);
|
||||
s.wide = got > 0 ? fnv(wb, (size_t)got * 4, 0xcbf29ce484222325ull) : 0;
|
||||
return s;
|
||||
}
|
||||
static int same(Shot a, Shot b) { return a.native == b.native && a.hires == b.hires && a.wide == b.wide; }
|
||||
static int region_equal(const uint16_t *a, const uint16_t *b, int x, int y, int w, int h) {
|
||||
for (int r = y; r < y + h; r++)
|
||||
if (memcmp(a + r * 1024 + x, b + r * 1024 + x, (size_t)w * 2)) return 0;
|
||||
return 1;
|
||||
}
|
||||
/* Along the gradient's row 30: steps between neighbours, and the mean red. */
|
||||
static void row_stats(const uint16_t *v, int *steps, double *mean) {
|
||||
int s = 0; long sum = 0;
|
||||
for (int x = 0; x < 250; x++) {
|
||||
int r = v[30 * 1024 + x] & 31, n = v[30 * 1024 + x + 1] & 31;
|
||||
s += r != n; sum += r;
|
||||
}
|
||||
*steps = s; *mean = (double)sum / 250.0;
|
||||
}
|
||||
|
||||
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
|
||||
SDL_Window *win = SDL_CreateWindow("Dithering hidden test", 0, 0, 320, 240,
|
||||
SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN);
|
||||
if (!win) { fprintf(stderr, "SKIP no window\n"); return 77; }
|
||||
gr_set_backend(GR_BACKEND_OPENGL);
|
||||
gr_init(vram);
|
||||
gr_set_scale(k_test_scale);
|
||||
gl_renderer_set_dynamic_resolution(1);
|
||||
gl_renderer_set_swap_interval(0);
|
||||
if (!gl_renderer_init_context(win)) { fprintf(stderr, "SKIP no GL context\n"); return 77; }
|
||||
textures();
|
||||
gr_wide_configure(426, 53);
|
||||
static uint16_t ref[1024 * 512], dith[1024 * 512];
|
||||
|
||||
/* Off: the bit is ignored. */
|
||||
check(gl_renderer_dithering() == 0, "off by default");
|
||||
scene(0, 0); Shot off0 = shot(); memcpy(ref, nat, sizeof ref);
|
||||
scene(1, 0); Shot off1 = shot();
|
||||
check(same(off0, off1), "off: dither bit changes nothing");
|
||||
for (int mode = 1; mode <= 2; mode++) {
|
||||
gl_renderer_set_dithering(mode);
|
||||
scene(0, 0); Shot clear = shot();
|
||||
check(same(off0, clear), "on/scaled with the bit clear equals off");
|
||||
scene(1, 0); Shot on = shot();
|
||||
check(on.native != off0.native && on.hires != off0.hires, "bit set dithers");
|
||||
check(on.wide != off0.wide, "native-wide margin dithers too");
|
||||
memcpy(dith, nat, sizeof dith);
|
||||
check(region_equal(ref, dith, 0, 80, 150, 32), "flat polygons untouched");
|
||||
check(region_equal(ref, dith, 0, 130, 64, 64), "raw texture untouched");
|
||||
check(!region_equal(ref, dith, 160, 80, 140, 120), "modulated texture dithered");
|
||||
int s0, s1; double m0, m1;
|
||||
row_stats(ref, &s0, &m0); row_stats(dith, &s1, &m1);
|
||||
printf("mode=%d steps true=%d dithered=%d mean true=%.2f dithered=%.2f\n", mode, s0, s1, m0, m1);
|
||||
/* "on" dithers per internal pixel, so the native pack (each block's
|
||||
* top-left sample) sees one pattern column; "scaled" all four. */
|
||||
check(mode == 2 ? s1 > s0 * 3 : s1 > s0, "dither pattern adds 5-bit steps");
|
||||
check(fabs(m1 - m0) < 1.0, "dither keeps the mean brightness");
|
||||
for (int x = 0; x < 250; x++) {
|
||||
uint16_t p = dith[30 * 1024 + x];
|
||||
check((p & 31) == ((p >> 5) & 31) && (p & 31) == ((p >> 10) & 31), "grey stays grey");
|
||||
}
|
||||
/* PGXP depth: the same 2D-depth scene, depth-tested. */
|
||||
gl_renderer_set_pgxp_depth(1);
|
||||
scene(1, 1); Shot dep = shot();
|
||||
check(dep.native == on.native, "PGXP depth path dithers the same");
|
||||
gl_renderer_set_pgxp_depth(0);
|
||||
/* Render thread: dither bit and mode recorded with the draws. */
|
||||
check(gl_renderer_render_thread_start(2) == 1, "render thread started");
|
||||
scene(1, 0);
|
||||
gl_renderer_render_thread_sync("dither");
|
||||
Shot rt = shot();
|
||||
gl_renderer_render_thread_stop();
|
||||
check(same(rt, on), "render thread gives the same pixels");
|
||||
/* With accurate_blending the dither output uses the k*8 basis, like
|
||||
* every other write: every gradient channel at internal resolution
|
||||
* is a multiple of 8 (no k*255/31 values mixed in). */
|
||||
gl_renderer_set_accurate_blending(1);
|
||||
scene(1, 0); Shot acc = shot();
|
||||
int off_basis = 0;
|
||||
for (int y = 0; y < 60 * gr_scale(); y++)
|
||||
for (int x = 0; x < 250 * gr_scale(); x++) {
|
||||
uint32_t p = hi[y * DW * gr_scale() + x];
|
||||
off_basis += ((p & 0xff) % 8) != 0;
|
||||
}
|
||||
int sa; double ma;
|
||||
row_stats(nat, &sa, &ma);
|
||||
printf("mode=%d accurate: mean=%.2f channels off the k*8 grid=%d\n", mode, ma, off_basis);
|
||||
check(fabs(ma - m0) < 1.0, "dither + accurate blending keeps the mean");
|
||||
check(off_basis == 0, "dither output follows the accurate (k*8) basis");
|
||||
gl_renderer_set_accurate_blending(0);
|
||||
if (mode == 2) {
|
||||
/* Scaled: the pattern sits on the native grid at any scale. */
|
||||
check(gl_renderer_step_internal_scale_now(1) == 1, "step to 1x");
|
||||
scene(1, 0); Shot one = shot();
|
||||
check(one.native == on.native, "scaled: native VRAM at 4x equals 1x");
|
||||
check(gl_renderer_step_internal_scale_now(k_test_scale) == 1, "back to 4x");
|
||||
}
|
||||
}
|
||||
gl_renderer_set_dithering(0);
|
||||
scene(1, 0); Shot back = shot();
|
||||
check(same(back, off0), "back off restores the exact true-colour image");
|
||||
check(glGetError() == GL_NO_ERROR, "GL errors");
|
||||
printf("off native=%016llx hires=%016llx\n", (unsigned long long)off0.native,
|
||||
(unsigned long long)off0.hires);
|
||||
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