mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Merge pull request #586 from RetroPortingToolKit/feat/gl-xbr-filter
GL: opt-in xBR edge-directed texture filter (texture_filtering = "xbr")
This commit is contained in:
@@ -58,6 +58,36 @@ into `licenses/`. Developers can use the verified archive or an explicit
|
||||
`FETCHCONTENT_SOURCE_DIR_PSX_LIBWEBP` / `PSX_LIBWEBP_SOURCE_DIR` source override.
|
||||
PNG and JPEG continue to use the existing shared stb_image implementation.
|
||||
|
||||
## xBR — edge-directed texture filter (Hyllian)
|
||||
|
||||
The `texture_filtering = "xbr"` mode in `runtime/src/gpu_gl_renderer.c`
|
||||
(`xbr_d`, `xbr_sample`) follows Hyllian's xBR algorithm: its 5x5 neighbourhood,
|
||||
YUV-weighted colour distance and the `4*d(H,F)` / `4*d(E,I)` edge sums. It is
|
||||
an adaptation (CLUT texel fetches, primitive UV clamping, an anti-aliased corner
|
||||
cut), not a copy of a shader file, but it derives from that work, licensed MIT:
|
||||
|
||||
```
|
||||
Copyright (C) 2011-2016 Hyllian - sergiogdb@gmail.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
```
|
||||
|
||||
## Vendored libraries
|
||||
|
||||
These are checked in under `recompiler/lib/` and `runtime/third_party/` with
|
||||
|
||||
@@ -486,9 +486,9 @@ between Play and TCP availability.
|
||||
|
||||
## Complete command index (generated)
|
||||
|
||||
**367 commands registered** — 354 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
**368 commands registered** — 355 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
|
||||
82 of 367 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.
|
||||
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.
|
||||
|
||||
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
|
||||
|
||||
@@ -799,6 +799,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `stereo_dump` | ✓ | | |
|
||||
| `stereo_stats` | ✓ | | |
|
||||
| `synth_recurse` | ✓ | | |
|
||||
| `texture_filter` | ✓ | | |
|
||||
| `texture_lod` | ✓ | | ✓ |
|
||||
| `thread_ctx_ring` | ✓ | | |
|
||||
| `thread_trace` | ✓ | | |
|
||||
|
||||
@@ -698,6 +698,28 @@ 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.
|
||||
### Texture filtering (`texture_filtering`)
|
||||
|
||||
```toml
|
||||
[video]
|
||||
texture_filtering = "nearest" # nearest (default) | bilinear | xbr
|
||||
```
|
||||
|
||||
`nearest` is the PS1's point sampling. `bilinear` blends the four nearest
|
||||
texels (opacity-gated, so cutout edges keep their colour). `xbr` (OpenGL
|
||||
only) is edge-directed magnification after Hyllian's xBR (MIT; see
|
||||
`THIRD_PARTY_ATTRIBUTION.md`): for the texel
|
||||
corner nearest each sample it compares weighted colour distances over a 5x5
|
||||
neighbourhood, and where an edge runs along that corner's diagonal the corner
|
||||
is cut and takes the closer side neighbour, with an anti-aliased boundary one
|
||||
output pixel wide. Flat areas, straight edges and gradients stay crisp
|
||||
texels; diagonals and curves lose their staircase. It acts only where a
|
||||
texel spans two or more output pixels (high internal scale or a magnified
|
||||
texture), so a 1:1 draw is exactly nearest; cutouts and STP stay the centre
|
||||
texel's. It costs up to 13 texel fetches per pixel. JINC2 is not offered.
|
||||
`PSX_TEXTURE_FILTER=0..3` overrides it for one run (2 is the mod-only "stable
|
||||
world" minification filter) and the `texture_filter` TCP command switches it
|
||||
live. The launcher's toggle still offers nearest and bilinear.
|
||||
|
||||
The runtime clamps N per backend:
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
27437f9269d367ba4ad2e17fc66f3523c2c1e6fcd2ee5a39f6c07ee6516c84a2
|
||||
fcb3412a5245142593c6d5cdaabfda1f5d58ff878e09b175fd8aa10dee1554db
|
||||
|
||||
@@ -776,8 +776,9 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
const auto mode = toml::find<std::string>(video, "texture_filtering");
|
||||
if (mode == "nearest") rt.video_texture_filter = 0;
|
||||
else if (mode == "bilinear") rt.video_texture_filter = 1;
|
||||
else if (mode == "xbr") rt.video_texture_filter = 3;
|
||||
else throw std::runtime_error(fmt::format(
|
||||
"[video] texture_filtering must be \"nearest\" or \"bilinear\": {}", mode));
|
||||
"[video] texture_filtering must be \"nearest\", \"bilinear\" or \"xbr\": {}", mode));
|
||||
}
|
||||
if (video.contains("fmv_filter")) {
|
||||
const auto mode = toml::find<std::string>(video, "fmv_filter");
|
||||
@@ -2867,6 +2868,7 @@ UserSettings load_user_settings(const fs::path& path) {
|
||||
const auto m = toml::find<std::string>(v, "texture_filtering");
|
||||
if (m == "nearest") { s.texture_filter = 0; s.has_texture_filter = true; }
|
||||
else if (m == "bilinear") { s.texture_filter = 1; s.has_texture_filter = true; }
|
||||
else if (m == "xbr") { s.texture_filter = 3; s.has_texture_filter = true; }
|
||||
});
|
||||
if (v.contains("fmv_filter")) try_get([&]{
|
||||
const auto m = toml::find<std::string>(v, "fmv_filter");
|
||||
@@ -3268,7 +3270,7 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
|
||||
if (s.has_antialiasing)
|
||||
f << "antialiasing = " << (s.antialiasing ? "true" : "false") << "\n";
|
||||
if (s.has_texture_filter)
|
||||
f << "texture_filtering = \"" << (s.texture_filter ? "bilinear" : "nearest") << "\"\n";
|
||||
f << "texture_filtering = \"" << (s.texture_filter == 3 ? "xbr" : s.texture_filter ? "bilinear" : "nearest") << "\"\n";
|
||||
if (s.has_fmv_filter)
|
||||
f << "fmv_filter = \"" << video_fmv_filter_name(s.fmv_filter) << "\"\n";
|
||||
if (s.has_geometry_correction)
|
||||
|
||||
@@ -460,6 +460,7 @@ struct RuntimeConfig {
|
||||
int video_supersample_milli = 1000;
|
||||
|
||||
// texture_filtering: "nearest" (default, native PSX look) | "bilinear"
|
||||
// | "xbr" (3: edge-directed magnification, OpenGL only)
|
||||
// (smooths textures and 2D backgrounds). Stored as 0/1.
|
||||
int video_texture_filter = 0;
|
||||
|
||||
|
||||
+25
-13
@@ -265,6 +265,31 @@ if(BUILD_TESTING)
|
||||
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-frame-gen-results")
|
||||
set_tests_properties(gl_frame_gen_test PROPERTIES
|
||||
LABELS "gpu;opengl;hardware" TIMEOUT 300 SKIP_RETURN_CODE 77)
|
||||
# [video] texture_lod / anisotropic_filtering: off and untracked/
|
||||
# magnified prims are unchanged; minified world textures average
|
||||
# inside their UV bounds; render thread and PGXP depth agree.
|
||||
add_test(NAME gl_texture_lod_test
|
||||
COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_render_thread.py" --single
|
||||
--fixture "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_texture_lod.c"
|
||||
--cc "${CMAKE_C_COMPILER}"
|
||||
--sdl-include "${_gl_scale_sdl_inc}"
|
||||
--sdl-library "$<TARGET_FILE:SDL3::SDL3>"
|
||||
--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)
|
||||
# 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
|
||||
COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_render_thread.py" --single
|
||||
--fixture "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_xbr.c"
|
||||
--cc "${CMAKE_C_COMPILER}"
|
||||
--sdl-include "${_gl_scale_sdl_inc}"
|
||||
--sdl-library "$<TARGET_FILE:SDL3::SDL3>"
|
||||
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-xbr-results")
|
||||
set_tests_properties(gl_xbr_filter_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)")
|
||||
@@ -312,19 +337,6 @@ if(BUILD_TESTING)
|
||||
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-fmv-chroma-results")
|
||||
set_tests_properties(gl_fmv_chroma_test PROPERTIES
|
||||
LABELS "gpu;opengl;hardware" TIMEOUT 180 SKIP_RETURN_CODE 77)
|
||||
# [video] texture_lod / anisotropic_filtering: off and untracked/
|
||||
# magnified prims are unchanged; minified world textures average
|
||||
# inside their UV bounds; render thread and PGXP depth agree.
|
||||
add_test(NAME gl_texture_lod_test
|
||||
COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_render_thread.py" --single
|
||||
--fixture "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_texture_lod.c"
|
||||
--cc "${CMAKE_C_COMPILER}"
|
||||
--sdl-include "${_gl_scale_sdl_inc}"
|
||||
--sdl-library "$<TARGET_FILE:SDL3::SDL3>"
|
||||
--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)
|
||||
endif()
|
||||
add_executable(launcher_device_roundtrip_test
|
||||
tests/test_launcher_device_roundtrip.cpp)
|
||||
|
||||
@@ -8629,6 +8629,15 @@ static void handle_texture_lod(int id, const char *json)
|
||||
gl_renderer_texture_lod(), gl_renderer_anisotropy());
|
||||
}
|
||||
|
||||
/* 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)
|
||||
{
|
||||
int mode = json_get_int(json, "mode", -1);
|
||||
if (mode >= 0) gr_set_texture_filter(mode);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"mode\":%d}", id, gr_texture_filter());
|
||||
}
|
||||
|
||||
static void handle_frame_perf(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
@@ -15854,6 +15863,7 @@ static const CmdEntry s_commands[] = {
|
||||
{ "post_aa", handle_post_aa },
|
||||
{ "fmv_chroma", handle_fmv_chroma },
|
||||
{ "texture_lod", handle_texture_lod },
|
||||
{ "texture_filter", handle_texture_filter },
|
||||
{ "gl_ws_ablate", handle_gl_ws_ablate },
|
||||
{ "gl_interp", handle_gl_interp },
|
||||
{ "render_pass_stats", handle_render_pass_stats },
|
||||
|
||||
@@ -1921,7 +1921,7 @@ static const char *TEX_FS =
|
||||
"uniform int u_semipass; /* 0=all texels, 1=STP=0 only, 2=STP=1 only */\n"
|
||||
"uniform int u_semimode; /* PS1 blend mode; drives dual-source factors */\n"
|
||||
"uniform int u_maskset; /* GP0(E6h) set-mask: OR bit15 into output */\n"
|
||||
"uniform int u_filter; /* bits 0-3: 0 nearest, 1 bilinear, 2 stable; bit 4: LOD */\n"
|
||||
"uniform int u_filter; /* bits 0-3: 0 nearest, 1 bilinear, 2 stable, 3 xbr; bit 4: LOD */\n"
|
||||
"uniform int u_aniso; /* [video] anisotropic_filtering, 1..16 */\n"
|
||||
"uniform float u_shift;\n"
|
||||
"int vram_at(int x, int y){\n"
|
||||
@@ -2015,6 +2015,47 @@ static const char *TEX_FS =
|
||||
" }\n"
|
||||
" return sum.a > 0.0 ? sum.rgb / sum.a : base;\n"
|
||||
"}\n"
|
||||
/* [video] texture_filtering = "xbr": edge-directed magnification after
|
||||
* Hyllian's xBR (MIT; notice in THIRD_PARTY_ATTRIBUTION.md): the same
|
||||
* E/F/H/I neighbourhood and 4x-weighted edge sums, adapted to CLUT
|
||||
* texels and an anti-aliased corner cut. For the
|
||||
* texel corner nearest the sample, two weighted colour-distance sums
|
||||
* across a 5x5 neighbourhood decide whether an edge runs along the
|
||||
* corner's anti-diagonal; if so, the corner is cut by that diagonal and
|
||||
* takes the closer of the two side neighbours, with an anti-aliased
|
||||
* boundary one output pixel wide. Smooth gradients and straight edges are
|
||||
* left as nearest texels, so pixel art keeps its crisp blocks while
|
||||
* diagonals and curves lose their staircase. Transparent (0) texels never
|
||||
* supply a colour; cutout and STP stay the centre texel's. */
|
||||
"vec3 xbr_c(int raw){ return col5(raw); }\n"
|
||||
"float xbr_d(int a, int b){\n"
|
||||
" vec3 x = xbr_c(a), y = xbr_c(b);\n"
|
||||
" vec3 dv = x - y;\n"
|
||||
" float yy = dot(dv, vec3(0.299, 0.587, 0.114));\n"
|
||||
" float u = dot(dv, vec3(-0.169, -0.331, 0.5)), v = dot(dv, vec3(0.5, -0.419, -0.081));\n"
|
||||
" return 48.0*abs(yy) + 7.0*abs(u) + 6.0*abs(v) + (((a==0)!=(b==0)) ? 64.0 : 0.0);\n"
|
||||
"}\n"
|
||||
"int xbr_at(ivec2 e, ivec2 s, int x, int y){ return stable_texel(e + ivec2(x*s.x, y*s.y)); }\n"
|
||||
"vec3 xbr_sample(vec2 uv, int e_raw, vec2 fw){\n"
|
||||
" ivec2 e = ivec2(floor(uv)); vec2 f = fract(uv);\n"
|
||||
" ivec2 s = ivec2(f.x < 0.5 ? -1 : 1, f.y < 0.5 ? -1 : 1);\n"
|
||||
" vec2 l = vec2(s.x > 0 ? f.x : 1.0 - f.x, s.y > 0 ? f.y : 1.0 - f.y); /* 0.5..1 toward the corner */\n"
|
||||
" int E=e_raw;\n"
|
||||
" if (max(fw.x, fw.y) >= 0.5) return xbr_c(E); /* under 2x: stay nearest, no fetches */\n"
|
||||
" int F=xbr_at(e,s,1,0), H=xbr_at(e,s,0,1);\n"
|
||||
" if (E == F || E == H) return xbr_c(E);\n"
|
||||
" int I=xbr_at(e,s,1,1);\n"
|
||||
" int B=xbr_at(e,s,0,-1), C=xbr_at(e,s,1,-1), D=xbr_at(e,s,-1,0), G=xbr_at(e,s,-1,1);\n"
|
||||
" int F4=xbr_at(e,s,2,0), I4=xbr_at(e,s,2,1), H5=xbr_at(e,s,0,2), I5=xbr_at(e,s,1,2);\n"
|
||||
" float we = xbr_d(E,C)+xbr_d(E,G)+xbr_d(I,H5)+xbr_d(I,F4)+4.0*xbr_d(H,F);\n"
|
||||
" float wi = xbr_d(H,D)+xbr_d(H,I5)+xbr_d(F,I4)+xbr_d(F,B)+4.0*xbr_d(E,I);\n"
|
||||
" if (!(we < wi)) return xbr_c(E);\n"
|
||||
" int px = xbr_d(E,F) <= xbr_d(E,H) ? F : H;\n"
|
||||
" if (px == 0) return xbr_c(E);\n"
|
||||
" float w = max(fw.x + fw.y, 1e-3);\n"
|
||||
" float a = clamp((l.x + l.y - 1.5) / w + 0.5, 0.0, 1.0);\n"
|
||||
" return mix(xbr_c(E), xbr_c(px), a);\n"
|
||||
"}\n"
|
||||
"void main(){\n"
|
||||
" int stp; vec3 rgb;\n"
|
||||
" int fmode = u_filter & 15;\n"
|
||||
@@ -2044,6 +2085,12 @@ static const char *TEX_FS =
|
||||
" if (raw == 0) discard;\n"
|
||||
" rgb = col5(raw);\n"
|
||||
" stp = (raw >> 15) & 1;\n"
|
||||
" } else if (fmode == 3) {\n"
|
||||
" /* The nearest texel stays the PS1 one (floor of the unshifted uv). */\n"
|
||||
" int raw = stable_texel(ivec2(floor(uv)));\n"
|
||||
" if (raw == 0) discard;\n"
|
||||
" stp = (raw >> 15) & 1;\n"
|
||||
" rgb = xbr_sample(uv, raw, abs(dx) + abs(dy));\n"
|
||||
" } else if (fmode == 2) {\n"
|
||||
" uv+=vec2(u_shift); int raw=stable_texel(ivec2(floor(uv)));\n"
|
||||
" if(raw==0) discard; stp=(raw>>15)&1;\n"
|
||||
@@ -4941,7 +4988,7 @@ 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 void glb_set_texture_filter(int b) { s_tex_filter = b >= 0 && b <= 2 ? b : 0; sw_set_texture_filter(b != 0); }
|
||||
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; }
|
||||
|
||||
static void glb_set_semi_transparency(int e, int m) { s_semi_en = e; s_semi_mode = m & 3; sw_set_semi_transparency(e, m); }
|
||||
|
||||
@@ -9522,7 +9522,7 @@ static int debug_toggles_on(void) {
|
||||
}
|
||||
static int s_dbg_scale_target = 0;
|
||||
static const char *dbg_texfilter_name(int f) {
|
||||
return f == 1 ? "bilinear" : f == 2 ? "stable world" : "nearest";
|
||||
return f == 1 ? "bilinear" : f == 2 ? "stable world" : f == 3 ? "xbr" : "nearest";
|
||||
}
|
||||
static void debug_toggle_summary(char *buf, size_t cap) {
|
||||
GlDynresStats st; gl_renderer_dynres_stats(&st);
|
||||
@@ -9578,7 +9578,7 @@ static int debug_toggle_key(int key, char *out, int cap) {
|
||||
on && !g_render_thread ? " (inert: render thread off)" : "");
|
||||
break; }
|
||||
case SDLK_5: {
|
||||
const int f = (gr_texture_filter() + 1) % 3;
|
||||
const int f = (gr_texture_filter() + 1) % 4;
|
||||
gr_set_texture_filter(f);
|
||||
std::snprintf(msg, sizeof msg, "Texture filter: %s", dbg_texfilter_name(f));
|
||||
break; }
|
||||
@@ -18463,6 +18463,7 @@ session_reboot:
|
||||
(net_cfg.enabled && s_netplay_gl_present &&
|
||||
gl_renderer_cpu_auth_dual());
|
||||
g_video_scale = gr_scale(); /* reflect any clamp / alloc fallback */
|
||||
if (const char* e = std::getenv("PSX_TEXTURE_FILTER")) g_video_texfilter = std::atoi(e);
|
||||
gr_set_texture_filter(g_mod_texfilter < 0 ? g_video_texfilter : g_mod_texfilter);
|
||||
/* Sub-pixel vertex precision + perspective-correct UVs. Both default off;
|
||||
* with both off every setter below leaves the tracking caches disabled and
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/* [video] texture_filtering = "xbr" on a real, hidden OpenGL context, driven
|
||||
* through the renderer facade (gr_*).
|
||||
*
|
||||
* - a 45-degree red/blue edge magnified 8x loses its 8-pixel staircase: the
|
||||
* boundary column in each row follows the diagonal closely;
|
||||
* - straight edges and flat areas stay exactly the nearest-texel image, and
|
||||
* a 1:1 (unmagnified) draw is byte-identical to nearest;
|
||||
* - cutouts (texel 0) stay cutouts and never lend their colour;
|
||||
* - the render thread gives the same pixels as the direct path;
|
||||
* - the other filter modes are unchanged by the new one (nearest after xbr
|
||||
* restores the exact image).
|
||||
* Original source-owned scene; no retail payload. */
|
||||
#define main rt_fixture_main
|
||||
#include "test_gl_render_thread.c"
|
||||
#undef main
|
||||
|
||||
static uint16_t nat[1024 * 512];
|
||||
#define TP 0x0108 /* 15-bit page at (512,0) */
|
||||
|
||||
static void pages(void) {
|
||||
static uint16_t pg[32 * 16];
|
||||
for (int y = 0; y < 16; y++)
|
||||
for (int x = 0; x < 32; x++) {
|
||||
uint16_t c;
|
||||
if (x < 16) c = x > y ? 0x001f : 0x7c00; /* diagonal edge */
|
||||
else if (x < 24) c = x < 20 ? 0x001f : 0x7c00; /* vertical edge */
|
||||
else c = (x == 28 && y == 8) ? 0x0000 : 0x03e0; /* green with a hole */
|
||||
pg[y * 32 + x] = c;
|
||||
}
|
||||
cpu_upload(512, 0, 32, 16, pg);
|
||||
}
|
||||
/* Each triangle carries GTE provenance (precise vertices), as world geometry
|
||||
* does, so [video] texture_lod sets its LOD bit on these draws. */
|
||||
static void quad(int x, int y, int w, int h, int u0, int v0, int uw, int vh) {
|
||||
gr_set_precise_triangle(1, x << 16, y << 16, (x + w) << 16, y << 16, x << 16, (y + h) << 16);
|
||||
gr_draw_textured_triangle(x, y, u0, v0, x + w, y, u0 + uw, v0, x, y + h, u0, v0 + vh, 0, 0, TP);
|
||||
gr_set_precise_triangle(1, (x + w) << 16, y << 16, (x + w) << 16, (y + h) << 16, x << 16, (y + h) << 16);
|
||||
gr_draw_textured_triangle(x + w, y, u0 + uw, v0, x + w, y + h, u0 + uw, v0 + vh, x, y + h, u0, v0 + vh, 0, 0, TP);
|
||||
}
|
||||
static void scene(void) {
|
||||
gr_set_draw_area(0, 0, 319, 239);
|
||||
gr_set_draw_offset(0, 0);
|
||||
mask_bits(0, 0);
|
||||
gr_set_semi_transparency(0, 0);
|
||||
gr_set_color_modulation(128, 128, 128, 1);
|
||||
gr_fill_rect(0, 0, 320, 240, 0x1084);
|
||||
quad(0, 0, 128, 128, 0, 0, 16, 16); /* diagonal, 8x */
|
||||
quad(140, 0, 64, 128, 16, 0, 8, 16); /* vertical edge, 8x */
|
||||
quad(210, 0, 64, 128, 24, 0, 8, 16); /* flat green with a hole, 8x */
|
||||
quad(0, 150, 32, 16, 0, 0, 32, 16); /* 1:1 */
|
||||
gl_renderer_render_thread_frame_boundary();
|
||||
gr_vram_transfer_out(0, 0, 1024, 512, nat);
|
||||
}
|
||||
/* Mean distance between each row's first red-dominant column and the ideal
|
||||
* diagonal (x = y + 0.5 texel, in pixels). */
|
||||
static double stair_error(void) {
|
||||
double e = 0; int n = 0;
|
||||
for (int y = 4; y < 124; y++) {
|
||||
int edge = 128;
|
||||
for (int x = 0; x < 128; x++) {
|
||||
uint16_t p = nat[y * 1024 + x];
|
||||
if ((p & 31) > ((p >> 10) & 31)) { edge = x; break; }
|
||||
}
|
||||
e += fabs((double)edge - ((double)y + 4.0)); n++;
|
||||
}
|
||||
return e / n;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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("xBR 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(1);
|
||||
gl_renderer_set_swap_interval(0);
|
||||
if (!gl_renderer_init_context(win)) { fprintf(stderr, "SKIP no GL context\n"); return 77; }
|
||||
pages();
|
||||
static uint16_t ref[1024 * 512], x[1024 * 512];
|
||||
gr_set_texture_filter(0);
|
||||
scene(); memcpy(ref, nat, sizeof ref);
|
||||
const double e_near = stair_error();
|
||||
gr_set_texture_filter(3);
|
||||
check(gr_texture_filter() == 3, "xbr selectable");
|
||||
scene(); memcpy(x, nat, sizeof x);
|
||||
const double e_xbr = stair_error();
|
||||
printf("stair error nearest=%.2f xbr=%.2f\n", e_near, e_xbr);
|
||||
check(e_xbr < e_near * 0.7, "diagonal staircase smoothed");
|
||||
check(region_equal(ref, x, 140, 0, 64, 128), "straight edge unchanged");
|
||||
check(region_equal(ref, x, 210, 0, 64, 128), "flat area and cutout unchanged");
|
||||
check(region_equal(ref, x, 0, 150, 32, 16), "1:1 draw identical to nearest");
|
||||
/* texture_lod adds bit 4 to the filter key; xbr must still run (the
|
||||
* magnified quads keep their texels under LOD). */
|
||||
gl_renderer_set_texture_lod(1, 16);
|
||||
scene();
|
||||
check(memcmp(nat, x, sizeof x) == 0, "xbr unchanged with texture_lod on");
|
||||
gl_renderer_set_texture_lod(0, 1);
|
||||
check(gl_renderer_render_thread_start(2) == 1, "render thread started");
|
||||
scene();
|
||||
gl_renderer_render_thread_sync("xbr");
|
||||
check(memcmp(nat, x, sizeof x) == 0, "render thread gives the same pixels");
|
||||
gl_renderer_render_thread_stop();
|
||||
gr_set_texture_filter(0);
|
||||
scene();
|
||||
check(memcmp(nat, ref, sizeof ref) == 0, "nearest after xbr restores the exact image");
|
||||
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