diff --git a/docs/TCP_COMMANDS.md b/docs/TCP_COMMANDS.md index 1cbd3e8b..3d6bd0fa 100644 --- a/docs/TCP_COMMANDS.md +++ b/docs/TCP_COMMANDS.md @@ -429,9 +429,9 @@ The TCP server is the canonical instrumentation surface. Rule 3 in `CLAUDE.md` i ## Complete command index (generated) -**322 commands registered** — 309 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`). +**323 commands registered** — 310 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`). -61 of 322 have prose above; **261 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":""}` and read the reply, or find its `handle_*` function in the server source. +61 of 323 have prose above; **262 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":""}` 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. @@ -559,6 +559,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc | `gl_fbo_peek` | ✓ | | | | `gl_interp` | ✓ | | ✓ | | `gl_present_ring` | ✓ | | | +| `gl_texwin_batch` | ✓ | | | | `gl_vram_diff` | ✓ | | | | `gl_wide_fast` | ✓ | | | | `gl_ws_ablate` | ✓ | | | diff --git a/docs/config_schema.md b/docs/config_schema.md index 34eb4d24..40d8dc14 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -564,6 +564,30 @@ Settings surface. A game migrating Skip FMVs into its built-in mod catalog sets it to false. The runtime then hides the Settings row, ignores stale persisted values, and leaves activation to the selected trusted plugin. +### Texture-window batching (`texture_window_batching`, OpenGL) + +```toml +[video] +texture_window_batching = true # game.toml only; default false +``` + +The OpenGL renderer draws consecutive textured primitives in one batch while +their blend, mask and filter state match. By default a GP0(E2h) texture-window +change also ends the batch, although each vertex carries its primitive's +texture window. With this key on, primitives with different windows share a +batch. The image is the same either way; only the number of draws changes. +While mask checking (GP0(E6h) bit 1) is on, a window change still ends the +batch, because an opaque batch updates the mask bits of its own texels only +after its colour pass. + +It is for games that tile textures through per-primitive windows. Ridge Racer +Type 4's split screen changes the window about 515 times a frame, which drew +about 180 batches a frame instead of 17, and every batch that reaches the +native-wide margins is drawn again into the wide surface. The software and +Vulkan renderers ignore the key. +`PSX_GL_TEXWIN_BATCH=0|1` overrides it for one run, and the TCP command +`gl_texwin_batch on=<0|1>` switches it live. + ### Local rewind (`settings.toml`) Rewind is a player setting, not a game one: it lives in the user's diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index ab673327..7396c46d 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -728,6 +728,10 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& if (video.contains("low_latency_input")) { rt.video_low_latency_input = toml::find(video, "low_latency_input"); } + if (video.contains("texture_window_batching")) { + rt.video_texture_window_batching = + toml::find(video, "texture_window_batching"); + } if (video.contains("vsync")) { const auto mode = toml::find(video, "vsync"); if (mode == "on" || mode == "vsync") rt.video_vsync = 1; diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 089a4f02..cc6fa654 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -494,6 +494,13 @@ struct RuntimeConfig { // game once visuals are validated. bool video_offer_vulkan = false; + // texture_window_batching: OpenGL only. Let textured primitives with + // different GP0(E2h) texture windows share one draw (the window rides in + // each vertex) instead of ending the batch at every window change. The + // image is identical; games that tile textures with per-primitive windows + // draw in far fewer batches. Off by default; a game opts in. + bool video_texture_window_batching = false; + // low_latency_input: re-sample the pad after the wall-clock pacer (just // before present) so the next CPU frame reads near-fresh input instead of // input ~one frame stale. Default on. vsync: present/swap mode — diff --git a/recompiler/tests/video_enhancement_settings_test.cpp b/recompiler/tests/video_enhancement_settings_test.cpp index e9802f7b..5ff41d3d 100644 --- a/recompiler/tests/video_enhancement_settings_test.cpp +++ b/recompiler/tests/video_enhancement_settings_test.cpp @@ -278,6 +278,24 @@ static void test_internal_resolution_settings() { } } +/* [video] texture_window_batching: a game.toml-only OpenGL batching opt-in + * (the image is unchanged; see gpu_gl_renderer.c s_twin_batching). Off unless + * the game asks for it. */ +static void test_texture_window_batching() { + fs::path p = write_game_toml("psxrecomp_twin_default.toml", ""); + auto gc = PSXRecompV4::load_game_config(p); + check(!gc.runtime.video_texture_window_batching, + "texture_window_batching defaults OFF"); + fs::remove(p); + fs::path q = write_game_toml("psxrecomp_twin_on.toml", + "[video]\n" + "texture_window_batching = true\n"); + auto gq = PSXRecompV4::load_game_config(q); + check(gq.runtime.video_texture_window_batching, + "[video] texture_window_batching = true is honoured"); + fs::remove(q); +} + int main() { test_internal_resolution_game_toml(); test_internal_resolution_settings(); @@ -288,6 +306,7 @@ int main() { test_user_settings_read(); test_user_settings_absent_key(); test_user_settings_round_trip(); + test_texture_window_batching(); if (failures) { std::fprintf(stderr, "video_enhancement_settings_test: %d failure(s)\n", diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index ba2f4a7f..3fb4a6f9 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -332,6 +332,14 @@ uint64_t gl_renderer_perf_prim_split(double *out_tex_frac); * isolation, blend-mode, mask, filter, backdrop-gate, texture-window, capacity. */ void gl_renderer_batch_diag(uint64_t out[8]); +/* Texture-window batching ([video] texture_window_batching; default off). + * On: textured prims with different GP0(E2h) texture windows share a batch + * (each vertex carries its window), except while mask checking is on. The + * image is the same either way; only the number of draws changes. Safe to + * call before GL context creation. */ +void gl_renderer_set_texture_window_batching(int on); +int gl_renderer_get_texture_window_batching(void); + #ifdef __cplusplus } #endif diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index ea63b496..6b4f0f4c 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -8041,6 +8041,20 @@ static void handle_gl_wide_fast(int id, const char *json) send_fmt("{\"id\":%d,\"ok\":true,\"on\":%d}", id, gl_renderer_get_wide_fast()); } +/* gl_texwin_batch on=<0|1>: [video] texture_window_batching, live. 1 = textured + * prims with different GP0(E2h) texture windows share a batch; 0 = a window + * change ends the batch. The image is the same; for A/B of batch counts + * (frame_perf batch_diag[6] = window flushes) and frame time. */ +extern void gl_renderer_set_texture_window_batching(int on); +extern int gl_renderer_get_texture_window_batching(void); +static void handle_gl_texwin_batch(int id, const char *json) +{ + int on = json_get_int(json, "on", -1); + if (on >= 0) gl_renderer_set_texture_window_batching(on); + send_fmt("{\"id\":%d,\"ok\":true,\"on\":%d}", id, + gl_renderer_get_texture_window_batching()); +} + /* Live GTE widescreen-squash toggle (diagnostic for 8C far-backdrop void): * ws_aspect num= den= calls gte_set_display_aspect at runtime so we can * compare squash ON (e.g. 16/9) vs OFF (1/1) in-place without a relaunch. */ @@ -14117,6 +14131,7 @@ static const CmdEntry s_commands[] = { { "render_pass_dump", handle_render_pass_dump }, { "render_pass_refuse", handle_render_pass_refuse }, { "gl_wide_fast", handle_gl_wide_fast }, + { "gl_texwin_batch", handle_gl_texwin_batch }, { "synth_recurse", handle_synth_recurse }, { "gl_fbo_peek", handle_gl_fbo_peek }, { "gl_vram_diff", handle_gl_vram_diff }, diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 52ff57e5..1a01ce2c 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -528,13 +528,14 @@ static int s_scratch_w = 0, s_scratch_h = 0; static GLuint s_geo_prog = 0, s_geo_vao = 0, s_geo_vbo = 0; static GLuint s_tex_prog = 0, s_tex_vao = 0, s_tex_vbo = 0; /* Textured vertex: pos(2) uv(2) col(4) tpage(2) clut(2) depth(1) raw(1) limits(4) - * semi(1) q(1) + * semi(1) q(1) twin(1) * — per-prim texture state in flat attributes so prims batch (see flush_tex_batch). * q is the perspective weight ([video] perspective_texturing): 0 = affine, the * PS1-faithful default, which makes the vertex shader's w exactly 1.0 and the * fragment shader read the noperspective varying — i.e. bit-identical to the - * pre-feature pipeline. */ -#define TEXV 20 + * pre-feature pipeline. twin is the prim's GP0(E2h) texture window, its low 20 + * bits as a whole float (mask x, mask y, offset x, offset y; 5 bits each). */ +#define TEXV 21 static GLuint s_blit_prog = 0, s_blit_vao = 0, s_blit_vbo = 0; static GLuint s_blit_hi_prog = 0; /* windowed hi surface blit */ static GLint s_uBhSrc = -1, s_uBhPass = -1, s_uBhMaskset = -1, s_uBhSrcDiv = -1; @@ -558,7 +559,7 @@ static float s_pq[3]; static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1; static GLint s_uPalette = -1; static GLint s_uRaw = -1, s_uSemipass = -1, s_uSemimode = -1; -static GLint s_uTwin = -1, s_uMaskset = -1, s_uFilter = -1; +static GLint s_uMaskset = -1, s_uFilter = -1; static GLint s_uLimits = -1; /* Native-wide x-projection uniforms (per program). u_xoff = x translation in * native px (0 canonical), u_xhalf = x clip half-extent in native px (512 @@ -1260,9 +1261,10 @@ static const char *GEO_FS = /* Textured program. Per-prim texture state (texpage, clut, depth, raw, uv * limits) is carried in FLAT vertex attributes — constant across a prim's * vertices — instead of uniforms, so consecutive textured prims with the same - * blend/mask/texture-window state batch into one draw (see flush_tex_batch). - * The remaining uniforms (u_twin/u_maskset/u_filter/u_semipass) are the batch - * keys + per-pass state. */ + * blend/mask state batch into one draw (see flush_tex_batch). The texture + * window rides in the vertex too (a_twin), so a GP0(E2h) change need not end + * a batch. The remaining uniforms (u_maskset/u_filter/u_semipass) are the + * batch keys + per-pass state. */ static const char *TEX_VS = "#version 330\n" "layout(location=0) in vec2 a_pos;\n" @@ -1275,6 +1277,7 @@ static const char *TEX_VS = "layout(location=7) in vec4 a_limits;\n" "layout(location=8) in float a_semi;\n" "layout(location=9) in float a_q; /* persp weight; 0 = affine (default) */\n" + "layout(location=10) in float a_twin; /* GP0(E2h) bits 0..19 */\n" "uniform float u_shift;\n" "uniform float u_xoff; /* native-wide x translation (px); 0 canonical */\n" "uniform float u_xhalf; /* x clip half-extent (px); 512 canonical */\n" @@ -1285,11 +1288,13 @@ static const char *TEX_VS = "flat out int v_persp;\n" "flat out ivec2 v_tpage; flat out ivec2 v_clut; flat out int v_depth;\n" "flat out int v_raw; flat out ivec4 v_limits; flat out int v_semi;\n" + "flat out int v_twin;\n" "void main(){ v_uv = a_uv; v_uv_p = a_uv; v_col = a_col;\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" " v_semi = int(a_semi + 0.5);\n" + " v_twin = int(a_twin + 0.5);\n" " v_limits = ivec4(floor(a_limits + 0.5));\n" " /* u_shift: align GL's center-sample grid with the PS1 integer grid (see\n" " * GEO_VS) so interpolated uv at a fragment equals the PS1 DDA value. */\n" @@ -1317,11 +1322,11 @@ static const char *TEX_FS = "flat in int v_raw; /* 1 = no color modulation */\n" "flat in ivec4 v_limits; /* prim uv sampling bounds (inclusive, post-wrap) */\n" "flat in int v_semi; /* GP0 command has semi-transparency enabled */\n" + "flat in int v_twin; /* texture window, GP0(E2h) bits 0..19 */\n" "uniform usampler2D u_vram;\n" "uniform usampler2D u_palette;\n" "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 ivec4 u_twin; /* texture window: mask_x, mask_y, off_x, off_y */\n" "uniform int u_maskset; /* GP0(E6h) set-mask: OR bit15 into output */\n" "uniform int u_filter; /* 1 = bilinear */\n" "uniform float u_shift;\n" @@ -1337,9 +1342,12 @@ static const char *TEX_FS = "}\n" "int fetch_texel(int u, int v){\n" " u &= 255; v &= 255;\n" - " if ((u_twin.x | u_twin.y) != 0) {\n" - " u = (u & ~(u_twin.x * 8)) | ((u_twin.z & u_twin.x) * 8);\n" - " v = (v & ~(u_twin.y * 8)) | ((u_twin.w & u_twin.y) * 8);\n" + " /* texture window: mask_x, mask_y, off_x, off_y (8-px units) */\n" + " ivec4 tw = ivec4(v_twin & 31, (v_twin >> 5) & 31,\n" + " (v_twin >> 10) & 31, (v_twin >> 15) & 31);\n" + " if ((tw.x | tw.y) != 0) {\n" + " u = (u & ~(tw.x * 8)) | ((tw.z & tw.x) * 8);\n" + " v = (v & ~(tw.y * 8)) | ((tw.w & tw.y) * 8);\n" " } else {\n" " u = clamp(u, v_limits.x, v_limits.z);\n" " v = clamp(v, v_limits.y, v_limits.w);\n" @@ -2450,9 +2458,10 @@ static void wide_clear_bd_scale(GLint uScale, GLint uCenter) { /* ---- textured-prim batching -------------------------------------------- * * Consecutive textured prims sharing blend/mask/texwindow/filter coalesce into - * one draw. Per-prim texture state (texpage/clut/depth/raw/uv-limits) rides in - * the vertex (TEXV flat attributes), so only `semi` (blend) and the global - * mask/twin/filter are batch keys. flush_tex_batch() draws the queued verts; it + * one draw. Per-prim texture state (texpage/clut/depth/raw/uv-limits/texture + * window) rides in the vertex (TEXV flat attributes), so only `semi` (blend) + * and the global mask/filter are batch keys, plus the texture window unless + * texture-window batching is on (below). flush_tex_batch() draws the queued verts; it * is called before any op that reads VRAM, writes it outside the batch, or * changes batch state (see callers: flush_cpu_upload, flush_pack_if_sampling, * every non-textured glb_ wrapper, and the present path). Drawing reads only the @@ -2467,6 +2476,25 @@ static GLuint s_tb_bank_tex; static int s_tb_twin[4] = {0, 0, 0, 0}; static uint64_t s_batch_total = 0, s_batch_reason[7]; +/* Texture-window batching ([video] texture_window_batching, OpenGL; off by + * default). Off: a GP0(E2h) texture-window change ends the open textured batch + * (batch reason 5), as before. On: prims with different windows share a batch, + * since each vertex carries its own window. Games that tile far textures with + * per-prim windows (R4's split screen: ~515 E2 changes and ~180 batches a game + * frame, ~17 with this on) then draw in a tenth of the batches, which matters + * most on the native-wide path, where a batch reaching the margins is drawn + * again into the wide surface. + * The result is pixel-identical: an opaque batch draws its colour in one + * painter-ordered pass and semi-transparent prims are still drawn one per + * batch. The one exception is mask checking (GP0(E6h) bit 1): an opaque batch + * fixes the stencil for its STP=1 texels after its colour pass, so a later + * prim in the same batch is not stopped by an earlier one's mask bit. The + * window therefore stays a batch key while the check is on, and this mode + * never makes a batch longer than the default mode would there. */ +static int s_twin_batching = 0; +void gl_renderer_set_texture_window_batching(int on) { s_twin_batching = on ? 1 : 0; } +int gl_renderer_get_texture_window_batching(void) { return s_twin_batching; } + void gl_renderer_batch_diag(uint64_t out[8]) { out[0] = s_batch_total; for (int i = 0; i < 7; i++) out[i + 1] = s_batch_reason[i]; @@ -2599,7 +2627,6 @@ typedef struct { uint8_t check; /* mask-check state at draw time */ uint8_t filter; GLuint tex; /* sampled texture (bank or raw mirror) */ - int twin[4]; int ax0, ay0, ax1, ay1; /* draw area, inclusive */ size_t vfirst; /* float offset into s_hq_v */ int vcount; /* vertices */ @@ -2672,7 +2699,6 @@ static int hiw_enqueue_tex(int nverts, int semi, int mirror, int gate) { c->mask = (uint8_t)s_tb_mask; c->filter = (uint8_t)s_tb_filter; c->tex = s_tb_bank_tex ? s_tb_bank_tex : s_raw_tex; - memcpy(c->twin, s_tb_twin, sizeof c->twin); if (wq) hiw_wide_set(c, gate); return wq; } @@ -2730,7 +2756,6 @@ static void hiw_replay_wide(void) { p_glUniform1f(s_tex_uXscale, c->wscale); p_glUniform1f(s_tex_uXcenter, c->wcenter); glBindTexture(GL_TEXTURE_2D, c->tex); - p_glUniform4i(s_uTwin, c->twin[0], c->twin[1], c->twin[2], c->twin[3]); p_glUniform1i(s_uMaskset, c->mask); p_glUniform1i(s_uFilter, c->filter); p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)((size_t)c->vcount * TEXV * sizeof(float)), @@ -2807,7 +2832,6 @@ static void hiw_flush_queue(void) { cur = HQ_TEX; } glBindTexture(GL_TEXTURE_2D, c->tex); - p_glUniform4i(s_uTwin, c->twin[0], c->twin[1], c->twin[2], c->twin[3]); p_glUniform1i(s_uMaskset, c->mask); p_glUniform1i(s_uFilter, c->filter); p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)((size_t)c->vcount * TEXV * sizeof(float)), @@ -2859,7 +2883,6 @@ static void flush_tex_batch(void) { glBindTexture(GL_TEXTURE_2D, s_tb_bank_tex && !s_tb_bank_live_clut ? s_tb_bank_tex : s_raw_tex); p_glUniform1i(s_uPalette, 1); p_glActiveTexture(PSXGL_TEXTURE0); - p_glUniform4i(s_uTwin, s_tb_twin[0], s_tb_twin[1], s_tb_twin[2], s_tb_twin[3]); p_glUniform1i(s_uMaskset, s_tb_mask); p_glUniform1i(s_uFilter, s_tb_filter); p_glBindVertexArray(s_tex_vao); @@ -3260,7 +3283,8 @@ static void gpu_textured_triangle(const int *xs, const int *ys, /* Append to the textured batch. Flush first if this prim's blend/mask/twin/ * filter differ from the open batch, or the buffer is full. Per-prim texture - * state goes in the vertex; only these keys force a new draw. */ + * state goes in the vertex; only these keys force a new draw (the window + * not at all under texture-window batching, see s_twin_batching). */ { flush_flat_batch(); /* painter order: flat GEO before textured */ int twx = s_tw_mask_x, twy = s_tw_mask_y, tox = s_tw_off_x, toy = s_tw_off_y; @@ -3301,8 +3325,9 @@ static void gpu_textured_triangle(const int *xs, const int *ys, else if (s_mask_set != s_tb_mask) reason = 2; else if (s_tex_filter != s_tb_filter) reason = 3; else if (gate != s_tb_gate) reason = 4; - else if (twx != s_tb_twin[0] || twy != s_tb_twin[1] || - tox != s_tb_twin[2] || toy != s_tb_twin[3]) reason = 5; + else if ((!s_twin_batching || s_mask_check) && + (twx != s_tb_twin[0] || twy != s_tb_twin[1] || + tox != s_tb_twin[2] || toy != s_tb_twin[3])) reason = 5; } if (reason >= 0) { s_batch_reason[reason]++; @@ -3315,6 +3340,8 @@ static void gpu_textured_triangle(const int *xs, const int *ys, s_tb_bank_live_clut = s_selected_bank_live_clut; s_tb_twin[0] = twx; s_tb_twin[1] = twy; s_tb_twin[2] = tox; s_tb_twin[3] = toy; } + /* The prim's window, exact as a float (20 bits). */ + float twin = (float)(twx | (twy << 5) | (tox << 10) | (toy << 15)); float *vp = &s_tb[s_tb_n * TEXV]; for (int i = 0; i < 3; i++, vp += TEXV) { vp[0] = s_pc_valid ? s_pc_x[i] : (float)xs[i]; @@ -3328,6 +3355,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys, vp[16] = (float)lim[2]; vp[17] = (float)lim[3]; vp[18] = semi >= 0 ? (float)(semi + 1) : 0.0f; /* a_semi code */ vp[19] = s_pq_valid ? s_pq[i] : 0.0f; /* a_q; 0 = affine */ + vp[20] = twin; /* a_twin */ } s_tb_n += 3; if (isolate) flush_tex_batch(); /* draw this semi prim alone, in submission order */ @@ -4224,7 +4252,6 @@ static int init_gpu_raster(void) { s_uRaw = p_glGetUniformLocation(s_tex_prog, "u_raw"); s_uSemipass = p_glGetUniformLocation(s_tex_prog, "u_semipass"); s_uSemimode = p_glGetUniformLocation(s_tex_prog, "u_semimode"); - s_uTwin = p_glGetUniformLocation(s_tex_prog, "u_twin"); s_uMaskset = p_glGetUniformLocation(s_tex_prog, "u_maskset"); s_uFilter = p_glGetUniformLocation(s_tex_prog, "u_filter"); s_uLimits = p_glGetUniformLocation(s_tex_prog, "u_limits"); @@ -4317,6 +4344,7 @@ static int init_gpu_raster(void) { p_glVertexAttribPointer(7, 4, GL_FLOAT, GL_FALSE, st, (void*)(14*sizeof(float))); p_glEnableVertexAttribArray(7); /* limits */ p_glVertexAttribPointer(8, 1, GL_FLOAT, GL_FALSE, st, (void*)(18*sizeof(float))); p_glEnableVertexAttribArray(8); /* semi */ p_glVertexAttribPointer(9, 1, GL_FLOAT, GL_FALSE, st, (void*)(19*sizeof(float))); p_glEnableVertexAttribArray(9); /* q */ + p_glVertexAttribPointer(10, 1, GL_FLOAT, GL_FALSE, st, (void*)(20*sizeof(float))); p_glEnableVertexAttribArray(10); /* twin */ } p_glGenVertexArrays(1, &s_blit_vao); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index a14372ec..e0adc24b 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -13404,6 +13404,8 @@ int main(int argc, char** argv) { g_video_aspect_num = gc.runtime.video_aspect_num; g_video_aspect_den = gc.runtime.video_aspect_den; g_low_latency_input = gc.runtime.video_low_latency_input ? 1 : 0; + gl_renderer_set_texture_window_batching( + gc.runtime.video_texture_window_batching ? 1 : 0); g_video_vsync = gc.runtime.video_vsync; g_frame_interpolation = gc.runtime.video_frame_interpolation ? 1 : 0; g_frame_interpolation_fps = gc.runtime.video_frame_interpolation_fps; @@ -15553,6 +15555,9 @@ session_reboot: g_video_perspective_texturing = (*e && *e != '0') ? 1 : 0; if (const char* e = std::getenv("PSX_PGXP_CPU_MODE")) g_video_pgxp_cpu_mode = (*e && *e != '0') ? 1 : 0; + /* [video] texture_window_batching A/B (same image, fewer GL draws). */ + if (const char* e = std::getenv("PSX_GL_TEXWIN_BATCH")) + gl_renderer_set_texture_window_batching((*e && *e != '0') ? 1 : 0); gte_geometry_correction_set(g_video_geometry_correction); gpu_texture_correction_set(g_video_perspective_texturing); pgxp_set_cpu_mode(g_video_pgxp_cpu_mode); diff --git a/runtime/tests/run_gl_scale_invariance.py b/runtime/tests/run_gl_scale_invariance.py index 0f1991ad..83521336 100644 --- a/runtime/tests/run_gl_scale_invariance.py +++ b/runtime/tests/run_gl_scale_invariance.py @@ -24,6 +24,12 @@ Mask runs (mode mask) change the GP0(E6) mask-check bit after a line, a flat triangle or an opaque textured rect and before its batch is drawn: the draw must keep the check bit it was submitted under, at 1x, above it and in the window mode. +Texture-window runs (mode twin) draw prims that alternate GP0(E2h) windows +with [video] texture_window_batching off and on: the native VRAM, the frame at +S and the wide surface must be the same both ways, at 1x, above it and in the +window mode, and on must draw in fewer batches. Each of those runs also checks +raw rects drawn through every window against the PS1 window rule computed in C +from the VRAM, so a decode error both ways share still fails. Pass runs (mode passes, where the renderer has the frame-rate stack's render passes) check that the window mode refuses them and the full-VRAM surface offers them. @@ -255,6 +261,29 @@ def main(): r.stderr.strip()[-600:]) if r.returncode or not parsed or parsed[1]: ok = False + # Texture-window batching off vs on: (label, scale, env). + for label, s, extra in (("full", 1, {}), ("full", 4, {}), ("full", 9, {}), + ("window", 9, {"PSX_GL_HIRES_WINDOW": "1"}), + ("window", 18, {"PSX_GL_HIRES_WINDOW": "1"})): + e = dict(env) + e.update(extra) + got = {} + for on in (0, 1): + r = run([dest / "probe", s, "twin", on], env=e) + parsed = parse_run(r.stdout) + m = re.search(r"^twin_flushes=(\d+) batches=(\d+)$", r.stdout, re.M) + print(f"twin {label} {s} batching={on}: exit={r.returncode}", + r.stdout.strip().splitlines()[-5:], r.stderr.strip()[-600:]) + if r.returncode or not parsed or parsed[1] or not m: + ok = False + got[on] = (parsed[2] if parsed else None, parse_hires(r.stdout), + parse_hires(r.stdout, "wide"), int(m[2]) if m else None) + if None in got[0][:3] or got[0][:3] != got[1][:3]: + print(f"FAIL twin {label} {s}x: batching changes the image:", got) + ok = False + if got[0][3] is None or got[1][3] is None or got[1][3] >= got[0][3]: + print(f"FAIL twin {label} {s}x: batching on did not draw fewer batches:", got) + ok = False # Render passes: offered on the full-VRAM surface, refused in the window mode. if not passes: print("passes: skipped (this renderer has no render passes)") diff --git a/runtime/tests/test_gl_scale_invariance.c b/runtime/tests/test_gl_scale_invariance.c index 1113b55b..4b94c130 100644 --- a/runtime/tests/test_gl_scale_invariance.c +++ b/runtime/tests/test_gl_scale_invariance.c @@ -44,6 +44,17 @@ * check on (then off) or off (then on) before their batch is drawn: with * the check on the rect's pixels stay, with it off they are overwritten, * in the native VRAM and in the frame at S. + * - mode "twin" (with or without the window), argv[3] 0/1: [video] + * texture_window_batching off/on. Textured prims alternating GP0(E2h) + * texture windows (opaque, semi-transparent, set-mask, mask check, + * bilinear; inside the frame and into native-wide margins). The runner + * checks the native VRAM, the frame at S and the wide surface are the same + * with it off and on; this run checks that on never flushes for a window + * change except under mask check, and off flushes at every one. An oracle + * independent of the renderer then draws raw rects through each window + * into both page depths: every pixel must be the texel the PS1 window rule + * picks from the VRAM as read back, in the native VRAM and at S, so a + * window decode error both settings share still fails. * - mode "passes" (built only where the renderer has render passes, the * frame-rate stack; the runner defines PSX_TEST_RENDER_PASSES): with the * flip-aware frame blend ready, the full-VRAM surface offers render passes @@ -381,6 +392,197 @@ static int mask_main(int scale, int window) { return failures ? 1 : 0; } +/* ---- mode "twin": texture-window batching -------------------------------- + * argv[3] "0"/"1" = [video] texture_window_batching off/on. Textured prims that + * alternate GP0(E2h) windows: opaque polys and rects inside the frame and into + * the native-wide margins, semi-transparent ones, a run under set-mask, a run + * under mask check (which must still split at every window change) and a + * bilinear run. The runner compares the native VRAM, the frame at S and the + * wide surface of the two runs; this run checks the window flushes and the + * window oracle (twin_oracle). */ +#define TWIN_TPAGE4 0x0008 /* 4-bit page at (512, 0), CLUT at (512, 256) */ +#define TWIN_TPAGE15 0x0109 /* 15-bit page at (576, 0) */ +static const uint32_t twin_windows[4] = { + 0, /* no window */ + 0x1Cu, /* u repeats every 32 */ + 0x18u | (0x1Eu << 5) | (0x08u << 10) | (0x02u << 15), + 0x1Fu | (0x1Fu << 5) | (0x05u << 10) | (0x0Au << 15), /* one 8x8 tile */ +}; +static void twin_quad(int i, int x, int y, int w, int h, uint16_t tp) { + glb_set_texture_window(twin_windows[i & 3]); + if (i % 3 == 2) { + glb_draw_textured_rect(x, y, w, h, 3 + i, 5 + 2 * i, 512, 256, tp); + return; + } + int u0 = 2 * i, v0 = i, u1 = u0 + 90 + i, v1 = v0 + 70; + glb_draw_shaded_textured_triangle(x, y, u0, v0, 0x808080, x + w, y + 3, u1, v0, 0x6090b0, + x, y + h, u0, v1, 0xb09060, 512, 256, tp, 0); + glb_draw_shaded_textured_triangle(x + w, y + 3, u1, v0, 0x6090b0, x, y + h, u0, v1, 0xb09060, + x + w - 7, y + h + 2, u1, v1, 0x80a080, 512, 256, tp, 0); +} +/* The window oracle: what the GP0(E2h) window must sample, from the PS1 rule + * and the VRAM as read back, so the off/on comparison cannot hide a decode + * error both modes share. Raw opaque rects (no dither, one texel per native + * pixel), one per window and page, drawn back to back so they share batches + * when the key is on. Every pixel is checked in the native VRAM and, at its + * centre, in the frame at S. Counts the pixels each window drew. */ +#define TWIN_OR_W 70 +#define TWIN_OR_H 20 +static uint16_t twin_texel(const uint16_t *v, uint16_t tp, int u, int t) { + int px = (tp & 15) * 64, py = ((tp >> 4) & 1) * 256; + if (((tp >> 7) & 3) == 2) return v[((py + t) & 511) * 1024 + ((px + u) & 1023)]; + int idx = (v[((py + t) & 511) * 1024 + ((px + (u >> 2)) & 1023)] >> ((u & 3) * 4)) & 15; + return v[256 * 1024 + 512 + idx]; /* CLUT at (512, 256) */ +} +static void twin_oracle(int scale, int drawn[4]) { + /* Texels for the windows that reach past the scene's 64x64 pages. */ + static uint16_t more[64 * 128]; + for (int i = 0; i < 64 * 128; i++) + more[i] = (uint16_t)(((i * 0x1d0bu) ^ (i >> 4)) | ((i & 7) == 5 ? 0x8000u : 0)); + glb_vram_transfer_in(512, 64, 64, 64, more); /* 4-bit rows 64.. */ + glb_vram_transfer_in(576, 64, 64, 64, more + 64 * 64); /* 15-bit rows 64.. */ + glb_vram_transfer_in(640, 0, 64, 128, more); /* 15-bit u 64..127 */ + glb_set_draw_area(0, 0, FRAME_W - 1, FRAME_H - 1); + glb_set_mask_bits(0, 0); + glb_set_semi_transparency(0, 0); + glb_set_texture_filter(0); + glb_set_color_modulation(128, 128, 128, 1); /* raw: the texel as is */ + gl_renderer_sync_cpu(); + check(gl_renderer_fbo_peek(0, 0, 1024, 512, vram), "oracle: VRAM before"); + int fw = FRAME_W * scale, fh = FRAME_H * scale, ow = 0, oh = 0; + uint32_t *before = (uint32_t *)malloc((size_t)fw * fh * 4); + uint32_t *after = (uint32_t *)malloc((size_t)fw * fh * 4); + int nb = before ? gl_renderer_read_display_hires(0, 0, FRAME_W, FRAME_H, before, fw * fh, &ow, &oh) : 0; + check(nb == fw * fh, "oracle: frame at S before"); + for (int i = 0; i < 8; i++) { + glb_set_texture_window(twin_windows[i & 3]); + glb_draw_textured_rect(4 + (i & 3) * 78, 2 + (i >> 2) * 22, TWIN_OR_W, TWIN_OR_H, + 9 * i + 3, 5 * i + 1, 512, 256, i < 4 ? TWIN_TPAGE4 : TWIN_TPAGE15); + } + glb_set_texture_window(0); + glb_set_color_modulation(128, 128, 128, 0); + gl_renderer_sync_cpu(); + check(gl_renderer_fbo_peek(0, 0, 1024, 512, peek), "oracle: native peek"); + int na = after ? gl_renderer_read_display_hires(0, 0, FRAME_W, FRAME_H, after, fw * fh, &ow, &oh) : 0; + check(na == fw * fh, "oracle: frame at S after"); + long bad = 0, bad_hi = 0; + for (int i = 0; i < 8; i++) { + uint32_t w = twin_windows[i & 3]; + int mx = w & 31, my = (w >> 5) & 31, ox = (w >> 10) & 31, oy = (w >> 15) & 31; + int x0 = 4 + (i & 3) * 78, y0 = 2 + (i >> 2) * 22; + for (int dy = 0; dy < TWIN_OR_H; dy++) + for (int dx = 0; dx < TWIN_OR_W; dx++) { + int u = (9 * i + 3 + dx) & 255, t = (5 * i + 1 + dy) & 255; + u = (u & ~(mx * 8)) | ((ox & mx) * 8); + t = (t & ~(my * 8)) | ((oy & my) * 8); + uint16_t texel = twin_texel(vram, i < 4 ? TWIN_TPAGE4 : TWIN_TPAGE15, u, t); + int x = x0 + dx, y = y0 + dy; + uint16_t want = texel ? texel : vram[y * 1024 + x]; + if (texel) drawn[i & 3]++; + if (peek[y * 1024 + x] != want) { + if (!bad) fprintf(stderr, "oracle: window %d page %d px (%d,%d) native %04x want %04x\n", + i & 3, i >> 2, x, y, peek[y * 1024 + x], want); + bad++; + } + if (nb == fw * fh && na == fw * fh) { + size_t c = (size_t)(y * scale + scale / 2) * fw + (size_t)(x * scale + scale / 2); + uint32_t got = after[c] & 0xFFFFFFu; /* BGRA: R bits 16.., B bits 0.. */ + int ok = texel ? (((got >> 19) & 31) == (texel & 31) && + ((got >> 11) & 31) == ((texel >> 5) & 31) && + ((got >> 3) & 31) == ((texel >> 10) & 31)) + : got == (before[c] & 0xFFFFFFu); + bad_hi += !ok; + } + } + } + free(before); + free(after); + printf("twin_oracle drawn=%d,%d,%d,%d native_bad=%ld hires_bad=%ld\n", + drawn[0], drawn[1], drawn[2], drawn[3], bad, bad_hi); + check(bad == 0, "oracle: native VRAM is what the texture window samples"); + check(bad_hi == 0, "oracle: frame at S is what the texture window samples"); + for (int k = 0; k < 4; k++) check(drawn[k] > TWIN_OR_W * TWIN_OR_H / 2, "oracle: every window draws"); +} +static int twin_main(int scale, int window, int on) { + static uint16_t page[64 * 64], page15[64 * 64], clut[16]; + for (int i = 0; i < 64 * 64; i++) page[i] = (uint16_t)((i * 0x2469u) ^ (i >> 2) ^ 0x1111u); + for (int i = 0; i < 64 * 64; i++) + page15[i] = (uint16_t)(((i * 0x0843u) & 0x7fffu) | (((i >> 5) & 3) == 1 ? 0x8000u : 0)); + for (int i = 0; i < 16; i++) clut[i] = (uint16_t)(i ? (0x0421u * (uint16_t)i) | ((i & 3) == 3 ? 0x8000u : 0) : 0); + glb_vram_transfer_in(512, 0, 64, 64, page); + glb_vram_transfer_in(576, 0, 64, 64, page15); + glb_vram_transfer_in(512, 256, 16, 1, clut); + gl_renderer_set_texture_window_batching(on); + check(gl_renderer_get_texture_window_batching() == on, "texture-window batching set"); + glb_set_draw_area(0, 0, 1023, 511); + glb_set_draw_offset(0, 0); + glb_set_mask_bits(0, 0); + glb_set_semi_transparency(0, 0); + glb_set_color_modulation(128, 128, 128, 0); + if (window) check(hiw_ensure(0, FRAME_W) != NULL, "window covers the frame"); + glb_fill_rect(0, 0, FRAME_W, FRAME_H, 0x0c63); + glb_wide_configure(426, 53); + glb_wide_set_target(0); + glb_set_draw_area(0, 0, FRAME_W - 1, FRAME_H - 1); + uint64_t r5 = s_batch_reason[5], total = s_batch_total; + /* A: 24 opaque prims, a new window each (23 changes); 4-bit and 15-bit, + * some reaching the margins. */ + for (int i = 0; i < 24; i++) + twin_quad(i, -40 + (i % 6) * 66, 8 + (i / 6) * 40, 70 + (i & 1) * 20, 44, + (i & 4) ? TWIN_TPAGE15 : TWIN_TPAGE4); + /* B: semi-transparent, every mode (drawn one per batch either way). */ + for (int i = 0; i < 4; i++) { + glb_set_semi_transparency(1, i); + twin_quad(i + 1, 20 + i * 70, 150, 80, 40, TWIN_TPAGE15); + } + glb_set_semi_transparency(0, 0); + /* C1: set-mask, 4 prims (3 changes). */ + glb_set_mask_bits(1, 0); + for (int i = 0; i < 4; i++) twin_quad(i, 30 + i * 60, 180, 50, 30, TWIN_TPAGE4); + /* C2: mask check, 6 overlapping prims (5 changes; split in both modes). */ + glb_set_mask_bits(0, 1); + for (int i = 0; i < 6; i++) twin_quad(i + 2, 10 + i * 40, 170, 90, 50, TWIN_TPAGE15); + glb_set_mask_bits(0, 0); + /* D: bilinear, 4 prims (3 changes). */ + glb_set_texture_filter(1); + for (int i = 0; i < 4; i++) twin_quad(i + 3, -20 + i * 95, 205, 90, 30, TWIN_TPAGE4); + flush_tex_batch(); + glb_set_texture_filter(0); + glb_set_texture_window(0); + uint64_t flushes = s_batch_reason[5] - r5, batches = s_batch_total - total; + printf("twin_flushes=%llu batches=%llu\n", (unsigned long long)flushes, + (unsigned long long)batches); + check(flushes == (on ? 5u : 34u), on ? "batching on: window flushes only under mask check" + : "batching off: every window change flushes"); + int drawn[4] = {0, 0, 0, 0}; + twin_oracle(scale, drawn); + glb_set_draw_area(0, 0, 1023, 511); + gl_renderer_sync_cpu(); + check(gl_renderer_fbo_peek(0, 0, 1024, 512, peek), "native peek"); + uint64_t digest = fnv(peek, sizeof peek, 0xcbf29ce484222325ull); + int fw = FRAME_W * scale, fh = FRAME_H * scale, ow = 0, oh = 0; + uint32_t *img = (uint32_t *)malloc((size_t)fw * fh * 4); + int n = img ? gl_renderer_read_display_hires(0, 0, FRAME_W, FRAME_H, img, fw * fh, &ow, &oh) : 0; + check(n == fw * fh && ow == fw && oh == fh, "hires readback size"); + uint64_t hires = n ? fnv(img, (size_t)fw * fh * 4, 0xcbf29ce484222325ull) : 0; + free(img); + uint64_t wide = 0; + { + int ww = 426 * scale, wh = 512 * scale, gw = 0, gh = 0; + uint32_t *wb = (uint32_t *)malloc((size_t)ww * wh * 4); + int got = wb ? glb_wide_dump_full(wb, ww * wh, &gw, &gh, 0) : 0; + check(got == ww * wh && gw == ww && gh == wh, "wide surface dump size"); + if (got) wide = fnv(wb, (size_t)ww * wh * 4, 0xcbf29ce484222325ull); + free(wb); + } + check(glGetError() == GL_NO_ERROR, "GL error"); + printf("digest=%016llx\n", (unsigned long long)digest); + printf("hires=%016llx\n", (unsigned long long)hires); + printf("wide=%016llx\n", (unsigned long long)wide); + printf("checks=%d failures=%d\n", checks, failures); + return failures ? 1 : 0; +} + #if defined(PSX_TEST_RENDER_PASSES) /* Mode "passes": see the header. */ static int passes_main(int scale, int window) { @@ -642,6 +844,12 @@ int main(int argc, char **argv) { return rc; } #endif + if (!strcmp(mode, "twin")) { + if (si.windowed) check(si.hr_scale == 1, "window mode engaged"); + int rc = twin_main(scale, si.windowed, argc > 3 && argv[3][0] == '1'); + gl_renderer_shutdown(); SDL_DestroyWindow(win); SDL_Quit(); + return rc; + } if (!strcmp(mode, "mask")) { if (si.windowed) check(si.hr_scale == 1, "window mode engaged"); int rc = mask_main(scale, si.windowed);