Avoid clipped GL draws and redundant draw-state flushes

This commit is contained in:
Matthew Stanley
2026-10-24 14:20:21 -07:00
parent 3748851a4f
commit a0738464af
3 changed files with 109 additions and 4 deletions
+31
View File
@@ -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.
+36 -4
View File
@@ -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;i<n;i++) {
if(xs[i]<x0)x0=xs[i];if(xs[i]>x1)x1=xs[i];
if(ys[i]<y0)y0=ys[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.
+42
View File
@@ -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");