Skip to content

fix(asset): KTX2 refused on WeChat — WebGL2 check used a DOM global #476

fix(asset): KTX2 refused on WeChat — WebGL2 check used a DOM global

fix(asset): KTX2 refused on WeChat — WebGL2 check used a DOM global #476

Workflow file for this run

name: Build
# Push-gated CI: every master push must keep the architecture invariants green —
# boundary guards, full-component ABI static_asserts (compiled by the web build),
# the C++ doctest harnesses, and both TS suites. workflow_dispatch stays for
# re-runs and branch experiments.
on:
push:
branches: [master]
workflow_dispatch:
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
jobs:
build-emscripten:
name: Web (Emscripten) + C++ tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'pnpm'
- name: Setup Emscripten
uses: mymindstorm/setup-emsdk@v14
with:
version: 5.0.0
- name: Setup ccache
uses: hendrikmuhs/ccache-action@v1.2.20
with:
key: ${{ runner.os }}-emscripten
max-size: 500M
- name: Install dependencies
run: pnpm install
- name: Build Web target
run: node build-tools/cli.js build -t web --no-sync
- name: Build Playable target
run: node build-tools/cli.js build -t playable --no-sync
- name: Build WeChat target
run: node build-tools/cli.js build -t wechat --no-sync
- name: Verify build outputs
run: |
test -f build/wasm/web/esengine.js && echo "✓ Web JS"
test -f build/wasm/web/esengine.wasm && echo "✓ Web WASM"
test -f build/wasm/playable/esengine.single.js && echo "✓ Playable JS"
test -f build/wasm/wechat/esengine.wxgame.js && echo "✓ WeChat JS"
test -f build/wasm/wechat/esengine.wxgame.wasm && echo "✓ WeChat WASM"
- name: Build C++ test harnesses
run: |
emcmake cmake -S . -B build/wasm/web-tests -DCMAKE_BUILD_TYPE=Release -DES_BUILD_WEB=ON -DES_BUILD_TESTS=ON
cmake --build build/wasm/web-tests -j "$(nproc)" --target \
test_ecs test_events test_particle test_tilemap test_sparse_set \
test_resource_pool test_shader_variant test_log_format test_registry_safety \
test_shader_device test_draw_params test_texture_fb test_buffer_device \
test_compressed_format test_batch_builder test_boundary_span test_webgpu_device \
webgpu_engine_bringup
# The root package.json is ESM, so the emscripten-emitted .js must run as .cjs.
- name: Run C++ test harnesses
run: |
for t in test_ecs test_events test_particle test_tilemap test_sparse_set \
test_resource_pool test_shader_variant test_log_format test_registry_safety \
test_shader_device test_draw_params test_texture_fb test_buffer_device \
test_compressed_format test_batch_builder test_boundary_span test_webgpu_device; do
cp "build/wasm/web-tests/bin/$t.js" "build/wasm/web-tests/bin/$t.cjs"
echo "=== $t"
node "build/wasm/web-tests/bin/$t.cjs"
done
# No --continue-on-error: a side-module compile break must FAIL the gate.
# It used to be swallowed here, so a physics-module error lived on master
# for days and only surfaced when a release ran `build -t all` for real.
- name: Build remaining targets (spine, physics)
run: node build-tools/cli.js build -t all --no-sync
- name: Upload web wasm artifact
uses: actions/upload-artifact@v4
with:
name: wasm-web
path: build/wasm/web/
retention-days: 3
test:
name: Tests (SDK + Desktop)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'pnpm'
- name: Check GL boundary (no gl* outside GLDevice)
run: node tools/check-gl-boundary.mjs
- name: Check render-command boundary (DrawCommands built only by BatchBuilder)
run: node tools/check-draw-command-boundary.mjs
- name: Install dependencies
run: pnpm install
- name: Type-check SDK
run: pnpm exec tsc --noEmit -p sdk/tsconfig.json
- name: Check API surface (snapshot + release-tag policy)
run: node tools/api-surface.mjs --check
- name: Build SDK
run: pnpm --filter ./sdk build
- name: Type-check Examples
run: node build-tools/cli.js check-examples
- name: Run SDK tests
run: pnpm --filter ./sdk exec vitest run
- name: Type-check Desktop
run: pnpm --filter @estella/editor exec tsc --noEmit
- name: Run Desktop tests
run: pnpm --filter @estella/editor exec vitest run
# Headless pixel verification: real scenes rendered by the editor's electron
# host under xvfb (SwiftShader WebGL), asserted pixel-by-pixel.
verify-render:
name: Render verify (headless)
runs-on: ubuntu-latest
needs: build-emscripten
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'pnpm'
- name: Download web wasm artifact
uses: actions/download-artifact@v4
with:
name: wasm-web
path: build/wasm/web/
- name: Install dependencies
run: pnpm install
- name: Build SDK
run: pnpm --filter ./sdk build
- name: Stage wasm for the editor host
run: |
mkdir -p desktop/public/wasm
cp build/wasm/web/* desktop/public/wasm/
- name: Build editor renderer bundle
working-directory: desktop
run: pnpm exec vite build
# Twin coverage guard: every fixture .esshader must carry its WGSL twin
# (hand-written, generated, or switch-skipped) — a twin-less shader is
# WebGPU-dead by design and gen-shader-twins --check exits 1 on it.
- name: Check shader twins
run: node tools/gen-shader-twins.mjs --check $(find desktop/public/scenes -name '*.esshader')
# Pipeline smoke: strip a fixture's twins into a temp copy and require the
# vendored converters (glslang wasm + naga WASI wasm) to produce one —
# exercises the whole GLSL→SPIR-V→WGSL chain on every CI run.
- name: Shader-twin pipeline smoke
run: |
tmp=$(mktemp -d)
sed '/#pragma vertex wgsl/,$d' desktop/public/scenes/mat-tint/tint.esshader > "$tmp/smoke.esshader"
if node tools/gen-shader-twins.mjs --check "$tmp/smoke.esshader"; then
echo "expected would-generate (exit 1) for a twin-less shader"; exit 1
fi
- name: Run headless verify scenes
working-directory: desktop
env:
# CI runners can't use the SUID sandbox helper (not root-owned in
# node_modules); rendering verification doesn't need it.
ELECTRON_DISABLE_SANDBOX: '1'
run: |
run_scene() {
echo "=== $1"
shift
env "$@" xvfb-run -a pnpm exec electron scripts/headless-verify.mjs
}
run_scene sprite-default
# Editor grid on/off pixel diff — guards the custom-draw reflected
# material path (drawMeshWithMaterial + MaterialConstants UBO).
run_scene editor-grid ESTELLA_VERIFY_GRID=64 ESTELLA_VERIFY_STEPS=3
run_scene mesh2d \
ESTELLA_VERIFY_SCENE=/scenes/mesh2d.esscene \
ESTELLA_VERIFY_MANIFEST=/scenes/mesh2d.textures.json \
ESTELLA_VERIFY_W=256 ESTELLA_VERIFY_H=256 ESTELLA_VERIFY_STEPS=4 \
'ESTELLA_VERIFY_EXPECT=[{"x":0.30,"y":0.556,"rgb":[255,0,0],"tol":40},{"x":0.70,"y":0.556,"rgb":[0,255,0],"tol":40},{"x":0.30,"y":0.40,"rgb":[255,0,0],"tol":40}]'
run_scene parallax-shape \
ESTELLA_VERIFY_SCENE=/scenes/parallax-shape.esscene \
ESTELLA_VERIFY_MANIFEST=/scenes/parallax-shape.textures.json \
ESTELLA_VERIFY_STEPS=4 \
'ESTELLA_VERIFY_EXPECT=[{"x":0.5,"y":0.5,"rgb":[0,255,0],"tol":40},{"x":0.25,"y":0.5,"rgb":[255,0,0],"tol":40}]'
run_scene tilemap-flip \
ESTELLA_VERIFY_SCENE=/scenes/tilemap-flip.esscene \
ESTELLA_VERIFY_MANIFEST=/scenes/tilemap-flip.textures.json \
ESTELLA_VERIFY_W=256 ESTELLA_VERIFY_H=256 ESTELLA_VERIFY_STEPS=2 \
'ESTELLA_VERIFY_EXPECT=[{"x":0.40,"y":0.36,"rgb":[255,0,0]},{"x":0.60,"y":0.36,"rgb":[0,255,0]},{"x":0.40,"y":0.58,"rgb":[0,0,255]},{"x":0.60,"y":0.58,"rgb":[255,255,0]}]'
# Tiled tile (GID) object: an object-layer object with a gid renders as a
# positioned Sprite (bottom-left anchor, tileset UV) — the green tile centered.
run_scene tilemap-gidobj \
ESTELLA_VERIFY_SCENE=/scenes/tilemap-gidobj.esscene \
ESTELLA_VERIFY_MANIFEST=/scenes/tilemap-gidobj.textures.json \
ESTELLA_VERIFY_W=256 ESTELLA_VERIFY_H=256 ESTELLA_VERIFY_STEPS=10 \
'ESTELLA_VERIFY_EXPECT=[{"x":0.5,"y":0.5,"rgb":[0,255,0],"tol":40},{"x":0.5,"y":0.12,"rgb":[0,0,0],"tol":30}]'
run_scene ui-mask \
ESTELLA_VERIFY_SCENE=/scenes/ui-list-mask.esscene \
ESTELLA_VERIFY_MANIFEST=/scenes/ui-list-mask.textures.json \
ESTELLA_VERIFY_W=800 ESTELLA_VERIFY_H=600 \
'ESTELLA_VERIFY_EXPECT=[{"x":0.5,"y":0.5,"rgb":[255,0,0],"tol":60},{"x":0.9,"y":0.5,"rgb":[0,0,255],"tol":60},{"x":0.5,"y":0.85,"rgb":[0,0,255],"tol":60},{"x":0.5,"y":0.15,"rgb":[0,0,255],"tol":60}]'
# Y-sort: same scene twice — mask on flips the overlap to the lower
# (higher-Y-key) entity; mask off preserves submission order. Both
# asserted so neither path can silently regress.
run_scene ysort-on \
ESTELLA_VERIFY_YSORT=1 \
ESTELLA_VERIFY_SCENE=/scenes/ysort.esscene \
ESTELLA_VERIFY_MANIFEST=/scenes/ysort.textures.json \
ESTELLA_VERIFY_W=256 ESTELLA_VERIFY_H=256 ESTELLA_VERIFY_STEPS=2 \
'ESTELLA_VERIFY_EXPECT=[{"x":0.5,"y":0.5,"rgb":[0,255,0],"tol":30},{"x":0.5,"y":0.25,"rgb":[255,0,0],"tol":30},{"x":0.5,"y":0.75,"rgb":[0,255,0],"tol":30}]'
run_scene ysort-off \
ESTELLA_VERIFY_SCENE=/scenes/ysort.esscene \
ESTELLA_VERIFY_MANIFEST=/scenes/ysort.textures.json \
ESTELLA_VERIFY_W=256 ESTELLA_VERIFY_H=256 ESTELLA_VERIFY_STEPS=2 \
'ESTELLA_VERIFY_EXPECT=[{"x":0.5,"y":0.5,"rgb":[255,0,0],"tol":30}]'
# Motion trail: move a TrailRenderer entity across the frame in play mode,
# asserting the ribbon rasterizes as a green band (head/mid/tail) with black
# above it — the CPU-ribbon-through-the-batch-face path.
run_scene trail \
ESTELLA_VERIFY_PLAY=1 \
ESTELLA_VERIFY_SCENE=/scenes/trail.esscene \
ESTELLA_VERIFY_MANIFEST=/scenes/trail.textures.json \
ESTELLA_VERIFY_W=256 ESTELLA_VERIFY_H=256 \
'ESTELLA_VERIFY_TRAIL={"from":[-200,0],"to":[200,0],"steps":20}' \
'ESTELLA_VERIFY_EXPECT=[{"x":0.80,"y":0.5,"rgb":[0,255,0],"tol":50},{"x":0.5,"y":0.5,"rgb":[0,255,0],"tol":50},{"x":0.25,"y":0.5,"rgb":[0,255,0],"tol":60},{"x":0.5,"y":0.15,"rgb":[0,0,0],"tol":30}]'
# The SAME pixel assertions on the WebGPU backend — Dawn renders through the
# bundled SwiftShader Vulkan (--use-webgpu-adapter=swiftshader), so a GPU-less
# runner verifies the second backend for real: WGSL twins, the reflected
# material path, the editor grid, and the async readback (material preview).
- name: Run headless verify scenes (WebGPU / SwiftShader)
working-directory: desktop
env:
ELECTRON_DISABLE_SANDBOX: '1'
ESTELLA_VERIFY_BACKEND: webgpu
ESTELLA_VERIFY_WEBGPU_ADAPTER: swiftshader
run: |
run_scene() {
echo "=== $1"
shift
env "$@" xvfb-run -a pnpm exec electron scripts/headless-verify.mjs
}
run_scene webgpu-sprite-grid ESTELLA_VERIFY_GRID=64 ESTELLA_VERIFY_STEPS=3
run_scene webgpu-parallax-shape \
ESTELLA_VERIFY_SCENE=/scenes/parallax-shape.esscene \
ESTELLA_VERIFY_MANIFEST=/scenes/parallax-shape.textures.json \
ESTELLA_VERIFY_STEPS=4 \
'ESTELLA_VERIFY_EXPECT=[{"x":0.5,"y":0.5,"rgb":[0,255,0],"tol":40},{"x":0.25,"y":0.5,"rgb":[255,0,0],"tol":40}]'
run_scene webgpu-mat-lit-point \
ESTELLA_VERIFY_SCENE=/scenes/mat-lit-point.esscene \
ESTELLA_VERIFY_MANIFEST=/scenes/mat-lit-point.textures.json \
ESTELLA_VERIFY_W=256 ESTELLA_VERIFY_H=256 ESTELLA_VERIFY_STEPS=2 \
'ESTELLA_VERIFY_EXPECT=[{"x":0.5,"y":0.5,"rgb":[0,255,0],"tol":50},{"x":0.75,"y":0.5,"rgb":[0,64,0],"tol":60}]'
run_scene webgpu-mat-tint-preview \
ESTELLA_VERIFY_SCENE=/scenes/mat-tint.esscene \
ESTELLA_VERIFY_MANIFEST=/scenes/mat-tint.textures.json \
ESTELLA_VERIFY_W=256 ESTELLA_VERIFY_H=256 ESTELLA_VERIFY_STEPS=2 \
'ESTELLA_VERIFY_EXPECT=[{"x":0.5,"y":0.5,"rgb":[255,0,0],"tol":40}]' \
'ESTELLA_VERIFY_PREVIEW={"w":64,"h":64,"rgb":[255,0,0],"tol":45}'
run_scene webgpu-ysort \
ESTELLA_VERIFY_YSORT=1 \
ESTELLA_VERIFY_SCENE=/scenes/ysort.esscene \
ESTELLA_VERIFY_MANIFEST=/scenes/ysort.textures.json \
ESTELLA_VERIFY_W=256 ESTELLA_VERIFY_H=256 ESTELLA_VERIFY_STEPS=2 \
'ESTELLA_VERIFY_EXPECT=[{"x":0.5,"y":0.5,"rgb":[0,255,0],"tol":30},{"x":0.5,"y":0.25,"rgb":[255,0,0],"tol":30},{"x":0.5,"y":0.75,"rgb":[0,255,0],"tol":30}]'
run_scene webgpu-trail \
ESTELLA_VERIFY_PLAY=1 \
ESTELLA_VERIFY_SCENE=/scenes/trail.esscene \
ESTELLA_VERIFY_MANIFEST=/scenes/trail.textures.json \
ESTELLA_VERIFY_W=256 ESTELLA_VERIFY_H=256 \
'ESTELLA_VERIFY_TRAIL={"from":[-200,0],"to":[200,0],"steps":20}' \
'ESTELLA_VERIFY_EXPECT=[{"x":0.80,"y":0.5,"rgb":[0,255,0],"tol":50},{"x":0.5,"y":0.5,"rgb":[0,255,0],"tol":50},{"x":0.25,"y":0.5,"rgb":[0,255,0],"tol":60},{"x":0.5,"y":0.15,"rgb":[0,0,0],"tol":30}]'