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.
日本語の概要は準備中です。原文の説明を表示しています。
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.
インストールする前に、エージェントに与えられる指示の中身を確認できます。
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.
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.
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"
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.
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'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:
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.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.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.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.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.
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.
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.
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:
libskottie.a leaves skjson::*
undefined and the zip ships no jsonreader/skresources archive. PULP_LOTTIE
cannot be enabled for wasm.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.SK_TRIVIAL_ABI → a silently trapping linkThe 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.
SkFontMgr_New_Custom_Empty returns a non-null, glyph-less fontmgrEmscripten's font manager is the pathological case for every "do we have fonts?" guard you would naturally write:
countFamilies() > 0 guard passes;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.
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:
Library not loaded: @rpath/libwgpu_native.dylibPulp'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.
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).
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.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.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.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.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.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.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).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.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.
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.
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).
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.
まだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
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.
日本語の概要は準備中です。原文の説明を表示しています。
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.
日本語の概要は準備中です。原文の説明を表示しています。
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.
日本語の概要は準備中です。原文の説明を表示しています。
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.
日本語の概要は準備中です。原文の説明を表示しています。
Optional ARA support for Pulp, including developer-supplied ARA SDK setup, CMake enablement, adapter companion APIs, validation, and ARA-aware plugin implementation guidance.
日本語の概要は準備中です。原文の説明を表示しています。
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.
日本語の概要は準備中です。原文の説明を表示しています。