Two more dEQP tests down:
dEQP-GLES3.functional.fbo.blit.rect.nearest_consistency_mag -- Pass
dEQP-GLES3.functional.fbo.blit.rect.nearest_consistency_min -- Pass
This one was a fun geometry puzzle.
The problem
Mesa’s u_blitter is the utility that drivers use for framebuffer blits – copying pixel data between surfaces, optionally scaling and filtering. It works by drawing a textured quad: set up the source as a texture, the destination as a render target, and draw a rectangle with the appropriate texture coordinates. Simple.
Except the quad is made of two triangles. And that diagonal seam between them is where the trouble starts.
v2 -------- v3
| \ T2 |
| T1 \ |
| \ |
v0 -------- v1
For LINEAR filtering, this is fine – the interpolation across the seam is smooth enough that nobody notices. But for NEAREST filtering, a texel is selected based on which texel center is closest to the interpolated texture coordinate. At the diagonal seam, the two triangles can produce slightly different texture coordinates for pixels that sit right on the boundary. Different coordinates mean different nearest-texel selection, and that means an inconsistent stripe of wrong texels running diagonally across the blit.
The dEQP nearest_consistency tests specifically check for this: they blit with NEAREST filtering and verify that every pixel picks the same texel regardless of which triangle it fell into.

What others do
This isn’t a new problem. V3D already has a workaround in u_blitter: it sets use_index_buffer to reorder the triangle indices so that the shared edge of the two triangles is along a different diagonal. This changes which pixels land on the seam and can be enough to pass the tests on some hardware.
On GC7000, that wasn’t sufficient. The floating-point interpolation differences are large enough that the seam remains visible regardless of which diagonal you pick.
What the blob does
Looking at command stream traces from the proprietary Vivante driver, the answer was clear: they don’t draw a quad at all. They draw a single oversized triangle and let the scissor clip it to the destination rectangle.
No seam, no problem.
The single triangle transform
The idea is simple: take a rectangle and find a single triangle that fully covers it. The smallest such triangle is a right triangle with legs twice the width and twice the height of the rectangle:
v2
|\
| \
| rect\
|------+\
| | \
v0 -----|--- v1
Vertex 0 stays at the rectangle’s top-left corner. Vertex 1 extends to twice the rectangle width. Vertex 2 extends to twice the rectangle height. The original rectangle is fully contained within this triangle.
The math for transforming the blitter’s 4-vertex quad into a 3-vertex triangle is:
for (unsigned a = 0; a < 2; a++) { /* pos and texcoord */
for (unsigned c = 0; c < 4; c++) { /* xyzw components */
float v0 = ctx->vertices[0][a][c];
ctx->vertices[1][a][c] = 2.0f * ctx->vertices[1][a][c] - v0;
ctx->vertices[2][a][c] = 2.0f * ctx->vertices[3][a][c] - v0;
}
}
This transforms both position and texture coordinates consistently, so the texture mapping across the visible (scissored) region is identical to what the full quad would have produced – minus the seam.
Scissor is essential
The oversized triangle extends beyond the destination rectangle, so we need scissor to clip it. The blitter doesn’t always have a scissor set up, so when use_single_triangle is enabled, we synthesize one from the destination box:
if (ctx->base.use_single_triangle && !scissor) {
synth_scissor.minx = MAX2(dstbox->x, 0);
synth_scissor.miny = MAX2(dstbox->y, 0);
synth_scissor.maxx = dstbox->x + dstbox->width;
synth_scissor.maxy = dstbox->y + dstbox->height;
scissor = &synth_scissor;
}
Keeping it scoped
The single-triangle transform should only apply to blit operations, not to clears or other blitter draws. A transient single_triangle_active flag is set around the actual blit draw calls and checked in the vertex emission code. Drivers opt in by setting use_single_triangle on the blitter context at creation time.
For etnaviv, that’s a single line:
ctx->blitter->use_single_triangle = true;
The design
The implementation is split into two commits: the u_blitter.c infrastructure (vertex transform, synthesized scissor, gating flag, 3-vertex draw path) that any driver can opt into, and the one-line etnaviv enablement. Keeping them separate means another driver hitting the same seam can flip the flag without touching u_blitter code.
The work landed upstream in u_blitter: Add single-triangle draw mode for NEAREST blit consistency.
The takeaway
Sometimes the fix for a rendering artifact isn’t better math or tighter tolerances – it’s removing the geometric feature that causes the problem in the first place. Two triangles have a seam. One triangle doesn’t.