本文へ移動
cccskills
無料GitHub で公開

skia-gpu-build

Enable a Skia + Dawn GPU build of Pulp (MacGpuWindowHost, Skia Graphite). Covers the prebuilt skia-builder binaries, the headers-only fresh-worktree trap, reusing another checkout's cached libs via SKIA_DIR, FindSkia layouts, verifying PULP_HAS_SKIA / MacGpuWindowHost, and the raster-fallback + GPU-wedge gotchas. Use whenever GPU rendering "doesn't work" or a build silently came up CPU-only.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md42.3 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Building Pulp with Skia + GPU

Pulp's GPU path is Skia Graphite over Dawn (WebGPU). PULP_ENABLE_GPU is ON by default, but GPU only actually turns on when CMake can find the prebuilt Skia libraries. If it can't, the build silently comes up CPU-only (CoreGraphics raster, MacWindowHost) with no hard error — that's the #1 "GPU doesn't work" cause.

Start with installed GPU health evidence

Before inspecting libraries or opening a window, run:

pulp doctor gpu --json

This performs bounded render/readback and compute/map work through the installed Pulp paths. Exit 0 means all required real-work proofs passed and at least one required probe has authentic identity; exit 1 is a completed measured failure, and exit 2 means requested evidence is unavailable or unverified. Use pulp doctor gpu --no-render --json for inventory/preflight without acquiring a GPU device; it returns unverified, never passed. The pulp_gpu_doctor MCP tool returns the same typed evidence and both installed surfaces work independently of the current directory.

Record adapter identity exactly as Dawn reports it. A backend label such as Metal, D3D12, or Vulkan does not prove that the adapter is hardware or discrete, and null/software adapters are never hardware passes. Probe identities are independent; do not infer that separately acquired Renderer3D, HeadlessSurface, and GpuCompute devices are the same device. Continue with the manual bundle and symbol checks below when the result is unavailable/unverified or when a completed probe fails.

The fresh-worktree trap (most common)

external/skia-build/ in a fresh worktree often contains only headers — include/, modules/, VERSION.md — and no compiled libs. FindSkia needs external/skia-build/build/<platform>-gpu/lib/Release/*.a (e.g. libskia.a, libdawn_combined.a, libskparagraph.a, libskunicode_icu.a). Headers-only ⇒ -- Skia: SKIA_DIR not set — Skia rendering disabled (or "not found") ⇒ PULP_HAS_SKIA undefined ⇒ CPU-only build.

# Are the compiled libs actually present in THIS worktree?
ls external/skia-build/build/*/lib/Release/libskia.a 2>/dev/null || echo "headers-only — no GPU"

Fastest fix: materialize the pinned immutable cache generation

Do not rebuild Skia and do not borrow mutable bytes from another checkout. Populate the manifest-selected, platform-plus-asset-SHA generation through the release fetcher, then point SKIA_DIR at that exact stamped generation:

SKIA_CACHE_ROOT="${PULP_SKIA_CACHE_ROOT:-$HOME/.cache/pulp/skia}"
python3 tools/scripts/fetch_skia_for_release.py darwin-arm64 \
  --cache-root "$SKIA_CACHE_ROOT"
SKIA_DIR="$(python3 tools/scripts/fetch_skia_for_release.py darwin-arm64 \
  --cache-root "$SKIA_CACHE_ROOT" --print-cache-dest)"

# Configure a SEPARATE GPU build dir (keep the CPU build/ for deterministic
# CoreGraphics goldens — see "Don't mix" below).
cmake -S . -B build-gpu -DCMAKE_BUILD_TYPE=Release \
  -DSKIA_DIR="$SKIA_DIR" \
  -DPULP_ENABLE_GPU=ON

SKIA_DIR may be an env var or a -D cache entry; FindSkia also auto-discovers external/skia-build and $SKIA_DIR. SKIA_DIR must point at the dir that contains build/<platform>-gpu/lib/Release + build/include (skia-builder layout); flat mac/lib + include layouts are also accepted (see tools/cmake/FindSkia.cmake).

For Android, use tools/build-skia-android.sh so the shared manifest locks the exact Skia/Dawn pair used by desktop. The script checks out the immutable Skia revision and stages the skunicode headers; mutable milestone tips and stale module headers are not valid provenance.

A valid cached generation keeps headers and libraries together and carries both the verified archive stamp and extracted-file digest receipt. An arbitrary sibling checkout is not provenance: never use its mutable external/skia-build directory as SKIA_DIR, and never mix one generation's headers with another's libraries.

Building from source (only when no cached libs anywhere)

tools/build-skia.sh builds the currently pinned chrome/m153 Skia+Dawn from source — slow (tens of minutes). Prefer the release zip from danielraffel/skia-builder (chrome/m153, see external/skia-build/VERSION.md) or the immutable fetcher-owned cache generation above.

For m153, prove the provider exposes both new integration surfaces before a product build:

python3 tools/scripts/verify_skia_m153_capabilities.py \
  --platform darwin-arm64 --skia-dir "$SKIA_DIR"

The directory must be the fetcher's exact stamped manifest generation. This compile/links/runs SkLogHandler and skgpu::graphite::ContextOptions::fExecutor; headers or unstamped local libraries alone are not a pass. SkLogHandler is process-global and first-install-wins, while the executor must outlive the Graphite context. Generic installation and executor policy are Vellum-owned; a Pulp build should consume those contracts rather than install a second global handler or duplicate context policy.

Pulp does install a handler — into an empty slot only

Pulp's GPU diagnostics sink (core/render/src/gpu_diagnostics.cpp) installs its own SkLogHandler, which reads as a violation of the consume-don't-install rule above. It is a deliberate, bounded deviation, and these are the properties that make it safe:

  • Never displaces. The installer calls SkLogHandler::GetInstance() first and returns declined_handler_present when the slot is taken. A SetInstance that returns false (somebody won the race between the read and the write) is recorded as the same declined status, not retried. A host DAW or a Vellum build that owns the handler keeps it.
  • Off unless asked. Installation is gated on skia_log_bridge_enabled() (tracing built in, or PULP_GPU_LOG_BRIDGE), so an ordinary product build installs nothing. The macOS GPU window host asks once, for a primary window only; a secondary window never does.
  • Bounded text copy. An installed bridge also writes each record to the text log (skia: [<severity>] …), because the Perfetto event does not exist in a PULP_TRACING=OFF release build. Only the first kSkiaLogTextLimit (50) are copied; the rest are counted and reported once by log_gpu_diagnostics_summary(). Check the GpuDiagnostics: skia_bridge=… line a headless screenshot run prints before concluding Skia was silent.
  • Degrades, never breaks. PULP_RENDER_HAS_SK_LOG_HANDLER requires both PULP_HAS_SKIA and __has_include("include/utils/SkLogHandler.h"), so a headers-only or pre-m153 tree compiles to unavailable_no_skia.
  • Additive to logging. Dawn's callbacks still make their runtime::log_error calls; the sink adds Perfetto instant events beside them.

tools/scripts/verify_skia_m153_capabilities.py already does the same GetInstance-then-conditionally-SetInstance dance, so the pattern is in-repo precedent rather than a new local convention. The generic installation and executor policy remain Vellum-owned; what Pulp claims is the empty slot when nobody else wants it.

For a universal macOS generation, use --platform darwin-universal --result <receipt.json> on an Apple-Silicon host. That is deliberately a dual-slice gate: arm64 runs natively and x86_64 runs through explicit Rosetta, and the single result binds both compile/link/run records to the exact universal asset, generation receipt, probe source, and checkout SHA. A successful universal product link alone does not prove these currently-unused m153 symbols exist in both slices.

Verify it's REALLY a GPU build (necessary AND sufficient)

Configure log must show BOTH:

-- Pulp: WebGPU (Dawn) enabled
-- Skia: found at <dir> (mac)

Then check the binary actually contains the GPU host (the CLAUDE.md GPU-host gotcha — a CPU-only binary returns the non-overriding MacWindowHost, so set_design_viewport / aspect-lock silently no-op):

nm build-gpu/examples/<app>/<app> 2>/dev/null | grep -q MacGpuWindowHost \
  && echo "OK: GPU host" || echo "FAIL: CPU-only"
# or for a packaged .app:  strings .../MyApp-Bin | grep -F "[gpu-host]"

A headless render_to_png(root, w, h, scale, ScreenshotBackend::skia) (or an example's --screenshot) is the cheapest proof the Skia pipeline links + runs without opening a window.

For terminal A4 native DPR measurements, a normal GPU build is insufficient: configure a separate Release build with both PULP_BENCHMARK=ON and PULP_TRACING=ON, then build only pulp-gpu-dpr-native-measurement through tools/ci/governed-build.sh. The producer opts into GPU timing on its public editor surface, refuses non-hardware Dawn adapters, and reports incomplete when timestamp queries or authentic counter evidence are unavailable. Its empirical timer floor is derived from positive samples and must distinguish five baseline trials from five trials with eight times the known GPU work; a quantized nonzero timer with no detectable control is not valid measurement. Do not reuse this dev tracing build as a shipping artifact.

Gotchas (each cost real time)

  • Windows: LNK2019 __imp_CompareObjectHandles from dawn_combined.lib(SharedFenceD3D12.cpp.obj). Dawn's D3D12 shared fence calls CompareObjectHandles, which kernel32.lib does not import. Microsoft's docs name Kernelbase.lib, but the Windows SDK on the hosted runners has no such file (LNK1181: cannot open input file 'kernelbase.lib' on every target that links Skia), so FindSkia.cmake adds the onecore umbrella import library next to dxguid on the skia::skia interface. A target that links Dawn through its own path needs the same. Dawn calls it directly, so a runtime GetProcAddress lookup would mean patching Dawn. It surfaces only in targets that pull in the D3D12 device path (GPU compute, headless surface, renderer3d), which is why other Windows targets link fine.

  • A failed fetch_skia_for_release.py is not always a bad pin. The pinned archive is a few hundred megabytes from a release CDN, fetched fresh on lanes with no warm cache, so a single transient 5xx or dropped connection there fails the whole configure. The fetcher now retries that class with backoff, so a failure that survives it is worth reading literally: a 403/404 is refused on the first attempt and means the pin names a missing or revoked asset, and an OSError is refused too because it is usually a full disk rather than the network. Check free space before re-running a fetch that failed on write.

  • On a tartci gate VM the archive may come from the host, not the CDN. When TARTCI_ARTIFACT_CACHE is set, the fetcher first copies $TARTCI_ARTIFACT_CACHE/sha256/<pinned digest> and logs Copied from host artifact cache; otherwise it logs Downloaded N bytes from <url>. The copied bytes are hashed like a download and a mismatch falls back to the network, so a pin bump is safe before the host cache holds the new archive: that job just downloads. To serve a new pin from the cache, run tartci's scripts/artifact-cache.sh add --url <asset> --sha256 <pin> on each host.

  • Release Skia archives require SK_RELEASE in every consumer, including Debug Pulp builds. The published skia-builder libraries live under lib/Release and compile Skia's inline ref-counting code with release semantics. FindSkia.cmake therefore exports SK_RELEASE from the skia::skia imported target for every backend. Without it, a Debug Pulp translation unit can compile a derived SkRefCnt destructor with SK_DEBUG, while the archive's internal_dispose() uses release behavior; teardown then traps at SkRefCnt.h:41 (fRefCnt was 0) even though ownership is correct. If a Graphite Recorder teardown hits that assertion, inspect the target's INTERFACE_COMPILE_DEFINITIONS before changing sk_sp ownership or destruction order. tools/scripts/test_findskia_arch_assert.py guards this imported-target ABI contract.

  • Generation receipts must use archive-path ordering, not host Path ordering. Windows compares path case-insensitively while ZIP/POSIX member ordering is case-sensitive. A direct comparison can therefore report payload drift even when every extracted path, SHA-256, and byte count matches. Receipt sealing must canonicalize both projections by the serialized archive path; do not weaken the underlying path/hash/size identity checks.

  • Graphite drops any raster SkImage it is handed — it never uploads one on its own. When Graphite meets a non-GPU-backed image while building a paint key it asks Recorder::clientImageProvider()->findOrCreate(), and Skia's default provider returns nothing; the draw is discarded, the draw call still reports success, and the only trace is [skia] WARNING - Couldn't convert SkImage to a Graphite-backed representation. That is why SkiaSurface installs GraphiteImageProvider (core/render/src/graphite_image_provider.hpp) on every Recorder it creates. SkiaCanvas::ensure_gpu_image pre-uploads at Pulp's own draw entry points, but it can only reach images Pulp constructs — a Skia module that decodes internally (SkSVGDOM turning <image href="data:..."> into a raster image) bypasses it entirely. If you add a code path that hands Graphite an image, do not add another manual pre-upload — the provider already covers it. Raster is the control here: an asset that composites on the raster backend and vanishes on the GPU one is this class of bug, not a bad asset. Covered by test/test_headless_surface.cpp ([headless-surface][image] / [headless-surface][svg]), which asserts the asset's own pixels, since asserting the draw call returned true is exactly what missed it.

  • SkPathBuilder::setFillType() does nothing — pass the fill type to the CONSTRUCTOR. The pinned bundle's headers and its compiled libskia.a disagree on SkPathBuilder's member layout. setFillType and fillType() are defined inline in the header, so they touch fFillType at the offset the header computes (measured: byte 400), while snapshot/detach are compiled inside the library and read the offset it computes (byte 96). The setter is therefore accepted, the builder's own getter agrees with it, and the produced SkPath still comes out kWinding — every even-odd fill renders as a solid nonzero one with nothing logged. Use SkPathBuilder b(fillType) (or SkPath::makeFillType on an already-built path); both are library-compiled and write the field the library reads. SkPath::setFillType is a different method and is unaffected. The library object is the smaller of the two (it touches 115 bytes against the header's 424), so this drops writes rather than overrunning memory — but treat any header-inline SkPathBuilder accessor that touches a member (fillType, setIsVolatile, isEmpty) as unreliable until the bundle's headers and libraries are re-cut from one revision. external/skia-build/VERSION.md notes the macOS slices come from a separate a milestone-specific min-OS re-cut while include/ is the shared drop, which is the likely origin. To re-measure after a Skia bump: construct at one fixed address via placement new (two SkPathBuilders at different addresses differ at byte 0 because STArray holds a pointer into its own inline buffer, which confounds a naive diff).

  • Skia raster FALLS BACK to CoreGraphics when libs are absent. In a CPU-only build, render_to_png(..., ScreenshotBackend::skia) produces byte-identical output to CoreGraphics (silent fallback). So a "Skia re-render" proves nothing on a headers-only checkout, and any widget that relies on a Skia/GPU shader (e.g. Canvas::draw_waveform's area fill) shows nothing on CPU — draw such fills with raster primitives (fill_rect/strokes) if they must render on both paths. Confirm Skia is real (MacGpuWindowHost present) before trusting a skia-backend render.

  • Don't mix GPU and CPU build dirs. Keep build/ CPU for the deterministic CoreGraphics visual-regression goldens (gallery / per-component) and a separate build-gpu/ for GPU work. Reconfiguring build/ with SKIA_DIR flips the default screenshot backend and will drift those goldens.

  • Reconfigure after adding a target. A new example/subdirectory needs cmake -S . -B build-gpu … again before --build --target <new> ("No rule to make target").

  • Never launch-then-kill a headless GPU window. Killing a headless Metal/Dawn process wedges the GPU for the rest of the session (see the verify-gpu-ui-via-skia-raster memory). Verify with render_to_png(skia) / --screenshot and the MacGpuWindowHost symbol check; let a human launch the live window.

  • First GPU link is slow-ish (~45s here for the view lib + Skia/Dawn link), but subsequent incremental builds are fast.

  • Linux release-path RawPtrBackupRefImpl / PartitionAddressSpace::setup_ link failures mean the Skia bundle omitted Chromium PartitionAlloc support. Current milestone Linux archives can reference Chromium BackupRefPtr / PartitionAlloc symbols from libskia.a(libskia.SkSLParser.o) even though the standalone skia-builder bundle does not ship a partition_alloc archive. FindSkia.cmake inspects libskia.a with CMAKE_NM; when those symbols are unresolved and no bundled Skia archive defines them, it appends pulp-skia-chromium-raw-ptr-compat after the Skia archive group. The source is core/canvas/src/skia_chromium_raw_ptr_compat.cpp in-tree and is installed to src/pulp/canvas/ for SDK consumers. If the failure returns, confirm the install-layout regression cmake-pulp-install-skia-compat-source passes and inspect the release asset with nm -uC libskia.a | rg 'RawPtrBackupRefImpl|PartitionAddressSpace::setup_'.

  • The external/skia-build/build symlink loop → Shipyard tree-drift. This path is materialized per-machine (a symlink into the shared ~/.cache/pulp/skia-build cache) and is untracked + .gitignored as of PR #5588 (4cd76c0f5). Before that fix it was tracked with a machine-specific absolute target that formed a two-way self-referential loop across worktrees; at CMake configure, PULP_SKIA_AUTOFETCH deletes the looped/dangling symlink to repopulate, which shipyard run/pr/ship sees as working tree changed during shipyard run (stage=configure) / D external/skia-build/build and fails validation — so the local macOS lane never posts its required macos status and the PR stays BLOCKED. Symptoms if you still hit it on a pre-#5588 checkout (the stale tracked symlink lingers until you pull main + let autofetch re-materialize): repoint the PRIMARY checkout's external/skia-build/build at a real cache — ln -sfn ~/.cache/pulp/skia-build/build external/skia-build/build — every worktree's symlink chains through the primary, so that one fix resolves all of them. --allow-tree-drift exists only on shipyard run (not pr/ship), so fixing the symlink is the durable answer, not suppressing the guard.

Emscripten / wasm: the slice is Ganesh on WebGL2, not Graphite/Dawn

The one platform where the sentence at the top of this skill ("Pulp's GPU path is Skia Graphite over Dawn") is false. The wasm-gpu slice published by skia-builder contains zero wgpu symbols and no libdawn_combined.a, so SK_GRAPHITE / SK_DAWN must NOT be defined there — FindSkia.cmake's Emscripten arm defines SK_GANESH + SK_GL instead, and tools/scripts/verify_wasm_skia_slice.py asserts that invariant so a future slice that quietly changes backends fails the CI lane instead of a demo page. Everything Ganesh-specific is confined to core/render/src/skia_surface_ganesh.cpp; nothing above the render boundary knows the backend.

Consequences worth knowing before you debug for an hour:

  • WebGL2 has no compute shaders. There is no GPU-compute path in wasm at all. GPU audio is not, and cannot be, in the browser on this slice — it would need WebGPU (emdawnwebgpu) in a worker. Never describe the browser lane as GPU-accelerated DSP; it is a GPU-rendered UI over CPU DSP.
  • No skottie/sksg in the wasm slice — its libskottie.a leaves skjson::* undefined and the zip ships no jsonreader/skresources archive. PULP_LOTTIE cannot be enabled for wasm.
  • Emscripten also sets UNIX=1. The elseif(EMSCRIPTEN) arm in FindSkia.cmake MUST precede the UNIX arm, or the probe looks for build/linux-gpu/ and reports Skia missing — which, per the top of this skill, silently degrades to a CPU-only build rather than erroring. Same ordering hazard applies to any new platform arm.

Landmine: no SK_TRIVIAL_ABI → a silently trapping link

The wasm slice is built by gn with is_trivial_abi=true. That flag changes the calling convention of sk_sp<T> (and friends) — the callee, not the caller, destroys the argument. If Pulp's TUs compile without SK_TRIVIAL_ABI, the two sides disagree about who runs the destructor, and wasm-ld does not error: it links a trapping stub for the cross-boundary call. The failure surfaces at runtime as the first frame dying with a bare, context-free:

RuntimeError: unreachable

No symbol, no stack, nothing to grep. SK_TRIVIAL_ABI is now an INTERFACE define on the skia::skia target for the Emscripten arm. If you ever see a bare RuntimeError: unreachable on the first paint of a wasm build, check that define before you suspect your own code. Any new ABI-affecting gn flag in the slice needs the same treatment.

Landmine: SkFontMgr_New_Custom_Empty returns a non-null, glyph-less fontmgr

Emscripten's font manager is the pathological case for every "do we have fonts?" guard you would naturally write:

  • it is not null, so a null-check passes;
  • it reports 1 family with 1 face, so a countFamilies() > 0 guard passes;
  • and that face has no glyphs, so every string measures at zero width.

The result is text that silently lays out to nothing — no error, no warning, just an empty UI that looks like a layout bug. Do not probe font-DB usability by counting families or null-checking the manager. Probe it by asking for an actual glyph:

// Usable iff a real typeface maps a real character to a real glyph id.
const bool usable = typeface && typeface->unicharToGlyph('A') != 0;

core/canvas/src/text_font_context.cpp does this now. The same trap applies to any host that hands you an "empty" custom fontmgr, not just wasm — bundled fonts must be registered and then proven to draw.

GPU bundles MUST be relocatable (the libwgpu_native.dylib rpath footgun)

Start debugging from the installed recipe catalog

Use pulp gpu recipes list --json or an exact --symptom filter before choosing a probe. The catalog always explains all four canonical workflows, while callable is derived from the matched native registry. In particular, Three.js metadata can be visible in a QuickJS release without claiming that the V8-only recipe can run. pulp_gpu_recipes is the read-only MCP equivalent; it projects the linked CPU-only model directly, so GPU-disabled installations do not need a sibling pulp-cpp merely to discover the catalog. GPU-disabled builds report every execution recipe non-callable, and Renderer3D stays non-callable until Scene3D is compiled. use CLI recipes scaffold only when an explicit local evidence workspace is wanted. Run two baselines and the seeded negative control before treating a pass as useful localization evidence.

The Catch2 native-recipe suite deliberately skips when no real adapter is available; this keeps GPU-less build lanes viable and is not terminal evidence. For fail-closed real-work proof, build and run the EXCLUDE_FROM_ALL pulp-gpu-probe-native-acceptance and pulp-gpu-probe-stft-native-acceptance executables on a suitable host, then validate the digest-bound receipt with gpu-probe-current-acceptance. Those surfaces reject unavailable evidence. There is no compile-time PULP_GPU_PROBE_REQUIRE_WORK policy branch in the portable unit suite.

Skia-on builds no longer link wgpu-native. core/render links the webgpu target only when the renderer has no Skia Dawn archive; with Skia's static Dawn, every WebGPU call binds inside the image to that Dawn, and staging/build-info read the same decision (pulp_wgpu_runtime_linked), so a Skia-on bundle neither ships nor loads libwgpu_native.dylib. That also removes a per-process hazard: dyld shares one @rpath/libwgpu_native.dylib by install name, so two Pulp plug-ins in one host used to run on whichever copy loaded first. The link-floor ctest native-ui-link-floor-artifacts asserts Dawn present and wgpu-native absent, with a positive control that really loads it. Gotcha: a reused build dir that once staged the dylib still has it in each bundle until that target relinks; pulp_stage_runtime_dependencies removes it POST_BUILD, so rebuild the bundle rather than trusting an old one. The rpath guidance below still applies to a no-Skia (wgpu-native fallback) build and to any other bundled runtime dylib (V8).

A GPU plugin/app built without Skia's Dawn links libwgpu_native.dylib. The upstream WebGPU FetchContent copies the dylib INTO the bundle's Contents/MacOS but rpaths the binary only at the build cache (~/Library/Caches/Pulp/fetchcontent-src/.../lib). On the build machine that path exists, so the build, codesign, notarize, auval, pluginval, and even loading in a local DAW all PASS — a false pass. Copied to any other Mac (or after the cache is cleared) the dylib isn't found:

  • standalone app crashes at launch — Library not loaded: @rpath/libwgpu_native.dylib
  • AU/VST3/CLAP show no editor / "couldn't load" in the DAW

Pulp's @loader_path-adding override only runs on the installed-SDK path, so source-built example/plugin bundles do NOT get it automatically. Fix + guard every distributable GPU bundle target with PulpBundleRelocatable.cmake:

include(${CMAKE_SOURCE_DIR}/tools/cmake/PulpBundleRelocatable.cmake)
pulp_make_bundle_relocatable(MyPlugin_CLAP)      # bakes @loader_path (BUILD_WITH_INSTALL_RPATH)
pulp_validate_bundle_relocatable(MyPlugin_CLAP)  # POST_BUILD: FAILS the build if not self-contained

tools/cmake/scripts/check_bundle_relocatable.py <bundle> --strict is the standalone validator (reads the Mach-O dependency graph — stronger than the string-based check_portable_binary.py). It rejects every non-system dependency outside the bundle closure, not only unresolved @rpath entries: @loader_path and @executable_path must normalize inside the bundle and name an existing file, while absolute external paths fail. Any copied runtime closure must also be signed, hash-pinned, and included in the immutable launch snapshot. Wire the validator into pulp ship / CI too.

pulp_add_plugin(... FORMATS AU) now adds @loader_path to the AU target without replacing any engine-specific build rpaths, then runs the strict relocatability validator after linking. Sanitizer builds are the one deliberate exception: their compiler-injected libclang_rt.*_dynamic.dylib remains in the Xcode toolchain and is accepted only when CMake detects PULP_SANITIZER and passes --allow-toolchain-runtime. Do not pass that flag to release/package validation; the validator's default must continue to reject the same external runtime.

Definitive manual proof that a bundle is self-contained — hide the build cache and confirm it still loads (this is what a string/auval check can't tell you):

CACHE=~/Library/Caches/Pulp/fetchcontent-src/wgpu-macos-aarch64-*/lib/libwgpu_native.dylib
mv "$CACHE" "$CACHE.hidden"
python3 -c "import ctypes; ctypes.CDLL('.../MyPlugin.clap/Contents/MacOS/MyPlugin')"  # loads?
mv "$CACHE.hidden" "$CACHE"

otool -l <binary> | grep -A2 LC_RPATH should show @loader_path, NOT a /Users/.../Caches/... path. Caveat for V8/other-dylib plugins: prefer an additive install_name_tool -add_rpath @loader_path over BUILD_WITH_INSTALL_RPATH (which drops ALL auto build rpaths) — see the note in PulpBundleRelocatable.cmake.

macOS x86_64 + universal builds (G3)

Pulp builds macOS arm64, x86_64, or universal (arm64;x86_64). Select with -DCMAKE_OSX_ARCHITECTURES — the whole toolchain keys off the TARGET arch, never the host CMAKE_SYSTEM_PROCESSOR (which reports the build machine and is wrong for a cross/Intel build on Apple Silicon).

  • Three Skia slices, per-slice caches. skia-builder publishes mac-arm64, mac-x86_64, and mac-universal (all three pinned in tools/deps/manifest.json → Skia release_assets; the old PulpDependencies.cmake comment "the only published mac asset" was false). Each slice's libskia.a flattens to the SAME path (build/mac-gpu/lib/Release/libskia.a), so the autofetch uses per-slice immutable cache generations under the configured cache root, keyed as <matrix-platform>-<asset-sha256>-<receipt-schema> — a mutable shared directory would silently reuse a wrong-arch archive after an arch switch. fetch_skia_for_release.py matrix keys: darwin-arm64 / darwin-x64 / darwin-universal. Legacy ~/.cache/pulp/skia-build* directories are not valid inputs unless repopulated through the current fetcher into a stamped, receipt-bound generation.
  • FindSkia fails LOUD on an arch mismatch. If SKIA_DIR points at the wrong slice, FindSkia.cmake FATALs at configure time (lipo -archs vs CMAKE_OSX_ARCHITECTURES) with "architecture mismatch … missing: <arch>" — one actionable error instead of a wall of ld64 "building for macOS-x86_64 but linking arm64" warnings + hundreds of undefined symbols.
  • FindSkia also fails LOUD when an archive requires a newer macOS than the consumer target. A matching architecture is insufficient: ld64 only warns when an object inside libskia.a or libdawn_combined.a carries a newer LC_BUILD_VERSION, leaving a deceptively successful bundle. Configure now runs pulp_assert_macos_archive_floor over every archive member and rejects a floor above CMAKE_OSX_DEPLOYMENT_TARGET. If this fires, select/rebuild the correct published slice or raise the product floor deliberately; do not suppress the check or trust the link warning. The focused regression is test/cmake/test_macos_archive_floor.cmake.
  • wgpu-native has NO universal dylib. For a universal build, Pulp fetches BOTH pinned mac wgpu zips, lipo -creates them, and — REQUIRED — codesign -f -s - re-signs the fat dylib (PulpWgpuUniversal.cmake, wired from PulpDependencies.cmake). The re-sign is not optional: a raw lipo output fails codesign --verify ("code object is not signed at all") because the per-slice adhoc linker signatures don't merge, and an unsigned arm64 dylib is killed at load. LC_ID_DYLIB is already @rpath/… and the dylib has no LC_RPATH, so the @loader_path relocatable contract above still holds.
  • No universal libv8. PULP_JS_ENGINE=v8 + a universal target is a hard FindV8.cmake FATAL (v8-builder ships only thin mac slices). Use quickjs/jsc for universal, or lipo two thin V8 builds yourself.
  • Ship the arch gate on distributable bundles. Alongside pulp_validate_bundle_relocatable, add pulp_validate_bundle_architectures(<target> [ARCHS "arm64;x86_64"]) (PulpBundleRelocatable.cmake). POST_BUILD it asserts lipo -archs == the requested set AND codesign --verify on the main binary AND every embedded dylib (libwgpu_native.dylib, libv8.dylib) — catching a thin embedded dylib in an otherwise-universal bundle (crashes on the missing arch) and an unsigned fat dylib. Standalone validator: tools/scripts/check_bundle_architectures.py <bundle> --archs arm64,x86_64 --strict.
  • Min-OS floor is the MAX across requested arches (PulpMinOs.cmake, no longer hardcoded to macos-arm64). Both arm64 and x86_64 slices stamp Skia/Dawn minos 13.0; the real cross-SDK floor is libc++ 13.4 (the macOS 15.4 SDK gates the floating-point std::to_chars overloads reached by std::format), arch-independent — so arm64, x86_64, and universal all pin 13.4 (tools/deps/min_os.json macos-arm64 + macos-x64, MEASURED with measure_min_os.py --measure, never hardcoded).
  • Ships experimental. GitHub VMs have no representative Intel GPU and Rosetta caps SIMD at SSE4.2/AVX2, so Metal-on-Intel-GPU and AVX3 dispatch are unverified until a real Intel smoke — the support-matrix experimental note.

Embedding Pulp as a submodule (standalone plugin repos)

When Pulp is consumed via add_subdirectory(pulp) from another repo (a standalone plugin like pulp-gpu-nam that pins Pulp as a git submodule), CMAKE_SOURCE_DIR is the consumer's root, not Pulp's. Anything that resolves Pulp-relative paths off CMAKE_SOURCE_DIR breaks — including FindSkia.cmake's external/skia-build autodiscovery, which would look under the consumer repo and silently fall back to no-Skia (CPU-only host, no GPU). Pulp now keys these off PULP_ROOT_DIR (a CACHE INTERNAL set to Pulp's own source dir in the root CMakeLists.txt) so submodule builds find the prebuilt Skia libs. If a submodule GPU build comes out CPU-only, confirm PULP_ROOT_DIR points at the Pulp checkout and that external/skia-build/*-gpu/lib/Release (or SKIA_DIR env) is populated there — a headers-only submodule checkout hits the same locked-raster trap as the in-tree case above.

Lottie / skottie in the bundle

The Skia bundle links skottie + sksg + svg via FindSkia.cmake's glob, but skottie also needs SkJSON (libjsonreader.a) + skresources to actually link. Bundles before Skia chrome/m151 shipped skottie's headers/archive but omitted those, so the opt-in PULP_LOTTIE try-link in core/canvas/CMakeLists.txt auto-disabled (LottieView degraded to a no-op). chrome/m151 onward ships libjsonreader.a (SkJSON) + libskresources.a, so the try-link now succeeds and LottieAnimation composites real frames. PULP_LOTTIE stays default OFF (it retains skottie in the binary); the macOS CI lane builds it ON for coverage (test_canvas.cpp pixel-tests the render, test_lottie_view.cpp the playback). If Lottie silently no-ops, check the configure line for Pulp: PULP_LOTTIE requested but this Skia bundle cannot link skottie — that means a pre-m151 (or otherwise skjson-less) bundle.

When to reach for this

Any time GPU rendering "isn't working", a window looks CPU-ish (no aspect-lock, dark fill past the design surface), a skia-backend screenshot looks identical to CoreGraphics, you need a live GPU window of native UI (e.g. the ink-signal-showcase / gpu-demo examples), or a GPU plugin/app loads on the build machine but crashes / shows no UI on another Mac (the dylib rpath footgun above).

A3 first-visible evidence

GPU startup health has two independent axes. dropped_event_count and truncated describe capture integrity; missing_trace_categories describes instrumentation coverage. Never turn a missing Vellum event into loss or a zero-valued duration. Visible Standalone/DAW/Forge campaigns bind native-compositor-presentation; only the constrained headless campaign binds headless-capture-complete. The closed A3 verifier permits nullable causal fields only for passing no-change or a budget-miss investigation with every missing event, argument, interval, and transferred route named.

Run real roles through tools/scripts/gpu_first_visible_a3_campaign.py run-role. Its adapter request fixes the role endpoint and requires 10 cold plus 10 warm trials with explicit lifecycle, process, and cache provenance; it never derives cold/warm from elapsed time. The runner snapshots the adapter and ratified budget, validates product/host/health/trace artifacts, and preserves timeout, SKIP, or INCONCLUSIVE as nonterminal. Use --require-controls on one real role to bind the blank negative and external audio-thread exclusion. The existing Standalone product test remains a one-observation wiring preflight, not a 20-trial campaign. When the adapter owns the controls, use the two receipt-producing focused test invocations in docs/validation/gpu-first-visible-a3-acceptance.md; setting a seed alone is not durable evidence. Set PULP_A3_CONTROL_SOURCE_ROOT to the clean exact Pulp revision that built both native control targets. The adapter verifies each embedded target/source/revision/blob/build identity and retains the executable and provenance refs; a copied script or shape-valid receipt fails closed.

The checked-in gpu_first_visible_a3_external_adapter.py is the reusable Pulp-owned envelope for those roles. Configure the exact role producer with PULP_A3_CAMPAIGN_PRODUCER; for the one controls run, also configure the two focused built test binaries documented in the acceptance guide. The envelope pins all three executables and fails closed on missing configuration or protocol drift. It does not provide generic present/cache instrumentation and must not be used to relabel capture completion as native presentation.

There is a second, likelier relabel. DawnGpuSurface::end_frame() emits a gpu_present span, which reads like a presentation bracket, so a steady_clock::now() after it looks like the present endpoint. It is not. That call is surface_.Present() plus instance_.ProcessEvents() -- no fence, no wait, no presented callback -- so it hands the drawable to the compositor, which displays it at a later vsync. Returning therefore precedes display by up to a refresh interval and UNDER-reports first-visible latency, making a late frame look on time.

Back-buffer capture is not the safer choice either, and the two candidates are not the same error with different magnitudes -- they miss in OPPOSITE directions, so neither bounds the other. read_current_rgba does run before the present call, but the health adapter stamps its timestamp after capture_back_buffer_png() RETURNS, which is after that present call plus PNG encode. Worse, the readback blocks on context_->submit(SyncToCpu::kYes) and then spins for up to five seconds waiting on the GPU, BETWEEN submit and present -- delaying the very present it claims to measure. Capture completion therefore OVER-reports first-visible latency, so it fails frames that met their budget instead of passing frames that did not. Nothing on any Pulp backend can currently satisfy native_presented_at: Pulp never acquires the CAMetalDrawable (no nextDrawable, presentDrawable, addPresentedHandler, or presentedTime anywhere in core/, inspect/, or apple/), so the only home for a real presented-drawable callback is where the drawable is created -- inside Dawn, behind core/render/src/metal_surface_mac.mm and gpu_surface_dawn.cpp, both framework-authoritative-transferred. Leave native_present_timing listed in missing_trace_categories rather than filling the field from either substitute. Select the checked-in standalone, constrained-headless, REAPER, or Forge role producer rather than an undocumented producer path. The adapter pins the role entry point and its checked-in support from the exact Pulp source root. Each role entry point pins the configured product, host, lifecycle driver, source-build driver, sealed source-bound trace analyzer, embedded-build verifier, and supplemental build attestation/receipt. The source-build driver receives clean exact-revision roots and a fresh output directory, never the measured product path; rebuilt executable and DAW/Forge bundle digests must match the measurement. Every lifecycle row answers a producer nonce while the exact host is alive, named replay selects that challenged trace-host PID, and all reported processes must then be gone before replay and PASS. The lifecycle driver remains an explicit external prerequisite wherever the current Dawn/AppKit/host seam cannot expose native presentation without Vellum-owned work.

Terminal A3 also requires the four-state product trace-producer overhead control from the acceptance guide. Compare the exact pre-producer parent with final-head compile-out, compiled-in idle, and active 128 MiB capture using one host, workload, build family, and source-bound driver. The compiled-in idle and active states use identical executable bytes; every state requires zero xruns and audio-thread trace events. A passing role campaign or offline A2T no-producer classification does not waive this control.

Use the acceptance guide's collect-state command for every row. It wraps the source-bound product driver with 55 independent live-process challenges, keeps compiled-in idle and active executable bytes identical, and replays active binary Perfetto traces with the exact pinned processor. Acceptance binds both the health-first-visible producer and the complete b4ba exact 20-signature state/render/js package. Acquire/submit/present are mandatory per active sample; every other signature is counted and a zero is reported not-covered, not zero-cost. Raw driver JSON or Chrome trace cannot stand in for the collector receipt. The same request pins a candidate-relative state_build_driver. The collector exports the exact row revision, rebuilds it under default-deny/no-network confinement without the measured product or ambient build tree, byte-compares the rebuilt executable, and verifies the compile-in sentinel. Retain its source archive, closed build request/receipt, product, logs, and exact toolchain snapshots; an existing Skia/Dawn build directory is not source proof.

If a validated B4 disposition routes follow-up to Vellum, test a bounded Graphite PipelineManager SkExecutor supplied through ContextOptions before designing custom prewarm. Trace pipeline queued/start/end, cache hit/miss, signature, and render wait over the exact 10-cold/10-warm workload; keep work off the audio thread and bound executor ownership, lifetime, and shutdown. Ship only a causal, material improvement, otherwise record no-change. A generic Vellum-installed SkLogHandler is a later diagnostic producer, not Horizon A.

A3 v2 terminal proof also requires digest-bound blank-frame and external audio-thread controls plus the derived four-state trace-producer overhead receipt. Keep the checked-in receipt blocked until all controls, seven campaigns, pinned trace replays, and fresh protected-main publication verification pass.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

aax

無料

Optional AAX support for Pulp, including developer-supplied Avid SDK setup, CMake enablement, DigiShell/AAX Validator workflows, and local AAX builds on macOS or Windows.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月12日 更新

Configure, implement, and test Pulp's optional desktop Ableton Link tempo-sync adapter while preserving the developer-supplied SDK, licensing, realtime, latency-compensation, and no-install boundaries.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月12日 更新

Maintain Pulp's installed design-time agent capability manifest and public-surface ledger. Use when adding, removing, renaming, or materially changing public audio, MIDI, signal, timebase, or sequence APIs; registering a new algorithm for generators; changing capability support or deprecation state; or repairing agent-capabilities freshness, schema, fingerprint, tombstone, or installed-SDK tests.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月12日 更新

android

無料

Android platform development for Pulp — NDK cross-compilation, Oboe audio, Dawn/Skia GPU rendering, JNI bridge, touch interaction, emulator workflows, and end-to-end smoke validation. Covers build, deploy, debug, and the gotchas discovered during bringup.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月12日 更新

ara

無料

Optional ARA support for Pulp, including developer-supplied ARA SDK setup, CMake enablement, adapter companion APIs, validation, and ARA-aware plugin implementation guidance.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月12日 更新

The measurement surface for ALL Pulp DSP and audio-pipeline work — read it BEFORE writing or gating DSP, not only when something already sounds wrong. Covers the C++ harness (signal generators, metrics, assertions, RenderScenario, contracts), the offline Audio Doctor (magnitude/frequency response, THD/THD+N, phase/group delay), and their Python sibling the Audio Quality Lab (tools/audio/quality-lab — null residual + alignment, LTAS log-spectral distance, spectral flux/centroid, HNR, Theil-Sen drift slope, Kaiser-sinc resampling, license-guarded corpus, regression-net ratchet). TRIGGER on AUTHORING work — "build/design an oscillator/filter/synth/effect", "add a DSP module", "what should the acceptance gate be", "how do I measure aliasing / anti-aliasing / alias floor", "null against a reference", "is this DSP correct", "choose a tolerance", "golden/regression corpus for audio", "measure drift or jitter", "A/B two renders" — AND on DEBUGGING work — "is there sound / no audio / I hear nothing", "does this filter/compressor/synth/delay produce the right signal", "prove the DSP / prove the contract", "measure the frequency response", "what's the THD / is it distorting", "what's the group delay / phase response / measured latency", "magnitude response curve", "render a test tone and assert", "audio regression", "64-frame works but 128 is silent", "sample-rate change pitch-shifted it", "describe what's in this buffer", "audio doctor", "compare before/after a DSP refactor". Reach for this BEFORE hand-rolling any FFT, null test, alias measurement, pitch tracker, or golden-render script — most of it already exists in one of the two lanes. Test/tool layer over HeadlessHost — deterministic, no audio device, no speakers. Off the realtime thread entirely.

日本語の概要は準備中です。原文の説明を表示しています。

Generous-Corp/pulp222026年10月12日 更新

Generous-Corp のスキルをすべて見る

このスキルの問題を報告する