penpot/render-wasm
Andrey Antukh 76a83387d9
⬆️ Update JVM, Node, pnpm and devenv dependencies (#12600)
Move the backend JDK to its latest maintenance build within Java 26 and
refresh the dependency versions pinned by hand in the development and
production images.

- Backend: Zulu JDK 26.0.2 to 26.0.2.1. The jlink module list keeps
  jdk.management, so the prometheus process metrics stay exported.
- Node.js 24.22.1 in .nvmrc, the development image tarball and the two
  plugins deploy workflows that pin it.
- pnpm 12.15.0 in the four Dockerfiles, the 34 packageManager fields and
  the eleven lockfiles.
- Development tools: clj-kondo 2026.10.20, Babashka 1.13.226, Pixi
  0.83.0, GitHub CLI 2.103.0, AWS CLI 2.38.5 and uv 0.13.5.
- Infra services: RustFS 1.0.3, Mailpit v1.32.0, and Valkey pinned to
  8.1.10 instead of the floating 8.1 tag.

Fix SERENA_UPDATE_VERSION in the compose file. A value there makes the
start script reinstall that Serena release on every start, overriding
the one baked into the image; it was replacing 1.7.0 with 1.6.1.

The hardened base image updates are left to Renovate.

Closes #12599

AI-assisted-by: deepseek-v4.1-flash

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-10-23 12:36:11 +02:00
..
2026-03-18 18:05:30 +01:00
2026-10-13 16:33:51 +02:00

Penpot WASM render

This is the canvas-based WebAssembly render engine for Penpot.

Rust & Emscripten

This project is a Rust crate that targets Emscripten (wasm32-unknown-emscripten).

We use wasm32-unknown-emscripten compilation target:

  • It compiles Rust code into WASM
  • It generates the JavaScript code (“glue”) to load and run the WASM code

How Rust, Emscripten, and WASM are connected

Skia

We use Skia, an Open Source 2D graphics library. In particular, the render engine uses Skia via custom binaries of the rust-skia crate.

How to build

With the Penpot Development Environment running, create a new tab in the tmux.

cd penpot/render-wasm
./build

You can also use ./watch to run the build on every change.

The build script compiles the project and copies the .js and .wasm files to the app that uses each target.

Render targets

The same Rust source produces two artifacts, which differ only in compiler options:

Target Tuned for Cargo profile Consumed by
frontend speed release (-O3) frontend/resources/public/js
export size size (-Oz) exporter/resources/wasm
./build            # both targets, frontend first
./build frontend   # workspace / viewer renderer
./build export     # headless exporter renderer

./watch still follows a single target (frontend unless you pass one), since watching both would rebuild twice on every keystroke.

Each target keeps its own CARGO_TARGET_DIR (target/<target>), so switching between them does not invalidate the other's cache. Set BUILD_MODE=release (or NODE_ENV=production) for an optimized build; the default is debug.

Serialize builds in one checkout

All targets in one checkout share one flock lock stored at render-wasm/.render-wasm-build.lock. The lock covers dependency setup, the Cargo build, artifact copy, and target cleanup. If another build or cleanup already holds the lock, the new process prints a waiting message and starts only after the first process exits.

The watch command takes the lock for each build, then releases it while it waits for source changes. The lock file stays in the checkout after a build, but the operating system releases its lock when the process exits, including after an error or signal.

The lock applies to one checkout and only to commands that use these scripts. A manual Cargo build can still write to the same target without taking the lock. Set RENDER_WASM_LOCK_FILE to the same path in each process when you make different checkouts share a CARGO_TARGET_DIR. The supported Linux build environment must provide flock from util-linux; flock --version checks this dependency.

Use the target cleanup commands instead of running cargo clean on the shared target/ directory:

./clean frontend   # remove target/frontend only
./clean export     # remove target/export only

Each target writes its own generated shared.js (the enum discriminants the CLJS side compiles against) next to the code that imports it — respectively frontend/src/app/render_wasm/api/shared.js and exporter/src/app/wasm/shared.js. Neither build writes to the other's paths.

Architecture overview

Edit your local frontend/resources/public/js/config.js to add the following flags:

  • enable-feature-render-wasm to enable this render engine.
  • enable-render-wasm-dpr (optional), to enable using the device pixel ratio.

How to test

We currently have two types of tests:

  • Unit tests
cd penpot/render-wasm
./test

Technical documentation