diff --git a/scripts/kicad/build-kicad-target.sh b/scripts/kicad/build-kicad-target.sh index 0c6ac07..75fd3ad 100755 --- a/scripts/kicad/build-kicad-target.sh +++ b/scripts/kicad/build-kicad-target.sh @@ -388,14 +388,18 @@ if [ "${APP_NAME}" = "sym_convert" ]; then fi # 3D viewer (experimental): opt in with BUILD_3D_VIEWER=ON. Default OFF keeps -# existing/CI builds unchanged and the 3D stubs in place. When ON, the 3D -# viewer's fixed-function OpenGL renderer needs Emscripten's legacy GL emulation -# plus our immediate-mode shim (color-per-vertex + double-precision helpers). +# existing/CI builds unchanged and the 3D stubs in place. When ON, the 3D viewer +# renders with the GL-free CPU raytracer (RENDER_3D_RAYTRACE_RAM) blitted to the +# canvas through a plain WebGL2 textured quad — no -sLEGACY_GL_EMULATION. KiCad's +# fixed-function OpenGL renderer is still compiled (shared files reference it) but +# never executed on WASM, so its FFP/GLU entry points are satisfied at link time +# by no-op stubs (gl_ffp_stub.c). See docs/features/fork-cleanup/10-3d-viewer.md. BUILD_3D_VIEWER="${BUILD_3D_VIEWER:-OFF}" GL3D_LINK_FLAGS="" if [ "${BUILD_3D_VIEWER}" = "ON" ]; then log_info "3D viewer ENABLED for WASM (BUILD_3D_VIEWER=ON)" - GL3D_LINK_FLAGS="-sLEGACY_GL_EMULATION --js-library ${WASM_LAYER}/shims/gl_immediate_shim.js" + emcc -c -I"${STUBS_DIR}" "${STUBS_DIR}/gl_ffp_stub.c" -o "${STUBS_BUILD}/gl_ffp_stub.o" + GL3D_LINK_FLAGS="${STUBS_BUILD}/gl_ffp_stub.o" fi emcmake cmake "${KICAD_DIR}" \ diff --git a/tests/kicad/3d-viewer.spec.ts b/tests/kicad/3d-viewer.spec.ts index b274f5e..0454340 100644 --- a/tests/kicad/3d-viewer.spec.ts +++ b/tests/kicad/3d-viewer.spec.ts @@ -24,8 +24,10 @@ import { waitForPcbnew } from './utils/pcbnew-ready'; const KICAD_VERSION_DIR = '9.99'; const PROJECT_DIR_MEMFS = `/home/kicad/documents/kicad/${KICAD_VERSION_DIR}/projects`; -// Small, no-external-libs RF board — fast to load, no missing-libs dialog. -const DEMO = { name: 'microwave', dir: 'microwave', stem: 'microwave' } as const; +// pic_programmer frames correctly in the default 3D camera (the microwave demo +// has a known board-bounding-box scale bug that projects it off-screen — a +// separate follow-up). Loads cleanly in this harness (see 2D load tests). +const DEMO = { name: 'pic_programmer', dir: 'pic_programmer', stem: 'pic_programmer' } as const; async function loadBoard(page: Page, testLogger: { consoleLogs: string[]; errors: string[] }): Promise { const pcbFilename = `${DEMO.stem}.kicad_pcb`; @@ -115,17 +117,48 @@ test.describe('3D viewer from pcbnew', () => { console.log(`[TEST] glcanvas count after opening 3D viewer: ${glAfter}`); expect(glAfter, 'a new WebGL canvas should appear for the 3D viewer').toBeGreaterThan(glBefore); - // The 3D reload runs through asyncify; give it time to build & render the - // board, then screenshot for visual validation (per CLAUDE.md). - await page.waitForTimeout(4000); + // The 3D reload + raytrace run through asyncify; give them time to build + // the scene and render a few progressive passes. + await page.waitForTimeout(5000); + await page.screenshot({ path: `test-results/3d-viewer-${DEMO.name}.png`, scale: 'device' }); - // The newest GL canvas is the 3D viewer's — screenshot it on its own too. - const newCanvas = page.locator('canvas[id^="glcanvas-"]').last(); - if (await newCanvas.isVisible().catch(() => false)) { - await newCanvas.screenshot({ path: `test-results/3d-viewer-${DEMO.name}-canvas.png` }) - .catch((e: unknown) => console.log(`[TEST] canvas screenshot failed: ${e}`)); - } + // Read the 3D viewer canvas (the newest glcanvas) directly from its backing + // store: copy it onto a 2D canvas with drawImage and sample pixels. This is + // reliable thanks to preserveDrawingBuffer=true, whereas Playwright's CDP + // screenshot of a WebGL canvas on swiftshader comes back blank. We save the + // copy as a PNG (the real visual artifact) and assert the board rendered by + // checking the canvas is not a single uniform colour. + const render = await page.evaluate(() => { + const list = document.querySelectorAll('canvas[id^="glcanvas-"]'); + const el = list[list.length - 1] as HTMLCanvasElement; + const tmp = document.createElement('canvas'); + tmp.width = el.width; + tmp.height = el.height; + const ctx = tmp.getContext('2d')!; + ctx.drawImage(el, 0, 0); + + const colors = new Set(); + for (let i = 0; i < 16; i++) { + for (let j = 0; j < 16; j++) { + const d = ctx.getImageData(Math.floor(el.width * i / 16), + Math.floor(el.height * j / 16), 1, 1).data; + colors.add(`${d[0]},${d[1]},${d[2]}`); + } + } + return { id: el.id, w: el.width, h: el.height, distinctColors: colors.size, + dataUrl: tmp.toDataURL('image/png') }; + }); + console.log(`[TEST] 3D canvas ${render.id} ${render.w}x${render.h}, distinct colours: ${render.distinctColors}`); + + const b64 = render.dataUrl.replace(/^data:image\/png;base64,/, ''); + require('fs').writeFileSync(`test-results/3d-viewer-${DEMO.name}-render.png`, + Buffer.from(b64, 'base64')); + + // A blank/uniform canvas yields ~1 colour; the rendered board has many. + expect(render.distinctColors, + 'the 3D viewer canvas should render the board (many colours), not a blank fill') + .toBeGreaterThan(8); // ── Console-clean gates (same signatures load-pcb.spec.ts guards). ── const allLines = [...testLogger.consoleLogs, ...testLogger.errors]; diff --git a/wasm/shims/gl_immediate_shim.js b/wasm/shims/gl_immediate_shim.js deleted file mode 100644 index 11970be..0000000 --- a/wasm/shims/gl_immediate_shim.js +++ /dev/null @@ -1,418 +0,0 @@ -/** - * gl_immediate_shim.js - * - * Custom legacy-OpenGL shims for the KiCad WASM 3D viewer, layered on top of - * Emscripten's -sLEGACY_GL_EMULATION. Fills the gaps that emulation leaves: - * - * 1. Immediate mode (glBegin/glEnd): inject the current color before each - * vertex (Emscripten requires a color per vertex) + double-precision - * glVertex / glColor overloads. - * - * 2. Display lists (glGenLists/glNewList/glCallList/...): NOT implemented by - * Emscripten at all. KiCad's 3D renderer compiles every board layer into a - * display list (client vertex arrays + glDrawArrays, or immediate mode for - * the grid) and replays it each frame. We record the GL calls between - * glNewList/glEndList and replay them on glCallList. For glDrawArrays the - * client array data may be freed after compile, so we snapshot it into a - * real (Emscripten-tracked) VBO at record time and replay from the VBO. - * - * 3. Fixed-function lighting entry points Emscripten lacks (glColorMaterial, - * glLightModeli) — stubbed so the link resolves; lighting falls back to - * flat/vertex color, which still yields a recognizable board. - * - * Usage: emcc ... --js-library=wasm/shims/gl_immediate_shim.js - */ - -addToLibrary({ - $GLImmediateShim__deps: [ - '$GL', '$GLImmediate', - 'glBegin', 'glEnd', 'glVertex2f', 'glVertex3f', 'glColor3f', 'glColor4f', - 'glNormal3f', 'glDrawArrays', 'glClear', - 'glEnable', 'glDisable', 'glBlendFunc', 'glBindTexture', 'glLineWidth', - 'glDepthMask', - 'glLightfv', 'glMaterialfv', 'glLightModelfv', 'glLightModelf', - 'glGenBuffers', 'glBindBuffer', 'glBufferData', - 'glEnableClientState', 'glDisableClientState', - 'glVertexPointer', 'glNormalPointer', 'glColorPointer', 'glTexCoordPointer', - 'malloc', 'free', - ], - $GLImmediateShim__postset: 'GLImmediateShim.init();', - $GLImmediateShim: { - // ---- GL constants we use directly ---- - GL_ARRAY_BUFFER: 0x8892, - GL_STATIC_DRAW: 0x88E4, - GL_FLOAT: 0x1406, - GL_COMPILE: 0x1300, - // Client-state enums, indexed by GLImmediate attribute slot. - CLIENT_STATE: [0x8074 /*VERTEX*/, 0x8075 /*NORMAL*/, 0x8076 /*COLOR*/, 0x8078 /*TEXCOORD*/], - - // ---- immediate-mode color state ---- - currentColor: null, - inBeginEnd: false, - colorCalledSinceLastVertex: false, - - // ---- display-list state ---- - lists: null, // id -> array of replay closures - listBuffers: null, // id -> array of malloc'd HEAP ptrs to free on delete - nextListId: 1, - compiling: 0, // id currently being compiled, or 0 - tmpIdPtr: 0, // scratch HEAP slot for glGenBuffers output - lastImmediateContext: null, // GL context GLImmediate's FFP programs were built for - - // ---- temporary diagnostics (remove once rendering is confirmed) ---- - dbg: { clears: 0, newLists: 0, snaps: 0, calls: 0, replayDraws: 0 }, - dbgLog: function(cat, msg) { - var d = GLImmediateShim.dbg; - if (d[cat] === undefined) d[cat] = 0; - d[cat]++; - if (d[cat] <= 30) console.log('[DL] ' + msg); - else if (d[cat] === 31) console.log('[DL] ...(' + cat + ' further logs suppressed)'); - }, - - origFns: {}, - initialized: false, - - init: function() { - if (GLImmediateShim.initialized) return; - if (typeof GLImmediate === 'undefined') return; // retried via postset chain - - console.log('[GLImmediateShim] Initializing legacy-GL + display-list shims'); - GLImmediateShim.currentColor = new Float32Array([1.0, 1.0, 1.0, 1.0]); - GLImmediateShim.lists = {}; - GLImmediateShim.listBuffers = {}; - - var S = GLImmediateShim; - S.origFns = { - glBegin: _glBegin, glEnd: _glEnd, - glVertex2f: _glVertex2f, glVertex3f: _glVertex3f, - glColor3f: _glColor3f, glColor4f: _glColor4f, - glNormal3f: _glNormal3f, glDrawArrays: _glDrawArrays, glClear: _glClear, - glEnable: _glEnable, glDisable: _glDisable, glBlendFunc: _glBlendFunc, - glBindTexture: _glBindTexture, glLineWidth: _glLineWidth, glDepthMask: _glDepthMask, - }; - - // Immediate-mode wrappers (color injection + display-list recording). - _glBegin = S.shimBegin; - _glEnd = S.shimEnd; - _glVertex2f = S.shimVertex2f; - _glVertex3f = S.shimVertex3f; - _glColor3f = S.shimColor3f; - _glColor4f = S.shimColor4f; - _glNormal3f = S.shimNormal3f; - _glDrawArrays = S.shimDrawArrays; - _glClear = S.shimClear; - - // State-setting calls that may appear inside a display list. - _glEnable = S.recWrap('glEnable'); - _glDisable = S.recWrap('glDisable'); - _glBlendFunc = S.recWrap('glBlendFunc'); - _glBindTexture = S.recWrap('glBindTexture'); - _glLineWidth = S.recWrap('glLineWidth'); - _glDepthMask = S.recWrap('glDepthMask'); - - // Emscripten's glemu THROWS on fixed-function light/material pnames it - // doesn't implement (e.g. glLightfv GL_SPECULAR/GL_POSITION). KiCad's - // lighting setup runs every frame right after the clear, so an unguarded - // throw aborts the whole 3D render before the board is drawn. Wrap these - // so the supported pnames still take effect and the rest are skipped. - S.origFns.glLightfv = _glLightfv; - _glLightfv = function(light, pname, params) { - try { S.origFns.glLightfv(light, pname, params); } - catch (e) { S.dbgLog('lightSkip', 'glLightfv skipped pname=0x' + pname.toString(16)); } - }; - S.origFns.glMaterialfv = _glMaterialfv; - _glMaterialfv = function(face, pname, params) { - try { S.origFns.glMaterialfv(face, pname, params); } - catch (e) { S.dbgLog('matSkip', 'glMaterialfv skipped pname=0x' + pname.toString(16)); } - }; - S.origFns.glLightModelfv = _glLightModelfv; - _glLightModelfv = function(pname, params) { - try { S.origFns.glLightModelfv(pname, params); } catch (e) {} - }; - S.origFns.glLightModelf = _glLightModelf; - _glLightModelf = function(pname, param) { - try { S.origFns.glLightModelf(pname, param); } catch (e) {} - }; - - GLImmediateShim.initialized = true; - console.log('[GLImmediateShim] Initialized successfully'); - }, - - record: function(closure) { GLImmediateShim.lists[GLImmediateShim.compiling].push(closure); }, - - freeListBuffers: function(list) { - var bufs = GLImmediateShim.listBuffers[list]; - if (bufs) { for (var i = 0; i < bufs.length; i++) _free(bufs[i]); bufs.length = 0; } - }, - - // Emscripten generates GLImmediate's per-context temp vertex/quad buffer pool - // (GL.currentContext.tempVertexBuffers1/2) only once, in GLEmulation.init(), - // for whichever context is current then — i.e. the FIRST WebGL context (the - // 2D board canvas). The 3D viewer owns a SECOND context that never gets the - // pool, so its first immediate-mode/client-array draw throws - // ("tempVertexBuffers1 is undefined"). Lazily generate the pool for any - // context that lacks it. (quads=true also sets up the quad index buffer the - // background gradient's GL_QUADS needs.) - ensureTempBuffers: function() { - if (typeof GL === 'undefined' || !GL.currentContext || typeof GLImmediate === 'undefined') return; - var ctx = GL.currentContext; - - // GLImmediate (legacy GL emulation) is single-context: GLEmulation.init() - // sets up its per-context temp buffers + FFP shader programs only for the - // FIRST WebGL context (KiCad's 2D board GAL). The 3D viewer owns a SECOND - // context that GLImmediate never initialised — we detect it as the one - // lacking the temp vertex buffer pool and patch GLImmediate to work on it. - if (ctx.tempVertexBuffers1 === undefined && typeof GL.generateTempBuffers === 'function') { - GL.generateTempBuffers(true, ctx); // per-context temp vertex/quad buffers - ctx.__glsForeign = true; // mark: not GLImmediate's home context - GLImmediateShim.dbgLog('tempBuf', 'initialised GLImmediate for the 3D viewer context'); - } - - if (ctx.__glsForeign) { - // createRenderer() reuses the bound user program (GL.currProgram) instead - // of building its own FFP program when one is set; the 2D GAL leaves its - // (other-context) program bound, which is "not linked" here → FFP draws - // silently produce nothing. Zero it every time we render on this context - // so the FFP builds + uses a program that belongs to THIS context. - GL.currProgram = 0; - - // The FFP renderer/program cache is global and holds the 2D context's - // program; drop it once so it regenerates for this context. - if (GLImmediateShim.lastImmediateContext !== ctx) { - GLImmediateShim.lastImmediateContext = ctx; - GLImmediate.currentRenderer = null; - GLImmediate.fixedFunctionProgram = 0; - if (GLImmediate.MapTreeLib && GLImmediate.rendererCache) { - GLImmediate.rendererCache = GLImmediate.MapTreeLib.create(); - } - // Force the regenerated FFP program to receive the current matrices / - // light state (a fresh program starts with default uniforms). - GLImmediate.matricesModified = true; - GLImmediate.lightingModified = true; - GLImmediateShim.dbgLog('ctxReset', 'reset FFP program cache for the 3D viewer context'); - } - } - }, - - // Generic recorder for state calls: when compiling, defer the original call - // to replay time; otherwise pass through immediately. The non-compiling path - // (the overwhelmingly common case — these wrap hot calls like glEnable that - // the 2D GAL makes constantly) forwards arguments without allocating. - recWrap: function(name) { - return function() { - var orig = GLImmediateShim.origFns[name]; - if (!GLImmediateShim.compiling) return orig.apply(null, arguments); - var args = Array.prototype.slice.call(arguments); - GLImmediateShim.record(function() { orig.apply(null, args); }); - }; - }, - - // ---- immediate mode ---- - shimBegin: function(mode) { - if (GLImmediateShim.compiling) { GLImmediateShim.record(function() { _glBegin(mode); }); return; } - GLImmediateShim.ensureTempBuffers(); - GLImmediateShim.inBeginEnd = true; - GLImmediateShim.colorCalledSinceLastVertex = false; - GLImmediateShim.origFns.glBegin(mode); - }, - shimEnd: function() { - if (GLImmediateShim.compiling) { GLImmediateShim.record(function() { _glEnd(); }); return; } - GLImmediateShim.inBeginEnd = false; - GLImmediateShim.origFns.glEnd(); - }, - shimColor3f: function(r, g, b) { - if (GLImmediateShim.compiling) { GLImmediateShim.record(function() { _glColor3f(r, g, b); }); return; } - var c = GLImmediateShim.currentColor; c[0] = r; c[1] = g; c[2] = b; c[3] = 1.0; - if (GLImmediateShim.inBeginEnd) GLImmediateShim.colorCalledSinceLastVertex = true; - GLImmediateShim.origFns.glColor3f(r, g, b); - }, - shimColor4f: function(r, g, b, a) { - if (GLImmediateShim.compiling) { GLImmediateShim.record(function() { _glColor4f(r, g, b, a); }); return; } - var c = GLImmediateShim.currentColor; c[0] = r; c[1] = g; c[2] = b; c[3] = a; - if (GLImmediateShim.inBeginEnd) GLImmediateShim.colorCalledSinceLastVertex = true; - GLImmediateShim.origFns.glColor4f(r, g, b, a); - }, - injectColor: function() { - if (!GLImmediateShim.inBeginEnd) return; - if (!GLImmediateShim.colorCalledSinceLastVertex) { - var c = GLImmediateShim.currentColor; - GLImmediateShim.origFns.glColor4f(c[0], c[1], c[2], c[3]); - } - GLImmediateShim.colorCalledSinceLastVertex = false; - }, - shimVertex2f: function(x, y) { - if (GLImmediateShim.compiling) { GLImmediateShim.record(function() { _glVertex2f(x, y); }); return; } - GLImmediateShim.injectColor(); GLImmediateShim.origFns.glVertex2f(x, y); - }, - shimVertex3f: function(x, y, z) { - if (GLImmediateShim.compiling) { GLImmediateShim.record(function() { _glVertex3f(x, y, z); }); return; } - GLImmediateShim.injectColor(); GLImmediateShim.origFns.glVertex3f(x, y, z); - }, - shimNormal3f: function(x, y, z) { - if (GLImmediateShim.compiling) { GLImmediateShim.record(function() { _glNormal3f(x, y, z); }); return; } - GLImmediateShim.origFns.glNormal3f(x, y, z); - }, - shimClear: function(mask) { - if (GLImmediateShim.compiling) { GLImmediateShim.record(function() { _glClear(mask); }); return; } - GLImmediateShim.ensureTempBuffers(); - var vp = '?', bw = '?', bh = '?'; - if (typeof GLctx !== 'undefined' && GLctx) { - try { var v = GLctx.getParameter(GLctx.VIEWPORT); vp = v[0] + ',' + v[1] + ',' + v[2] + ',' + v[3]; } catch (e) {} - bw = GLctx.drawingBufferWidth; bh = GLctx.drawingBufferHeight; - } - GLImmediateShim.dbgLog('clears', 'glClear mask=0x' + mask.toString(16) + - ' viewport=[' + vp + '] drawingBuffer=' + bw + 'x' + bh + - ' listsAlive=' + Object.keys(GLImmediateShim.lists).length); - GLImmediateShim.origFns.glClear(mask); - }, - - // ---- glDrawArrays: snapshot client arrays into a VBO when compiling ---- - shimDrawArrays: function(mode, first, count) { - if (!GLImmediateShim.compiling) { GLImmediateShim.origFns.glDrawArrays(mode, first, count); return; } - - var S = GLImmediateShim; - - // Snapshot each enabled client array into a PERSISTENT HEAP buffer (the - // source container may be freed once the list is compiled). At replay we - // re-point the client arrays at these buffers with NO VBO bound, so glemu - // takes its native client-array path: it reads each separate array from - // HEAP and builds its own interleaved per-context temp buffer. (Binding our - // own VBOs can't work — glemu binds a single GL_ARRAY_BUFFER, so separate - // vertex/normal/texcoord VBOs collide.) - var captured = []; - for (var slot = 0; slot < 4; slot++) { - if (!GLImmediate.enabledClientAttributes[slot]) continue; - var a = GLImmediate.clientAttributes[slot]; - if (!a) continue; - var size = a.size; - var stride = a.stride || size * 4; // GL_FLOAT arrays; stride 0 = tight - var heapPtr = _malloc(count * size * 4); // persists for the list's lifetime - var dst = heapPtr >> 2, src = (a.pointer + first * stride) >> 2, strideF = stride >> 2; - for (var i = 0; i < count; i++) - for (var j = 0; j < size; j++) - HEAPF32[dst + i * size + j] = HEAPF32[src + i * strideF + j]; - captured.push({ slot: slot, size: size, heapPtr: heapPtr }); - } - if (S.listBuffers[S.compiling]) - for (var b = 0; b < captured.length; b++) S.listBuffers[S.compiling].push(captured[b].heapPtr); - - S.dbgLog('snaps', 'snapshot list=' + S.compiling + ' mode=0x' + mode.toString(16) + - ' count=' + count + ' capturedSlots=[' + captured.map(function(c){return c.slot;}).join(',') + ']'); - - S.record(function() { - S.ensureTempBuffers(); - _glBindBuffer(S.GL_ARRAY_BUFFER, 0); // client arrays from HEAP, not a VBO - for (var k = 0; k < captured.length; k++) { - var c = captured[k]; - _glEnableClientState(S.CLIENT_STATE[c.slot]); - switch (c.slot) { - case 0: _glVertexPointer(c.size, S.GL_FLOAT, 0, c.heapPtr); break; - case 1: _glNormalPointer(S.GL_FLOAT, 0, c.heapPtr); break; - case 2: _glColorPointer(c.size, S.GL_FLOAT, 0, c.heapPtr); break; - case 3: _glTexCoordPointer(c.size, S.GL_FLOAT, 0, c.heapPtr); break; - } - } - S.origFns.glDrawArrays(mode, 0, count); - if (S.dbg.replayDraws < 8) { - S.dbg.replayDraws++; - var e = (typeof GLctx !== 'undefined' && GLctx) ? GLctx.getError() : -1; - var v0 = captured.length ? (captured[0].heapPtr >> 2) : 0; - var fv = captured.length - ? '(' + HEAPF32[v0].toFixed(3) + ',' + HEAPF32[v0 + 1].toFixed(3) + ',' + HEAPF32[v0 + 2].toFixed(3) + ')' - : 'none'; - var mv = GLImmediate.matrix && GLImmediate.matrix[0], pr = GLImmediate.matrix && GLImmediate.matrix[1]; - var fmt = function(m) { return m ? '[' + m[0].toFixed(2) + ',' + m[5].toFixed(2) + ',' + m[10].toFixed(2) + ',' + m[12].toFixed(2) + ',' + m[13].toFixed(2) + ',' + m[14].toFixed(2) + ']' : '?'; }; - console.log('[DL] replay draw count=' + count + ' slots=[' + - captured.map(function(c){return c.slot;}).join(',') + '] glError=0x' + e.toString(16) + - ' v0=' + fv + ' mv' + fmt(mv) + ' proj' + fmt(pr)); - } - for (var m = 0; m < captured.length; m++) - _glDisableClientState(S.CLIENT_STATE[captured[m].slot]); - }); - }, - }, - - // ================================================================== - // Display lists - // ================================================================== - glGenLists__deps: ['$GLImmediateShim'], - glGenLists: function(range) { - if (range <= 0) return 0; - var base = GLImmediateShim.nextListId; - GLImmediateShim.nextListId += range; - for (var i = 0; i < range; i++) { - GLImmediateShim.lists[base + i] = []; - GLImmediateShim.listBuffers[base + i] = []; - } - return base; - }, - glIsList__deps: ['$GLImmediateShim'], - glIsList: function(list) { - return (GLImmediateShim.lists && GLImmediateShim.lists[list]) ? 1 : 0; - }, - glNewList__deps: ['$GLImmediateShim'], - glNewList: function(list, mode) { - GLImmediateShim.freeListBuffers(list); // rebuilding: free the prior data - GLImmediateShim.lists[list] = []; - GLImmediateShim.listBuffers[list] = []; - GLImmediateShim.compiling = list; - }, - glEndList__deps: ['$GLImmediateShim'], - glEndList: function() { GLImmediateShim.compiling = 0; }, - glCallList__deps: ['$GLImmediateShim'], - glCallList: function(list) { - var cmds = GLImmediateShim.lists[list]; - if (!cmds) return; - GLImmediateShim.dbgLog('calls', 'callList ' + list + ' cmds=' + cmds.length); - for (var i = 0; i < cmds.length; i++) cmds[i](); - }, - glDeleteLists__deps: ['$GLImmediateShim'], - glDeleteLists: function(list, range) { - for (var i = 0; i < range; i++) { - GLImmediateShim.freeListBuffers(list + i); - delete GLImmediateShim.lists[list + i]; - delete GLImmediateShim.listBuffers[list + i]; - } - }, - - // ================================================================== - // Fixed-function lighting entry points Emscripten lacks (no-op stubs). - // Lighting falls back to flat/vertex color — still a recognizable board. - // ================================================================== - glColorMaterial: function(face, mode) {}, - glLightModeli: function(pname, param) {}, - glMaterialf: function(face, pname, param) {}, - - // ================================================================== - // GLU quadrics (sphere/cylinder/disk) — used for the navigation gizmo and - // rounded via/segment ends. Emscripten ships no GLU quadric runtime, so stub - // them: a non-null quadric handle plus no-op draws. The board's layer - // geometry comes from the display lists, not these, so it still renders. - // ================================================================== - gluNewQuadric: function() { return 1; }, - gluDeleteQuadric: function(q) {}, - gluQuadricDrawStyle: function(q, style) {}, - gluQuadricNormals: function(q, normals) {}, - gluCylinder: function(q, base, top, height, slices, stacks) {}, - gluDisk: function(q, inner, outer, slices, loops) {}, - gluSphere: function(q, radius, slices, stacks) {}, - - // ================================================================== - // Double-precision vertex/color overloads (missing in Emscripten) - // ================================================================== - glVertex2d__deps: ['glVertex2f'], - glVertex2d: function(x, y) { _glVertex2f(x, y); }, - glVertex3d__deps: ['glVertex3f'], - glVertex3d: function(x, y, z) { _glVertex3f(x, y, z); }, - glVertex4d__deps: ['glVertex4f'], - glVertex4d: function(x, y, z, w) { _glVertex4f(x, y, z, w); }, - glColor3d__deps: ['glColor3f'], - glColor3d: function(r, g, b) { _glColor3f(r, g, b); }, - glColor4d__deps: ['glColor4f'], - glColor4d: function(r, g, b, a) { _glColor4f(r, g, b, a); }, -}); - -// Ensure GLImmediateShim is included in the build -if (typeof extraLibraryFuncs !== 'undefined') { - extraLibraryFuncs.push('$GLImmediateShim'); -} diff --git a/wasm/stubs/gl_ffp_stub.c b/wasm/stubs/gl_ffp_stub.c new file mode 100644 index 0000000..4a92338 --- /dev/null +++ b/wasm/stubs/gl_ffp_stub.c @@ -0,0 +1,104 @@ +/* + * Fixed-function-pipeline (FFP) no-op stubs for the WASM 3D-viewer build. + * + * KiCad's 3D "OpenGL" renderer (under 3d-viewer/3d_rendering/opengl) is pure + * GL 1.x fixed-function: immediate mode, display lists, the matrix stack, + * FFP lighting/material, client-state vertex arrays and GLU quadrics. None of + * those entry points exist in WebGL2 / GLES3. + * + * On WASM we do NOT render through that renderer — the 3D viewer draws with the + * GL-free CPU raytracer (RENDER_3D_RAYTRACE_RAM) and blits the result with a + * plain WebGL2 textured quad (see EDA_3D_CANVAS). The fixed-function renderer is + * still compiled (it is referenced from several shared files: eda_3d_canvas, + * appearance_controls_3D, panel_preview_3d_model, eda_3d_controller), so its FFP + * calls must *link*. These no-op definitions satisfy the linker without dragging + * in Emscripten's -sLEGACY_GL_EMULATION (which is module-global, ~200 KB, and was + * rejected for this project — see docs/features/fork-cleanup/10-3d-viewer.md). + * The functions are never executed at runtime; they only need to exist. + * + * Signatures are taken verbatim from Emscripten's (and the project's + * GLU stub ) so the compiler validates them against the same + * declarations the renderer is built against. + * + * Only the symbols absent from core WebGL2/GLES3 are stubbed here. Modern entry + * points (glDrawArrays, glGenBuffers, glUseProgram, glTexImage2D, …) are provided + * by Emscripten's WebGL library and must NOT be redefined. + */ + +#include +#include /* resolved to wasm/stubs/GL/glu.h via -I${STUBS_DIR} */ + +/* ---- Immediate mode --------------------------------------------------- */ +void glBegin( GLenum mode ) { (void) mode; } +void glEnd( void ) {} +void glVertex2f( GLfloat x, GLfloat y ) { (void) x; (void) y; } +void glVertex3f( GLfloat x, GLfloat y, GLfloat z ) { (void) x; (void) y; (void) z; } +void glVertex3d( GLdouble x, GLdouble y, GLdouble z ) { (void) x; (void) y; (void) z; } +void glNormal3f( GLfloat nx, GLfloat ny, GLfloat nz ) { (void) nx; (void) ny; (void) nz; } +void glColor3f( GLfloat r, GLfloat g, GLfloat b ) { (void) r; (void) g; (void) b; } +void glColor4f( GLfloat r, GLfloat g, GLfloat b, GLfloat a ) { (void) r; (void) g; (void) b; (void) a; } + +/* ---- Display lists ---------------------------------------------------- */ +GLuint glGenLists( GLsizei range ) { (void) range; return 1; } +void glNewList( GLuint list, GLenum mode ) { (void) list; (void) mode; } +void glEndList( void ) {} +void glCallList( GLuint list ) { (void) list; } +void glDeleteLists( GLuint list, GLsizei range ) { (void) list; (void) range; } +GLboolean glIsList( GLuint list ) { (void) list; return GL_FALSE; } + +/* ---- Matrix stack ----------------------------------------------------- */ +void glMatrixMode( GLenum mode ) { (void) mode; } +void glLoadIdentity( void ) {} +void glLoadMatrixf( const GLfloat* m ) { (void) m; } +void glPushMatrix( void ) {} +void glPopMatrix( void ) {} +void glTranslatef( GLfloat x, GLfloat y, GLfloat z ) { (void) x; (void) y; (void) z; } +void glRotatef( GLfloat angle, GLfloat x, GLfloat y, GLfloat z ) { (void) angle; (void) x; (void) y; (void) z; } +void glScalef( GLfloat x, GLfloat y, GLfloat z ) { (void) x; (void) y; (void) z; } +void glScaled( GLdouble x, GLdouble y, GLdouble z ) { (void) x; (void) y; (void) z; } + +/* ---- Fixed-function lighting / material ------------------------------- */ +void glShadeModel( GLenum mode ) { (void) mode; } +void glLightfv( GLenum light, GLenum pname, const GLfloat* params ) { (void) light; (void) pname; (void) params; } +void glLightModeli( GLenum pname, GLint param ) { (void) pname; (void) param; } +void glLightModelfv( GLenum pname, const GLfloat* params ) { (void) pname; (void) params; } +void glMaterialf( GLenum face, GLenum pname, GLfloat param ) { (void) face; (void) pname; (void) param; } +void glMaterialfv( GLenum face, GLenum pname, const GLfloat* params ) { (void) face; (void) pname; (void) params; } +void glColorMaterial( GLenum face, GLenum mode ) { (void) face; (void) mode; } + +/* ---- Client-state vertex arrays --------------------------------------- */ +void glEnableClientState( GLenum cap ) { (void) cap; } +void glDisableClientState( GLenum cap ) { (void) cap; } +void glClientActiveTexture( GLenum texture ) { (void) texture; } +void glVertexPointer( GLint size, GLenum type, GLsizei stride, const GLvoid* ptr ) { (void) size; (void) type; (void) stride; (void) ptr; } +void glNormalPointer( GLenum type, GLsizei stride, const GLvoid* ptr ) { (void) type; (void) stride; (void) ptr; } +void glColorPointer( GLint size, GLenum type, GLsizei stride, const GLvoid* ptr ) { (void) size; (void) type; (void) stride; (void) ptr; } +void glTexCoordPointer( GLint size, GLenum type, GLsizei stride, const GLvoid* ptr ) { (void) size; (void) type; (void) stride; (void) ptr; } + +/* ---- Fixed-function texture environment ------------------------------- */ +void glTexEnvf( GLenum target, GLenum pname, GLfloat param ) { (void) target; (void) pname; (void) param; } +void glTexEnvi( GLenum target, GLenum pname, GLint param ) { (void) target; (void) pname; (void) param; } +void glTexEnvfv( GLenum target, GLenum pname, const GLfloat* params ) { (void) target; (void) pname; (void) params; } + +/* ---- Misc fixed-function state absent from GLES3 ---------------------- */ +void glAlphaFunc( GLenum func, GLclampf ref ) { (void) func; (void) ref; } +void glPolygonMode( GLenum face, GLenum mode ) { (void) face; (void) mode; } +void glClearDepth( GLclampd depth ) { (void) depth; } +void glPointSize( GLfloat size ) { (void) size; } + +/* ---- GLU quadrics (vias/pads/gizmo) + gluPerspective ------------------ */ +/* GLUquadric is an opaque/incomplete type; hand back a stable non-null pointer + * the renderer can store and pass to the (no-op) quadric calls below. */ +static int g_dummyQuadric; +GLUquadric* gluNewQuadric( void ) { return (GLUquadric*) &g_dummyQuadric; } +void gluDeleteQuadric( GLUquadric* q ) { (void) q; } +void gluQuadricDrawStyle( GLUquadric* q, GLenum style ) { (void) q; (void) style; } +void gluQuadricNormals( GLUquadric* q, GLenum normals ) { (void) q; (void) normals; } +void gluCylinder( GLUquadric* q, double base, double top, double height, int slices, int stacks ) +{ (void) q; (void) base; (void) top; (void) height; (void) slices; (void) stacks; } +void gluDisk( GLUquadric* q, double inner, double outer, int slices, int loops ) +{ (void) q; (void) inner; (void) outer; (void) slices; (void) loops; } +void gluSphere( GLUquadric* q, double radius, int slices, int stacks ) +{ (void) q; (void) radius; (void) slices; (void) stacks; } +void gluPerspective( double fovy, double aspect, double zNear, double zFar ) +{ (void) fovy; (void) aspect; (void) zNear; (void) zFar; } diff --git a/wxwidgets b/wxwidgets index c299d71..97359f9 160000 --- a/wxwidgets +++ b/wxwidgets @@ -1 +1 @@ -Subproject commit c299d716aac59de6f86c6711213d4ced3338b4c8 +Subproject commit 97359f9331fcd60e6d005534dd2ed09af8909901