feat(3d): gl1 shim M4+M6+M7 — GLU quadrics, production link, stub retired; 47/47 parity

- gl1_glu: gluCylinder/gluDisk/gluSphere per the SGI GLU reference
  tessellation (sin/cos phase, strip emission order, pole fans — Apple's
  GLU derives from the same source, so vertices match the goldens);
  emission goes through the public glBegin/glVertex entry points so
  quadrics record into display lists
- glLineWidth>1 one-time warning (browsers clamp to 1; affected coverage
  stays under the parity floor — zero floors.json overrides needed)
- production link site: build-kicad-target.sh now compiles wasm/gl1 from
  sources.txt and applies wrapped_symbols.txt --wrap flags (same manifests
  as the test harness); kicad_editor's RENDER_3D_OPENGL links against the
  real emulation layer instead of no-ops
- wasm/stubs/gl_ffp_stub.c deleted (role fully superseded; history in git)
- docs: 3d-regression README port-status section (floor-blind small
  geometry + line-width caveats), harness comments de-red-stated

Parity: 47/47 under the 0.02 floor, zero overrides. Native golden path
byte-identical to bb0e238 (whose gate ran green) — scenarios/, native/,
baseline/, manifest, floors, spec all untouched by the port.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Istvan Matejcsok 2026-07-03 11:56:19 +02:00
commit 27992f4f18
8 changed files with 240 additions and 157 deletions

View file

@ -1,19 +1,49 @@
/*
* gl1_glu — GLU quadrics and gluPerspective.
*
* M4 ports the SGI GLU reference tessellation (quad.c) for
* gluCylinder/gluDisk/gluSphere — the native goldens were rendered with
* Apple's SGI-derived GLU, so vertex placement and emission order must match.
* The quadrics emit through the shim's internal immediate-mode path
* (immBegin/immVertex/immEnd) so they also record into display lists.
* The tessellation follows the SGI GLU reference implementation (the
* OpenGL Sample Implementation's libutil/quad.c, SGI Free Software License
* B) restricted to what the renderer uses: GLU_FILL draw style, GLU_SMOOTH
* normals, GLU_OUTSIDE orientation, no texture coordinates. Apple's GLU —
* which rendered the native goldens — derives from the same source, so
* vertex placement (sin/cos phase), emission order and stack/slice loops
* must match exactly for pixel parity.
*
* Note: the GLU *tesselator* (gluNewTess & co, declared in the same
* Geometry is emitted through the shim's public immediate-mode entry points
* (glBegin/glNormal3f/glVertex3f/glEnd), so quadrics drawn between
* glNewList/glEndList record into the display list like any other geometry.
*
* The GLU *tesselator* (gluNewTess & co, declared in the same
* wasm/stubs/GL/glu.h) is a separate concern implemented in
* kicad/libs/kimath/glu_tess/ — not part of this shim.
*/
#include "gl1_shim.h"
#include <cmath>
namespace
{
const double GLU_PI = 3.14159265358979323846;
// SGI's normal3f helper: normalize, then emit.
void emitNormal( float x, float y, float z )
{
const float mag = std::sqrt( x * x + y * y + z * z );
if( mag > 0.00001f )
{
x /= mag;
y /= mag;
z /= mag;
}
glNormal3f( x, y, z );
}
} // namespace
extern "C"
{
@ -68,33 +98,164 @@ void gluQuadricNormals( GLUquadric* q, GLenum normals )
void gluCylinder( GLUquadric* q, double base, double top, double height, int slices, int stacks )
{
(void) q;
(void) base;
(void) top;
(void) height;
(void) slices;
(void) stacks;
GL1_WARN_ONCE( "gluCylinder not implemented yet (M4) — geometry dropped" );
if( slices < 2 || stacks < 1 || height == 0.0 )
return;
const float da = (float) ( 2.0 * GLU_PI / slices );
const float dr = (float) ( ( top - base ) / stacks );
const float dz = (float) ( height / stacks );
const float nz = (float) ( ( base - top ) / height ); // combined with (x,y), normalized
float t = 0.0f;
float z = 0.0f;
float r = (float) base;
for( int j = 0; j < stacks; j++ )
{
glBegin( GL_QUAD_STRIP );
for( int i = 0; i <= slices; i++ )
{
// SGI closes the strip with the angle-0 vertex, not slice*da.
const float a = ( i == slices ) ? 0.0f : (float) i * da;
const float x = std::sin( a );
const float y = std::cos( a );
emitNormal( x, y, nz );
glVertex3f( x * r, y * r, z );
emitNormal( x, y, nz );
glVertex3f( x * ( r + dr ), y * ( r + dr ), z + dz );
}
glEnd();
r += dr;
t += dz; // (texture t unused; kept for structural parity)
z += dz;
}
(void) t;
}
void gluDisk( GLUquadric* q, double inner, double outer, int slices, int loops )
{
(void) q;
(void) inner;
(void) outer;
(void) slices;
(void) loops;
GL1_WARN_ONCE( "gluDisk not implemented yet (M4) — geometry dropped" );
if( slices < 2 || loops < 1 )
return;
// GLU_OUTSIDE: disk normal is +Z; the innermost ring of a full disk
// (inner == 0) degenerates its inner vertices at the origin, exactly as
// the reference implementation renders it.
glNormal3f( 0.0f, 0.0f, 1.0f );
const float da = (float) ( 2.0 * GLU_PI / slices );
const float dr = (float) ( ( outer - inner ) / loops );
float r1 = (float) inner;
for( int l = 0; l < loops; l++ )
{
const float r2 = r1 + dr;
glBegin( GL_QUAD_STRIP );
for( int s = 0; s <= slices; s++ )
{
const float a = ( s == slices ) ? 0.0f : (float) s * da;
const float sa = std::sin( a );
const float ca = std::cos( a );
glVertex2f( r2 * sa, r2 * ca );
glVertex2f( r1 * sa, r1 * ca );
}
glEnd();
r1 = r2;
}
}
void gluSphere( GLUquadric* q, double radius, int slices, int stacks )
{
(void) q;
(void) radius;
(void) slices;
(void) stacks;
GL1_WARN_ONCE( "gluSphere not implemented yet (M4) — geometry dropped" );
if( slices < 2 || stacks < 2 )
return;
const float drho = (float) ( GLU_PI / stacks );
const float dtheta = (float) ( 2.0 * GLU_PI / slices );
const float rad = (float) radius;
// +Z pole cap (triangle fan).
glBegin( GL_TRIANGLE_FAN );
glNormal3f( 0.0f, 0.0f, 1.0f );
glVertex3f( 0.0f, 0.0f, rad );
for( int j = 0; j <= slices; j++ )
{
const float theta = ( j == slices ) ? 0.0f : (float) j * dtheta;
const float x = -std::sin( theta ) * std::sin( drho );
const float y = std::cos( theta ) * std::sin( drho );
const float z = std::cos( drho );
glNormal3f( x, y, z );
glVertex3f( x * rad, y * rad, z * rad );
}
glEnd();
// Intermediate stacks as quad strips.
for( int i = 1; i < stacks - 1; i++ )
{
const float rho = (float) i * drho;
glBegin( GL_QUAD_STRIP );
for( int j = 0; j <= slices; j++ )
{
const float theta = ( j == slices ) ? 0.0f : (float) j * dtheta;
float x = -std::sin( theta ) * std::sin( rho );
float y = std::cos( theta ) * std::sin( rho );
float z = std::cos( rho );
glNormal3f( x, y, z );
glVertex3f( x * rad, y * rad, z * rad );
x = -std::sin( theta ) * std::sin( rho + drho );
y = std::cos( theta ) * std::sin( rho + drho );
z = std::cos( rho + drho );
glNormal3f( x, y, z );
glVertex3f( x * rad, y * rad, z * rad );
}
glEnd();
}
// -Z pole cap (triangle fan, reverse slice order).
glBegin( GL_TRIANGLE_FAN );
glNormal3f( 0.0f, 0.0f, -1.0f );
glVertex3f( 0.0f, 0.0f, -rad );
const float rho = (float) GLU_PI - drho;
for( int j = slices; j >= 0; j-- )
{
const float theta = ( j == slices ) ? 0.0f : (float) j * dtheta;
const float x = -std::sin( theta ) * std::sin( rho );
const float y = std::cos( theta ) * std::sin( rho );
const float z = std::cos( rho );
glNormal3f( x, y, z );
glVertex3f( x * rad, y * rad, z * rad );
}
glEnd();
}

View file

@ -101,6 +101,10 @@ void stateBlendFunc( GLenum sfactor, GLenum dfactor )
void stateLineWidth( GLfloat width )
{
if( width > 1.0f )
GL1_WARN_ONCE( "glLineWidth(%g): browsers clamp line width to 1 — lines render thinner "
"than native (known WebGL2 limitation)", (double) width );
S().lineWidth = width;
__real_glLineWidth( width );
}

View file

@ -1,104 +0,0 @@
/*
* Fixed-function-pipeline (FFP) no-op stubs for the WASM 3D-viewer build.
*
* KiCad's 3D "OpenGL" renderer (under 3d-viewer/3d_rendering/opengl) is pure
* GL 1.x fixed-function: immediate mode, display lists, the matrix stack,
* FFP lighting/material, client-state vertex arrays and GLU quadrics. None of
* those entry points exist in WebGL2 / GLES3.
*
* On WASM we do NOT render through that renderer — the 3D viewer draws with the
* GL-free CPU raytracer (RENDER_3D_RAYTRACE_RAM) and blits the result with a
* plain WebGL2 textured quad (see EDA_3D_CANVAS). The fixed-function renderer is
* still compiled (it is referenced from several shared files: eda_3d_canvas,
* appearance_controls_3D, panel_preview_3d_model, eda_3d_controller), so its FFP
* calls must *link*. These no-op definitions satisfy the linker without dragging
* in Emscripten's -sLEGACY_GL_EMULATION (which is module-global, ~200 KB, and was
* rejected for this project — see docs/features/fork-cleanup/10-3d-viewer.md).
* The functions are never executed at runtime; they only need to exist.
*
* Signatures are taken verbatim from Emscripten's <GL/gl.h> (and the project's
* GLU stub <GL/glu.h>) so the compiler validates them against the same
* declarations the renderer is built against.
*
* Only the symbols absent from core WebGL2/GLES3 are stubbed here. Modern entry
* points (glDrawArrays, glGenBuffers, glUseProgram, glTexImage2D, …) are provided
* by Emscripten's WebGL library and must NOT be redefined.
*/
#include <GL/gl.h>
#include <GL/glu.h> /* resolved to wasm/stubs/GL/glu.h via -I${STUBS_DIR} */
/* ---- Immediate mode --------------------------------------------------- */
void glBegin( GLenum mode ) { (void) mode; }
void glEnd( void ) {}
void glVertex2f( GLfloat x, GLfloat y ) { (void) x; (void) y; }
void glVertex3f( GLfloat x, GLfloat y, GLfloat z ) { (void) x; (void) y; (void) z; }
void glVertex3d( GLdouble x, GLdouble y, GLdouble z ) { (void) x; (void) y; (void) z; }
void glNormal3f( GLfloat nx, GLfloat ny, GLfloat nz ) { (void) nx; (void) ny; (void) nz; }
void glColor3f( GLfloat r, GLfloat g, GLfloat b ) { (void) r; (void) g; (void) b; }
void glColor4f( GLfloat r, GLfloat g, GLfloat b, GLfloat a ) { (void) r; (void) g; (void) b; (void) a; }
/* ---- Display lists ---------------------------------------------------- */
GLuint glGenLists( GLsizei range ) { (void) range; return 1; }
void glNewList( GLuint list, GLenum mode ) { (void) list; (void) mode; }
void glEndList( void ) {}
void glCallList( GLuint list ) { (void) list; }
void glDeleteLists( GLuint list, GLsizei range ) { (void) list; (void) range; }
GLboolean glIsList( GLuint list ) { (void) list; return GL_FALSE; }
/* ---- Matrix stack ----------------------------------------------------- */
void glMatrixMode( GLenum mode ) { (void) mode; }
void glLoadIdentity( void ) {}
void glLoadMatrixf( const GLfloat* m ) { (void) m; }
void glPushMatrix( void ) {}
void glPopMatrix( void ) {}
void glTranslatef( GLfloat x, GLfloat y, GLfloat z ) { (void) x; (void) y; (void) z; }
void glRotatef( GLfloat angle, GLfloat x, GLfloat y, GLfloat z ) { (void) angle; (void) x; (void) y; (void) z; }
void glScalef( GLfloat x, GLfloat y, GLfloat z ) { (void) x; (void) y; (void) z; }
void glScaled( GLdouble x, GLdouble y, GLdouble z ) { (void) x; (void) y; (void) z; }
/* ---- Fixed-function lighting / material ------------------------------- */
void glShadeModel( GLenum mode ) { (void) mode; }
void glLightfv( GLenum light, GLenum pname, const GLfloat* params ) { (void) light; (void) pname; (void) params; }
void glLightModeli( GLenum pname, GLint param ) { (void) pname; (void) param; }
void glLightModelfv( GLenum pname, const GLfloat* params ) { (void) pname; (void) params; }
void glMaterialf( GLenum face, GLenum pname, GLfloat param ) { (void) face; (void) pname; (void) param; }
void glMaterialfv( GLenum face, GLenum pname, const GLfloat* params ) { (void) face; (void) pname; (void) params; }
void glColorMaterial( GLenum face, GLenum mode ) { (void) face; (void) mode; }
/* ---- Client-state vertex arrays --------------------------------------- */
void glEnableClientState( GLenum cap ) { (void) cap; }
void glDisableClientState( GLenum cap ) { (void) cap; }
void glClientActiveTexture( GLenum texture ) { (void) texture; }
void glVertexPointer( GLint size, GLenum type, GLsizei stride, const GLvoid* ptr ) { (void) size; (void) type; (void) stride; (void) ptr; }
void glNormalPointer( GLenum type, GLsizei stride, const GLvoid* ptr ) { (void) type; (void) stride; (void) ptr; }
void glColorPointer( GLint size, GLenum type, GLsizei stride, const GLvoid* ptr ) { (void) size; (void) type; (void) stride; (void) ptr; }
void glTexCoordPointer( GLint size, GLenum type, GLsizei stride, const GLvoid* ptr ) { (void) size; (void) type; (void) stride; (void) ptr; }
/* ---- Fixed-function texture environment ------------------------------- */
void glTexEnvf( GLenum target, GLenum pname, GLfloat param ) { (void) target; (void) pname; (void) param; }
void glTexEnvi( GLenum target, GLenum pname, GLint param ) { (void) target; (void) pname; (void) param; }
void glTexEnvfv( GLenum target, GLenum pname, const GLfloat* params ) { (void) target; (void) pname; (void) params; }
/* ---- Misc fixed-function state absent from GLES3 ---------------------- */
void glAlphaFunc( GLenum func, GLclampf ref ) { (void) func; (void) ref; }
void glPolygonMode( GLenum face, GLenum mode ) { (void) face; (void) mode; }
void glClearDepth( GLclampd depth ) { (void) depth; }
void glPointSize( GLfloat size ) { (void) size; }
/* ---- GLU quadrics (vias/pads/gizmo) + gluPerspective ------------------ */
/* GLUquadric is an opaque/incomplete type; hand back a stable non-null pointer
* the renderer can store and pass to the (no-op) quadric calls below. */
static int g_dummyQuadric;
GLUquadric* gluNewQuadric( void ) { return (GLUquadric*) &g_dummyQuadric; }
void gluDeleteQuadric( GLUquadric* q ) { (void) q; }
void gluQuadricDrawStyle( GLUquadric* q, GLenum style ) { (void) q; (void) style; }
void gluQuadricNormals( GLUquadric* q, GLenum normals ) { (void) q; (void) normals; }
void gluCylinder( GLUquadric* q, double base, double top, double height, int slices, int stacks )
{ (void) q; (void) base; (void) top; (void) height; (void) slices; (void) stacks; }
void gluDisk( GLUquadric* q, double inner, double outer, int slices, int loops )
{ (void) q; (void) inner; (void) outer; (void) slices; (void) loops; }
void gluSphere( GLUquadric* q, double radius, int slices, int stacks )
{ (void) q; (void) radius; (void) slices; (void) stacks; }
void gluPerspective( double fovy, double aspect, double zNear, double zFar )
{ (void) fovy; (void) aspect; (void) zNear; (void) zFar; }