GL: opt-in texture LOD + anisotropic filtering for world textures

[video] texture_lod = "mipmap" emulates mip levels for PS1 texture pages,
which are decoded per fetch from VRAM and so have no GL mip chain: the
screen-space UV footprint of a minified, proven-3D primitive is box-averaged
(opaque texels of the fragment's STP class only), and anisotropic_filtering
(1..16) spreads that many taps along the footprint's major axis. Every tap
clamps to the primitive's UV bounds or texture window, so packed atlases
never bleed. The mode rides in the textured batch's filter key; off (the
default), sprites/HUD and magnified textures are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-08 14:01:07 -07:00
parent 67a21b7395
commit cf756f0b2d
12 changed files with 304 additions and 7 deletions
+4 -2
View File
@@ -55,6 +55,7 @@ Columns: **N** = native, **D** = DuckStation oracle.
| `screenshot_wide_hires` | ✓ | | `path`, `base_x` | The displayed band of the native-wide surface at internal resolution (`wide_w×S` by `height×S`). `present_shot` is capped at the window; this checks a widescreen + internal-resolution combination at full size |
| `present_shot` | ✓ | | `path` | PNG of the **composed present surface** — the frame after the backend fits the display buffer to the window, so it carries the presented aspect. ⚠ every other capture resolves the display buffer *before* that fit: on a 508×256 display in a 4:3 window they answer 508×256 while the player sees 640×480. Use this one for anything aspect-shaped (widescreen, letterbox), where a pre-fit buffer would hide the very stage the change touches. Staged and fulfilled on the next present, so the ack means *queued* — poll `present_shot_seq`. Unavailable headless and on the Vulkan backend (its swapchain has no readback hook) |
| `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 |
| `texture_lod` | ✓ | | `mode` (optional: 0 off, 1 mipmap), `aniso` (optional: 1..16) | Read or set `[video] texture_lod` / `anisotropic_filtering` live. Replies `mode`, `aniso` |
| `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) |
@@ -480,9 +481,9 @@ between Play and TCP availability.
## Complete command index (generated)
**362 commands registered** — 349 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
**363 commands registered** — 350 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
74 of 362 have prose above; **288 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.
75 of 363 have prose above; **288 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.
@@ -789,6 +790,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `stereo_dump` | ✓ | | |
| `stereo_stats` | ✓ | | |
| `synth_recurse` | ✓ | | |
| `texture_lod` | ✓ | | ✓ |
| `thread_ctx_ring` | ✓ | | |
| `thread_trace` | ✓ | | |
| `thread_trace_clear` | ✓ | | |
+23
View File
@@ -656,6 +656,29 @@ is a renderer redesign, not an option. SMAA needs its area/search lookup
textures and three passes; on an image that is already area-resolved from a
higher internal resolution it buys little over FXAA, so it is left for later.
### Texture LOD and anisotropic filtering (`texture_lod`, `anisotropic_filtering`)
```toml
[video]
texture_lod = "off" # off (default) | mipmap
anisotropic_filtering = 1 # 1..16, taps along the footprint (with mipmap)
```
PS1 texture pages and CLUTs live in VRAM and are decoded per fetch, so there
is no GL mip chain to sample, and a far road or wall at high internal scale
still point-samples one texel per pixel (shimmer). `mipmap` emulates the mip
level in the textured shader: the screen-space UV footprint is box-averaged
(opaque texels of the same STP class only), and `anisotropic_filtering` > 1
spreads up to that many taps along the footprint's long axis so grazing
surfaces stay sharp across it. Every tap clamps to the primitive's own UV
bounds (or its texture window), so packed atlases never bleed into each
other, and a live CLUT change is seen at once (nothing is cached). Only
proven 3D world primitives are affected; sprites, HUD and magnified textures
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.
The runtime clamps N per backend:
- Software and Vulkan stop at 4.
+1 -1
View File
@@ -1 +1 @@
1a049b61e3b599c223bf00aa56dc55ddb9d5b6e2a894b9c34f6abaddf0cec844
e08d3438b6881c8085e9440dda32664b665404140d4e0a96c2f2c5dff264a922
+13
View File
@@ -813,6 +813,19 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
rt.video_pgxp_preserve_projection =
toml::find<bool>(video, "pgxp_preserve_projection");
}
if (video.contains("texture_lod")) {
const auto mode = toml::find<std::string>(video, "texture_lod");
if (mode == "off") rt.video_texture_lod = 0;
else if (mode == "mipmap") rt.video_texture_lod = 1;
else throw std::runtime_error(fmt::format(
"[video] texture_lod must be \"off\" or \"mipmap\": {}", mode));
}
if (video.contains("anisotropic_filtering")) {
const auto n = toml::find<int>(video, "anisotropic_filtering");
if (n < 1 || n > 16) throw std::runtime_error(fmt::format(
"[video] anisotropic_filtering must be 1..16: {}", n));
rt.video_anisotropic_filtering = n;
}
if (video.contains("pgxp_depth_buffer"))
rt.video_pgxp_depth_buffer = toml::find<bool>(video, "pgxp_depth_buffer");
if (video.contains("pgxp_color_correction"))
+7
View File
@@ -563,6 +563,13 @@ struct RuntimeConfig {
// pgxp_depth_buffer) to close T-junction cracks above 1x ("off",
// "fine" = 1 output px, "wide" = half a native px).
bool video_pgxp_depth_buffer = false;
// texture_lod: "off" (default) or "mipmap" — mip-level emulation for
// minified 3D world textures (CLUT pages have no GL mip chain; the
// footprint is box-averaged inside the primitive's UV bounds).
// anisotropic_filtering: taps along the footprint's major axis (1..16,
// default 1); only with texture_lod. OpenGL only.
int video_texture_lod = 0;
int video_anisotropic_filtering = 1;
bool video_pgxp_color_correction = false;
int video_pgxp_seam = 0;
double video_pgxp_depth_threshold = 4096.0;
+13
View File
@@ -285,6 +285,19 @@ if(BUILD_TESTING)
--sdl-library "$<TARGET_FILE:SDL3::SDL3-static>"
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-hd-texture-results")
set_tests_properties(gl_hd_texture_pack_test PROPERTIES LABELS "gpu;opengl;hardware" TIMEOUT 180)
# [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)
+7
View File
@@ -282,6 +282,13 @@ void gl_renderer_set_wide_fast(int on);
* gr_set_depth_triangle (no extra command per triangle otherwise). Any thread. */
int gl_renderer_pgxp_render_wanted(void);
void gl_renderer_set_pgxp_depth(int on);
/* [video] texture_lod / anisotropic_filtering: mip-level emulation (box
* average of the screen footprint, clamped to the primitive's UV bounds) for
* minified world textures, with up to `aniso` taps along the footprint's
* major axis. mode 0 (default) leaves sampling exactly as before. */
void gl_renderer_set_texture_lod(int mode, int aniso);
int gl_renderer_texture_lod(void);
int gl_renderer_anisotropy(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);
+14
View File
@@ -8573,6 +8573,19 @@ static void handle_post_aa(int id, const char *json)
gl_renderer_post_aa_gpu_us());
}
/* texture_lod: {"cmd":"texture_lod"} reads, {"cmd":"texture_lod","mode":0|1,
* "aniso":1..16} sets [video] texture_lod / anisotropic_filtering live. */
static void handle_texture_lod(int id, const char *json)
{
int mode = json_get_int(json, "mode", -1);
int aniso = json_get_int(json, "aniso", -1);
if (mode >= 0 || aniso >= 0)
gl_renderer_set_texture_lod(mode >= 0 ? mode : gl_renderer_texture_lod(),
aniso >= 0 ? aniso : gl_renderer_anisotropy());
send_fmt("{\"id\":%d,\"ok\":true,\"mode\":%d,\"aniso\":%d}", id,
gl_renderer_texture_lod(), gl_renderer_anisotropy());
}
static void handle_frame_perf(int id, const char *json)
{
(void)json;
@@ -15665,6 +15678,7 @@ static const CmdEntry s_commands[] = {
{ "nclip_stats", handle_nclip_stats },
{ "frame_perf", handle_frame_perf },
{ "post_aa", handle_post_aa },
{ "texture_lod", handle_texture_lod },
{ "gl_ws_ablate", handle_gl_ws_ablate },
{ "gl_interp", handle_gl_interp },
{ "render_pass_stats", handle_render_pass_stats },
+67 -4
View File
@@ -887,6 +887,9 @@ static int s_mod_r = 128, s_mod_g = 128, s_mod_b = 128, s_mod_raw = 0;
static int s_mask_set = 0, s_mask_check = 0;
static int s_tw_mask_x = 0, s_tw_mask_y = 0, s_tw_off_x = 0, s_tw_off_y = 0;
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;
/* 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;
@@ -1885,7 +1888,8 @@ 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; /* 0 nearest, 1 bilinear, 2 stable */\n"
"uniform int u_filter; /* bits 0-3: 0 nearest, 1 bilinear, 2 stable; 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"
" ivec2 p = ivec2(x & 1023, y & 511);\n"
@@ -1947,13 +1951,46 @@ static const char *TEX_FS =
" ivec2 at=clamp(ivec2(floor(t*vec2(size))),ivec2(0),size-ivec2(1));\n"
" return texelFetch(u_hd_texture,at,0);\n"
"}\n"
/* [video] texture_lod: mip-level emulation for CLUT/15-bit pages, which
* have no GL mip chain (they are decoded per fetch from raw VRAM). The
* screen-space UV footprint gives a major and a minor axis; up to u_aniso
* taps go along the major axis (anisotropic filtering) and each averages
* a k x k texel box the size of the minor footprint (the mip level).
* Every fetch clamps to the primitive's own UV bounds (or its texture
* window), so a packed atlas never bleeds into the neighbour; texels
* that are transparent or of the other STP class carry no weight. */
"vec3 lod_sample(vec2 uv, vec2 dx, vec2 dy, int stp, vec3 base){\n"
" float lx=length(dx), ly=length(dy);\n"
" vec2 ax = lx >= ly ? dx : dy;\n"
" float major=max(lx,ly), minor=max(min(lx,ly),1.0);\n"
" if (major <= 1.0) return base;\n"
" int n = clamp(int(ceil(major/minor)), 1, u_aniso);\n"
" float box = max(minor, major/float(n));\n"
" int k = clamp(int(ceil(box)), 1, 8);\n"
" while (k > 1 && n*k*k > 64) k--;\n"
" vec2 bx = normalize(dx) * box, by = normalize(dy) * box;\n"
" if (lx < 1e-6) bx = vec2(box, 0.0);\n"
" if (ly < 1e-6) by = vec2(0.0, box);\n"
" vec4 sum = vec4(0.0);\n"
" for (int t=0; t<n; ++t) {\n"
" vec2 c = uv + ax * ((float(t)+0.5)/float(n) - 0.5);\n"
" for (int j=0; j<k; ++j) for (int i=0; i<k; ++i) {\n"
" vec2 o = bx*((float(i)+0.5)/float(k)-0.5) + by*((float(j)+0.5)/float(k)-0.5);\n"
" int raw = stable_texel(ivec2(floor(c+o)));\n"
" if (raw != 0 && ((raw>>15)&1) == stp) sum += vec4(col5(raw), 1.0);\n"
" }\n"
" }\n"
" return sum.a > 0.0 ? sum.rgb / sum.a : base;\n"
"}\n"
"void main(){\n"
" int stp; vec3 rgb;\n"
" int fmode = u_filter & 15;\n"
" /* v_persp is 0 for every prim unless [video] perspective_texturing is on\n"
" * AND this prim's packet carried full GTE projection provenance, so the\n"
" * default is the PS1's affine (noperspective) mapping. */\n"
" vec2 uv = (v_persp != 0) ? v_uv_p : v_uv;\n"
" vec2 dx=dFdx(uv), dy=dFdy(uv);\n"
" vec2 uv0=uv, dx0=dx, dy0=dy; /* the PS1 sample point (texture_lod) */\n"
" if (v_hd_mode != 0) {\n"
" vec4 hd=hd_texel(uv);\n"
" rgb=hd.rgb;\n"
@@ -1969,12 +2006,12 @@ static const char *TEX_FS =
" stp=hd.a<=242.0/255.0 ? 1 : 0;\n"
" if(all(equal(hd,vec4(0.0))) || (stp==0 && all(equal(hd.rgb,vec3(0.0))))) discard;\n"
" }\n"
" } else if (u_filter == 0) {\n"
" } else if (fmode == 0) {\n"
" int raw = fetch_texel(int(floor(uv.x)), int(floor(uv.y)));\n"
" if (raw == 0) discard;\n"
" rgb = col5(raw);\n"
" stp = (raw >> 15) & 1;\n"
" } else if (u_filter == 2) {\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"
" float lx=length(dx), ly=length(dy);\n"
@@ -2015,6 +2052,7 @@ static const char *TEX_FS =
" + float((c01 >> 15) & 1) * w01 + float((c11 >> 15) & 1) * w11) / opac;\n"
" stp = stpf >= 0.5 ? 1 : 0;\n"
" }\n"
" 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"
@@ -4447,7 +4485,11 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
int semi, const int *lim) {
// Mode 2 needs proven world geometry. Sprites, HUD and untracked packets
// retain point sampling; their cutout pixels must stay sharp.
const int filter=s_tex_filter==2 && ((lim && !s_projected_uv_valid) || (!s_pc_valid && !s_pq_valid)) ? 0 : s_tex_filter;
const int untracked=(lim && !s_projected_uv_valid) || (!s_pc_valid && !s_pq_valid);
/* [video] texture_lod rides in the filter batch key (bit 4) for proven
* world geometry only; sprites and HUD keep their exact texels. */
const int filter=(s_tex_filter==2 && untracked ? 0 : s_tex_filter) |
(s_tex_lod && !untracked ? 16 : 0);
int lim_buf[4];
int uv_buf[6];
if (!lim) {
@@ -4842,6 +4884,25 @@ static int glb_scale(void) { return s_out_scale; } /* real internal SSAA scal
native-wide CPU present path + gr_scale() callers
need the true scale — the FBO-direct present is
unaffected since it never reads gr_scale()) */
static GLint s_uAniso = -1;
static void lod_apply_uniforms(void) {
if (!s_tex_prog) return;
GLint prev = 0; glGetIntegerv(GL_CURRENT_PROGRAM, &prev);
if (s_uAniso < 0) s_uAniso = p_glGetUniformLocation(s_tex_prog, "u_aniso");
p_glUseProgram(s_tex_prog); p_glUniform1i(s_uAniso, s_tex_aniso);
p_glUseProgram((GLuint)prev);
}
void gl_renderer_set_texture_lod(int mode, int aniso) {
GL_RT_SYNC("texture_lod");
if (aniso < 1) aniso = 1;
if (aniso > 16) aniso = 16;
if (s_ctx) { flush_flat_batch(); flush_tex_batch(); hiw_flush_queue(); }
s_tex_lod = mode ? 1 : 0;
s_tex_aniso = aniso;
if (s_ctx) lod_apply_uniforms();
}
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 int glb_texture_filter(void) { return s_tex_filter; }
@@ -5693,6 +5754,8 @@ static int init_gpu_raster(void) {
s_uSemimode = p_glGetUniformLocation(s_tex_prog, "u_semimode");
s_uMaskset = p_glGetUniformLocation(s_tex_prog, "u_maskset");
s_uFilter = p_glGetUniformLocation(s_tex_prog, "u_filter");
s_uAniso = -1;
lod_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");
+7
View File
@@ -1280,6 +1280,8 @@ static int g_video_pgxp_position_fallback = 1;
static int g_video_pgxp_preserve_projection = 0;
/* PGXP renderer features (G1.14); PSX_PGXP_DEPTH / _COLOR / _SEAM override. */
static int g_video_pgxp_depth_buffer = 0;
/* [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_pgxp_color_correction = 0;
static int g_video_pgxp_seam = 0;
static float g_video_pgxp_depth_threshold = 4096.0f;
@@ -16069,6 +16071,8 @@ int main(int argc, char** argv) {
gc.runtime.video_pgxp_preserve_projection ? 1 : 0;
g_video_pgxp_mod_only = gc.runtime.video_pgxp_mod_only ? 1 : 0;
g_video_pgxp_depth_buffer = gc.runtime.video_pgxp_depth_buffer ? 1 : 0;
g_video_texture_lod = gc.runtime.video_texture_lod;
g_video_aniso = gc.runtime.video_anisotropic_filtering;
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;
@@ -18455,6 +18459,9 @@ session_reboot:
if (const char* e = std::getenv("PSX_PGXP_COLOR")) g_video_pgxp_color_correction = (*e && *e != '0');
if (const char* e = std::getenv("PSX_PGXP_SEAM")) g_video_pgxp_seam = std::atoi(e);
gl_renderer_set_pgxp_depth(g_video_pgxp_depth_buffer);
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);
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);
+9
View File
@@ -56,6 +56,8 @@ def main():
ap.add_argument("--fixture", type=pathlib.Path)
ap.add_argument("--scales", default="1,4")
ap.add_argument("--frames", default="80")
ap.add_argument("--single", action="store_true",
help="run the fixture once with no arguments; its exit code is the result")
args = ap.parse_args()
sdl_includes = [str(pathlib.Path(d).resolve()) for d in args.sdl_include.split(";") if d]
args.sdl_library = str(pathlib.Path(args.sdl_library).resolve())
@@ -116,6 +118,13 @@ def main():
print(r.stderr[-3000:])
return 2
if args.single:
r = run([probe])
print(r.stdout[-6000:], r.stderr[-3000:])
if r.returncode == SKIP_EXIT:
print("SKIP:", r.stderr.strip()[-600:])
return r.returncode
ok = True
first = True
for s in [int(v) for v in args.scales.split(",") if v]:
+139
View File
@@ -0,0 +1,139 @@
/* [video] texture_lod / anisotropic_filtering on a real, hidden OpenGL
* context, driven through the renderer facade (gr_*).
*
* - off (the default) draws exactly what it always drew; with it on,
* untracked (HUD / sprite) primitives and magnified textures are still
* byte-identical to off;
* - on, a heavily minified one-texel red/blue checker on proven world
* geometry averages towards purple instead of aliasing;
* - the footprint never leaves the primitive's UV bounds: a green border
* packed right next to the sampled texture never bleeds in;
* - anisotropic: a texture minified along u only keeps its v detail with
* 16 taps, where a single isotropic tap box blurs it;
* - the render thread and PGXP depth give the same pixels as the direct
* path, at 4x internal resolution.
* 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];
/* 15-bit page at (512,0): 128x128 one-texel checker red/blue, in a 2-texel
* green border (outside every UV range drawn). Rows 0..127 of a second page
* at (640,0): horizontal stripes, 2 texels each, red/blue. */
static void pages(void) {
static uint16_t pg[132 * 132], st[128 * 128];
for (int y = 0; y < 132; y++)
for (int x = 0; x < 132; x++) {
int in = x >= 2 && x < 130 && y >= 2 && y < 130;
pg[y * 132 + x] = !in ? 0x03e0 : (((x + y) & 1) ? 0x001f : 0x7c00);
}
cpu_upload(510, 0, 132, 132, pg); /* texel (0,0) of page 8 is pg(2,2) */
for (int y = 0; y < 128; y++)
for (int x = 0; x < 128; x++) st[y * 128 + x] = ((y >> 1) & 1) ? 0x001f : 0x7c00;
cpu_upload(640, 0, 128, 128, st);
}
#define TP_CHECK 0x0108 /* 15-bit page at (512,0) */
#define TP_STRIPE 0x010A /* 15-bit page at (640,0) */
static void tri(int tracked, int x, int y, int w, int h, int u0, int v0, int uw, int vh, uint16_t tp) {
gr_set_precise_triangle(tracked, 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(tracked, (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(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);
gr_set_color_modulation(128, 128, 128, 1);
gr_fill_rect(0, 0, 320, 240, 0x0000);
if (depth) gl_renderer_set_pgxp_depth(1);
tri(1, 10, 10, 16, 16, 2, 2, 120, 120, TP_CHECK); /* 7.5:1 minified world */
tri(0, 40, 10, 16, 16, 2, 2, 120, 120, TP_CHECK); /* same, untracked (HUD) */
tri(1, 70, 10, 64, 64, 2, 2, 32, 32, TP_CHECK); /* magnified world */
tri(1, 10, 100, 16, 100, 0, 0, 127, 100, TP_STRIPE); /* u minified only */
gl_renderer_render_thread_frame_boundary();
if (depth) gl_renderer_set_pgxp_depth(0);
gr_vram_transfer_out(0, 0, 1024, 512, nat);
}
static int channel(uint16_t p, int c) { return (p >> (c * 5)) & 31; }
/* Mean |red - blue| over the minified checker: 31 = aliasing, 0 = averaged. */
static double chroma(int x0) {
long d = 0; int n = 0;
for (int y = 13; y < 23; y++)
for (int x = x0 + 3; x < x0 + 9; x++) { uint16_t p = nat[y * 1024 + x]; d += labs(channel(p, 0) - channel(p, 2)); n++; }
return (double)d / n;
}
static int any_green(void) {
for (int y = 0; y < 240; y++)
for (int x = 0; x < 320; x++) if (channel(nat[y * 1024 + x], 1) > 2) return 1;
return 0;
}
/* Stripe contrast down column 12 of the u-minified quad. */
static double stripe_contrast(void) {
long d = 0; int n = 0;
for (int y = 104; y < 190; y++) { uint16_t p = nat[y * 1024 + 12]; d += labs(channel(p, 0) - channel(p, 2)); n++; }
return (double)d / 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("Texture LOD 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 off[1024 * 512], on[1024 * 512];
check(gl_renderer_texture_lod() == 0, "off by default");
scene(0); memcpy(off, nat, sizeof off);
const double c_off = chroma(10), s_off = stripe_contrast();
check(!any_green(), "baseline samples no border");
gl_renderer_set_texture_lod(1, 1);
scene(0); memcpy(on, nat, sizeof on);
const double c_on = chroma(10), s_iso = stripe_contrast();
check(region_equal(off, on, 40, 10, 16, 16), "untracked (HUD) primitive untouched");
check(region_equal(off, on, 70, 10, 64, 64), "magnified texture untouched");
check(c_on < c_off / 3.0, "minified checker averages instead of aliasing");
check(!any_green(), "footprint stays inside the primitive's UV bounds");
gl_renderer_set_texture_lod(1, 16);
scene(0);
const double s_aniso = stripe_contrast();
printf("chroma off=%.2f on=%.2f stripes off=%.2f iso=%.2f aniso16=%.2f\n",
c_off, c_on, s_off, s_iso, s_aniso);
check(s_iso < s_off / 2.0, "isotropic box blurs the minor axis");
check(s_aniso > s_off * 0.8, "16 anisotropic taps keep the minor-axis detail");
check(!any_green(), "anisotropic taps stay inside the UV bounds");
memcpy(on, nat, sizeof on);
scene(1);
check(memcmp(nat, on, sizeof on) == 0, "PGXP depth path samples the same");
check(gl_renderer_render_thread_start(2) == 1, "render thread started");
scene(0);
gl_renderer_render_thread_sync("lod");
check(memcmp(nat, on, sizeof on) == 0, "render thread gives the same pixels");
gl_renderer_render_thread_stop();
gl_renderer_set_texture_lod(0, 1);
scene(0);
check(memcmp(nat, off, sizeof off) == 0, "back off 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;
}