diff --git a/tests/3d-regression/wasm/Makefile b/tests/3d-regression/wasm/Makefile index 3199816..87c0957 100644 --- a/tests/3d-regression/wasm/Makefile +++ b/tests/3d-regression/wasm/Makefile @@ -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 diff --git a/wasm/gl1/README.md b/wasm/gl1/README.md new file mode 100644 index 0000000..cf7c399 --- /dev/null +++ b/wasm/gl1/README.md @@ -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=` 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 +``` diff --git a/wasm/gl1/include/gl1_shim.h b/wasm/gl1/include/gl1_shim.h new file mode 100644 index 0000000..338546b --- /dev/null +++ b/wasm/gl1/include/gl1_shim.h @@ -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 +#include // wasm/stubs/GL/glu.h via -I wasm/stubs + +#include + +#include +#include +#include + +// 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 mv; // modelview stack, top = back() + std::vector 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& 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 diff --git a/wasm/gl1/sources.txt b/wasm/gl1/sources.txt new file mode 100644 index 0000000..4b2dd20 --- /dev/null +++ b/wasm/gl1/sources.txt @@ -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 diff --git a/wasm/gl1/src/gl1_dlist.cpp b/wasm/gl1/src/gl1_dlist.cpp new file mode 100644 index 0000000..efdb6c7 --- /dev/null +++ b/wasm/gl1/src/gl1_dlist.cpp @@ -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 + +namespace gl1 +{ + +struct DList +{ + // M3: recorded command stream + baked static VBO. + bool empty = true; +}; + +static std::map 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 diff --git a/wasm/gl1/src/gl1_draw.cpp b/wasm/gl1/src/gl1_draw.cpp new file mode 100644 index 0000000..2fd6fe3 --- /dev/null +++ b/wasm/gl1/src/gl1_draw.cpp @@ -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 diff --git a/wasm/gl1/src/gl1_entry_ffp.cpp b/wasm/gl1/src/gl1_entry_ffp.cpp new file mode 100644 index 0000000..69ea909 --- /dev/null +++ b/wasm/gl1/src/gl1_entry_ffp.cpp @@ -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 / the project's (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 + +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" diff --git a/wasm/gl1/src/gl1_entry_wrapped.cpp b/wasm/gl1/src/gl1_entry_wrapped.cpp new file mode 100644 index 0000000..4df492b --- /dev/null +++ b/wasm/gl1/src/gl1_entry_wrapped.cpp @@ -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= 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 + +#include + +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" diff --git a/wasm/gl1/src/gl1_glu.cpp b/wasm/gl1/src/gl1_glu.cpp new file mode 100644 index 0000000..07271c1 --- /dev/null +++ b/wasm/gl1/src/gl1_glu.cpp @@ -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" diff --git a/wasm/gl1/src/gl1_immediate.cpp b/wasm/gl1/src/gl1_immediate.cpp new file mode 100644 index 0000000..386e02d --- /dev/null +++ b/wasm/gl1/src/gl1_immediate.cpp @@ -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 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& verts ) +{ + const size_t quadCount = verts.size() / 4; + std::vector 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 diff --git a/wasm/gl1/src/gl1_matrix.cpp b/wasm/gl1/src/gl1_matrix.cpp new file mode 100644 index 0000000..01c01de --- /dev/null +++ b/wasm/gl1/src/gl1_matrix.cpp @@ -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 +#include + +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 diff --git a/wasm/gl1/src/gl1_shaders.cpp b/wasm/gl1/src/gl1_shaders.cpp new file mode 100644 index 0000000..53ace7a --- /dev/null +++ b/wasm/gl1/src/gl1_shaders.cpp @@ -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 +#include + +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 diff --git a/wasm/gl1/src/gl1_state.cpp b/wasm/gl1/src/gl1_state.cpp new file mode 100644 index 0000000..43dab39 --- /dev/null +++ b/wasm/gl1/src/gl1_state.cpp @@ -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 diff --git a/wasm/gl1/wrapped_symbols.txt b/wasm/gl1/wrapped_symbols.txt new file mode 100644 index 0000000..1541700 --- /dev/null +++ b/wasm/gl1/wrapped_symbols.txt @@ -0,0 +1,10 @@ +glEnable +glDisable +glIsEnabled +glDrawArrays +glDrawElements +glGetFloatv +glBindTexture +glBlendFunc +glLineWidth +glHint