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:
Matt Stanley
2026-10-09 23:19:31 -07:00
committed by GitHub
11 changed files with 270 additions and 22 deletions
+30
View File
@@ -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
+3 -2
View File
@@ -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` | ✓ | | |
+22
View File
@@ -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
View File
@@ -1 +1 @@
27437f9269d367ba4ad2e17fc66f3523c2c1e6fcd2ee5a39f6c07ee6516c84a2
fcb3412a5245142593c6d5cdaabfda1f5d58ff878e09b175fd8aa10dee1554db
+4 -2
View File
@@ -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)
+1
View File
@@ -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
View File
@@ -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)
+10
View File
@@ -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 },
+49 -2
View File
@@ -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); }
+3 -2
View File
@@ -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
+122
View File
@@ -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;
}