feat(3d): gl1 shim M0-M2 — FFP-on-WebGL2 substrate + lighting; 12 scenarios truly green

wasm/gl1: the GL1.x fixed-function emulation layer replacing the
gl_ffp_stub.c no-ops in the 3d-regression harness link. This batch:

- symbol split: 52 FFP-only entry points implemented; 10 Emscripten-owned
  names intercepted via wasm-ld --wrap (sources.txt/wrapped_symbols.txt are
  the shared manifests for both link sites; production hookup lands in M7)
- matrix stacks (MODELVIEW/PROJECTION, glGetFloatv readback), immediate
  mode with all 8 GL1 primitive conversions, GL1-default state mirror
- full GL 1.5 Gouraud lighting uber-shader (eye-space light capture at
  glLightfv time, color-material, two-side, COMBINE evaluator + alpha test
  wired but inert until M3/M5)
- draw routing: FFP traffic identified by GL_VERTEX_ARRAY client state;
  blit/2D-GAL draws pass through untouched
- display lists: correct glGenLists/glIsList existing-empty semantics;
  recorder itself is M3 (recorded commands drop with a one-time warning)

Parity: 20/47 under the 0.02 floor, of which 12 verified genuinely
rendering (bg-gradient x2, bounding-box, half-open-cylinder, segment x2,
material x3, light x3 — eyeballed against baselines); the other 8 are
small-geometry scenarios whose missing GLU/display-list content sits under
the floor (become real in M3/M4).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Istvan Matejcsok 2026-07-03 11:37:51 +02:00
commit 071eef5454
14 changed files with 2496 additions and 6 deletions

267
wasm/gl1/include/gl1_shim.h Normal file
View file

@ -0,0 +1,267 @@
/*
* gl1_shim — GL 1.x fixed-function pipeline emulated on WebGL2.
*
* Internal header shared by the wasm/gl1/src modules. The public surface
* is the set of C entry points in gl1_entry_ffp.cpp (FFP-only names absent from
* WebGL2, previously no-op'd by wasm/stubs/gl_ffp_stub.c) plus the __wrap_*
* interceptors in gl1_entry_wrapped.cpp (Emscripten-owned names the emulator
* must observe; see wrapped_symbols.txt and the -Wl,--wrap flags both link
* sites derive from it).
*
* Everything here is main-thread-only (both consumers render on the main
* browser thread) — no TLS, no atomics, no exceptions.
*/
#ifndef GL1_SHIM_H
#define GL1_SHIM_H
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glu.h> // wasm/stubs/GL/glu.h via -I wasm/stubs
#include <glm/glm.hpp>
#include <cstdint>
#include <cstdio>
#include <vector>
// One-time diagnostics: the suite requires a clean console, but unsupported
// paths must not fail silently. Each call site warns exactly once.
#define GL1_WARN_ONCE( fmt, ... ) \
do \
{ \
static bool _gl1_warned = false; \
if( !_gl1_warned ) \
{ \
_gl1_warned = true; \
std::fprintf( stderr, "[gl1] " fmt "\n", ##__VA_ARGS__ ); \
} \
} while( 0 )
// The __real_* counterparts of every wrapped symbol (resolved by wasm-ld back
// to the Emscripten WebGL JS-library import). Shim-internal code that needs
// the true WebGL behavior calls these directly — never the public names.
extern "C"
{
void __real_glEnable( GLenum cap );
void __real_glDisable( GLenum cap );
GLboolean __real_glIsEnabled( GLenum cap );
void __real_glDrawArrays( GLenum mode, GLint first, GLsizei count );
void __real_glDrawElements( GLenum mode, GLsizei count, GLenum type, const GLvoid* indices );
void __real_glGetFloatv( GLenum pname, GLfloat* params );
void __real_glBindTexture( GLenum target, GLuint texture );
void __real_glBlendFunc( GLenum sfactor, GLenum dfactor );
void __real_glLineWidth( GLfloat width );
void __real_glHint( GLenum target, GLenum mode );
}
namespace gl1
{
// ---------------------------------------------------------------------------
// State mirror — the GL 1.x state the emulator owns. WebGL-native state
// (blend, depth, stencil, cull, viewport, textures, buffers...) is NOT
// mirrored; those calls pass through untouched.
// ---------------------------------------------------------------------------
struct Light
{
// GL 1.5 defaults: LIGHT0 gets white diffuse/specular, others black
// (applied in State::State).
glm::vec4 ambient{ 0.0f, 0.0f, 0.0f, 1.0f };
glm::vec4 diffuse{ 0.0f, 0.0f, 0.0f, 1.0f };
glm::vec4 specular{ 0.0f, 0.0f, 0.0f, 1.0f };
// GL_POSITION is transformed by the modelview CURRENT AT THE glLightfv CALL
// and stored in eye space — this is what eye-anchors KiCad's directional
// lights (init_lights() runs under an identity modelview).
glm::vec4 posEye{ 0.0f, 0.0f, 1.0f, 0.0f };
};
struct Material
{
glm::vec4 ambient{ 0.2f, 0.2f, 0.2f, 1.0f };
glm::vec4 diffuse{ 0.8f, 0.8f, 0.8f, 1.0f };
glm::vec4 specular{ 0.0f, 0.0f, 0.0f, 1.0f };
glm::vec4 emission{ 0.0f, 0.0f, 0.0f, 1.0f };
float shininess = 0.0f;
};
struct ClientArray
{
bool enabled = false;
GLint size = 4;
GLenum type = GL_FLOAT;
GLsizei stride = 0;
const GLvoid* pointer = nullptr;
// GL1 semantics: gl*Pointer captures the GL_ARRAY_BUFFER binding current at
// the call; 0 = client memory, nonzero = offset into that VBO.
GLuint boundBuffer = 0;
};
// Interleaved immediate-mode vertex (glBegin/glEnd stream and display-list
// geometry bake share this layout). Matches the attribute setup in gl1_draw.
struct ImmVertex
{
float px, py, pz;
float nx, ny, nz;
float r, g, b, a;
float u, v;
};
static_assert( sizeof( ImmVertex ) == 12 * sizeof( float ), "ImmVertex must stay tightly packed" );
enum ClientArrayIndex
{
CA_VERTEX = 0,
CA_NORMAL,
CA_COLOR,
CA_TEXCOORD,
CA_COUNT
};
struct State
{
// --- matrix stacks ---
GLenum matrixMode = GL_MODELVIEW;
std::vector<glm::mat4> mv; // modelview stack, top = back()
std::vector<glm::mat4> proj; // projection stack, top = back()
// --- immediate-mode current attributes (persist across Begin/End) ---
glm::vec4 currentColor{ 1.0f, 1.0f, 1.0f, 1.0f };
glm::vec3 currentNormal{ 0.0f, 0.0f, 1.0f };
// --- FFP capabilities (tracked; never forwarded to WebGL) ---
bool lighting = false;
bool lightEnabled[8] = {};
bool colorMaterial = false;
bool texture2D = false;
bool normalizeNormals = false;
bool alphaTest = false;
// Tracked only so glIsEnabled stays consistent; no rendering effect here
// (forwarding them would raise INVALID_ENUM in WebGL2).
bool lineSmooth = false;
bool pointSmooth = false;
bool multisample = false;
// --- lighting rig / materials ---
Light lights[8];
glm::vec4 lightModelAmbient{ 0.2f, 0.2f, 0.2f, 1.0f };
bool twoSide = false;
Material material; // GL_FRONT_AND_BACK only (asserted at the entry point)
GLenum colorMaterialMode = GL_AMBIENT_AND_DIFFUSE;
// --- texture environment, unit 0 (GL 1.5 initial values) ---
GLenum texEnvMode = GL_MODULATE;
glm::vec4 texEnvColor{ 0.0f, 0.0f, 0.0f, 0.0f };
GLenum combineRGB = GL_MODULATE;
GLenum combineAlpha = GL_MODULATE;
GLenum srcRGB[3] = { GL_TEXTURE, GL_PREVIOUS, GL_CONSTANT };
GLenum operandRGB[3] = { GL_SRC_COLOR, GL_SRC_COLOR, GL_SRC_ALPHA };
GLenum srcAlpha[3] = { GL_TEXTURE, GL_PREVIOUS, GL_CONSTANT };
GLenum operandAlpha[3] = { GL_SRC_ALPHA, GL_SRC_ALPHA, GL_SRC_ALPHA };
// --- alpha test ---
GLenum alphaFunc = GL_ALWAYS;
float alphaRef = 0.0f;
// --- misc ---
GLenum shadeModel = GL_SMOOTH;
float pointSize = 1.0f;
float lineWidth = 1.0f;
GLuint boundTexture2D = 0; // mirror of the unit-0 GL_TEXTURE_2D binding
// --- client arrays ---
ClientArray clientArrays[CA_COUNT];
// Nonzero while a non-shim GLSL program is bound (raytracer blit, 2D GAL):
// routed draws pass through untouched.
GLuint externalProgram = 0;
// --- dirty flags (uniform re-upload gates) ---
bool matricesDirty = true;
bool lightingDirty = true;
bool texEnvDirty = true;
bool miscDirty = true;
State()
{
mv.reserve( 64 );
proj.reserve( 8 );
mv.push_back( glm::mat4( 1.0f ) );
proj.push_back( glm::mat4( 1.0f ) );
lights[0].diffuse = glm::vec4( 1.0f, 1.0f, 1.0f, 1.0f );
lights[0].specular = glm::vec4( 1.0f, 1.0f, 1.0f, 1.0f );
}
glm::mat4& mvTop() { return mv.back(); }
glm::mat4& projTop() { return proj.back(); }
glm::mat4& currentTop() { return matrixMode == GL_PROJECTION ? proj.back() : mv.back(); }
std::vector<glm::mat4>& currentStack() { return matrixMode == GL_PROJECTION ? proj : mv; }
};
State& S();
// Returns the tracked-flag slot for FFP-only glEnable/glDisable caps, or
// nullptr for caps WebGL owns natively (forward those).
bool* ffpCapSlot( GLenum cap );
// Marks the state blocks a cap flip invalidates.
void onCapChanged( GLenum cap );
// --- matrix module (gl1_matrix.cpp) ---
void matrixLoadIdentity();
void matrixLoadf( const GLfloat* m );
void matrixPush();
void matrixPop();
void matrixTranslate( float x, float y, float z );
void matrixRotate( float angleDeg, float x, float y, float z );
void matrixScale( float x, float y, float z );
void matrixPerspective( double fovyDeg, double aspect, double zNear, double zFar );
// --- immediate-mode module (gl1_immediate.cpp) ---
// Also the emission path for GLU quadrics and display-list replay, so all
// geometry funnels through one primitive-conversion + draw pipeline.
void immBegin( GLenum mode );
void immVertex( float x, float y, float z );
void immEnd();
bool immActive();
// --- draw module (gl1_draw.cpp) ---
// Uploads `count` interleaved ImmVertex records to the streaming VBO and draws
// them with the FFP program. `mode` must already be a WebGL2-legal primitive.
void drawImmVertices( GLenum mode, const ImmVertex* verts, GLsizei count );
// Routed glDrawArrays/glDrawElements over FFP client-array state (M3/M5).
void drawClientArrays( GLenum mode, GLint first, GLsizei count );
void drawClientElements( GLenum mode, GLsizei count, GLenum type, const GLvoid* indices );
// --- shader module (gl1_shaders.cpp) ---
// Binds the FFP program and re-uploads whatever state is dirty. Returns false
// (once, with a console warning) if the program failed to build.
bool programSync();
GLuint programId();
// --- display-list module (gl1_dlist.cpp) ---
bool dlistRecording();
GLuint dlistGenLists( GLsizei range );
GLboolean dlistIsList( GLuint list );
void dlistNewList( GLuint list, GLenum mode );
void dlistEndList();
void dlistCallList( GLuint list );
void dlistDeleteLists( GLuint list, GLsizei range );
// Recording hooks (called from entry points / wrappers while recording).
void dlistRecordEnable( GLenum cap, bool enable );
void dlistRecordBindTexture( GLenum target, GLuint texture );
void dlistRecordBlendFunc( GLenum sfactor, GLenum dfactor );
void dlistRecordLineWidth( GLfloat width );
void dlistRecordAlphaFunc( GLenum func, GLclampf ref );
void dlistRecordNormal( float nx, float ny, float nz );
void dlistRecordColor( float r, float g, float b, float a );
void dlistRecordBegin( GLenum mode );
void dlistRecordVertex( float x, float y, float z );
void dlistRecordEnd();
void dlistRecordDrawArrays( GLenum mode, GLint first, GLsizei count );
} // namespace gl1
#endif // GL1_SHIM_H