From 7c9e13ec55b3c2bf948726ceaeb6d74d89a04f39 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Tue, 6 Oct 2026 19:09:30 -0700 Subject: [PATCH] Batch mixed texture filters as per-primitive sampling state --- runtime/src/gpu_gl_renderer.c | 41 ++++++++------- runtime/tests/test_gl_texture_filter.c | 72 ++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 19 deletions(-) diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 6f6fa2b5..21e7d48f 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -567,7 +567,7 @@ static GLuint s_tex_prog = 0, s_tex_vao = 0, s_tex_vbo = 0; * fragment shader read the noperspective varying — i.e. bit-identical to the * 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 +#define TEXV 22 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; @@ -593,7 +593,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_uMaskset = -1, s_uFilter = -1; +static GLint s_uMaskset = -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 @@ -1311,7 +1311,8 @@ static const char *GEO_FS = * vertices — instead of uniforms, so consecutive textured prims with the same * 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 + * a batch. Sampling mode also travels per primitive, so sharp UI and filtered + * world geometry can share a draw. The remaining uniforms (u_maskset/u_semipass) are the * batch keys + per-pass state. */ static const char *TEX_VS = "#version 330\n" @@ -1326,6 +1327,7 @@ static const char *TEX_VS = "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" + "layout(location=11) in float a_filter; /* 0 nearest, 1 bilinear, 2 stable */\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" @@ -1336,13 +1338,14 @@ 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" + "flat out int v_twin; flat out int v_filter;\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_filter = int(a_filter + 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" @@ -1371,12 +1374,12 @@ static const char *TEX_FS = "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" + "flat in int v_filter; /* sampling mode belongs to this primitive */\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 int u_maskset; /* GP0(E6h) set-mask: OR bit15 into output */\n" - "uniform int u_filter; /* 0 nearest, 1 bilinear, 2 stable */\n" "uniform float u_shift;\n" "int vram_at(int x, int y){\n" " ivec2 p = ivec2(x & 1023, y & 511);\n" @@ -1434,12 +1437,12 @@ static const char *TEX_FS = " * 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" - " if (u_filter == 0) {\n" + " if (v_filter == 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 (v_filter == 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" @@ -2559,10 +2562,10 @@ static void wide_clear_bd_scale(GLint uScale, GLint uCenter) { } /* ---- textured-prim batching -------------------------------------------- * - * Consecutive textured prims sharing blend/mask/texwindow/filter coalesce into + * Consecutive textured prims sharing blend/mask/texwindow coalesce into * 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 + * window/filter) rides in the vertex (TEXV flat attributes), so only `semi` (blend) + * and the global mask 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, @@ -2727,7 +2730,6 @@ typedef struct { int8_t semi; /* tex: batch semi key; geo: blend mode, -1 opaque */ uint8_t mask; /* tex: batch set-mask; geo: stencil write value */ uint8_t check; /* mask-check state at draw time */ - uint8_t filter; GLuint tex; /* sampled texture (bank or raw mirror) */ int ax0, ay0, ax1, ay1; /* draw area, inclusive */ size_t vfirst; /* float offset into s_hq_v */ @@ -2799,7 +2801,6 @@ static int hiw_enqueue_tex(int nverts, int semi, int mirror, int gate) { if (!c) { hiw_flush_queue(); return 0; } c->semi = (int8_t)semi; 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; if (wq) hiw_wide_set(c, gate); return wq; @@ -2859,7 +2860,6 @@ static void hiw_replay_wide(void) { p_glUniform1f(s_tex_uXcenter, c->wcenter); glBindTexture(GL_TEXTURE_2D, c->tex); 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)), s_hq_v + c->vfirst, PSXGL_STREAM_DRAW); tex_draw_passes_ex(c->vcount, c->semi, c->mask, c->check, 0); @@ -2935,7 +2935,6 @@ static void hiw_flush_queue(void) { } glBindTexture(GL_TEXTURE_2D, c->tex); 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)), s_hq_v + c->vfirst, PSXGL_STREAM_DRAW); tex_draw_passes_ex(c->vcount, c->semi, c->mask, c->check, 0); @@ -2986,7 +2985,6 @@ static void flush_tex_batch(void) { p_glUniform1i(s_uPalette, 1); p_glActiveTexture(PSXGL_TEXTURE0); p_glUniform1i(s_uMaskset, s_tb_mask); - p_glUniform1i(s_uFilter, s_tb_filter); p_glBindVertexArray(s_tex_vao); p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo); p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(nverts * TEXV * sizeof(float)), s_tb, PSXGL_STREAM_DRAW); @@ -3388,8 +3386,9 @@ static void gpu_textured_triangle(const int *xs, const int *ys, flush_pack_if_sampling(-1, 0, depth, clut_x, clut_y); mark_prim_dirty(xs, ys, 3, 1 /* textured */); - /* 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 + /* Append to the textured batch. Flush first if this prim's blend/mask/twin + * differ from the open batch, or the buffer is full. Under mask checking, + * retain filter boundaries for stencil visibility. Per-prim texture * 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). */ { @@ -3430,7 +3429,10 @@ static void gpu_textured_triangle(const int *xs, const int *ys, else if (isolate) reason = 0; else if (batch_semi != s_tb_semi) reason = 1; else if (s_mask_set != s_tb_mask) reason = 2; - else if (filter != s_tb_filter) reason = 3; + /* With destination-mask checking, a batch's STP stencil fixup + * happens after its color pass. Keep the old filter boundary so + * a later primitive observes an earlier primitive's mask. */ + else if (s_mask_check && filter != s_tb_filter) reason = 3; else if (gate != s_tb_gate) reason = 4; else if ((!s_twin_batching || s_mask_check) && (twx != s_tb_twin[0] || twy != s_tb_twin[1] || @@ -3464,6 +3466,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys, 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 */ + vp[21] = (float)filter; /* a_filter */ } s_tb_n += 3; if (isolate) flush_tex_batch(); /* draw this semi prim alone, in submission order */ @@ -4444,7 +4447,6 @@ static int init_gpu_raster(void) { s_uSemipass = p_glGetUniformLocation(s_tex_prog, "u_semipass"); 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_uLimits = p_glGetUniformLocation(s_tex_prog, "u_limits"); s_uBlitSrc = p_glGetUniformLocation(s_blit_prog, "u_src"); s_uBlitPass = p_glGetUniformLocation(s_blit_prog, "u_stp_pass"); @@ -4536,6 +4538,7 @@ static int init_gpu_raster(void) { 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_glVertexAttribPointer(11, 1, GL_FLOAT, GL_FALSE, st, (void*)(21*sizeof(float))); p_glEnableVertexAttribArray(11); /* filter */ } p_glGenVertexArrays(1, &s_blit_vao); diff --git a/runtime/tests/test_gl_texture_filter.c b/runtime/tests/test_gl_texture_filter.c index 9906e99a..d3880833 100644 --- a/runtime/tests/test_gl_texture_filter.c +++ b/runtime/tests/test_gl_texture_filter.c @@ -96,6 +96,77 @@ static void native_hold_images(void) { } glDeleteTextures(2,textures); } + +/* Compare mixed sampling modes with the original filter-boundary + * submission. Include overlap, STP masking, perspective, native-wide margins + * and scaled pixels: a lower batch count is useful only if all stay identical. */ +static void mixed_filter_scene(int depth,int mask_check,int semi,int separate, + uint64_t hashes[3],uint64_t* batches) { + upload_checker(depth,mask_check || semi); + glb_set_draw_area(0,0,319,239); 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,1); + glb_fill_rect(0,0,320,240,0x03e0); + /* Clear the reveal too, including untouched rows, before hashing it. */ + p_glBindFramebuffer(PSXGL_FRAMEBUFFER,g_wide_cur); + glDisable(GL_SCISSOR_TEST); glColorMask(GL_TRUE,GL_TRUE,GL_TRUE,GL_TRUE); + glStencilMask(0xff); glClearColor(0,0,0,0); glClearStencil(0); + glClear(GL_COLOR_BUFFER_BIT|GL_STENCIL_BUFFER_BIT); + glb_set_mask_bits(0,mask_check); glb_set_semi_transparency(semi,0); + uint64_t before=s_batch_total; + int previous_filter=-1; + for(int i=0;i<9;++i) { + int x=-24+i*6, y=16+i*2, mode=i%3; + int tracked=i!=5; /* stable mode must keep this untracked primitive sharp */ + int filter=mode==2 && !tracked ? 0 : mode; + /* Recreate the old uniform batch key. Keeping equal-filter primitives + * together matters under destination-mask checking: their stencil + * update is deferred to the end of that original batch too. */ + if(separate && filter!=previous_filter) flush_tex_batch(); + previous_filter=filter; + glb_set_texture_filter(mode); + glb_set_precise_triangle(tracked,x*65536,y*65536, + (x+60)*65536,y*65536,x*65536,(y+48)*65536); + glb_set_perspective_triangle(tracked,1.0f,0.8f,0.6f); + glb_draw_textured_triangle(x,y,0,0,x+60,y,127,0,x,y+48,0,127, + 512,256,(uint16_t)(8|(depth<<7))); + } + gl_renderer_sync_cpu(); + *batches=s_batch_total-before; + check(gl_renderer_fbo_peek(0,0,320,240,peek),"mixed filters native readback"); + hashes[0]=fnv(peek,320*240*2,0xcbf29ce484222325ull); + int scale=s_out_scale, w=320*scale,h=240*scale,ow=0,oh=0; + uint32_t* pixels=(uint32_t*)malloc((size_t)426*512*scale*scale*4); + check(pixels!=NULL,"mixed filters image allocation"); + if(!pixels) return; + int got=gl_renderer_read_display_hires(0,0,320,240,pixels,w*h,&ow,&oh); + check(got==w*h && ow==w && oh==h,"mixed filters scaled readback"); + hashes[1]=fnv(pixels,(size_t)got*4,0xcbf29ce484222325ull); + w=426*scale; h=512*scale; + got=glb_wide_dump_full(pixels,w*h,&ow,&oh,0); + check(got==w*h && ow==w && oh==h,"mixed filters wide readback"); + hashes[2]=fnv(pixels,(size_t)got*4,0xcbf29ce484222325ull); + free(pixels); +} +static void mixed_filter_batches(void) { + for(int scale=1;scale<=5;scale+=4) { + glb_set_scale(scale); + glb_wide_configure(426,53); glb_wide_set_target(0); + for(int depth=0;depth<3;++depth) for(int kind=0;kind<3;++kind) { + uint64_t ref[3]={0},batch[3]={0},nr=0,nb=0; + mixed_filter_scene(depth,kind==1,kind==2,1,ref,&nr); + mixed_filter_scene(depth,kind==1,kind==2,0,batch,&nb); + check(memcmp(ref,batch,sizeof ref)==0, + "mixed filters preserve native, scaled and wide pixels"); + if(kind==0) check(nb%llu\n", + scale,depth,kind,(unsigned long long)nr,(unsigned long long)nb); + } + } + glb_set_mask_bits(0,0); glb_set_semi_transparency(0,0); + glb_wide_configure(0,0); glb_set_scale(1); +} int main(void) { if(SDL_Init(SDL_INIT_VIDEO)!=0) return 2; SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION,3); @@ -145,6 +216,7 @@ int main(void) { upload_checker(2,1); triangle(2,2,0,1); check((peek[24*64+24]&0x7fff)==0x001f || (peek[24*64+24]&0xffff)==0xfc00, "filter never averages colors across opaque and STP classes"); + mixed_filter_batches(); check(glGetError()==GL_NO_ERROR,"GL errors"); printf("checks=%d failures=%d\n",checks,failures); gl_renderer_shutdown(); SDL_DestroyWindow(win); SDL_Quit();