eeschema simulator: lazy ngspice_service worker — static sharedspice (XSPICE registry + CIDER), init_dll ifdef, e2e both engines
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
004412c53d
commit
703cb010b7
30 changed files with 2872 additions and 82 deletions
|
|
@ -1,6 +1,30 @@
|
|||
#!/bin/bash
|
||||
# Build ngspice for WebAssembly
|
||||
# ngspice provides SPICE simulation for KiCad's Eeschema
|
||||
w# Build ngspice for WebAssembly: sharedspice STATIC library + statically
|
||||
# registered XSPICE code models. Consumed by the ngspice_service worker that
|
||||
# backs KiCad eeschema's simulator.
|
||||
#
|
||||
# Deviations from a stock ngspice build, all load-bearing for wasm:
|
||||
# - --with-ngshared + --disable-shared: libtool builds libngspice.a with the
|
||||
# sharedspice API (ngSpice_Init & co.) compiled in and no CLI programs
|
||||
# (bin_PROGRAMS is gated !SHARED_MODULE upstream, so no duplicate main()).
|
||||
# - XSPICE code models (.cm) are dlopen'd at runtime natively, which a static
|
||||
# wasm module cannot do. Under NGCM_STATIC the icm build archives each code
|
||||
# model instead of linking a shared object (per-cm renamed table symbols via
|
||||
# ngcm_dlmain_static.c), and a registry appended to dev.c resolves the seven
|
||||
# bundled .cm basenames without dlopen. dlopen remains the fallback for
|
||||
# unknown paths so user code models fail with ngspice's normal error text.
|
||||
# Sources in scripts/deps/ngspice-wasm/; edits to the ngspice tree are
|
||||
# idempotent (marker-guarded) since the tree is an extracted tarball.
|
||||
# - /proc/meminfo header check is forced off: configure runs on the build
|
||||
# host (Linux in docker), the browser runtime has no procfs, and ngspice's
|
||||
# memory guard treats "0 bytes available" as out-of-memory.
|
||||
# - -pthread everywhere: sharedspice's bg_run/bg_halt background thread is
|
||||
# gated on HAVE_LIBPTHREAD; without it bg_run silently degrades to a
|
||||
# synchronous blocking call.
|
||||
# - Exception model must match the rest of the tree (DEPS_EH_FLAGS).
|
||||
# - XSPICE + CIDER enabled: parity with native KiCad's bundled ngspice.
|
||||
# - cmpp (XSPICE preprocessor) runs on the build host; ngspice's configure
|
||||
# handles that itself when cross_compiling=yes (src/xspice/cmpp/build/).
|
||||
|
||||
set -e
|
||||
|
||||
|
|
@ -12,6 +36,7 @@ source "${SCRIPT_DIR}/../common/functions.sh"
|
|||
NGSPICE_DIR="${DEPS_ROOT}/ngspice-${NGSPICE_VERSION}"
|
||||
NGSPICE_BUILD="${BUILD_ROOT}/deps/ngspice"
|
||||
NGSPICE_STAMP="${BUILD_ROOT}/stamps/ngspice.stamp"
|
||||
NGCM_SRC_DIR="${SCRIPT_DIR}/ngspice-wasm"
|
||||
|
||||
# Parse arguments
|
||||
CLEAN=0
|
||||
|
|
@ -41,50 +66,285 @@ if [ ! -d "${NGSPICE_DIR}" ]; then
|
|||
mkdir -p "${DEPS_ROOT}"
|
||||
cd "${DEPS_ROOT}"
|
||||
|
||||
NGSPICE_URL="https://sourceforge.net/projects/ngspice/files/ng-spice-rework/${NGSPICE_VERSION}/ngspice-${NGSPICE_VERSION}.tar.gz/download"
|
||||
curl -L "${NGSPICE_URL}" -o "ngspice-${NGSPICE_VERSION}.tar.gz"
|
||||
download_file "${NGSPICE_URL}" "ngspice-${NGSPICE_VERSION}.tar.gz"
|
||||
tar -xzf "ngspice-${NGSPICE_VERSION}.tar.gz"
|
||||
rm "ngspice-${NGSPICE_VERSION}.tar.gz"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Static code-model support (NGCM_STATIC) - idempotent source edits
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DEV_C="${NGSPICE_DIR}/src/spicelib/devices/dev.c"
|
||||
ICM_MK="${NGSPICE_DIR}/src/xspice/icm/GNUmakefile.in"
|
||||
|
||||
# 1. Hook load_opus(): consult the registry before attempting dlopen.
|
||||
# Must run BEFORE the registry append: the registry body contains the
|
||||
# ngcm_static_load definition, so a plain grep for the name would mask the
|
||||
# hook forever. The guard matches the call site only.
|
||||
if ! grep -q "ngcm_static_load(name)" "${DEV_C}"; then
|
||||
log_info "Inserting registry hook into load_opus()"
|
||||
python3 - "${DEV_C}" <<'EOF'
|
||||
import sys
|
||||
|
||||
path = sys.argv[1]
|
||||
src = open(path).read()
|
||||
anchor = " lib = dlopen(name, RTLD_NOW);"
|
||||
hook = """#ifdef NGCM_STATIC
|
||||
{
|
||||
extern int ngcm_static_load(const char *path);
|
||||
int ngcm_ret = ngcm_static_load(name);
|
||||
if (ngcm_ret >= 0)
|
||||
return ngcm_ret;
|
||||
/* not a bundled code model: fall through to dlopen */
|
||||
}
|
||||
#endif
|
||||
"""
|
||||
assert src.count(anchor) == 1, "load_opus dlopen anchor not unique"
|
||||
open(path, "w").write(src.replace(anchor, hook + anchor))
|
||||
EOF
|
||||
fi
|
||||
|
||||
# 2. Append the code-model registry to dev.c (marker-guarded).
|
||||
if ! grep -q "NGCM_REGISTRY_MARKER" "${DEV_C}"; then
|
||||
log_info "Appending static code-model registry to dev.c"
|
||||
cat "${NGCM_SRC_DIR}/ngcm_registry.c" >> "${DEV_C}"
|
||||
fi
|
||||
|
||||
# 3. Fix an upstream 32-bit union-punning bug in the CIDER card parser
|
||||
# (found by the Gate-1 smoke, ngspice 46). The cleanup tests use
|
||||
# `dataType & IF_REALVEC` (0x8004): every scalar IF_SET|IF_REAL parameter
|
||||
# (0x2004) matches through the shared 0x4 bit, and the code then frees the
|
||||
# vec-pointer union member overlaying the parsed scalar double. On 64-bit
|
||||
# hosts that misread lands in zeroed padding after the double
|
||||
# (free(NULL) no-op), which is why upstream never noticed; on wasm32 the
|
||||
# pointer member overlays the HIGH HALF of the double and free() faults.
|
||||
# TODO: upstream this to ngspice.
|
||||
python3 - "${NGSPICE_DIR}/src/spicelib/parser/inpgmod.c" <<'EOF'
|
||||
import sys
|
||||
|
||||
path = sys.argv[1]
|
||||
src = open(path).read()
|
||||
old = """ if (info->cardParms[idx].dataType & IF_STRING) {
|
||||
FREE(value->sValue);
|
||||
} else if (info->cardParms[idx].dataType & IF_REALVEC) {
|
||||
FREE(value->v.vec.rVec);
|
||||
} else if (info->cardParms[idx].dataType & IF_INTVEC) {
|
||||
FREE(value->v.vec.iVec);
|
||||
}"""
|
||||
new = """ /* kicad-wasm: exact variant-type tests. The original
|
||||
* `& IF_REALVEC` composite masks also match scalar
|
||||
* IF_SET|IF_REAL parameters (shared 0x4 bit) and free the
|
||||
* vec-pointer union member overlaying the scalar double -
|
||||
* benign on 64-bit (lands in zero padding), heap fault on
|
||||
* wasm32. */
|
||||
int ngcm_vt = info->cardParms[idx].dataType & IF_VARTYPES;
|
||||
if (ngcm_vt == IF_STRING) {
|
||||
FREE(value->sValue);
|
||||
} else if (ngcm_vt == IF_REALVEC) {
|
||||
FREE(value->v.vec.rVec);
|
||||
} else if (ngcm_vt == IF_INTVEC) {
|
||||
FREE(value->v.vec.iVec);
|
||||
}"""
|
||||
if new in src:
|
||||
pass # already applied
|
||||
else:
|
||||
assert src.count(old) == 1, "inpgmod.c cleanup-tests anchor not found"
|
||||
open(path, "w").write(src.replace(old, new))
|
||||
print("patched inpgmod.c IF_VARTYPES cleanup tests")
|
||||
EOF
|
||||
|
||||
# 4. Redirect the icm build: archive code models instead of shared-linking,
|
||||
# compile our renamed-tables TU instead of dlmain.c, and pass the cm name.
|
||||
# All three edits are exact-string replacements, applied once.
|
||||
python3 - "${ICM_MK}" <<'EOF'
|
||||
import sys
|
||||
|
||||
path = sys.argv[1]
|
||||
src = open(path).read()
|
||||
|
||||
edits = [
|
||||
# .cm link recipe -> archive under NGCM_STATIC. The three common objects
|
||||
# are excluded: dstring.o duplicates the core's, and the tline commons are
|
||||
# shipped once via ngcm_common.a (they are prerequisites of every cm here
|
||||
# but only the tlines models reference them).
|
||||
("\t$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(VIS_CFLAGS) $(LDFLAGS) $^ $(LIBS) -o $@",
|
||||
"\t$(if $(NGCM_STATIC),emar rcs $@ $(filter-out dstring.o msline_common.o tline_common.o,$^),$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(VIS_CFLAGS) $(LDFLAGS) $^ $(LIBS) -o $@)"),
|
||||
# dlmain.o compiles our static-tables TU under NGCM_STATIC ($< follows the
|
||||
# first prerequisite).
|
||||
("$(cm)/dlmain.o : $(srcdir)/dlmain.c $(cm-descr)",
|
||||
"$(cm)/dlmain.o : $(if $(NGCM_STATIC),$(NGCM_DLMAIN),$(srcdir)/dlmain.c) $(cm-descr)"),
|
||||
# Per-cm symbol prefix for the tables TU (harmless for the other objects).
|
||||
("COMPILE = $(CC) $(INCLUDES) -I$(cm) -I$(srcdir)/$(cm) $(CFLAGS) $(EXTRA_CFLAGS) $(VIS_CFLAGS)",
|
||||
"COMPILE = $(CC) $(INCLUDES) -I$(cm) -I$(srcdir)/$(cm) $(CFLAGS) $(EXTRA_CFLAGS) $(VIS_CFLAGS) $(if $(NGCM_STATIC),-DNGCM_NAME=$(cm))"),
|
||||
# The $(shell cmpp -p) model-list calls hardcode the in-tree cmpp, which
|
||||
# is the CROSS-compiled (wasm) binary the build host cannot execute -
|
||||
# the lists come back empty and the code models silently lose all their
|
||||
# cfunc/ifspec objects. makedefs' CMPP is the host-built one under
|
||||
# cross-compilation (configure.ac:1470-1475).
|
||||
("""ifeq ($(OS),Windows_NT)
|
||||
cmpp = ../cmpp/cmpp.exe
|
||||
else
|
||||
cmpp = ../cmpp/cmpp
|
||||
endif""",
|
||||
"cmpp = $(CMPP)"),
|
||||
]
|
||||
|
||||
changed = False
|
||||
for old, new in edits:
|
||||
if new in src:
|
||||
continue # already applied
|
||||
assert old in src, f"icm GNUmakefile.in anchor not found: {old!r}"
|
||||
src = src.replace(old, new)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
open(path, "w").write(src)
|
||||
print("patched icm GNUmakefile.in")
|
||||
EOF
|
||||
|
||||
log_info "Building ngspice ${NGSPICE_VERSION} for WASM..."
|
||||
|
||||
mkdir -p "${NGSPICE_BUILD}"
|
||||
cd "${NGSPICE_BUILD}"
|
||||
|
||||
# ngspice uses autoconf
|
||||
# Set compiler flags based on debug mode
|
||||
# ngspice must run at real speed even in debug builds of the rest of the tree:
|
||||
# the simulator is compute-bound and its own module is finalized separately.
|
||||
if [ "${DEBUG_BUILD:-1}" = "1" ]; then
|
||||
export CFLAGS="-g -O0 -pthread"
|
||||
export CXXFLAGS="-g -O0 -pthread"
|
||||
NGSPICE_DEBUG_FLAG="--enable-debug"
|
||||
NGSPICE_OPT="-O2 -g"
|
||||
else
|
||||
export CFLAGS="-O2 -pthread"
|
||||
export CXXFLAGS="-O2 -pthread"
|
||||
NGSPICE_DEBUG_FLAG="--disable-debug"
|
||||
NGSPICE_OPT="-O2"
|
||||
fi
|
||||
export LDFLAGS="-pthread"
|
||||
|
||||
# Configure ngspice as a static library for WASM
|
||||
# Note: --with-ngshared requires shared libs which WASM doesn't support
|
||||
# We build static lib instead
|
||||
# -Wno-error guards: CIDER and parts of XSPICE are legacy C that modern clang
|
||||
# (emcc >= llvm 16) rejects by default.
|
||||
export CFLAGS="${NGSPICE_OPT} -pthread ${DEPS_EH_FLAGS} -DNGCM_STATIC \
|
||||
-Wno-error=implicit-function-declaration -Wno-error=implicit-int"
|
||||
export CXXFLAGS="${NGSPICE_OPT} -pthread ${DEPS_EH_FLAGS}"
|
||||
export LDFLAGS="-pthread ${DEPS_EH_FLAGS}"
|
||||
|
||||
# Makefile-level knobs for the icm (code model) build, read from the
|
||||
# environment by our GNUmakefile.in edits above.
|
||||
export NGCM_STATIC=1
|
||||
export NGCM_DLMAIN="${NGCM_SRC_DIR}/ngcm_dlmain_static.c"
|
||||
|
||||
emconfigure "${NGSPICE_DIR}/configure" \
|
||||
--prefix="${SYSROOT}" \
|
||||
--host=wasm32-unknown-emscripten \
|
||||
--build=$(uname -m)-linux-gnu \
|
||||
--build="$("${NGSPICE_DIR}/config.guess")" \
|
||||
--with-ngshared \
|
||||
--disable-shared \
|
||||
--enable-static \
|
||||
${NGSPICE_DEBUG_FLAG} \
|
||||
--disable-dependency-tracking \
|
||||
--disable-openmp \
|
||||
--enable-cider \
|
||||
--enable-xspice \
|
||||
--enable-cider \
|
||||
--disable-openmp \
|
||||
--disable-debug \
|
||||
--without-x \
|
||||
--without-readline \
|
||||
--without-editline
|
||||
--with-readline=no \
|
||||
--without-editline \
|
||||
ac_cv_header__proc_meminfo=no
|
||||
|
||||
emmake make -j${JOBS}
|
||||
emmake make install
|
||||
# ngspice hardwires libtool's -shared mode for the ngshared build: configure
|
||||
# sets STATIC=-shared (consumed as AM_CFLAGS by every convenience lib) and
|
||||
# src/Makefile.am gives libngspice_la_{CFLAGS,LDFLAGS} a literal -shared.
|
||||
# libtool refuses -shared outright on a target without shared-library support
|
||||
# ("Fatal configuration error"), so force static mode: the make command line
|
||||
# overrides $(STATIC) everywhere, and the two hardwired lines are rewritten in
|
||||
# the generated Makefile (regenerated by configure on every build, so this
|
||||
# stays idempotent).
|
||||
sed -i.ngcm.bak \
|
||||
-e 's/^\(libngspice_la_[A-Z]*FLAGS *=.*\)-shared/\1-static/' \
|
||||
src/Makefile
|
||||
|
||||
# The XSPICE verilog/vhdl subdirs build VPI co-simulation shims (ivlng.la,
|
||||
# ivlngvpi.la) that are inherently SHARED objects plugging into an external
|
||||
# Icarus/GHDL process - impossible in wasm and unbuildable without shared-lib
|
||||
# support. Skipping them matches native behaviour when no cosimulator is
|
||||
# installed: the d_cosim code model fails at runtime with ngspice's normal
|
||||
# error message.
|
||||
sed -i.ngcm.bak \
|
||||
-e 's/^\(SUBDIRS = mif cm enh evt idn cmpp icm\) verilog vhdl$/\1/' \
|
||||
src/xspice/Makefile
|
||||
|
||||
emmake make -j${JOBS} STATIC=-static
|
||||
emmake make install STATIC=-static
|
||||
|
||||
# dlmain.c's tail (fopen_with_path, cm_message_printf, cm_is_inertial) is
|
||||
# utility code the cfunc objects call but that exists nowhere in the core -
|
||||
# natively each .cm DLL carries its own copy. Extract it once from the
|
||||
# pristine dlmain.c (BSD-3) so the seven archives stay collision-free; the
|
||||
# coreitf wrapper section above the marker must NOT come along (it would
|
||||
# shadow real core functions with calls through a never-initialized coreitf).
|
||||
python3 - "${NGSPICE_DIR}/src/xspice/icm/dlmain.c" "${NGSPICE_BUILD}/ngcm_cmutil.c" <<'EOF'
|
||||
import sys
|
||||
|
||||
src_path, out_path = sys.argv[1], sys.argv[2]
|
||||
src = open(src_path).read()
|
||||
marker = "#define DFLT_BUF_SIZE 256"
|
||||
assert src.count(marker) == 1, "dlmain.c utility-tail marker not unique"
|
||||
tail = src[src.index(marker):]
|
||||
preamble = """/* Generated by build-ngspice.sh: utility tail of ngspice's
|
||||
* src/xspice/icm/dlmain.c (BSD-3-Clause, Copyright 2000 The ngspice team),
|
||||
* shared once across the statically linked code models. */
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ngspice/config.h"
|
||||
#include "ngspice/cpextern.h"
|
||||
#include "ngspice/devdefs.h"
|
||||
#include "ngspice/dstring.h"
|
||||
#include "ngspice/dllitf.h"
|
||||
#include "ngspice/evtudn.h"
|
||||
#include "ngspice/inpdefs.h"
|
||||
#include "ngspice/inertial.h"
|
||||
#include "ngspice/cmproto.h"
|
||||
|
||||
/* In the DLL world cm_getvar is a dlmain.c wrapper that reaches the core's
|
||||
* cp_getvar through coreitf (see cmexport.c binding dllitf_cm_getvar to
|
||||
* cp_getvar). Statically there is no coreitf; bind it directly. */
|
||||
bool cm_getvar(char *name, enum cp_types type, void *retval, size_t rsize)
|
||||
{
|
||||
return cp_getvar(name, type, retval, rsize);
|
||||
}
|
||||
|
||||
"""
|
||||
open(out_path, "w").write(preamble + tail)
|
||||
EOF
|
||||
|
||||
emcc -c "${NGSPICE_BUILD}/ngcm_cmutil.c" -o "${NGSPICE_BUILD}/ngcm_cmutil.o" \
|
||||
${CFLAGS} \
|
||||
-I"${NGSPICE_DIR}/src/include" \
|
||||
-I"${NGSPICE_BUILD}/src/include"
|
||||
|
||||
# The tline/msline common objects every cm build compiles but only the tlines
|
||||
# models reference; shipped once so whole-archiving the .cm archives stays
|
||||
# duplicate-free.
|
||||
emar rcs "${SYSROOT}/lib/ngspice/ngcm_common.a" \
|
||||
src/xspice/icm/msline_common.o \
|
||||
src/xspice/icm/tline_common.o \
|
||||
src/xspice/icm/dstring.o \
|
||||
"${NGSPICE_BUILD}/ngcm_cmutil.o"
|
||||
|
||||
# Sanity: everything the ngspice_service link needs must exist.
|
||||
for f in \
|
||||
"${SYSROOT}/lib/libngspice.a" \
|
||||
"${SYSROOT}/include/ngspice/sharedspice.h" \
|
||||
"${SYSROOT}/lib/ngspice/analog.cm" \
|
||||
"${SYSROOT}/lib/ngspice/digital.cm" \
|
||||
"${SYSROOT}/lib/ngspice/spice2poly.cm" \
|
||||
"${SYSROOT}/lib/ngspice/table.cm" \
|
||||
"${SYSROOT}/lib/ngspice/tlines.cm" \
|
||||
"${SYSROOT}/lib/ngspice/xtradev.cm" \
|
||||
"${SYSROOT}/lib/ngspice/xtraevt.cm" \
|
||||
"${SYSROOT}/share/ngspice/scripts/spinit"; do
|
||||
if [ ! -f "$f" ]; then
|
||||
log_error "ngspice install incomplete: missing $f"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
create_stamp "${NGSPICE_STAMP}"
|
||||
log_info "ngspice build complete!"
|
||||
|
|
|
|||
57
scripts/deps/ngspice-wasm/ngcm_dlmain_static.c
Normal file
57
scripts/deps/ngspice-wasm/ngcm_dlmain_static.c
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Static-build replacement for ngspice's src/xspice/icm/dlmain.c.
|
||||
*
|
||||
* In the WASM build the XSPICE code models (.cm) cannot be dlopen'd, so each
|
||||
* code-model directory is compiled into a static archive instead of a shared
|
||||
* object (see build-ngspice.sh, NGCM_STATIC). This TU provides only the two
|
||||
* model tables, renamed per code model (ngcm_<cm>_cmDEVices etc.) so all seven
|
||||
* archives can coexist in one image. Everything else dlmain.c contains - the
|
||||
* CMdevs()/CMudns() accessor exports and the coreitf-forwarding wrappers for
|
||||
* the MIF core functions - is deliberately omitted: the registry appended to
|
||||
* dev.c (ngcm_registry.c) reads the tables directly, and in a static link the
|
||||
* code-model objects bind straight to the real core functions, which the
|
||||
* wrappers would otherwise collide with.
|
||||
*
|
||||
* Compiled once per code model with -DNGCM_NAME=<cm> and -I<cm-build-dir> so
|
||||
* the cmpp-generated cmextrn.h/cminfo.h/udnextrn.h/udninfo.h of that model
|
||||
* are picked up.
|
||||
*/
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ngspice/config.h"
|
||||
#include "ngspice/cpextern.h"
|
||||
#include "ngspice/devdefs.h"
|
||||
#include "ngspice/dstring.h"
|
||||
#include "ngspice/dllitf.h"
|
||||
#include "ngspice/evtudn.h"
|
||||
#include "ngspice/inpdefs.h"
|
||||
#include "ngspice/inertial.h"
|
||||
#include "cmextrn.h"
|
||||
#include "udnextrn.h"
|
||||
|
||||
#ifndef NGCM_NAME
|
||||
#error "ngcm_dlmain_static.c must be compiled with -DNGCM_NAME=<code model name>"
|
||||
#endif
|
||||
|
||||
#define NGCM_PASTE2(a, b) a##b
|
||||
#define NGCM_PASTE(a, b) NGCM_PASTE2(a, b)
|
||||
#define NGCM_SYM(s) NGCM_PASTE(NGCM_PASTE(ngcm_, NGCM_NAME), NGCM_PASTE2(_, s))
|
||||
|
||||
SPICEdev *NGCM_SYM(cmDEVices)[] = {
|
||||
#include "cminfo.h"
|
||||
NULL
|
||||
};
|
||||
|
||||
int NGCM_SYM(cmDEVicesCNT) =
|
||||
sizeof(NGCM_SYM(cmDEVices)) / sizeof(SPICEdev *) - 1;
|
||||
|
||||
Evt_Udn_Info_t *NGCM_SYM(cmEVTudns)[] = {
|
||||
#include "udninfo.h"
|
||||
NULL
|
||||
};
|
||||
|
||||
int NGCM_SYM(cmEVTudnCNT) =
|
||||
sizeof(NGCM_SYM(cmEVTudns)) / sizeof(Evt_Udn_Info_t *) - 1;
|
||||
82
scripts/deps/ngspice-wasm/ngcm_registry.c
Normal file
82
scripts/deps/ngspice-wasm/ngcm_registry.c
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/* NGCM_REGISTRY_MARKER - appended to src/spicelib/devices/dev.c by
|
||||
* scripts/deps/build-ngspice.sh (idempotent: guarded by this marker).
|
||||
*
|
||||
* Registry of the statically-linked XSPICE code models. In the WASM build the
|
||||
* bundled .cm files are static archives (see ngcm_dlmain_static.c) and cannot
|
||||
* be dlopen'd; load_opus() consults this registry first (hook inserted by
|
||||
* build-ngspice.sh) and only falls back to dlopen - which then fails with
|
||||
* ngspice's normal error reporting - for paths it does not recognize, e.g.
|
||||
* user-compiled code models, which cannot exist as loadable binaries in wasm.
|
||||
*
|
||||
* Matching is by basename so the spinit "codemodel <path>/analog.cm" lines
|
||||
* keep working regardless of the install prefix baked into spinit.
|
||||
*
|
||||
* No coreitf wiring happens here: in a static link the code-model objects call
|
||||
* the MIF and cm_ core functions directly, so the dlmain.c indirection table
|
||||
* the dlopen path has to fill in does not exist.
|
||||
*/
|
||||
#if defined(NGCM_STATIC) && defined(XSPICE)
|
||||
|
||||
#include "ngspice/devdefs.h"
|
||||
#include "ngspice/evtudn.h"
|
||||
#include <string.h>
|
||||
|
||||
#define NGCM_DECL(cm) \
|
||||
extern SPICEdev *ngcm_##cm##_cmDEVices[]; \
|
||||
extern int ngcm_##cm##_cmDEVicesCNT; \
|
||||
extern Evt_Udn_Info_t *ngcm_##cm##_cmEVTudns[]; \
|
||||
extern int ngcm_##cm##_cmEVTudnCNT;
|
||||
|
||||
NGCM_DECL(analog)
|
||||
NGCM_DECL(digital)
|
||||
NGCM_DECL(spice2poly)
|
||||
NGCM_DECL(table)
|
||||
NGCM_DECL(tlines)
|
||||
NGCM_DECL(xtradev)
|
||||
NGCM_DECL(xtraevt)
|
||||
|
||||
struct ngcm_static_entry {
|
||||
const char *basename;
|
||||
SPICEdev **devs;
|
||||
int *devnum;
|
||||
Evt_Udn_Info_t **udns;
|
||||
int *udnnum;
|
||||
};
|
||||
|
||||
#define NGCM_ENTRY(cm) \
|
||||
{ #cm ".cm", ngcm_##cm##_cmDEVices, &ngcm_##cm##_cmDEVicesCNT, \
|
||||
ngcm_##cm##_cmEVTudns, &ngcm_##cm##_cmEVTudnCNT }
|
||||
|
||||
static const struct ngcm_static_entry ngcm_static_entries[] = {
|
||||
NGCM_ENTRY(analog),
|
||||
NGCM_ENTRY(digital),
|
||||
NGCM_ENTRY(spice2poly),
|
||||
NGCM_ENTRY(table),
|
||||
NGCM_ENTRY(tlines),
|
||||
NGCM_ENTRY(xtradev),
|
||||
NGCM_ENTRY(xtraevt),
|
||||
};
|
||||
|
||||
/* Returns load_opus()-compatible status: 0 = registered, -1 = not a bundled
|
||||
* code model (caller falls through to the dlopen path). */
|
||||
int ngcm_static_load(const char *path)
|
||||
{
|
||||
const char *base = strrchr(path, '/');
|
||||
size_t i;
|
||||
|
||||
base = base ? base + 1 : path;
|
||||
|
||||
for (i = 0; i < sizeof(ngcm_static_entries) / sizeof(ngcm_static_entries[0]); i++) {
|
||||
const struct ngcm_static_entry *e = &ngcm_static_entries[i];
|
||||
|
||||
if (strcmp(base, e->basename) == 0) {
|
||||
add_device(*e->devnum, e->devs, 1);
|
||||
add_udn(*e->udnnum, e->udns);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
#endif /* NGCM_STATIC && XSPICE */
|
||||
40
scripts/deps/ngspice-wasm/smoke/run-smoke.sh
Executable file
40
scripts/deps/ngspice-wasm/smoke/run-smoke.sh
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
#!/bin/bash
|
||||
# Gate-1 smoke for the wasm ngspice build: compiles smoke.c against the
|
||||
# installed sysroot artifacts and runs it under node. Proves, in one shot:
|
||||
# static sharedspice links; RC transient numerics; XSPICE code models resolve
|
||||
# through the static registry; CIDER (numd) simulates; bg_run/bg_halt work on
|
||||
# a real pthread. See smoke.c for the assertions.
|
||||
#
|
||||
# Usage: scripts/deps/ngspice-wasm/smoke/run-smoke.sh
|
||||
# (build ngspice first: scripts/deps/build-ngspice.sh)
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
QUIET=1 source "${SCRIPT_DIR}/../../../common/env.sh"
|
||||
|
||||
SMOKE_BUILD="${BUILD_ROOT}/ngspice-smoke"
|
||||
mkdir -p "${SMOKE_BUILD}"
|
||||
|
||||
# NODERAWFS: the wasm module sees the real filesystem, so the NGSPICEDATADIR
|
||||
# baked into libngspice (the sysroot prefix) resolves and spinit + code-model
|
||||
# registration run exactly as they will in the service worker.
|
||||
# PROXY_TO_PTHREAD: main() may block (usleep) while ngspice's bg thread runs.
|
||||
emcc "${SCRIPT_DIR}/smoke.c" -o "${SMOKE_BUILD}/smoke.js" \
|
||||
-I"${SYSROOT}/include" \
|
||||
-O1 -g -pthread ${DEPS_EH_FLAGS} \
|
||||
"${SYSROOT}/lib/libngspice.a" \
|
||||
"${SYSROOT}"/lib/ngspice/*.cm \
|
||||
"${SYSROOT}/lib/ngspice/ngcm_common.a" \
|
||||
-sENVIRONMENT=node \
|
||||
-sNODERAWFS=1 \
|
||||
-sALLOW_MEMORY_GROWTH=1 \
|
||||
-sINITIAL_MEMORY=256MB \
|
||||
-sPROXY_TO_PTHREAD \
|
||||
-sPTHREAD_POOL_SIZE=8 \
|
||||
-sEXIT_RUNTIME=1 \
|
||||
-sSTACK_SIZE=4MB \
|
||||
-sDEFAULT_PTHREAD_STACK_SIZE=2MB
|
||||
|
||||
# Optional argument: run a single scenario (rc|xspice|cider|halt).
|
||||
node "${SMOKE_BUILD}/smoke.js" "$@"
|
||||
330
scripts/deps/ngspice-wasm/smoke/smoke.c
Normal file
330
scripts/deps/ngspice-wasm/smoke/smoke.c
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
/*
|
||||
* Gate-1 smoke test for the wasm ngspice sharedspice static build.
|
||||
* Runs under node (see run-smoke.sh). Four scenarios, each printing
|
||||
* "SMOKE PASS <name>" on success; any failure prints "SMOKE FAIL <name>: why"
|
||||
* and exits non-zero at the end.
|
||||
*
|
||||
* rc - foreground .tran of an RC charge curve, numeric check
|
||||
* xspice - gain a-device .op (proves the static code-model registry:
|
||||
* the model only exists if spinit's codemodel lines resolved)
|
||||
* cider - numd (CIDER) silicon resistor DC sweep completes
|
||||
* halt - bg_run on a heavy deck, bg_halt mid-run, BGThreadRunning
|
||||
* callback fires with finished=true (proves the real pthread path)
|
||||
*
|
||||
* Built with -sPROXY_TO_PTHREAD so main() may block (usleep) while ngspice's
|
||||
* own background thread simulates.
|
||||
*/
|
||||
|
||||
#include <stdatomic.h>
|
||||
#include <stdbool.h> /* sharedspice.h's NG_BOOL fallback typedef needs it */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "ngspice/sharedspice.h"
|
||||
|
||||
static atomic_int g_bg_finished_events;
|
||||
static atomic_int g_exit_called;
|
||||
static atomic_int g_char_lines;
|
||||
static int g_failures;
|
||||
|
||||
static int cb_send_char(char *what, int id, void *user)
|
||||
{
|
||||
(void) id;
|
||||
(void) user;
|
||||
atomic_fetch_add(&g_char_lines, 1);
|
||||
if (getenv("SMOKE_VERBOSE"))
|
||||
fprintf(stderr, "[ngspice] %s\n", what);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cb_send_stat(char *what, int id, void *user)
|
||||
{
|
||||
(void) what;
|
||||
(void) id;
|
||||
(void) user;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cb_controlled_exit(int status, NG_BOOL immediate, NG_BOOL quit,
|
||||
int id, void *user)
|
||||
{
|
||||
(void) immediate;
|
||||
(void) quit;
|
||||
(void) id;
|
||||
(void) user;
|
||||
fprintf(stderr, "[smoke] ControlledExit status=%d\n", status);
|
||||
atomic_store(&g_exit_called, 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cb_bg_running(NG_BOOL finished, int id, void *user)
|
||||
{
|
||||
(void) id;
|
||||
(void) user;
|
||||
if (finished)
|
||||
atomic_fetch_add(&g_bg_finished_events, 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void fail(const char *name, const char *why)
|
||||
{
|
||||
printf("SMOKE FAIL %s: %s\n", name, why);
|
||||
g_failures++;
|
||||
}
|
||||
|
||||
static void pass(const char *name)
|
||||
{
|
||||
printf("SMOKE PASS %s\n", name);
|
||||
}
|
||||
|
||||
/* Fetch a vector, returning its length and (optionally) the last real value. */
|
||||
static int vec_last(const char *vec, int *len_out, double *last_out)
|
||||
{
|
||||
pvector_info vi = ngGet_Vec_Info((char *) vec);
|
||||
|
||||
if (!vi || vi->v_length <= 0)
|
||||
return -1;
|
||||
if (len_out)
|
||||
*len_out = vi->v_length;
|
||||
if (last_out) {
|
||||
if (vi->v_realdata)
|
||||
*last_out = vi->v_realdata[vi->v_length - 1];
|
||||
else if (vi->v_compdata)
|
||||
*last_out = vi->v_compdata[vi->v_length - 1].cx_real;
|
||||
else
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int run_circ(const char *const *lines)
|
||||
{
|
||||
/* ngSpice_Circ wants a NULL-terminated array of writable strings. */
|
||||
int n = 0;
|
||||
while (lines[n])
|
||||
n++;
|
||||
|
||||
char **arr = malloc((size_t) (n + 1) * sizeof(char *));
|
||||
for (int i = 0; i < n; i++)
|
||||
arr[i] = strdup(lines[i]);
|
||||
arr[n] = NULL;
|
||||
|
||||
int ret = ngSpice_Circ(arr);
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
free(arr[i]);
|
||||
free(arr);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void test_rc(void)
|
||||
{
|
||||
static const char *const deck[] = {
|
||||
"rc smoke",
|
||||
"V1 in 0 1",
|
||||
"R1 in out 1k",
|
||||
"C1 out 0 1u",
|
||||
".tran 10u 5m",
|
||||
".end",
|
||||
NULL,
|
||||
};
|
||||
|
||||
if (run_circ(deck) != 0)
|
||||
return fail("rc", "ngSpice_Circ failed");
|
||||
if (ngSpice_Command("run") != 0)
|
||||
return fail("rc", "run command failed");
|
||||
|
||||
int len = 0;
|
||||
double last = 0.0;
|
||||
if (vec_last("out", &len, &last) != 0)
|
||||
return fail("rc", "vector 'out' missing");
|
||||
|
||||
/* 5 tau: v = 1 - exp(-5) = 0.99326 */
|
||||
if (last < 0.98 || last > 1.0) {
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof buf, "v(out) final %.5f not in [0.98, 1.0] (len %d)",
|
||||
last, len);
|
||||
return fail("rc", buf);
|
||||
}
|
||||
pass("rc");
|
||||
}
|
||||
|
||||
static void test_xspice(void)
|
||||
{
|
||||
static const char *const deck[] = {
|
||||
"xspice smoke",
|
||||
"V1 in 0 2",
|
||||
"A1 in aout gainblk",
|
||||
".model gainblk gain(gain=3)",
|
||||
"R1 aout 0 1k",
|
||||
".op",
|
||||
".end",
|
||||
NULL,
|
||||
};
|
||||
|
||||
if (run_circ(deck) != 0)
|
||||
return fail("xspice", "ngSpice_Circ failed (code models not registered?)");
|
||||
if (ngSpice_Command("run") != 0)
|
||||
return fail("xspice", "run command failed");
|
||||
|
||||
double v = 0.0;
|
||||
if (vec_last("aout", NULL, &v) != 0)
|
||||
return fail("xspice", "vector 'aout' missing");
|
||||
if (v < 5.999 || v > 6.001) {
|
||||
char buf[96];
|
||||
snprintf(buf, sizeof buf, "v(aout) %.6f != 6.0", v);
|
||||
return fail("xspice", buf);
|
||||
}
|
||||
pass("xspice");
|
||||
}
|
||||
|
||||
static void test_cider(void)
|
||||
{
|
||||
/* Reduced examples/cider/resistor/sires.cir: numd level=1 needs the whole
|
||||
* CIDER machinery (mesh, doping, mobility models) to produce a current. */
|
||||
static const char *const deck[] = {
|
||||
"cider smoke - silicon resistor",
|
||||
"VPP 1 0 2v",
|
||||
"VNN 2 0 0.0v",
|
||||
"D1 1 2 M_RES AREA=1",
|
||||
".MODEL M_RES numd level=1",
|
||||
"+ options resistor defa=1p",
|
||||
"+ x.mesh loc=0.0 num=1",
|
||||
"+ x.mesh loc=1.0 num=21",
|
||||
"+ domain num=1 material=1",
|
||||
"+ material num=1 silicon",
|
||||
"+ doping unif n.type conc=2.5e16",
|
||||
"+ models bgn srh conctau auger concmob fieldmob",
|
||||
".DC VPP 0.0v 2.01v 0.5v",
|
||||
".END",
|
||||
NULL,
|
||||
};
|
||||
|
||||
if (run_circ(deck) != 0)
|
||||
return fail("cider", "ngSpice_Circ failed (CIDER not compiled in?)");
|
||||
if (ngSpice_Command("run") != 0)
|
||||
return fail("cider", "run command failed");
|
||||
|
||||
int len = 0;
|
||||
double i_last = 0.0;
|
||||
if (vec_last("vpp#branch", &len, &i_last) != 0)
|
||||
return fail("cider", "vector 'vpp#branch' missing");
|
||||
if (len < 4)
|
||||
return fail("cider", "DC sweep produced too few points");
|
||||
if (!(i_last < 0.0) || i_last < -1.0)
|
||||
return fail("cider", "resistor current magnitude implausible");
|
||||
pass("cider");
|
||||
}
|
||||
|
||||
static void test_halt(void)
|
||||
{
|
||||
/* Heavy enough that bg_halt lands mid-run: a long transient of a 100-stage
|
||||
* nonlinear RC/diode ladder, storage bounded via .save. */
|
||||
const int stages = 100;
|
||||
const char **deck = malloc((size_t) (stages * 3 + 8) * sizeof(char *));
|
||||
char **owned = malloc((size_t) (stages * 3 + 8) * sizeof(char *));
|
||||
int n = 0;
|
||||
|
||||
owned[n] = strdup("halt smoke - rc/diode ladder");
|
||||
deck[n] = owned[n];
|
||||
n++;
|
||||
owned[n] = strdup("V1 n0 0 SIN(0 5 10k)");
|
||||
deck[n] = owned[n];
|
||||
n++;
|
||||
for (int i = 0; i < stages; i++) {
|
||||
char line[96];
|
||||
snprintf(line, sizeof line, "R%d n%d n%d 100", i + 1, i, i + 1);
|
||||
owned[n] = strdup(line);
|
||||
deck[n] = owned[n];
|
||||
n++;
|
||||
snprintf(line, sizeof line, "C%d n%d 0 10n", i + 1, i + 1);
|
||||
owned[n] = strdup(line);
|
||||
deck[n] = owned[n];
|
||||
n++;
|
||||
snprintf(line, sizeof line, "D%d n%d 0 dmod", i + 1, i + 1);
|
||||
owned[n] = strdup(line);
|
||||
deck[n] = owned[n];
|
||||
n++;
|
||||
}
|
||||
owned[n] = strdup(".model dmod d(is=1e-14)");
|
||||
deck[n] = owned[n];
|
||||
n++;
|
||||
owned[n] = strdup(".save v(n100)");
|
||||
deck[n] = owned[n];
|
||||
n++;
|
||||
owned[n] = strdup(".tran 100n 10");
|
||||
deck[n] = owned[n];
|
||||
n++;
|
||||
owned[n] = strdup(".end");
|
||||
deck[n] = owned[n];
|
||||
n++;
|
||||
deck[n] = NULL;
|
||||
owned[n] = NULL;
|
||||
|
||||
int circ_ret = ngSpice_Circ((char **) deck);
|
||||
for (int i = 0; i < n; i++)
|
||||
free(owned[i]);
|
||||
free(owned);
|
||||
free(deck);
|
||||
|
||||
if (circ_ret != 0)
|
||||
return fail("halt", "ngSpice_Circ failed");
|
||||
|
||||
int before = atomic_load(&g_bg_finished_events);
|
||||
|
||||
if (ngSpice_Command("bg_run") != 0)
|
||||
return fail("halt", "bg_run command failed");
|
||||
|
||||
/* Give the background thread time to actually start and chew. */
|
||||
usleep(400 * 1000);
|
||||
|
||||
if (!ngSpice_running())
|
||||
return fail("halt", "ngSpice_running false 400ms into a 10s transient "
|
||||
"(bg thread never started or deck too light)");
|
||||
|
||||
if (ngSpice_Command("bg_halt") != 0)
|
||||
return fail("halt", "bg_halt command failed");
|
||||
|
||||
/* bg_halt joins the bg thread; give the finished callback a moment. */
|
||||
for (int i = 0; i < 50 && atomic_load(&g_bg_finished_events) == before; i++)
|
||||
usleep(100 * 1000);
|
||||
|
||||
if (atomic_load(&g_bg_finished_events) == before)
|
||||
return fail("halt", "BGThreadRunning(finished) never fired after bg_halt");
|
||||
if (ngSpice_running())
|
||||
return fail("halt", "still running after bg_halt");
|
||||
|
||||
pass("halt");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
/* Optional argv[1]: run only the named scenario (rc|xspice|cider|halt). */
|
||||
const char *only = argc > 1 ? argv[1] : NULL;
|
||||
|
||||
int ret = ngSpice_Init(cb_send_char, cb_send_stat, cb_controlled_exit,
|
||||
NULL, NULL, cb_bg_running, NULL);
|
||||
if (ret != 0) {
|
||||
printf("SMOKE FAIL init: ngSpice_Init returned %d\n", ret);
|
||||
return 1;
|
||||
}
|
||||
printf("SMOKE PASS init\n");
|
||||
|
||||
if (!only || !strcmp(only, "rc"))
|
||||
test_rc();
|
||||
if (!only || !strcmp(only, "xspice"))
|
||||
test_xspice();
|
||||
if (!only || !strcmp(only, "cider"))
|
||||
test_cider();
|
||||
if (!only || !strcmp(only, "halt"))
|
||||
test_halt();
|
||||
|
||||
if (atomic_load(&g_exit_called))
|
||||
fail("exit", "ControlledExit fired during the smoke run");
|
||||
|
||||
printf(g_failures ? "SMOKE RESULT: %d failure(s)\n" : "SMOKE RESULT: all passed\n",
|
||||
g_failures);
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
Loading…
Reference in a new issue