From a0738464afe8bdf8cb2e9e1eb939699e37027ac4 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 24 Oct 2026 14:20:21 -0700 Subject: [PATCH] Avoid clipped GL draws and redundant draw-state flushes --- docs/MULTICAMERA_RENDER_COST.md | 31 ++++++++++++++++++++ runtime/src/gpu_gl_renderer.c | 40 ++++++++++++++++++++++--- runtime/tests/test_gl_render_thread.c | 42 +++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 docs/MULTICAMERA_RENDER_COST.md diff --git a/docs/MULTICAMERA_RENDER_COST.md b/docs/MULTICAMERA_RENDER_COST.md new file mode 100644 index 00000000..5e7a034f --- /dev/null +++ b/docs/MULTICAMERA_RENDER_COST.md @@ -0,0 +1,31 @@ +# Shared GL draw submission cost + +The native GL raster path rejects primitives whose entire integer bounding box +lies outside the current GPU drawing area. GPU decoding has already applied the +draw offset. A one-pixel guard keeps boundary lines and triangles conservative. +Wide backing targets, high-resolution wide mirrors, precision/depth coordinates +and seam correction keep their existing path. Fills, transfers and VRAM copies +are excluded because they do not obey primitive drawing-area clipping. + +Repeated drawing-area and drawing-offset packets retain their pending batch +when the values have not changed. Changed state still flushes in painter order; +software state is forwarded in either case. These are host submission savings: +GP0 decoding, simulation, interrupts, GPU timing and software raster calls keep +their existing behavior. Both HLE and LLE textured batchers use this path. No +CPU-specific instructions or new user control are needed. + +`runtime/tests/test_gl_render_thread.c` includes a clipped workload, a visible +texture control, a scissor-edge sliver and repeated state packets. It verifies +the skipped batches, retained visible pixels, clipping and shared batching. +The existing real-context stream runs at 1x/4x, with the worker disabled/enabled +and with dynamic resolution changes. On Windows/NVIDIA the native readback, +VRAM, hires and actual displayed composition hashes remain unchanged in all +six configurations under both batchers. The 22 blend/mask oracle checks pass. + +Mega Man X4's experimental four-view build is the measured caller. Its separate +title config enables the existing audited PsyQ store-counter idle detector at +USA address `0x800E4F6C`; that is not a new framework default. The detector +retains device/IRQ boundaries and rejects unqualified loops. The title's +source compatibility hash includes this config. See its heavy-gameplay QA +record for performance and native multi-peer evidence. These checks do not +qualify every game or guarantee full-speed four-view play. diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index e60737ff..99bfd805 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -3228,6 +3228,27 @@ static void ensure_cpu(void) { static uint64_t s_scene_prims = 0; /* frame_perf: scene primitives submitted (pre double-draw) */ static uint64_t s_scene_prims_tex = 0; /* frame_perf: of which textured (vs flat geometry) */ +static uint64_t s_clipped_prims = 0; +/* gpu.c has already applied the drawing offset to these integer vertices. + * Reject only a wholly clipped native draw. Wide mirrors can reveal pixels + * outside the native area; precision/seam/windowed paths keep their existing + * clipping. A one-pixel guard also retains boundary lines/triangles. Fills + * and VRAM copies do not use this test: their writes ignore the draw area. */ +static int prim_wholly_clipped(const int *xs, const int *ys, int n) { + if (g_wide_cur || hiw_on() || s_pc_valid || s_pq_valid || s_pz_valid || s_pgxp_seam) + return 0; + int x0=xs[0],x1=xs[0],y0=ys[0],y1=ys[0]; + for (int i=1;ix1)x1=xs[i]; + if(ys[i]y1)y1=ys[i]; + } + if (x1 < s_area_x1-1 || x0 > s_area_x2+1 || + y1 < s_area_y1-1 || y0 > s_area_y2+1) { + s_clipped_prims++; + return 1; + } + return 0; +} static void mark_prim_dirty(const int *xs, const int *ys, int n, int textured) { s_scene_prims++; int x0 = xs[0], x1 = xs[0], y0 = ys[0], y1 = ys[0]; @@ -4564,6 +4585,7 @@ static void seam_sz(const float B[3][3], float z[3]) { static void gpu_geometry(GLenum mode, const int *xs, const int *ys, const uint16_t *cs, int n, int semi) { + if (prim_wholly_clipped(xs,ys,n)) return; flush_tex_batch(); /* flat prim: drain textured draws first (order + program) */ flush_cpu_upload(); /* also drains flat batch if an upload was pending */ mark_prim_dirty(xs, ys, n, 0 /* flat */); @@ -4812,6 +4834,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys, const float *col, uint16_t texpage, uint16_t clut_x, uint16_t clut_y, int rawtex, int semi, const int *lim) { + if (prim_wholly_clipped(xs,ys,3)) return; /* PS1 dithering: texture-modulated polygons only (never raw texels or * rectangles), while E1 bit 9 is set. */ const int dither_tex = s_dither_mode && s_dither_bit && !rawtex && !s_prim_rect; @@ -5324,11 +5347,20 @@ static void glb_set_depth_triangle(int enabled, float z0, float z1, float z2) { s_pz[0] = z0; s_pz[1] = z1; s_pz[2] = z2; } static void glb_set_draw_area(int x1,int y1,int x2,int y2) { - if (s_depth_used && (x1 != s_area_x1 || y1 != s_area_y1 || x2 != s_area_x2 || y2 != s_area_y2)) - DEPTH_REQUEST_CLEAR(DCLR_AREA); /* PGXP depth: a new drawing area starts clean */ - flush_flat_batch(); flush_tex_batch(); s_area_x1=x1; s_area_y1=y1; s_area_x2=x2; s_area_y2=y2; sw_set_draw_area(x1,y1,x2,y2); } + if (x1 != s_area_x1 || y1 != s_area_y1 || x2 != s_area_x2 || y2 != s_area_y2) { + if (s_depth_used) DEPTH_REQUEST_CLEAR(DCLR_AREA); + flush_flat_batch(); flush_tex_batch(); + s_area_x1=x1; s_area_y1=y1; s_area_x2=x2; s_area_y2=y2; + } + sw_set_draw_area(x1,y1,x2,y2); +} static void glb_get_draw_area(int *x1,int *y1,int *x2,int *y2) { sw_get_draw_area(x1,y1,x2,y2); } -static void glb_set_draw_offset(int x,int y) { flush_flat_batch(); flush_tex_batch(); s_off_x=x; s_off_y=y; sw_set_draw_offset(x,y); } +static void glb_set_draw_offset(int x,int y) { + if (x != s_off_x || y != s_off_y) { + flush_flat_batch(); flush_tex_batch(); s_off_x=x; s_off_y=y; + } + sw_set_draw_offset(x,y); +} /* Pre-context draws (s_raster_ok == 0) fall back to the software rasterizer * over CPU VRAM; the initial full-VRAM upload at context init folds them in. diff --git a/runtime/tests/test_gl_render_thread.c b/runtime/tests/test_gl_render_thread.c index 39f6571f..2d155587 100644 --- a/runtime/tests/test_gl_render_thread.c +++ b/runtime/tests/test_gl_render_thread.c @@ -210,6 +210,47 @@ static void textures(void) { cpu_upload(512, 256, 16, 1, clut); } +static void test_clipped_and_redundant_state(void) { + gr_wide_configure(0,0); + gr_wide_disable_target(); + gr_set_draw_area(64,64,127,127); + gr_set_draw_offset(0,0); + gr_set_semi_transparency(0,0); + gr_set_color_modulation(128,128,128,0); + mask_bits(0,0); + gl_renderer_render_thread_sync("clipped_fixture_before"); + flush_flat_batch();flush_tex_batch();flush_cpu_upload(); + const uint64_t batches=s_batch_total,clipped=s_clipped_prims; + for(int i=0;i<16;i++) { + gr_draw_flat_rect(-100-i,-100,4,4,0x7fff); + gr_draw_textured_rect(200+i,200,8,8,0,0,512,256,0x0008); + gr_draw_line(-100,200+i,-80,220+i,0x7fff); + } + gl_renderer_render_thread_sync("clipped_fixture_after"); + flush_flat_batch();flush_tex_batch();flush_cpu_upload(); + check(s_batch_total==batches,"wholly clipped draws emit no GPU batches"); + check(s_clipped_prims>clipped,"clipped primitive counter observes rejected work"); + check(gr_vram_read(64,64)==0,"clipped draws preserve VRAM"); + uint16_t page[8*8],palette[16]={0}; + for(int i=0;i<8*8;i++)page[i]=0x1111; + palette[1]=0x7fff; + cpu_upload(512,0,8,8,page);cpu_upload(512,256,16,1,palette); + gr_set_color_modulation(128,128,128,1); + const uint64_t visible=s_batch_total; + gr_draw_textured_rect(64,64,8,8,0,0,512,256,0x0008); + gr_set_draw_area(64,64,127,127); /* repeated GP0 environment packets */ + gr_set_draw_offset(0,0); + gr_draw_textured_rect(127,80,8,8,0,0,512,256,0x0008); /* retain the scissor-edge sliver */ + gl_renderer_render_thread_sync("redundant_state_fixture"); + flush_flat_batch();flush_tex_batch();flush_cpu_upload(); + check(s_batch_total==visible+1,"redundant draw state preserves one painter-ordered batch"); + check((gr_vram_read(64,64)&0x7fff)==0x7fff,"visible control draw survives"); + check((gr_vram_read(127,80)&0x7fff)==0x7fff,"scissor-edge sliver survives"); + check(gr_vram_read(128,80)==0,"draw area still clips the adjacent pixel"); + gr_set_draw_area(0,0,1023,511); + textures(); +} + static void frame(int f, int buf_x) { gr_set_draw_area(buf_x, 0, buf_x + FRAME_W - 1, FRAME_H - 1); gr_set_draw_offset(buf_x, 0); @@ -328,6 +369,7 @@ int main(int argc, char **argv) { if (!gl_renderer_init_context(win)) { fprintf(stderr, "FAIL context\n"); return 2; } check(gr_backend() == GR_BACKEND_OPENGL, "OpenGL backend"); textures(); + test_clipped_and_redundant_state(); gr_wide_configure(426, 53); if (threaded) { check(gl_renderer_render_thread_start(2) == 1, "render thread started");