feat: ✨ enable the 3D viewer in the WASM port (WIP)
Builds KiCad's 3D viewer for the browser (opt in with BUILD_3D_VIEWER=ON) and wires up the legacy-GL emulation it needs. - build-kicad-target.sh / docker/build.sh: BUILD_3D_VIEWER → -DKICAD_BUILD_3D_VIEWER_WASM=ON + -sLEGACY_GL_EMULATION + the GL js-library. - wasm/shims/gl_immediate_shim.js: display-list emulation (record/replay board layers), fixed-function + GLU stubs, throw-guards for unsupported pnames, and per-context GLImmediate init for the viewer's SECOND WebGL context. - tests/kicad/3d-viewer.spec.ts + utils/pcbnew-ready.ts: open View → 3D Viewer. - bumps the kicad submodule (EMSCRIPTEN-guarded GL changes). WIP: the viewer window + UI render and geometry draws (glError=0x0), but the board is not yet visibly rendered — GLImmediate's FFP shader program isn't linked on the viewer's 2nd WebGL context. Full status + next steps in features/feat/add-3d-view/notes.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
61760ea155
commit
68c711e633
7 changed files with 560 additions and 107 deletions
|
|
@ -188,6 +188,7 @@ compile_app() {
|
||||||
# emsdk_env.sh, so the build shell would lack emcc/embuilder on PATH. Setting
|
# emsdk_env.sh, so the build shell would lack emcc/embuilder on PATH. Setting
|
||||||
# EMSDK lets scripts/common/env.sh source /emsdk/emsdk_env.sh and activate the toolchain.
|
# EMSDK lets scripts/common/env.sh source /emsdk/emsdk_env.sh and activate the toolchain.
|
||||||
docker compose -f docker/docker-compose.yml exec -e EMSDK=/emsdk \
|
docker compose -f docker/docker-compose.yml exec -e EMSDK=/emsdk \
|
||||||
|
-e BUILD_3D_VIEWER="${BUILD_3D_VIEWER:-OFF}" \
|
||||||
kicad-wasm-builder \
|
kicad-wasm-builder \
|
||||||
"/workspace/scripts/kicad/build-${app}.sh" "${ARGS[@]}"
|
"/workspace/scripts/kicad/build-${app}.sh" "${ARGS[@]}"
|
||||||
|
|
||||||
|
|
|
||||||
2
kicad
2
kicad
|
|
@ -1 +1 @@
|
||||||
Subproject commit b75cff2f5c1fcf121a47af7cf2bec13f0389505d
|
Subproject commit 793c26bbb4377c31ec32ff182ee06deb27edd3d9
|
||||||
|
|
@ -387,6 +387,17 @@ if [ "${APP_NAME}" = "sym_convert" ]; then
|
||||||
SYM_CONVERTER_CMAKE_FLAG="-DKICAD_SYM_CONVERTER_WASM=ON"
|
SYM_CONVERTER_CMAKE_FLAG="-DKICAD_SYM_CONVERTER_WASM=ON"
|
||||||
fi
|
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).
|
||||||
|
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"
|
||||||
|
fi
|
||||||
|
|
||||||
emcmake cmake "${KICAD_DIR}" \
|
emcmake cmake "${KICAD_DIR}" \
|
||||||
${CCACHE_OPTS} \
|
${CCACHE_OPTS} \
|
||||||
${SYM_CONVERTER_CMAKE_FLAG} \
|
${SYM_CONVERTER_CMAKE_FLAG} \
|
||||||
|
|
@ -397,7 +408,7 @@ emcmake cmake "${KICAD_DIR}" \
|
||||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
|
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
|
||||||
-DCMAKE_CXX_FLAGS="${EXTRA_FLAGS} -pthread -sUSE_ZLIB=1 -DKICAD_USE_PLATFORM_WASM=1${DIAG_DEFINES} -I${SYSROOT}/include -I${STUBS_DIR} -include ${STUBS_DIR}/char_traits_uint16_workaround.h" \
|
-DCMAKE_CXX_FLAGS="${EXTRA_FLAGS} -pthread -sUSE_ZLIB=1 -DKICAD_USE_PLATFORM_WASM=1${DIAG_DEFINES} -I${SYSROOT}/include -I${STUBS_DIR} -include ${STUBS_DIR}/char_traits_uint16_workaround.h" \
|
||||||
-DCMAKE_C_FLAGS="${EXTRA_FLAGS} -pthread -sUSE_ZLIB=1 -I${SYSROOT}/include -I${STUBS_DIR}" \
|
-DCMAKE_C_FLAGS="${EXTRA_FLAGS} -pthread -sUSE_ZLIB=1 -I${SYSROOT}/include -I${STUBS_DIR}" \
|
||||||
-DCMAKE_EXE_LINKER_FLAGS="${LINKER_DEBUG_FLAGS} -pthread -sUSE_ZLIB=1 -sASYNCIFY=1 -sDYNCALLS=1 -sASYNCIFY_STACK_SIZE=65536 -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE='navigator.hardwareConcurrency' -sPTHREAD_POOL_SIZE_STRICT=0 -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=256MB -sMAXIMUM_MEMORY=4GB -sMAX_WEBGL_VERSION=2 -sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','dynCall'] -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=['\$dynCall'] --bind -L${SYSROOT}/lib ${STUBS_BUILD}/libgit2_stub.a ${STUBS_BUILD}/libcurl_stub.a${APP_STUB_LINK} ${STUBS_BUILD}/libnng_stub.a ${EMBIND_OBJ}" \
|
-DCMAKE_EXE_LINKER_FLAGS="${LINKER_DEBUG_FLAGS} -pthread -sUSE_ZLIB=1 -sASYNCIFY=1 -sDYNCALLS=1 -sASYNCIFY_STACK_SIZE=65536 -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE='navigator.hardwareConcurrency' -sPTHREAD_POOL_SIZE_STRICT=0 -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=256MB -sMAXIMUM_MEMORY=4GB -sMAX_WEBGL_VERSION=2 ${GL3D_LINK_FLAGS} -sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','dynCall'] -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=['\$dynCall'] --bind -L${SYSROOT}/lib ${STUBS_BUILD}/libgit2_stub.a ${STUBS_BUILD}/libcurl_stub.a${APP_STUB_LINK} ${STUBS_BUILD}/libnng_stub.a ${EMBIND_OBJ}" \
|
||||||
-DCMAKE_PREFIX_PATH="${SYSROOT};${WX_BUILD}" \
|
-DCMAKE_PREFIX_PATH="${SYSROOT};${WX_BUILD}" \
|
||||||
-DwxWidgets_CONFIG_EXECUTABLE="${WX_BUILD}/wx-config" \
|
-DwxWidgets_CONFIG_EXECUTABLE="${WX_BUILD}/wx-config" \
|
||||||
\
|
\
|
||||||
|
|
@ -405,7 +416,7 @@ emcmake cmake "${KICAD_DIR}" \
|
||||||
-DKICAD_SPICE=OFF \
|
-DKICAD_SPICE=OFF \
|
||||||
-DKICAD_USE_EGL=OFF \
|
-DKICAD_USE_EGL=OFF \
|
||||||
-DKICAD_USE_BUNDLED_GLEW=ON \
|
-DKICAD_USE_BUNDLED_GLEW=ON \
|
||||||
-DKICAD_BUILD_3D_VIEWER_WASM=OFF \
|
-DKICAD_BUILD_3D_VIEWER_WASM=${BUILD_3D_VIEWER} \
|
||||||
-DKICAD_IPC_API=ON \
|
-DKICAD_IPC_API=ON \
|
||||||
-DKICAD_USE_PCH=ON \
|
-DKICAD_USE_PCH=ON \
|
||||||
\
|
\
|
||||||
|
|
|
||||||
151
tests/kicad/3d-viewer.spec.ts
Normal file
151
tests/kicad/3d-viewer.spec.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
import { test, expect } from './fixtures';
|
||||||
|
import { clickMenuBarItem, clickMenuItem } from '../e2e/utils/element-tracker';
|
||||||
|
import { injectFromSubmodule } from './utils/fs-inject';
|
||||||
|
import { waitForBoardLoaded } from './utils/board-ready';
|
||||||
|
import { waitForPcbnew } from './utils/pcbnew-ready';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3D viewer e2e: load a real board in pcbnew, open the native 3D viewer
|
||||||
|
* (View → 3D Viewer / Alt+3), and verify a second top-level frame with its own
|
||||||
|
* WebGL canvas appears and renders the board.
|
||||||
|
*
|
||||||
|
* The 3D viewer (EDA_3D_VIEWER_FRAME) is a separate modeless KIWAY_PLAYER frame.
|
||||||
|
* In the wasm DOM port that surfaces as a new `#window-1` div in
|
||||||
|
* `#window-container` plus a dedicated `<canvas id="glcanvas-N">` (the main
|
||||||
|
* pcbnew board view is itself a wxGLCanvas, so we detect the viewer by the
|
||||||
|
* GL-canvas COUNT increasing, not by mere presence).
|
||||||
|
*
|
||||||
|
* Enabled by the WASM 3D-viewer build (BUILD_3D_VIEWER=ON →
|
||||||
|
* KICAD_BUILD_3D_VIEWER_WASM). With the bare board (no component STEP/WRL
|
||||||
|
* models — deferred), the viewer shows copper/silk/mask/edge geometry in 3D.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
async function loadBoard(page: Page, testLogger: { consoleLogs: string[]; errors: string[] }): Promise<void> {
|
||||||
|
const pcbFilename = `${DEMO.stem}.kicad_pcb`;
|
||||||
|
const proFilename = `${DEMO.stem}.kicad_pro`;
|
||||||
|
|
||||||
|
await injectFromSubmodule(page, `kicad/demos/${DEMO.dir}/${pcbFilename}`,
|
||||||
|
`${PROJECT_DIR_MEMFS}/${pcbFilename}`);
|
||||||
|
await injectFromSubmodule(page, `kicad/demos/${DEMO.dir}/${proFilename}`,
|
||||||
|
`${PROJECT_DIR_MEMFS}/${proFilename}`);
|
||||||
|
|
||||||
|
expect(await clickMenuBarItem(page, 'File'), 'File menu should be findable').toBe(true);
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
expect(await clickMenuItem(page, 'Open...'), 'Open… menu item should be findable').toBe(true);
|
||||||
|
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const registry = window.wxElementRegistry;
|
||||||
|
return !!registry && registry.findAll({ visible: true })
|
||||||
|
.some((el) => el.typeName === 'wxFileDialog');
|
||||||
|
}, null, { timeout: 15000 });
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
const filenameInput = await page.evaluate(() => {
|
||||||
|
const registry = window.wxElementRegistry;
|
||||||
|
if (!registry) return null;
|
||||||
|
const text = registry.findAll({ visible: true })
|
||||||
|
.find((el) => el.typeName === 'wxTextCtrl' && el.name === 'text');
|
||||||
|
return text ? { x: text.centerX, y: text.centerY } : null;
|
||||||
|
});
|
||||||
|
expect(filenameInput, 'filename text input should be visible').not.toBeNull();
|
||||||
|
if (!filenameInput) throw new Error('filename text input not found');
|
||||||
|
|
||||||
|
await page.mouse.click(filenameInput.x, filenameInput.y);
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.keyboard.type(pcbFilename);
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
const result = await waitForBoardLoaded(page, testLogger, 60000);
|
||||||
|
console.log(`[TEST] ${DEMO.name} board-ready result: ${result}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function countGlCanvases(page: Page): Promise<number> {
|
||||||
|
return page.evaluate(() => document.querySelectorAll('canvas[id^="glcanvas-"]').length);
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('3D viewer from pcbnew', () => {
|
||||||
|
// One 187 MB wasm runtime is already heavy; keep this serial and generous.
|
||||||
|
test.describe.configure({ mode: 'serial' });
|
||||||
|
test.setTimeout(240000);
|
||||||
|
|
||||||
|
test('opens the 3D viewer over a loaded board and renders it', async ({ page, testLogger }) => {
|
||||||
|
await page.goto('/kicad/pcbnew.html');
|
||||||
|
await waitForPcbnew(page);
|
||||||
|
|
||||||
|
await loadBoard(page, testLogger);
|
||||||
|
await page.screenshot({ path: `test-results/3d-viewer-00-board-loaded.png`, scale: 'device' });
|
||||||
|
|
||||||
|
const glBefore = await countGlCanvases(page);
|
||||||
|
console.log(`[TEST] glcanvas count before opening 3D viewer: ${glBefore}`);
|
||||||
|
|
||||||
|
// ── Open the 3D viewer: View → 3D Viewer, with an Alt+3 fallback. ──
|
||||||
|
let opened = false;
|
||||||
|
if (await clickMenuBarItem(page, 'View')) {
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
opened = await clickMenuItem(page, '3D Viewer');
|
||||||
|
}
|
||||||
|
if (!opened) {
|
||||||
|
console.log('[TEST] View → 3D Viewer not found via menu; trying Alt+3');
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.keyboard.press('Alt+3');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Wait for the secondary frame + a NEW GL canvas to appear. ──────
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
// A new top-level window div beyond the main pcbnew frame.
|
||||||
|
return !!document.querySelector('#window-container [id^="window-"]')
|
||||||
|
|| document.querySelectorAll('canvas[id^="glcanvas-"]').length > 0;
|
||||||
|
}, null, { timeout: 60000 });
|
||||||
|
|
||||||
|
await page.waitForFunction((before: number) =>
|
||||||
|
document.querySelectorAll('canvas[id^="glcanvas-"]').length > before,
|
||||||
|
glBefore, { timeout: 60000 });
|
||||||
|
|
||||||
|
const glAfter = await countGlCanvases(page);
|
||||||
|
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);
|
||||||
|
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}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Console-clean gates (same signatures load-pcb.spec.ts guards). ──
|
||||||
|
const allLines = [...testLogger.consoleLogs, ...testLogger.errors];
|
||||||
|
const aborts = allLines.filter((l) => l.includes('Aborted('));
|
||||||
|
expect(aborts, `WASM aborted while opening the 3D viewer:\n${aborts.join('\n\n')}`).toEqual([]);
|
||||||
|
|
||||||
|
const asyncifySignatures = [
|
||||||
|
'index out of bounds', 'indirect call to null', 'uncaught exception: unwind',
|
||||||
|
'invalid state', 'is not a function',
|
||||||
|
];
|
||||||
|
const asyncifyErrors = allLines.filter((l) =>
|
||||||
|
asyncifySignatures.some((sig) => l.toLowerCase().includes(sig)));
|
||||||
|
expect(asyncifyErrors,
|
||||||
|
`Asyncify corruption surfaced opening the 3D viewer:\n${asyncifyErrors.join('\n\n')}`)
|
||||||
|
.toEqual([]);
|
||||||
|
|
||||||
|
// The 3D viewer stub logs this when the real viewer is NOT compiled in —
|
||||||
|
// its presence means BUILD_3D_VIEWER=ON didn't take effect.
|
||||||
|
const stubbed = allLines.filter((l) => l.includes('3D Viewer is not available'));
|
||||||
|
expect(stubbed,
|
||||||
|
`3D viewer is still stubbed (build not enabled?):\n${stubbed.join('\n')}`).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
47
tests/kicad/utils/pcbnew-ready.ts
Normal file
47
tests/kicad/utils/pcbnew-ready.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
import { expect } from '../fixtures';
|
||||||
|
import { clickByLabel } from '../../e2e/utils/element-tracker';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared pcbnew bring-up helpers. Mirrors the bring-up sequence proven in
|
||||||
|
* load-pcb.spec.ts so multiple kicad specs (load-pcb, 3d-viewer, ...) can wait
|
||||||
|
* for a ready pcbnew the same way without duplicating the timing logic.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KiCad first-run setup wizard. Click "Next >" until it's gone, then "Finish".
|
||||||
|
* If no wizard is present, both clicks no-op immediately.
|
||||||
|
*/
|
||||||
|
export async function dismissWizardIfPresent(page: Page): Promise<void> {
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const advanced = await clickByLabel(page, 'Next >');
|
||||||
|
if (!advanced) break;
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
}
|
||||||
|
await clickByLabel(page, 'Finish');
|
||||||
|
await page.waitForTimeout(800);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for pcbnew to boot: 2D canvas visible, element registry populated, the
|
||||||
|
* first-run wizard dismissed, and the main PcbFrame registered & visible.
|
||||||
|
*/
|
||||||
|
export async function waitForPcbnew(page: Page): Promise<void> {
|
||||||
|
await expect(page.locator('#canvas')).toBeVisible({ timeout: 90000 });
|
||||||
|
await page.waitForFunction(() => !!window.wxElementRegistry, null, { timeout: 90000 });
|
||||||
|
// Registry object ≠ app booted: wait for real UI entries (wizard or main
|
||||||
|
// frame) before dismissing — CI boots slower and the dismiss loop is bounded.
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const registry = window.wxElementRegistry;
|
||||||
|
return !!registry && registry.findAll({}).length > 0;
|
||||||
|
}, null, { timeout: 150000 });
|
||||||
|
await page.waitForTimeout(2500);
|
||||||
|
await dismissWizardIfPresent(page);
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const registry = window.wxElementRegistry;
|
||||||
|
if (!registry) return false;
|
||||||
|
return registry.findAll({ visible: true })
|
||||||
|
.some((el) => el.name === 'PcbFrame');
|
||||||
|
}, null, { timeout: 90000 });
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
}
|
||||||
|
|
@ -87,6 +87,9 @@ const PCBNEW_FAMILY_SPECS = [
|
||||||
// boots pcbnew.html — must run on V8 (chromium-ci); on Firefox/x86 CI the
|
// boots pcbnew.html — must run on V8 (chromium-ci); on Firefox/x86 CI the
|
||||||
// ~190M module OOMs at instantiation and #canvas never appears (run 27626037849).
|
// ~190M module OOMs at instantiation and #canvas never appears (run 27626037849).
|
||||||
'**/pcbnew-move.spec.ts',
|
'**/pcbnew-move.spec.ts',
|
||||||
|
// 3D viewer specs boot pcbnew.html (3D-enabled build) — same V8 routing.
|
||||||
|
'**/3d-viewer.spec.ts',
|
||||||
|
'**/footprint-3d-preview.spec.ts',
|
||||||
];
|
];
|
||||||
|
|
||||||
const appsDir = 'apps';
|
const appsDir = 'apps';
|
||||||
|
|
|
||||||
|
|
@ -1,175 +1,415 @@
|
||||||
/**
|
/**
|
||||||
* gl_immediate_shim.js
|
* gl_immediate_shim.js
|
||||||
*
|
*
|
||||||
* Custom OpenGL immediate mode shims for KiCad WASM port.
|
* Custom legacy-OpenGL shims for the KiCad WASM 3D viewer, layered on top of
|
||||||
* Fixes Emscripten LEGACY_GL_EMULATION issues:
|
* Emscripten's -sLEGACY_GL_EMULATION. Fills the gaps that emulation leaves:
|
||||||
* 1. Color-per-vertex requirement - injects color before each vertex automatically
|
|
||||||
* 2. Missing double-precision functions (glVertex2d, glVertex3d, glColor3d, glColor4d)
|
|
||||||
*
|
*
|
||||||
* Usage: emcc ... --js-library=lib/gl_immediate_shim.js
|
* 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({
|
addToLibrary({
|
||||||
// ==================================================================
|
$GLImmediateShim__deps: [
|
||||||
// GLImmediateShim - State tracking and function wrapping
|
'$GL', '$GLImmediate',
|
||||||
// ==================================================================
|
'glBegin', 'glEnd', 'glVertex2f', 'glVertex3f', 'glColor3f', 'glColor4f',
|
||||||
|
'glNormal3f', 'glDrawArrays', 'glClear',
|
||||||
$GLImmediateShim__deps: ['$GLImmediate', 'glBegin', 'glEnd', 'glVertex2f', 'glVertex3f', 'glColor3f', 'glColor4f'],
|
'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__postset: 'GLImmediateShim.init();',
|
||||||
$GLImmediateShim: {
|
$GLImmediateShim: {
|
||||||
// Current color state (persistent across vertices)
|
// ---- 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,
|
currentColor: null,
|
||||||
|
|
||||||
// Track if we're inside glBegin/glEnd block
|
|
||||||
inBeginEnd: false,
|
inBeginEnd: false,
|
||||||
|
|
||||||
// Track if color was called since the last vertex
|
|
||||||
// This prevents double-injection when code already calls color per vertex
|
|
||||||
colorCalledSinceLastVertex: false,
|
colorCalledSinceLastVertex: false,
|
||||||
|
|
||||||
// Original functions we're wrapping
|
// ---- display-list state ----
|
||||||
origFns: {},
|
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
|
||||||
|
|
||||||
// Initialization flag
|
// ---- 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,
|
initialized: false,
|
||||||
|
|
||||||
init: function() {
|
init: function() {
|
||||||
if (GLImmediateShim.initialized) return;
|
if (GLImmediateShim.initialized) return;
|
||||||
if (typeof GLImmediate === 'undefined') {
|
if (typeof GLImmediate === 'undefined') return; // retried via postset chain
|
||||||
// GLImmediate not ready yet, will be called again
|
|
||||||
console.log('[GLImmediateShim] Waiting for GLImmediate...');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[GLImmediateShim] Initializing OpenGL immediate mode shims');
|
console.log('[GLImmediateShim] Initializing legacy-GL + display-list shims');
|
||||||
|
|
||||||
// Initialize current color to white (OpenGL default)
|
|
||||||
GLImmediateShim.currentColor = new Float32Array([1.0, 1.0, 1.0, 1.0]);
|
GLImmediateShim.currentColor = new Float32Array([1.0, 1.0, 1.0, 1.0]);
|
||||||
|
GLImmediateShim.lists = {};
|
||||||
|
GLImmediateShim.listBuffers = {};
|
||||||
|
|
||||||
// Store original functions
|
var S = GLImmediateShim;
|
||||||
GLImmediateShim.origFns = {
|
S.origFns = {
|
||||||
glBegin: _glBegin,
|
glBegin: _glBegin, glEnd: _glEnd,
|
||||||
glEnd: _glEnd,
|
glVertex2f: _glVertex2f, glVertex3f: _glVertex3f,
|
||||||
glVertex2f: _glVertex2f,
|
glColor3f: _glColor3f, glColor4f: _glColor4f,
|
||||||
glVertex3f: _glVertex3f,
|
glNormal3f: _glNormal3f, glDrawArrays: _glDrawArrays, glClear: _glClear,
|
||||||
glColor3f: _glColor3f,
|
glEnable: _glEnable, glDisable: _glDisable, glBlendFunc: _glBlendFunc,
|
||||||
glColor4f: _glColor4f,
|
glBindTexture: _glBindTexture, glLineWidth: _glLineWidth, glDepthMask: _glDepthMask,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Install shims
|
// Immediate-mode wrappers (color injection + display-list recording).
|
||||||
_glBegin = GLImmediateShim.shimBegin;
|
_glBegin = S.shimBegin;
|
||||||
_glEnd = GLImmediateShim.shimEnd;
|
_glEnd = S.shimEnd;
|
||||||
_glVertex2f = GLImmediateShim.shimVertex2f;
|
_glVertex2f = S.shimVertex2f;
|
||||||
_glVertex3f = GLImmediateShim.shimVertex3f;
|
_glVertex3f = S.shimVertex3f;
|
||||||
_glColor3f = GLImmediateShim.shimColor3f;
|
_glColor3f = S.shimColor3f;
|
||||||
_glColor4f = GLImmediateShim.shimColor4f;
|
_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;
|
GLImmediateShim.initialized = true;
|
||||||
console.log('[GLImmediateShim] Initialized successfully');
|
console.log('[GLImmediateShim] Initialized successfully');
|
||||||
},
|
},
|
||||||
|
|
||||||
// ==================================================================
|
record: function(closure) { GLImmediateShim.lists[GLImmediateShim.compiling].push(closure); },
|
||||||
// Shim implementations
|
|
||||||
// ==================================================================
|
|
||||||
|
|
||||||
|
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) {
|
shimBegin: function(mode) {
|
||||||
|
if (GLImmediateShim.compiling) { GLImmediateShim.record(function() { _glBegin(mode); }); return; }
|
||||||
|
GLImmediateShim.ensureTempBuffers();
|
||||||
GLImmediateShim.inBeginEnd = true;
|
GLImmediateShim.inBeginEnd = true;
|
||||||
GLImmediateShim.colorCalledSinceLastVertex = false;
|
GLImmediateShim.colorCalledSinceLastVertex = false;
|
||||||
GLImmediateShim.origFns.glBegin(mode);
|
GLImmediateShim.origFns.glBegin(mode);
|
||||||
},
|
},
|
||||||
|
|
||||||
shimEnd: function() {
|
shimEnd: function() {
|
||||||
|
if (GLImmediateShim.compiling) { GLImmediateShim.record(function() { _glEnd(); }); return; }
|
||||||
GLImmediateShim.inBeginEnd = false;
|
GLImmediateShim.inBeginEnd = false;
|
||||||
GLImmediateShim.origFns.glEnd();
|
GLImmediateShim.origFns.glEnd();
|
||||||
},
|
},
|
||||||
|
|
||||||
shimColor3f: function(r, g, b) {
|
shimColor3f: function(r, g, b) {
|
||||||
var c = GLImmediateShim.currentColor;
|
if (GLImmediateShim.compiling) { GLImmediateShim.record(function() { _glColor3f(r, g, b); }); return; }
|
||||||
c[0] = r;
|
var c = GLImmediateShim.currentColor; c[0] = r; c[1] = g; c[2] = b; c[3] = 1.0;
|
||||||
c[1] = g;
|
if (GLImmediateShim.inBeginEnd) GLImmediateShim.colorCalledSinceLastVertex = true;
|
||||||
c[2] = b;
|
|
||||||
c[3] = 1.0;
|
|
||||||
|
|
||||||
if (GLImmediateShim.inBeginEnd) {
|
|
||||||
// Mark that color was explicitly called for this vertex
|
|
||||||
GLImmediateShim.colorCalledSinceLastVertex = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLImmediateShim.origFns.glColor3f(r, g, b);
|
GLImmediateShim.origFns.glColor3f(r, g, b);
|
||||||
},
|
},
|
||||||
|
|
||||||
shimColor4f: function(r, g, b, a) {
|
shimColor4f: function(r, g, b, a) {
|
||||||
var c = GLImmediateShim.currentColor;
|
if (GLImmediateShim.compiling) { GLImmediateShim.record(function() { _glColor4f(r, g, b, a); }); return; }
|
||||||
c[0] = r;
|
var c = GLImmediateShim.currentColor; c[0] = r; c[1] = g; c[2] = b; c[3] = a;
|
||||||
c[1] = g;
|
if (GLImmediateShim.inBeginEnd) GLImmediateShim.colorCalledSinceLastVertex = true;
|
||||||
c[2] = b;
|
|
||||||
c[3] = a;
|
|
||||||
|
|
||||||
if (GLImmediateShim.inBeginEnd) {
|
|
||||||
// Mark that color was explicitly called for this vertex
|
|
||||||
GLImmediateShim.colorCalledSinceLastVertex = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLImmediateShim.origFns.glColor4f(r, g, b, a);
|
GLImmediateShim.origFns.glColor4f(r, g, b, a);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Inject current color before each vertex (only if not already called)
|
|
||||||
injectColor: function() {
|
injectColor: function() {
|
||||||
if (!GLImmediateShim.inBeginEnd) return;
|
if (!GLImmediateShim.inBeginEnd) return;
|
||||||
|
|
||||||
// Only inject color if it wasn't already called for this vertex
|
|
||||||
if (!GLImmediateShim.colorCalledSinceLastVertex) {
|
if (!GLImmediateShim.colorCalledSinceLastVertex) {
|
||||||
var c = GLImmediateShim.currentColor;
|
var c = GLImmediateShim.currentColor;
|
||||||
GLImmediateShim.origFns.glColor4f(c[0], c[1], c[2], c[3]);
|
GLImmediateShim.origFns.glColor4f(c[0], c[1], c[2], c[3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the flag for the next vertex
|
|
||||||
GLImmediateShim.colorCalledSinceLastVertex = false;
|
GLImmediateShim.colorCalledSinceLastVertex = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
shimVertex2f: function(x, y) {
|
shimVertex2f: function(x, y) {
|
||||||
GLImmediateShim.injectColor();
|
if (GLImmediateShim.compiling) { GLImmediateShim.record(function() { _glVertex2f(x, y); }); return; }
|
||||||
GLImmediateShim.origFns.glVertex2f(x, y);
|
GLImmediateShim.injectColor(); GLImmediateShim.origFns.glVertex2f(x, y);
|
||||||
},
|
},
|
||||||
|
|
||||||
shimVertex3f: function(x, y, z) {
|
shimVertex3f: function(x, y, z) {
|
||||||
GLImmediateShim.injectColor();
|
if (GLImmediateShim.compiling) { GLImmediateShim.record(function() { _glVertex3f(x, y, z); }); return; }
|
||||||
GLImmediateShim.origFns.glVertex3f(x, y, z);
|
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]);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ==================================================================
|
// ==================================================================
|
||||||
// Double-precision vertex functions (missing in Emscripten)
|
// 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__deps: ['glVertex2f'],
|
||||||
glVertex2d: function(x, y) {
|
glVertex2d: function(x, y) { _glVertex2f(x, y); },
|
||||||
_glVertex2f(x, y);
|
|
||||||
},
|
|
||||||
|
|
||||||
glVertex3d__deps: ['glVertex3f'],
|
glVertex3d__deps: ['glVertex3f'],
|
||||||
glVertex3d: function(x, y, z) {
|
glVertex3d: function(x, y, z) { _glVertex3f(x, y, z); },
|
||||||
_glVertex3f(x, y, z);
|
|
||||||
},
|
|
||||||
|
|
||||||
glVertex4d__deps: ['glVertex4f'],
|
glVertex4d__deps: ['glVertex4f'],
|
||||||
glVertex4d: function(x, y, z, w) {
|
glVertex4d: function(x, y, z, w) { _glVertex4f(x, y, z, w); },
|
||||||
_glVertex4f(x, y, z, w);
|
|
||||||
},
|
|
||||||
|
|
||||||
// ==================================================================
|
|
||||||
// Double-precision color functions
|
|
||||||
// ==================================================================
|
|
||||||
|
|
||||||
glColor3d__deps: ['glColor3f'],
|
glColor3d__deps: ['glColor3f'],
|
||||||
glColor3d: function(r, g, b) {
|
glColor3d: function(r, g, b) { _glColor3f(r, g, b); },
|
||||||
_glColor3f(r, g, b);
|
|
||||||
},
|
|
||||||
|
|
||||||
glColor4d__deps: ['glColor4f'],
|
glColor4d__deps: ['glColor4f'],
|
||||||
glColor4d: function(r, g, b, a) {
|
glColor4d: function(r, g, b, a) { _glColor4f(r, g, b, a); },
|
||||||
_glColor4f(r, g, b, a);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ensure GLImmediateShim is included in the build
|
// Ensure GLImmediateShim is included in the build
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue