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

View file

@ -48,6 +48,7 @@ KICAD_INCLUDES = -I../native \
-I$(KICAD_ROOT)/thirdparty/nlohmann_json \
-I$(KICAD_ROOT)/thirdparty \
-I$(PROJECT_ROOT)/wasm/stubs \
-I$(PROJECT_ROOT)/wasm/gl1/include \
-I$(SYSROOT)/include
ifdef DEBUG
@ -74,11 +75,11 @@ BASE_LDFLAGS = $(DEPS_EH_FLAGS) \
-sEXPORT_NAME='create3DTest' \
-sENVIRONMENT=web,worker
# Pure WebGL 2.0 context; the FFP calls are stubbed C no-ops (no
# Pure WebGL 2.0 context; the FFP surface is implemented by wasm/gl1 (no
# LEGACY_GL_EMULATION), same as the production kicad_editor build.
EM_GL_FLAGS = -sMAX_WEBGL_VERSION=2
LDFLAGS = $(DEBUG_LDFLAGS) $(BASE_LDFLAGS) $(EM_GL_FLAGS) $(WX_LDFLAGS)
LDFLAGS = $(DEBUG_LDFLAGS) $(BASE_LDFLAGS) $(EM_GL_FLAGS) $(WX_LDFLAGS) $(GL1_WRAP_LDFLAGS)
MAIN_SRCS = 3d_webgl_test.cpp
@ -157,11 +158,18 @@ KICAD_3D_SRCS = \
$(KICAD_ROOT)/thirdparty/clipper2/Clipper2Lib/src/clipper.offset.cpp \
$(KICAD_ROOT)/thirdparty/clipper2/Clipper2Lib/src/clipper.rectclip.cpp
# THE RED STATE: legacy fixed-function GL as no-ops.
FFP_STUB_SRCS = $(PROJECT_ROOT)/wasm/stubs/gl_ffp_stub.c
# The GL1->WebGL2 emulation layer (wasm/gl1): implements the FFP surface the
# renderer needs and --wrap-intercepts the Emscripten-owned names it must
# observe. sources.txt / wrapped_symbols.txt are shared with the production
# link site (scripts/kicad/build-kicad-target.sh).
GL1_DIR = $(PROJECT_ROOT)/wasm/gl1
GL1_SRCS := $(addprefix $(GL1_DIR)/src/,$(shell cat $(GL1_DIR)/sources.txt))
SRCS = $(MAIN_SRCS) $(SCENARIO_SRCS) $(TESTIMPL_SRCS) $(KICAD_3D_SRCS)
OBJS = $(SRCS:.cpp=.o) $(FFP_STUB_SRCS:.c=.o)
comma := ,
GL1_WRAP_LDFLAGS := $(foreach sym,$(shell cat $(GL1_DIR)/wrapped_symbols.txt),-Wl$(comma)--wrap=$(sym))
SRCS = $(MAIN_SRCS) $(SCENARIO_SRCS) $(TESTIMPL_SRCS) $(KICAD_3D_SRCS) $(GL1_SRCS)
OBJS = $(SRCS:.cpp=.o)
DEPS = $(SRCS:.cpp=.d)
TARGET = $(OUTPUT_DIR)/3d_webgl_test.js
@ -188,6 +196,7 @@ clean:
rm -f $(OUTPUT_DIR)/3d_webgl_test.wasm
rm -f $(OUTPUT_DIR)/3d_webgl_test.html
rm -f ../scenarios/*.o ../scenarios/*.d ../native/*.o ../native/*.d
rm -f $(GL1_DIR)/src/*.o $(GL1_DIR)/src/*.d
.PHONY: all clean

73
wasm/gl1/README.md Normal file
View file

@ -0,0 +1,73 @@
# wasm/gl1 — GL 1.x fixed-function pipeline on WebGL2
Emulation layer that lets KiCad's 3D viewer renderer (`RENDER_3D_OPENGL`,
pure GL 1.x fixed-function, compiled **unmodified**) render in the browser.
Supersedes the no-op link stubs of `wasm/stubs/gl_ffp_stub.c` (git history
has the old file): same public surface, real implementations.
TDD harness: `tests/3d-regression/` — 47 native-golden scenarios; parity via
`npm run 3d:check:parity` (see that README).
## How the symbols resolve
In the WASM build, `gl*` are plain C functions split between two providers:
- **Emscripten's WebGL library** (`-sMAX_WEBGL_VERSION=2`) owns every
modern/WebGL2 name (`glClear`, `glDrawArrays`, `glTexImage2D`, buffers,
shaders, stencil...).
- **This shim** owns the FFP-only names WebGL2 lacks (`glBegin`, display
lists, matrix stack, `glLight*`/`glMaterial*`, client-array pointers,
`glTexEnv*`, `glAlphaFunc`, GLU quadrics) — see `src/gl1_entry_ffp.cpp`.
The emulator must also *observe* a few Emscripten-owned calls (FFP `glEnable`
caps, draws over client arrays, matrix readback, state recorded inside
display lists). Those are intercepted with **wasm-ld `--wrap`**: every name in
`wrapped_symbols.txt` gets a `-Wl,--wrap=<sym>` flag at both link sites, the
interceptors live in `src/gl1_entry_wrapped.cpp`, and `__real_*` forwards to
the WebGL library. No kicad/wxwidgets sources are touched.
Link sites (both read `sources.txt` + `wrapped_symbols.txt`):
- `tests/3d-regression/wasm/Makefile` (the TDD harness)
- `scripts/kicad/build-kicad-target.sh` (`GL3D_LINK_FLAGS`, production
`kicad_editor`)
## Draw routing
A `glDrawArrays`/`glDrawElements` call is FFP traffic iff `GL_VERTEX_ARRAY`
client state is enabled: only GL1 code calls `glEnableClientState`, while the
raytracer blit (`eda_3d_canvas_wasm.cpp`) and the 2D WebGL GAL drive their own
GLSL programs and never touch client state — their draws pass through
untouched.
## Invariants that are easy to break (learned from the native goldens)
- **Display lists snapshot client arrays at record time.** The renderer sets
`gl*Pointer`, records `glDrawArrays` inside `glNewList`, then `delete[]`s
the arrays right after `glEndList` (`layer_triangles.cpp`) — a recorder
that stores pointers reads freed memory. Copy eagerly at record.
- **Lights live in eye space, captured at `glLightfv` time.** `init_lights()`
runs under an identity modelview → the directional lights are anchored to
the camera. Re-deriving light directions at draw time breaks every lit
scenario.
- **Do not "fix" mismatched normal counts.** `generate_middle_triangles`
rejecting countersink walls (normals ≠ vertices) is upstream KiCad behavior
the goldens document; tolerating it would *diverge* from native.
- **Lighting is per-vertex (Gouraud)** to match fixed-function output;
per-fragment lighting visibly mismatches speculars on coarse meshes.
## Layout
```
sources.txt / wrapped_symbols.txt single source of truth for both link sites
include/gl1_shim.h internal API + the state mirror
src/gl1_entry_ffp.cpp the 52 FFP-only public entry points
src/gl1_entry_wrapped.cpp __wrap_* interceptors (mechanism-aware TU)
src/gl1_state.cpp state singleton, capability routing
src/gl1_matrix.cpp MODELVIEW/PROJECTION stacks (+readback)
src/gl1_immediate.cpp glBegin/glEnd + primitive conversion
src/gl1_dlist.cpp display-list recorder/replayer
src/gl1_draw.cpp draw execution (stream VBO, attrib setup)
src/gl1_shaders.cpp the FFP uber-program (ES 3.00) + uniforms
src/gl1_glu.cpp GLU quadrics (SGI tessellation) + gluPerspective
```

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

9
wasm/gl1/sources.txt Normal file
View file

@ -0,0 +1,9 @@
gl1_entry_ffp.cpp
gl1_entry_wrapped.cpp
gl1_state.cpp
gl1_matrix.cpp
gl1_immediate.cpp
gl1_dlist.cpp
gl1_draw.cpp
gl1_shaders.cpp
gl1_glu.cpp

202
wasm/gl1/src/gl1_dlist.cpp Normal file
View file

@ -0,0 +1,202 @@
/*
* gl1_dlist display-list name allocation, recording and replay.
*
* Semantics the renderer depends on:
* - glGenLists creates EXISTING (empty) lists: KiCad's pattern is
* `id = glGenLists(1); if( glIsList(id) ) { glNewList(...); }`
* (layer_triangles.cpp, render_3d_opengl.cpp:1327) glIsList must be
* true right after allocation or nothing ever renders.
* - Only GL_COMPILE recording exists (COMPILE_AND_EXECUTE is asserted).
* - M3 adds the command recorder (state + geometry + glDrawArrays with
* client-array snapshots); until then recorded commands are dropped,
* which keeps the pre-M3 red scenarios blank instead of crashing.
*/
#include "gl1_shim.h"
#include <map>
namespace gl1
{
struct DList
{
// M3: recorded command stream + baked static VBO.
bool empty = true;
};
static std::map<GLuint, DList> s_lists;
static GLuint s_nextId = 1;
static bool s_recording = false;
static GLuint s_recordingId = 0;
bool dlistRecording()
{
return s_recording;
}
GLuint dlistGenLists( GLsizei range )
{
if( range <= 0 )
return 0;
const GLuint first = s_nextId;
for( GLsizei i = 0; i < range; ++i )
s_lists[s_nextId++] = DList{};
return first;
}
GLboolean dlistIsList( GLuint list )
{
return s_lists.count( list ) ? GL_TRUE : GL_FALSE;
}
void dlistNewList( GLuint list, GLenum mode )
{
if( list == 0 )
{
GL1_WARN_ONCE( "glNewList(0) is invalid — ignored" );
return;
}
if( mode != GL_COMPILE )
GL1_WARN_ONCE( "glNewList: only GL_COMPILE is supported (got 0x%x)", mode );
if( s_recording )
{
GL1_WARN_ONCE( "glNewList while already recording — previous list discarded" );
dlistEndList();
}
s_lists[list] = DList{}; // re-recording replaces the old content
s_recording = true;
s_recordingId = list;
}
void dlistEndList()
{
if( !s_recording )
{
GL1_WARN_ONCE( "glEndList without glNewList — ignored" );
return;
}
s_recording = false;
s_recordingId = 0;
}
void dlistCallList( GLuint list )
{
auto it = s_lists.find( list );
if( it == s_lists.end() )
return; // calling a nonexistent list is a silent no-op in GL
// M3: replay the recorded command stream.
}
void dlistDeleteLists( GLuint list, GLsizei range )
{
for( GLsizei i = 0; i < range; ++i )
s_lists.erase( list + (GLuint) i );
}
// --- recording hooks (M3 replaces these drops with the command recorder) ---
void dlistRecordEnable( GLenum cap, bool enable )
{
(void) cap;
(void) enable;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
}
void dlistRecordBindTexture( GLenum target, GLuint texture )
{
(void) target;
(void) texture;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
}
void dlistRecordBlendFunc( GLenum sfactor, GLenum dfactor )
{
(void) sfactor;
(void) dfactor;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
}
void dlistRecordLineWidth( GLfloat width )
{
(void) width;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
}
void dlistRecordAlphaFunc( GLenum func, GLclampf ref )
{
(void) func;
(void) ref;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
}
void dlistRecordNormal( float nx, float ny, float nz )
{
(void) nx;
(void) ny;
(void) nz;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
}
void dlistRecordColor( float r, float g, float b, float a )
{
(void) r;
(void) g;
(void) b;
(void) a;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
}
void dlistRecordBegin( GLenum mode )
{
(void) mode;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
}
void dlistRecordVertex( float x, float y, float z )
{
(void) x;
(void) y;
(void) z;
}
void dlistRecordEnd()
{
}
void dlistRecordDrawArrays( GLenum mode, GLint first, GLsizei count )
{
(void) mode;
(void) first;
(void) count;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
}
} // namespace gl1

88
wasm/gl1/src/gl1_draw.cpp Normal file
View file

@ -0,0 +1,88 @@
/*
* gl1_draw draw execution: streaming immediate-mode geometry and routed
* client-array / user-VBO draws, all through the FFP uber-program.
*
* VAO policy: default VAO only, all four attributes respecified per draw. The
* renderer binds its own VBO/IBO on the default VAO (3d_model.cpp), so a shim
* VAO would hide the user's GL_ELEMENT_ARRAY_BUFFER binding. The shim never
* binds GL_ELEMENT_ARRAY_BUFFER itself.
*
* GL_ARRAY_BUFFER discipline: GL1 code assumes the binding it last set the
* shim saves and restores it around its own streaming uploads so a later
* gl*Pointer call captures the app's binding, not the shim's scratch VBO.
*/
#include "gl1_shim.h"
namespace gl1
{
static GLuint s_streamVBO = 0;
enum
{
ATTR_POSITION = 0,
ATTR_NORMAL = 1,
ATTR_COLOR = 2,
ATTR_TEXCOORD = 3,
};
void drawImmVertices( GLenum mode, const ImmVertex* verts, GLsizei count )
{
if( !programSync() )
return;
if( !s_streamVBO )
glGenBuffers( 1, &s_streamVBO );
GLint prevArrayBuffer = 0;
glGetIntegerv( GL_ARRAY_BUFFER_BINDING, &prevArrayBuffer );
glBindBuffer( GL_ARRAY_BUFFER, s_streamVBO );
glBufferData( GL_ARRAY_BUFFER, (GLsizeiptr) ( count * sizeof( ImmVertex ) ), verts,
GL_STREAM_DRAW );
const GLsizei stride = (GLsizei) sizeof( ImmVertex );
glEnableVertexAttribArray( ATTR_POSITION );
glVertexAttribPointer( ATTR_POSITION, 3, GL_FLOAT, GL_FALSE, stride, (const void*) 0 );
glEnableVertexAttribArray( ATTR_NORMAL );
glVertexAttribPointer( ATTR_NORMAL, 3, GL_FLOAT, GL_FALSE, stride,
(const void*) ( 3 * sizeof( float ) ) );
glEnableVertexAttribArray( ATTR_COLOR );
glVertexAttribPointer( ATTR_COLOR, 4, GL_FLOAT, GL_FALSE, stride,
(const void*) ( 6 * sizeof( float ) ) );
glDisableVertexAttribArray( ATTR_TEXCOORD );
glVertexAttrib2f( ATTR_TEXCOORD, 0.0f, 0.0f );
__real_glDrawArrays( mode, 0, count );
glBindBuffer( GL_ARRAY_BUFFER, (GLuint) prevArrayBuffer );
}
// Routed glDrawArrays over FFP client-array state (M3).
void drawClientArrays( GLenum mode, GLint first, GLsizei count )
{
(void) mode;
(void) first;
(void) count;
GL1_WARN_ONCE( "client-array glDrawArrays not implemented yet (M3) — draw dropped" );
}
// Routed glDrawElements over user VBO/IBO state (M5).
void drawClientElements( GLenum mode, GLsizei count, GLenum type, const GLvoid* indices )
{
(void) mode;
(void) count;
(void) type;
(void) indices;
GL1_WARN_ONCE( "client-array glDrawElements not implemented yet (M5) — draw dropped" );
}
} // namespace gl1

View file

@ -0,0 +1,623 @@
/*
* gl1_entry_ffp the 52 public FFP entry points the shim owns.
*
* These are the GL 1.x names absent from Emscripten's WebGL2 library (the
* exact surface wasm/stubs/gl_ffp_stub.c used to no-op). Signatures match
* Emscripten's <GL/gl.h> / the project's <GL/glu.h> (GLU lives in gl1_glu.cpp).
*
* Per the GL_COMPILE contract, listable calls funnel into the display-list
* recorder while recording instead of executing; client-array state calls,
* glGenLists/glIsList/glDeleteLists and glGet* always execute immediately.
* Listable calls the renderer never records (matrix, lighting, texenv...)
* warn once and drop while recording rather than silently corrupting state.
*/
#include "gl1_shim.h"
#include <glm/gtc/type_ptr.hpp>
using namespace gl1;
// Guard for listable entry points that the recorder intentionally does not
// support because the renderer never records them.
#define GL1_UNRECORDED( name ) \
if( dlistRecording() ) \
{ \
GL1_WARN_ONCE( name " inside glNewList is not supported — call dropped" ); \
return; \
}
extern "C"
{
// ---- Immediate mode ----------------------------------------------------
void glBegin( GLenum mode )
{
if( dlistRecording() )
{
dlistRecordBegin( mode );
return;
}
immBegin( mode );
}
void glEnd( void )
{
if( dlistRecording() )
{
dlistRecordEnd();
return;
}
immEnd();
}
void glVertex2f( GLfloat x, GLfloat y )
{
if( dlistRecording() )
{
dlistRecordVertex( x, y, 0.0f );
return;
}
immVertex( x, y, 0.0f );
}
void glVertex3f( GLfloat x, GLfloat y, GLfloat z )
{
if( dlistRecording() )
{
dlistRecordVertex( x, y, z );
return;
}
immVertex( x, y, z );
}
void glVertex3d( GLdouble x, GLdouble y, GLdouble z )
{
glVertex3f( (GLfloat) x, (GLfloat) y, (GLfloat) z );
}
void glNormal3f( GLfloat nx, GLfloat ny, GLfloat nz )
{
if( dlistRecording() )
{
dlistRecordNormal( nx, ny, nz );
return;
}
S().currentNormal = glm::vec3( nx, ny, nz );
}
void glColor3f( GLfloat r, GLfloat g, GLfloat b )
{
if( dlistRecording() )
{
dlistRecordColor( r, g, b, 1.0f );
return;
}
S().currentColor = glm::vec4( r, g, b, 1.0f );
}
void glColor4f( GLfloat r, GLfloat g, GLfloat b, GLfloat a )
{
if( dlistRecording() )
{
dlistRecordColor( r, g, b, a );
return;
}
S().currentColor = glm::vec4( r, g, b, a );
}
// ---- Display lists ------------------------------------------------------
GLuint glGenLists( GLsizei range )
{
return dlistGenLists( range );
}
void glNewList( GLuint list, GLenum mode )
{
dlistNewList( list, mode );
}
void glEndList( void )
{
dlistEndList();
}
void glCallList( GLuint list )
{
if( dlistRecording() )
{
GL1_WARN_ONCE( "nested glCallList inside glNewList is not supported — dropped" );
return;
}
dlistCallList( list );
}
void glDeleteLists( GLuint list, GLsizei range )
{
dlistDeleteLists( list, range );
}
GLboolean glIsList( GLuint list )
{
return dlistIsList( list );
}
// ---- Matrix stack --------------------------------------------------------
void glMatrixMode( GLenum mode )
{
GL1_UNRECORDED( "glMatrixMode" );
if( mode != GL_MODELVIEW && mode != GL_PROJECTION )
{
GL1_WARN_ONCE( "glMatrixMode: unsupported mode 0x%x (only MODELVIEW/PROJECTION)", mode );
return;
}
S().matrixMode = mode;
}
void glLoadIdentity( void )
{
GL1_UNRECORDED( "glLoadIdentity" );
matrixLoadIdentity();
}
void glLoadMatrixf( const GLfloat* m )
{
GL1_UNRECORDED( "glLoadMatrixf" );
matrixLoadf( m );
}
void glPushMatrix( void )
{
GL1_UNRECORDED( "glPushMatrix" );
matrixPush();
}
void glPopMatrix( void )
{
GL1_UNRECORDED( "glPopMatrix" );
matrixPop();
}
void glTranslatef( GLfloat x, GLfloat y, GLfloat z )
{
GL1_UNRECORDED( "glTranslatef" );
matrixTranslate( x, y, z );
}
void glRotatef( GLfloat angle, GLfloat x, GLfloat y, GLfloat z )
{
GL1_UNRECORDED( "glRotatef" );
matrixRotate( angle, x, y, z );
}
void glScalef( GLfloat x, GLfloat y, GLfloat z )
{
GL1_UNRECORDED( "glScalef" );
matrixScale( x, y, z );
}
void glScaled( GLdouble x, GLdouble y, GLdouble z )
{
GL1_UNRECORDED( "glScaled" );
matrixScale( (float) x, (float) y, (float) z );
}
// ---- Fixed-function lighting / material ----------------------------------
void glShadeModel( GLenum mode )
{
GL1_UNRECORDED( "glShadeModel" );
if( mode != GL_SMOOTH )
GL1_WARN_ONCE( "glShadeModel: only GL_SMOOTH is supported (got 0x%x)", mode );
S().shadeModel = mode;
}
void glLightfv( GLenum light, GLenum pname, const GLfloat* params )
{
GL1_UNRECORDED( "glLightfv" );
if( light < GL_LIGHT0 || light > GL_LIGHT7 )
return;
State& s = S();
Light& l = s.lights[light - GL_LIGHT0];
switch( pname )
{
case GL_AMBIENT:
l.ambient = glm::make_vec4( params );
break;
case GL_DIFFUSE:
l.diffuse = glm::make_vec4( params );
break;
case GL_SPECULAR:
l.specular = glm::make_vec4( params );
break;
case GL_POSITION:
// GL 1.x: the position is transformed by the modelview matrix current
// AT THIS CALL and stored in eye coordinates. init_lights() runs under
// an identity modelview (directional lights anchored in eye space);
// the per-frame headlight is set under the camera view matrix.
l.posEye = s.mv.back() * glm::make_vec4( params );
break;
default:
GL1_WARN_ONCE( "glLightfv: unsupported pname 0x%x", pname );
return;
}
s.lightingDirty = true;
}
void glLightModeli( GLenum pname, GLint param )
{
GL1_UNRECORDED( "glLightModeli" );
if( pname == GL_LIGHT_MODEL_TWO_SIDE )
{
S().twoSide = ( param != 0 );
S().lightingDirty = true;
}
else
{
GL1_WARN_ONCE( "glLightModeli: unsupported pname 0x%x", pname );
}
}
void glLightModelfv( GLenum pname, const GLfloat* params )
{
GL1_UNRECORDED( "glLightModelfv" );
if( pname == GL_LIGHT_MODEL_AMBIENT )
{
S().lightModelAmbient = glm::make_vec4( params );
S().lightingDirty = true;
}
else
{
GL1_WARN_ONCE( "glLightModelfv: unsupported pname 0x%x", pname );
}
}
void glMaterialf( GLenum face, GLenum pname, GLfloat param )
{
GL1_UNRECORDED( "glMaterialf" );
if( face != GL_FRONT_AND_BACK )
GL1_WARN_ONCE( "glMaterialf: only GL_FRONT_AND_BACK is supported (got 0x%x)", face );
if( pname == GL_SHININESS )
{
S().material.shininess = param;
S().lightingDirty = true;
}
else
{
GL1_WARN_ONCE( "glMaterialf: unsupported pname 0x%x", pname );
}
}
void glMaterialfv( GLenum face, GLenum pname, const GLfloat* params )
{
GL1_UNRECORDED( "glMaterialfv" );
if( face != GL_FRONT_AND_BACK )
GL1_WARN_ONCE( "glMaterialfv: only GL_FRONT_AND_BACK is supported (got 0x%x)", face );
State& s = S();
switch( pname )
{
case GL_AMBIENT:
s.material.ambient = glm::make_vec4( params );
break;
case GL_DIFFUSE:
s.material.diffuse = glm::make_vec4( params );
break;
case GL_AMBIENT_AND_DIFFUSE:
s.material.ambient = glm::make_vec4( params );
s.material.diffuse = glm::make_vec4( params );
break;
case GL_SPECULAR:
s.material.specular = glm::make_vec4( params );
break;
case GL_EMISSION:
s.material.emission = glm::make_vec4( params );
break;
case GL_SHININESS:
s.material.shininess = params[0];
break;
default:
GL1_WARN_ONCE( "glMaterialfv: unsupported pname 0x%x", pname );
return;
}
s.lightingDirty = true;
}
void glColorMaterial( GLenum face, GLenum mode )
{
GL1_UNRECORDED( "glColorMaterial" );
if( face != GL_FRONT_AND_BACK || mode != GL_AMBIENT_AND_DIFFUSE )
{
GL1_WARN_ONCE( "glColorMaterial: only (GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE) is "
"supported (got 0x%x, 0x%x)", face, mode );
}
S().colorMaterialMode = mode;
S().lightingDirty = true;
}
// ---- Client-state vertex arrays (always execute, even while recording) ----
static ClientArray* clientArraySlot( GLenum cap )
{
State& s = S();
switch( cap )
{
case GL_VERTEX_ARRAY: return &s.clientArrays[CA_VERTEX];
case GL_NORMAL_ARRAY: return &s.clientArrays[CA_NORMAL];
case GL_COLOR_ARRAY: return &s.clientArrays[CA_COLOR];
case GL_TEXTURE_COORD_ARRAY: return &s.clientArrays[CA_TEXCOORD];
default:
GL1_WARN_ONCE( "unsupported client-state cap 0x%x", cap );
return nullptr;
}
}
void glEnableClientState( GLenum cap )
{
if( ClientArray* a = clientArraySlot( cap ) )
a->enabled = true;
}
void glDisableClientState( GLenum cap )
{
if( ClientArray* a = clientArraySlot( cap ) )
a->enabled = false;
}
void glClientActiveTexture( GLenum texture )
{
if( texture != GL_TEXTURE0 )
GL1_WARN_ONCE( "glClientActiveTexture: only GL_TEXTURE0 is supported (got 0x%x)",
texture );
}
// GL1 semantics: gl*Pointer captures the GL_ARRAY_BUFFER binding current at
// the call (0 = client memory, nonzero = byte offset into that VBO).
static GLuint currentArrayBufferBinding()
{
GLint binding = 0;
glGetIntegerv( GL_ARRAY_BUFFER_BINDING, &binding );
return (GLuint) binding;
}
void glVertexPointer( GLint size, GLenum type, GLsizei stride, const GLvoid* ptr )
{
ClientArray& a = S().clientArrays[CA_VERTEX];
a.size = size;
a.type = type;
a.stride = stride;
a.pointer = ptr;
a.boundBuffer = currentArrayBufferBinding();
}
void glNormalPointer( GLenum type, GLsizei stride, const GLvoid* ptr )
{
ClientArray& a = S().clientArrays[CA_NORMAL];
a.size = 3;
a.type = type;
a.stride = stride;
a.pointer = ptr;
a.boundBuffer = currentArrayBufferBinding();
}
void glColorPointer( GLint size, GLenum type, GLsizei stride, const GLvoid* ptr )
{
ClientArray& a = S().clientArrays[CA_COLOR];
a.size = size;
a.type = type;
a.stride = stride;
a.pointer = ptr;
a.boundBuffer = currentArrayBufferBinding();
}
void glTexCoordPointer( GLint size, GLenum type, GLsizei stride, const GLvoid* ptr )
{
ClientArray& a = S().clientArrays[CA_TEXCOORD];
a.size = size;
a.type = type;
a.stride = stride;
a.pointer = ptr;
a.boundBuffer = currentArrayBufferBinding();
}
// ---- Fixed-function texture environment -----------------------------------
static void texEnvSet( GLenum pname, GLenum param )
{
State& s = S();
switch( pname )
{
case GL_TEXTURE_ENV_MODE: s.texEnvMode = param; break;
case GL_COMBINE_RGB: s.combineRGB = param; break;
case GL_COMBINE_ALPHA: s.combineAlpha = param; break;
case GL_SRC0_RGB: s.srcRGB[0] = param; break;
case GL_SRC1_RGB: s.srcRGB[1] = param; break;
case GL_SRC2_RGB: s.srcRGB[2] = param; break;
case GL_OPERAND0_RGB: s.operandRGB[0] = param; break;
case GL_OPERAND1_RGB: s.operandRGB[1] = param; break;
case GL_OPERAND2_RGB: s.operandRGB[2] = param; break;
case GL_SRC0_ALPHA: s.srcAlpha[0] = param; break;
case GL_SRC1_ALPHA: s.srcAlpha[1] = param; break;
case GL_SRC2_ALPHA: s.srcAlpha[2] = param; break;
case GL_OPERAND0_ALPHA: s.operandAlpha[0] = param; break;
case GL_OPERAND1_ALPHA: s.operandAlpha[1] = param; break;
case GL_OPERAND2_ALPHA: s.operandAlpha[2] = param; break;
default:
GL1_WARN_ONCE( "glTexEnv: unsupported pname 0x%x", pname );
return;
}
s.texEnvDirty = true;
}
void glTexEnvi( GLenum target, GLenum pname, GLint param )
{
GL1_UNRECORDED( "glTexEnvi" );
if( target != GL_TEXTURE_ENV )
{
GL1_WARN_ONCE( "glTexEnvi: unsupported target 0x%x", target );
return;
}
texEnvSet( pname, (GLenum) param );
}
void glTexEnvf( GLenum target, GLenum pname, GLfloat param )
{
GL1_UNRECORDED( "glTexEnvf" );
if( target != GL_TEXTURE_ENV )
{
GL1_WARN_ONCE( "glTexEnvf: unsupported target 0x%x", target );
return;
}
texEnvSet( pname, (GLenum) param );
}
void glTexEnvfv( GLenum target, GLenum pname, const GLfloat* params )
{
GL1_UNRECORDED( "glTexEnvfv" );
if( target != GL_TEXTURE_ENV )
{
GL1_WARN_ONCE( "glTexEnvfv: unsupported target 0x%x", target );
return;
}
if( pname == GL_TEXTURE_ENV_COLOR )
{
S().texEnvColor = glm::make_vec4( params );
S().texEnvDirty = true;
}
else
{
texEnvSet( pname, (GLenum) params[0] );
}
}
// ---- Misc fixed-function state absent from GLES3 ---------------------------
void glAlphaFunc( GLenum func, GLclampf ref )
{
if( dlistRecording() )
{
dlistRecordAlphaFunc( func, ref );
return;
}
S().alphaFunc = func;
S().alphaRef = ref;
S().miscDirty = true;
}
void glPolygonMode( GLenum face, GLenum mode )
{
GL1_UNRECORDED( "glPolygonMode" );
(void) face;
if( mode != GL_FILL )
GL1_WARN_ONCE( "glPolygonMode: only GL_FILL is supported (got 0x%x)", mode );
}
void glClearDepth( GLclampd depth )
{
GL1_UNRECORDED( "glClearDepth" );
glClearDepthf( (GLclampf) depth );
}
void glPointSize( GLfloat size )
{
GL1_UNRECORDED( "glPointSize" );
S().pointSize = size;
S().miscDirty = true;
}
} // extern "C"

View file

@ -0,0 +1,188 @@
/*
* gl1_entry_wrapped interceptors for the Emscripten-owned GL names the
* emulator must observe. The ONLY mechanism-aware TU: both link sites pass
* -Wl,--wrap=<sym> for every name in ../wrapped_symbols.txt, so references
* land on __wrap_* here and __real_* resolves back to the WebGL JS library.
* (Fallback if --wrap ever breaks: macro-remap in gal/opengl/kiglew.h only
* this file would change.)
*
* Draw routing: a glDrawArrays/glDrawElements call belongs to the FFP
* pipeline iff GL_VERTEX_ARRAY client state is enabled only GL1 code uses
* glEnableClientState, while the raytracer blit and the 2D WebGL GAL drive
* their own GLSL programs with glVertexAttribPointer and never touch client
* state. Their draws pass through untouched.
*/
#include "gl1_shim.h"
#include <glm/gtc/type_ptr.hpp>
#include <cstring>
using namespace gl1;
extern "C"
{
void __wrap_glEnable( GLenum cap )
{
if( dlistRecording() )
{
dlistRecordEnable( cap, true );
return;
}
if( bool* slot = ffpCapSlot( cap ) )
{
if( !*slot )
{
*slot = true;
onCapChanged( cap );
}
return;
}
__real_glEnable( cap );
}
void __wrap_glDisable( GLenum cap )
{
if( dlistRecording() )
{
dlistRecordEnable( cap, false );
return;
}
if( bool* slot = ffpCapSlot( cap ) )
{
if( *slot )
{
*slot = false;
onCapChanged( cap );
}
return;
}
__real_glDisable( cap );
}
GLboolean __wrap_glIsEnabled( GLenum cap )
{
// glGet-class queries execute even during display-list recording.
if( bool* slot = ffpCapSlot( cap ) )
return *slot ? GL_TRUE : GL_FALSE;
return __real_glIsEnabled( cap );
}
void __wrap_glGetFloatv( GLenum pname, GLfloat* params )
{
if( pname == GL_MODELVIEW_MATRIX )
{
std::memcpy( params, glm::value_ptr( S().mv.back() ), 16 * sizeof( GLfloat ) );
return;
}
if( pname == GL_PROJECTION_MATRIX )
{
std::memcpy( params, glm::value_ptr( S().proj.back() ), 16 * sizeof( GLfloat ) );
return;
}
__real_glGetFloatv( pname, params );
}
void __wrap_glDrawArrays( GLenum mode, GLint first, GLsizei count )
{
if( dlistRecording() )
{
dlistRecordDrawArrays( mode, first, count );
return;
}
if( !S().clientArrays[CA_VERTEX].enabled )
{
// Modern-GL consumer (raytracer blit, 2D GAL, the shim itself never
// reaches here) — pass through untouched.
__real_glDrawArrays( mode, first, count );
return;
}
drawClientArrays( mode, first, count );
}
void __wrap_glDrawElements( GLenum mode, GLsizei count, GLenum type, const GLvoid* indices )
{
if( dlistRecording() )
{
GL1_WARN_ONCE( "glDrawElements inside glNewList is not supported — dropped" );
return;
}
if( !S().clientArrays[CA_VERTEX].enabled )
{
__real_glDrawElements( mode, count, type, indices );
return;
}
drawClientElements( mode, count, type, indices );
}
void __wrap_glBindTexture( GLenum target, GLuint texture )
{
if( dlistRecording() )
{
dlistRecordBindTexture( target, texture );
return;
}
if( target == GL_TEXTURE_2D )
S().boundTexture2D = texture;
__real_glBindTexture( target, texture );
}
void __wrap_glBlendFunc( GLenum sfactor, GLenum dfactor )
{
if( dlistRecording() )
{
dlistRecordBlendFunc( sfactor, dfactor );
return;
}
__real_glBlendFunc( sfactor, dfactor );
}
void __wrap_glLineWidth( GLfloat width )
{
if( dlistRecording() )
{
dlistRecordLineWidth( width );
return;
}
S().lineWidth = width;
__real_glLineWidth( width );
}
void __wrap_glHint( GLenum target, GLenum mode )
{
// Only the two WebGL2-legal hints pass through; the FFP hints
// (LINE_SMOOTH_HINT, PERSPECTIVE_CORRECTION_HINT...) would raise
// INVALID_ENUM and are swallowed.
if( target == GL_GENERATE_MIPMAP_HINT || target == GL_FRAGMENT_SHADER_DERIVATIVE_HINT )
__real_glHint( target, mode );
}
} // extern "C"

106
wasm/gl1/src/gl1_glu.cpp Normal file
View file

@ -0,0 +1,106 @@
/*
* 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.
*
* Note: 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"
extern "C"
{
// Real (heap-allocated) quadric state; only the modes the renderer uses are
// honored — GLU_FILL draw style, GLU_SMOOTH normals, GLU_OUTSIDE orientation.
struct GLUquadric
{
GLenum drawStyle;
GLenum normals;
};
GLUquadric* gluNewQuadric( void )
{
GLUquadric* q = new GLUquadric;
q->drawStyle = GLU_FILL;
q->normals = GLU_SMOOTH;
return q;
}
void gluDeleteQuadric( GLUquadric* q )
{
delete q;
}
void gluQuadricDrawStyle( GLUquadric* q, GLenum style )
{
if( !q )
return;
if( style != GLU_FILL )
GL1_WARN_ONCE( "gluQuadricDrawStyle: only GLU_FILL is supported (got 0x%x)", style );
q->drawStyle = style;
}
void gluQuadricNormals( GLUquadric* q, GLenum normals )
{
if( !q )
return;
if( normals != GLU_SMOOTH )
GL1_WARN_ONCE( "gluQuadricNormals: only GLU_SMOOTH is supported (got 0x%x)", normals );
q->normals = 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" );
}
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" );
}
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" );
}
void gluPerspective( double fovy, double aspect, double zNear, double zFar )
{
gl1::matrixPerspective( fovy, aspect, zNear, zFar );
}
} // extern "C"

View file

@ -0,0 +1,152 @@
/*
* gl1_immediate glBegin/glEnd vertex accumulation and primitive conversion.
*
* Also the internal emission path for GLU quadrics and display-list replay:
* every piece of shim geometry funnels through immBegin/immVertex/immEnd so
* primitive conversion and the draw pipeline live in exactly one place.
*
* WebGL2 has no GL_QUADS / GL_QUAD_STRIP / GL_LINE_LOOP / GL_POLYGON:
* QUADS -> GL_TRIANGLES, each quad (0,1,2)(0,2,3)
* QUAD_STRIP -> GL_TRIANGLE_STRIP (same vertex order covers the same area)
* LINE_LOOP -> GL_LINE_STRIP with the first vertex appended
* POLYGON -> unused by the renderer (assert-logged, drawn as a fan)
*/
#include "gl1_shim.h"
namespace gl1
{
static std::vector<ImmVertex> s_verts;
static GLenum s_mode = 0;
static bool s_active = false;
bool immActive()
{
return s_active;
}
void immBegin( GLenum mode )
{
if( s_active )
{
GL1_WARN_ONCE( "glBegin inside glBegin/glEnd — call ignored" );
return;
}
s_mode = mode;
s_active = true;
s_verts.clear();
if( mode == GL_POLYGON )
GL1_WARN_ONCE( "GL_POLYGON is not exercised by the renderer; drawing as a triangle fan" );
}
void immVertex( float x, float y, float z )
{
if( !s_active )
{
GL1_WARN_ONCE( "glVertex outside glBegin/glEnd — ignored" );
return;
}
const State& s = S();
ImmVertex v;
v.px = x;
v.py = y;
v.pz = z;
v.nx = s.currentNormal.x;
v.ny = s.currentNormal.y;
v.nz = s.currentNormal.z;
v.r = s.currentColor.r;
v.g = s.currentColor.g;
v.b = s.currentColor.b;
v.a = s.currentColor.a;
v.u = 0.0f;
v.v = 0.0f;
s_verts.push_back( v );
}
// Expands GL_QUADS into GL_TRIANGLES in place ((0,1,2)(0,2,3) per quad —
// preserves winding; any trailing partial quad is dropped, as in GL).
static void expandQuads( std::vector<ImmVertex>& verts )
{
const size_t quadCount = verts.size() / 4;
std::vector<ImmVertex> tris;
tris.reserve( quadCount * 6 );
for( size_t q = 0; q < quadCount; ++q )
{
const ImmVertex* v = &verts[q * 4];
tris.push_back( v[0] );
tris.push_back( v[1] );
tris.push_back( v[2] );
tris.push_back( v[0] );
tris.push_back( v[2] );
tris.push_back( v[3] );
}
verts.swap( tris );
}
void immEnd()
{
if( !s_active )
{
GL1_WARN_ONCE( "glEnd without glBegin — ignored" );
return;
}
s_active = false;
if( s_verts.empty() )
return;
GLenum drawMode = s_mode;
switch( s_mode )
{
case GL_QUADS:
expandQuads( s_verts );
drawMode = GL_TRIANGLES;
break;
case GL_QUAD_STRIP:
drawMode = GL_TRIANGLE_STRIP;
break;
case GL_LINE_LOOP:
s_verts.push_back( s_verts.front() );
drawMode = GL_LINE_STRIP;
break;
case GL_POLYGON:
drawMode = GL_TRIANGLE_FAN;
break;
case GL_POINTS:
case GL_LINES:
case GL_LINE_STRIP:
case GL_TRIANGLES:
case GL_TRIANGLE_STRIP:
case GL_TRIANGLE_FAN:
break;
default:
GL1_WARN_ONCE( "glBegin: unsupported primitive 0x%x", s_mode );
return;
}
drawImmVertices( drawMode, s_verts.data(), (GLsizei) s_verts.size() );
}
} // namespace gl1

106
wasm/gl1/src/gl1_matrix.cpp Normal file
View file

@ -0,0 +1,106 @@
/*
* gl1_matrix GL_MODELVIEW / GL_PROJECTION matrix stacks.
*
* Only the operations the KiCad 3D renderer uses exist (no glOrtho/glFrustum/
* glMultMatrix projection and view arrive prebuilt via glLoadMatrixf, and
* gluPerspective covers the gizmo). glGetFloatv(GL_MODELVIEW_MATRIX/
* GL_PROJECTION_MATRIX) readback is served from these stacks by the
* __wrap_glGetFloatv interceptor.
*/
#include "gl1_shim.h"
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
namespace gl1
{
// GL 1.5 minimums; KiCad never goes deeper than a few levels.
static constexpr size_t MV_STACK_MAX = 64;
static constexpr size_t PROJ_STACK_MAX = 8;
void matrixLoadIdentity()
{
S().currentTop() = glm::mat4( 1.0f );
S().matricesDirty = true;
}
void matrixLoadf( const GLfloat* m )
{
S().currentTop() = glm::make_mat4( m );
S().matricesDirty = true;
}
void matrixPush()
{
State& s = S();
auto& stack = s.currentStack();
const size_t maxDepth = ( s.matrixMode == GL_PROJECTION ) ? PROJ_STACK_MAX : MV_STACK_MAX;
if( stack.size() >= maxDepth )
{
GL1_WARN_ONCE( "glPushMatrix: stack overflow (mode 0x%x)", s.matrixMode );
return;
}
stack.push_back( stack.back() );
}
void matrixPop()
{
State& s = S();
auto& stack = s.currentStack();
if( stack.size() <= 1 )
{
GL1_WARN_ONCE( "glPopMatrix: stack underflow (mode 0x%x)", s.matrixMode );
return;
}
stack.pop_back();
s.matricesDirty = true;
}
void matrixTranslate( float x, float y, float z )
{
glm::mat4& top = S().currentTop();
top = glm::translate( top, glm::vec3( x, y, z ) );
S().matricesDirty = true;
}
void matrixRotate( float angleDeg, float x, float y, float z )
{
glm::mat4& top = S().currentTop();
top = glm::rotate( top, glm::radians( angleDeg ), glm::vec3( x, y, z ) );
S().matricesDirty = true;
}
void matrixScale( float x, float y, float z )
{
glm::mat4& top = S().currentTop();
top = glm::scale( top, glm::vec3( x, y, z ) );
S().matricesDirty = true;
}
void matrixPerspective( double fovyDeg, double aspect, double zNear, double zFar )
{
// gluPerspective multiplies onto the current matrix (KiCad calls it right
// after glLoadIdentity on GL_PROJECTION, but multiply is the GLU semantic).
glm::mat4 p = glm::perspective( glm::radians( (float) fovyDeg ), (float) aspect,
(float) zNear, (float) zFar );
glm::mat4& top = S().currentTop();
top = top * p;
S().matricesDirty = true;
}
} // namespace gl1

View file

@ -0,0 +1,591 @@
/*
* gl1_shaders the FFP uber-program (ES 3.00) and uniform synchronization.
*
* One program, uniform-flag branches (all dynamically uniform cheap), no
* variant cache: mid-frame FFP toggles (two-side in DrawCulled, texture/alpha
* flips inside display lists) become uniform stores instead of program
* switches.
*
* Lighting is computed PER-VERTEX (Gouraud) on purpose: the native goldens
* come from a fixed-function pipeline that evaluates lighting at vertices and
* interpolates colors per-fragment lighting would visibly mismatch specular
* highlights on the suite's coarse meshes.
*
* The GL 1.5 conventions implemented here (they are load-bearing for parity):
* - light GL_POSITION is pre-transformed to EYE space at glLightfv time
* - halfway vector H = normalize(L + (0,0,1)) (GL_LIGHT_MODEL_LOCAL_VIEWER
* defaults to FALSE)
* - no attenuation (KiCad leaves kc=1, kl=kq=0), no spotlights
* - single-color model: specular folds into the one color before texturing
* - GL_COLOR_MATERIAL(AMBIENT_AND_DIFFUSE): the per-vertex color replaces
* material ambient+diffuse; alpha comes from the diffuse alpha
* - texture COMBINE args resolve against the tracked+default state (GL 1.5
* initial values for the SRC/OPERAND slots KiCad never sets); PREVIOUS is
* the primary color at texture unit 0
*/
#include "gl1_shim.h"
#include <glm/gtc/matrix_inverse.hpp>
#include <glm/gtc/type_ptr.hpp>
namespace gl1
{
static const char* VS_SOURCE = R"(#version 300 es
precision highp float;
layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec4 aColor;
layout(location = 3) in vec2 aTexCoord;
uniform mat4 uModelView;
uniform mat4 uProjection;
uniform mat3 uNormalMatrix;
uniform float uPointSize;
uniform bool uLighting;
uniform bool uTwoSide;
uniform bool uColorMaterial;
struct FfpLight
{
bool enabled;
vec4 posEye;
vec4 ambient;
vec4 diffuse;
vec4 specular;
};
uniform FfpLight uLights[3];
uniform vec4 uLightModelAmbient;
uniform vec4 uMatAmbient;
uniform vec4 uMatDiffuse;
uniform vec4 uMatSpecular;
uniform vec4 uMatEmission;
uniform float uShininess;
out vec4 vFrontColor;
out vec4 vBackColor;
out vec2 vTexCoord;
vec4 lit( vec3 N, vec3 eyePos, vec4 matAmb, vec4 matDiff )
{
vec3 c = uMatEmission.rgb + matAmb.rgb * uLightModelAmbient.rgb;
for( int i = 0; i < 3; ++i )
{
if( !uLights[i].enabled )
continue;
vec3 L = ( uLights[i].posEye.w == 0.0 )
? normalize( uLights[i].posEye.xyz )
: normalize( uLights[i].posEye.xyz - eyePos );
float ndotl = max( dot( N, L ), 0.0 );
float spec = 0.0;
if( ndotl > 0.0 )
{
vec3 H = normalize( L + vec3( 0.0, 0.0, 1.0 ) );
float ndoth = max( dot( N, H ), 0.0 );
spec = ( uShininess > 0.0 ) ? pow( ndoth, uShininess ) : 1.0;
}
c += matAmb.rgb * uLights[i].ambient.rgb
+ ndotl * matDiff.rgb * uLights[i].diffuse.rgb
+ spec * uMatSpecular.rgb * uLights[i].specular.rgb;
}
return vec4( clamp( c, 0.0, 1.0 ), clamp( matDiff.a, 0.0, 1.0 ) );
}
void main()
{
vec4 eye = uModelView * vec4( aPosition, 1.0 );
vTexCoord = aTexCoord;
gl_PointSize = uPointSize;
gl_Position = uProjection * eye;
if( uLighting )
{
vec3 N = normalize( uNormalMatrix * aNormal );
vec4 matAmb = uColorMaterial ? aColor : uMatAmbient;
vec4 matDiff = uColorMaterial ? aColor : uMatDiffuse;
vFrontColor = lit( N, eye.xyz, matAmb, matDiff );
vBackColor = uTwoSide ? lit( -N, eye.xyz, matAmb, matDiff ) : vFrontColor;
}
else
{
vFrontColor = clamp( aColor, 0.0, 1.0 );
vBackColor = vFrontColor;
}
}
)";
static const char* FS_SOURCE = R"(#version 300 es
precision highp float;
in vec4 vFrontColor;
in vec4 vBackColor;
in vec2 vTexCoord;
uniform bool uTexEnabled;
uniform int uTexEnvMode; // 0=MODULATE, 1=COMBINE
uniform sampler2D uTex0;
uniform vec4 uTexEnvColor;
// COMBINE argument selectors: src 0=TEXTURE 1=CONSTANT 2=PRIMARY 3=PREVIOUS;
// RGB op 0=SRC_COLOR 1=ONE_MINUS_SRC_COLOR 2=SRC_ALPHA 3=ONE_MINUS_SRC_ALPHA;
// alpha op 0=SRC_ALPHA 1=ONE_MINUS_SRC_ALPHA;
// func 0=MODULATE 1=INTERPOLATE 2=REPLACE.
uniform int uCombineFuncRGB;
uniform int uCombineFuncA;
uniform ivec3 uCombineSrcRGB;
uniform ivec3 uCombineOpRGB;
uniform ivec3 uCombineSrcA;
uniform ivec3 uCombineOpA;
uniform bool uAlphaTest;
uniform int uAlphaFunc; // GL func - GL_NEVER, i.e. 0..7
uniform float uAlphaRef;
out vec4 fragColor;
vec4 combineSource( int src, vec4 tex, vec4 primary )
{
if( src == 0 )
return tex;
if( src == 1 )
return uTexEnvColor;
return primary; // PRIMARY, and PREVIOUS == primary at unit 0
}
vec3 combineArgRGB( int src, int op, vec4 tex, vec4 primary )
{
vec4 s = combineSource( src, tex, primary );
if( op == 0 )
return s.rgb;
if( op == 1 )
return vec3( 1.0 ) - s.rgb;
if( op == 2 )
return vec3( s.a );
return vec3( 1.0 - s.a );
}
float combineArgA( int src, int op, vec4 tex, vec4 primary )
{
vec4 s = combineSource( src, tex, primary );
return ( op == 0 ) ? s.a : 1.0 - s.a;
}
void main()
{
vec4 c = gl_FrontFacing ? vFrontColor : vBackColor;
if( uTexEnabled )
{
vec4 t = texture( uTex0, vTexCoord );
if( uTexEnvMode == 0 )
{
c = c * t;
}
else
{
vec3 a0 = combineArgRGB( uCombineSrcRGB.x, uCombineOpRGB.x, t, c );
vec3 a1 = combineArgRGB( uCombineSrcRGB.y, uCombineOpRGB.y, t, c );
vec3 a2 = combineArgRGB( uCombineSrcRGB.z, uCombineOpRGB.z, t, c );
vec3 rgb;
if( uCombineFuncRGB == 0 )
rgb = a0 * a1;
else if( uCombineFuncRGB == 1 )
rgb = a0 * a2 + a1 * ( vec3( 1.0 ) - a2 );
else
rgb = a0;
float b0 = combineArgA( uCombineSrcA.x, uCombineOpA.x, t, c );
float b1 = combineArgA( uCombineSrcA.y, uCombineOpA.y, t, c );
float b2 = combineArgA( uCombineSrcA.z, uCombineOpA.z, t, c );
float alpha;
if( uCombineFuncA == 0 )
alpha = b0 * b1;
else if( uCombineFuncA == 1 )
alpha = b0 * b2 + b1 * ( 1.0 - b2 );
else
alpha = b0;
c = clamp( vec4( rgb, alpha ), 0.0, 1.0 );
}
}
if( uAlphaTest )
{
bool pass;
if( uAlphaFunc == 0 ) pass = false; // NEVER
else if( uAlphaFunc == 1 ) pass = c.a < uAlphaRef; // LESS
else if( uAlphaFunc == 2 ) pass = c.a == uAlphaRef; // EQUAL
else if( uAlphaFunc == 3 ) pass = c.a <= uAlphaRef; // LEQUAL
else if( uAlphaFunc == 4 ) pass = c.a > uAlphaRef; // GREATER
else if( uAlphaFunc == 5 ) pass = c.a != uAlphaRef; // NOTEQUAL
else if( uAlphaFunc == 6 ) pass = c.a >= uAlphaRef; // GEQUAL
else pass = true; // ALWAYS
if( !pass )
discard;
}
fragColor = c;
}
)";
struct LightLocs
{
GLint enabled = -1;
GLint posEye = -1;
GLint ambient = -1;
GLint diffuse = -1;
GLint specular = -1;
};
struct ProgramLocs
{
GLint modelView = -1;
GLint projection = -1;
GLint normalMatrix = -1;
GLint pointSize = -1;
GLint lighting = -1;
GLint twoSide = -1;
GLint colorMaterial = -1;
LightLocs lights[3];
GLint lightModelAmbient = -1;
GLint matAmbient = -1;
GLint matDiffuse = -1;
GLint matSpecular = -1;
GLint matEmission = -1;
GLint shininess = -1;
GLint texEnabled = -1;
GLint texEnvMode = -1;
GLint tex0 = -1;
GLint texEnvColor = -1;
GLint combineFuncRGB = -1;
GLint combineFuncA = -1;
GLint combineSrcRGB = -1;
GLint combineOpRGB = -1;
GLint combineSrcA = -1;
GLint combineOpA = -1;
GLint alphaTest = -1;
GLint alphaFunc = -1;
GLint alphaRef = -1;
};
static GLuint s_program = 0;
static bool s_buildFailed = false;
static ProgramLocs s_locs;
static GLuint compileShader( GLenum type, const char* source )
{
GLuint shader = glCreateShader( type );
glShaderSource( shader, 1, &source, nullptr );
glCompileShader( shader );
GLint ok = GL_FALSE;
glGetShaderiv( shader, GL_COMPILE_STATUS, &ok );
if( !ok )
{
char log[1024] = {};
glGetShaderInfoLog( shader, sizeof( log ) - 1, nullptr, log );
std::fprintf( stderr, "[gl1] %s shader compile failed:\n%s\n",
type == GL_VERTEX_SHADER ? "vertex" : "fragment", log );
glDeleteShader( shader );
return 0;
}
return shader;
}
static bool buildProgram()
{
GLuint vs = compileShader( GL_VERTEX_SHADER, VS_SOURCE );
GLuint fs = compileShader( GL_FRAGMENT_SHADER, FS_SOURCE );
if( !vs || !fs )
return false;
GLuint prog = glCreateProgram();
glAttachShader( prog, vs );
glAttachShader( prog, fs );
glLinkProgram( prog );
glDeleteShader( vs );
glDeleteShader( fs );
GLint ok = GL_FALSE;
glGetProgramiv( prog, GL_LINK_STATUS, &ok );
if( !ok )
{
char log[1024] = {};
glGetProgramInfoLog( prog, sizeof( log ) - 1, nullptr, log );
std::fprintf( stderr, "[gl1] program link failed:\n%s\n", log );
glDeleteProgram( prog );
return false;
}
s_program = prog;
ProgramLocs& l = s_locs;
l.modelView = glGetUniformLocation( prog, "uModelView" );
l.projection = glGetUniformLocation( prog, "uProjection" );
l.normalMatrix = glGetUniformLocation( prog, "uNormalMatrix" );
l.pointSize = glGetUniformLocation( prog, "uPointSize" );
l.lighting = glGetUniformLocation( prog, "uLighting" );
l.twoSide = glGetUniformLocation( prog, "uTwoSide" );
l.colorMaterial = glGetUniformLocation( prog, "uColorMaterial" );
for( int i = 0; i < 3; ++i )
{
char name[48];
std::snprintf( name, sizeof( name ), "uLights[%d].enabled", i );
l.lights[i].enabled = glGetUniformLocation( prog, name );
std::snprintf( name, sizeof( name ), "uLights[%d].posEye", i );
l.lights[i].posEye = glGetUniformLocation( prog, name );
std::snprintf( name, sizeof( name ), "uLights[%d].ambient", i );
l.lights[i].ambient = glGetUniformLocation( prog, name );
std::snprintf( name, sizeof( name ), "uLights[%d].diffuse", i );
l.lights[i].diffuse = glGetUniformLocation( prog, name );
std::snprintf( name, sizeof( name ), "uLights[%d].specular", i );
l.lights[i].specular = glGetUniformLocation( prog, name );
}
l.lightModelAmbient = glGetUniformLocation( prog, "uLightModelAmbient" );
l.matAmbient = glGetUniformLocation( prog, "uMatAmbient" );
l.matDiffuse = glGetUniformLocation( prog, "uMatDiffuse" );
l.matSpecular = glGetUniformLocation( prog, "uMatSpecular" );
l.matEmission = glGetUniformLocation( prog, "uMatEmission" );
l.shininess = glGetUniformLocation( prog, "uShininess" );
l.texEnabled = glGetUniformLocation( prog, "uTexEnabled" );
l.texEnvMode = glGetUniformLocation( prog, "uTexEnvMode" );
l.tex0 = glGetUniformLocation( prog, "uTex0" );
l.texEnvColor = glGetUniformLocation( prog, "uTexEnvColor" );
l.combineFuncRGB = glGetUniformLocation( prog, "uCombineFuncRGB" );
l.combineFuncA = glGetUniformLocation( prog, "uCombineFuncA" );
l.combineSrcRGB = glGetUniformLocation( prog, "uCombineSrcRGB" );
l.combineOpRGB = glGetUniformLocation( prog, "uCombineOpRGB" );
l.combineSrcA = glGetUniformLocation( prog, "uCombineSrcA" );
l.combineOpA = glGetUniformLocation( prog, "uCombineOpA" );
l.alphaTest = glGetUniformLocation( prog, "uAlphaTest" );
l.alphaFunc = glGetUniformLocation( prog, "uAlphaFunc" );
l.alphaRef = glGetUniformLocation( prog, "uAlphaRef" );
// The FFP surface only ever uses texture unit 0 (asserted at
// glClientActiveTexture); bind the sampler once. glUseProgram is not a
// wrapped symbol (draw routing keys on client-array state instead), so
// this is the real WebGL entry point.
glUseProgram( prog );
glUniform1i( l.tex0, 0 );
return true;
}
GLuint programId()
{
return s_program;
}
static int encodeCombineSrc( GLenum src )
{
switch( src )
{
case GL_TEXTURE: return 0;
case GL_CONSTANT: return 1;
case GL_PRIMARY_COLOR: return 2;
case GL_PREVIOUS: return 3;
default:
GL1_WARN_ONCE( "unsupported COMBINE source 0x%x", src );
return 3;
}
}
static int encodeCombineOpRGB( GLenum op )
{
switch( op )
{
case GL_SRC_COLOR: return 0;
case GL_ONE_MINUS_SRC_COLOR: return 1;
case GL_SRC_ALPHA: return 2;
case GL_ONE_MINUS_SRC_ALPHA: return 3;
default:
GL1_WARN_ONCE( "unsupported COMBINE RGB operand 0x%x", op );
return 0;
}
}
static int encodeCombineOpA( GLenum op )
{
switch( op )
{
case GL_SRC_ALPHA: return 0;
case GL_ONE_MINUS_SRC_ALPHA: return 1;
default:
GL1_WARN_ONCE( "unsupported COMBINE alpha operand 0x%x", op );
return 0;
}
}
static int encodeCombineFunc( GLenum func )
{
switch( func )
{
case GL_MODULATE: return 0;
case GL_INTERPOLATE: return 1;
case GL_REPLACE: return 2;
default:
GL1_WARN_ONCE( "unsupported COMBINE function 0x%x", func );
return 0;
}
}
bool programSync()
{
if( s_buildFailed )
return false;
if( !s_program )
{
if( !buildProgram() )
{
s_buildFailed = true;
GL1_WARN_ONCE( "FFP program build failed — shim draws disabled" );
return false;
}
// First build: force a full upload.
State& s0 = S();
s0.matricesDirty = true;
s0.lightingDirty = true;
s0.texEnvDirty = true;
s0.miscDirty = true;
}
State& s = S();
const ProgramLocs& l = s_locs;
glUseProgram( s_program );
if( s.matricesDirty )
{
s.matricesDirty = false;
const glm::mat4& mv = s.mv.back();
glUniformMatrix4fv( l.modelView, 1, GL_FALSE, glm::value_ptr( mv ) );
glUniformMatrix4fv( l.projection, 1, GL_FALSE, glm::value_ptr( s.proj.back() ) );
const glm::mat3 nm = glm::inverseTranspose( glm::mat3( mv ) );
glUniformMatrix3fv( l.normalMatrix, 1, GL_FALSE, glm::value_ptr( nm ) );
}
if( s.lightingDirty )
{
s.lightingDirty = false;
glUniform1i( l.lighting, s.lighting ? 1 : 0 );
glUniform1i( l.twoSide, s.twoSide ? 1 : 0 );
glUniform1i( l.colorMaterial, s.colorMaterial ? 1 : 0 );
for( int i = 0; i < 3; ++i )
{
const Light& lt = s.lights[i];
glUniform1i( l.lights[i].enabled, s.lightEnabled[i] ? 1 : 0 );
glUniform4fv( l.lights[i].posEye, 1, glm::value_ptr( lt.posEye ) );
glUniform4fv( l.lights[i].ambient, 1, glm::value_ptr( lt.ambient ) );
glUniform4fv( l.lights[i].diffuse, 1, glm::value_ptr( lt.diffuse ) );
glUniform4fv( l.lights[i].specular, 1, glm::value_ptr( lt.specular ) );
}
for( int i = 3; i < 8; ++i )
{
if( s.lightEnabled[i] )
GL1_WARN_ONCE( "GL_LIGHT%d enabled but the shim models only lights 0-2", i );
}
glUniform4fv( l.lightModelAmbient, 1, glm::value_ptr( s.lightModelAmbient ) );
glUniform4fv( l.matAmbient, 1, glm::value_ptr( s.material.ambient ) );
glUniform4fv( l.matDiffuse, 1, glm::value_ptr( s.material.diffuse ) );
glUniform4fv( l.matSpecular, 1, glm::value_ptr( s.material.specular ) );
glUniform4fv( l.matEmission, 1, glm::value_ptr( s.material.emission ) );
glUniform1f( l.shininess, s.material.shininess );
}
if( s.texEnvDirty )
{
s.texEnvDirty = false;
int mode = 0;
if( s.texEnvMode == GL_MODULATE )
mode = 0;
else if( s.texEnvMode == GL_COMBINE )
mode = 1;
else
GL1_WARN_ONCE( "unsupported GL_TEXTURE_ENV_MODE 0x%x (treated as MODULATE)",
s.texEnvMode );
glUniform1i( l.texEnvMode, mode );
glUniform4fv( l.texEnvColor, 1, glm::value_ptr( s.texEnvColor ) );
glUniform1i( l.combineFuncRGB, encodeCombineFunc( s.combineRGB ) );
glUniform1i( l.combineFuncA, encodeCombineFunc( s.combineAlpha ) );
glUniform3i( l.combineSrcRGB, encodeCombineSrc( s.srcRGB[0] ),
encodeCombineSrc( s.srcRGB[1] ), encodeCombineSrc( s.srcRGB[2] ) );
glUniform3i( l.combineOpRGB, encodeCombineOpRGB( s.operandRGB[0] ),
encodeCombineOpRGB( s.operandRGB[1] ), encodeCombineOpRGB( s.operandRGB[2] ) );
glUniform3i( l.combineSrcA, encodeCombineSrc( s.srcAlpha[0] ),
encodeCombineSrc( s.srcAlpha[1] ), encodeCombineSrc( s.srcAlpha[2] ) );
glUniform3i( l.combineOpA, encodeCombineOpA( s.operandAlpha[0] ),
encodeCombineOpA( s.operandAlpha[1] ), encodeCombineOpA( s.operandAlpha[2] ) );
}
if( s.miscDirty )
{
s.miscDirty = false;
glUniform1i( l.texEnabled, s.texture2D ? 1 : 0 );
glUniform1i( l.alphaTest, s.alphaTest ? 1 : 0 );
glUniform1i( l.alphaFunc, (int) ( s.alphaFunc - GL_NEVER ) );
glUniform1f( l.alphaRef, s.alphaRef );
glUniform1f( l.pointSize, s.pointSize );
}
return true;
}
} // namespace gl1

View file

@ -0,0 +1,66 @@
/*
* gl1_state the shim's GL 1.x state singleton and capability routing.
*/
#include "gl1_shim.h"
namespace gl1
{
State& S()
{
static State s;
return s;
}
bool* ffpCapSlot( GLenum cap )
{
State& s = S();
switch( cap )
{
case GL_LIGHTING: return &s.lighting;
case GL_COLOR_MATERIAL: return &s.colorMaterial;
case GL_TEXTURE_2D: return &s.texture2D;
case GL_NORMALIZE: return &s.normalizeNormals;
case GL_ALPHA_TEST: return &s.alphaTest;
// Tracked-but-inert: WebGL2 has no equivalent caps and would raise
// INVALID_ENUM; the suite's goldens are single-sample/aliased anyway.
case GL_LINE_SMOOTH: return &s.lineSmooth;
case GL_POINT_SMOOTH: return &s.pointSmooth;
case GL_MULTISAMPLE: return &s.multisample;
default:
if( cap >= GL_LIGHT0 && cap <= GL_LIGHT7 )
return &s.lightEnabled[cap - GL_LIGHT0];
return nullptr; // WebGL-native cap: forward
}
}
void onCapChanged( GLenum cap )
{
State& s = S();
switch( cap )
{
case GL_LIGHTING:
case GL_COLOR_MATERIAL:
s.lightingDirty = true;
s.miscDirty = true;
break;
case GL_TEXTURE_2D:
case GL_ALPHA_TEST:
s.miscDirty = true;
break;
default:
if( cap >= GL_LIGHT0 && cap <= GL_LIGHT7 )
s.lightingDirty = true;
break;
}
}
} // namespace gl1

View file

@ -0,0 +1,10 @@
glEnable
glDisable
glIsEnabled
glDrawArrays
glDrawElements
glGetFloatv
glBindTexture
glBlendFunc
glLineWidth
glHint