Expand description
English | 简体中文
§unlit_wgpu_test_util
The shared GPU test harness for the workspace’s rendering crates. Every test
drives wgpu through a real [wgpu::Device] and reads buffer or texture data
back for assertions; this crate is what sets that device up, what reads the data
back, what turns a rendered frame into a perceptual snapshot assertion, and what
runs the same test in a browser as well as on the host.
It is at an early stage of development; APIs change freely. It is not part
of the public rendering API: nothing in the shipped rendering path depends on
it. It is a dev-dependency of
unlit_wgpu
and
unlit3d,
and an optional dependency of
unlit3d_examples
behind its snapshot feature.
§One test, two runners
A test is an async fn that takes no arguments and returns nothing, and the
crate’s macros register it twice over: natively it runs in a process, and on
wasm32-unknown-unknown the suite becomes a wasm module a browser drives.
Because wgpu’s adapter and device requests are asynchronous, so is the harness.
On the host that is invisible — block_on drives them to completion. In a
browser there is nothing to block on: the page has one thread and the request
resolves on a microtask it has to yield to, so the test itself is spawned onto
the browser’s task queue and its verdict reported back through
sessionStorage.
That asymmetry is why gpu_tests! ends every file while gpu_test_main!
appears once, in the suite’s root. A file registers only what it holds:
gpu_tests! {
renders_a_cube_over_the_clear_color,
#[should_panic(expected = "morphing without targets")]
}gpu_tests! lists the tests in the file and gives each one the body beside its
name: it expands to all_tests(), that file’s share of the catalog. A crate
has one test target for its whole suite — the files are modules of it — and the
root joins those shares and emits the main that runs them:
#[path = "gpu_lit.rs"]
mod gpu_lit;
gpu_test_main!([gpu_lit::all_tests()].concat());Native test targets are declared harness = false in their Cargo.toml,
because the list is what the harness runs rather than the one rustc generates.
#[should_panic] works on both runners, but the browsers reach it differently:
the host catches the unwind, while a wasm panic traps its instance and cannot be
caught at all — so the harness installs a panic hook that compares the message
against the expectation and reports through the same channel as a pass.
A test that only the host can run carries #[host_only] in the list instead of
a target cfg around a second call to gpu_tests!. The wasm suite lists trials
from the host build but runs them in a browser, so a host-only test is compiled
out of the wasm catalog and the proxy drops its trial: both sides then list the
same names, and one list stays the whole truth about what the file declares.
Such a file goes through the catalog like any other rather than putting the
whole file behind a target cfg, so the target still compiles a wasm module —
the one wasm-bindgen is run on — with the export gpu_test_main! adds.
There is a third mode. With UNLIT3D_WASM_TEST in the environment, a native run
becomes a proxy instead of a runner: it lists every test so cargo nextest can
drive it, and each trial asks a local Node runner to run that test in a real
browser, reporting the browser’s verdict as its own. cargo xtask test-wasm
sets it.
§What it provides
Ctx::headless— a ready-to-use headless [wgpu::Device] and [wgpu::Queue]. It installs the logger backend on first use.Ctx::headless_for— the same for a namedDeviceTier. A tier narrows the device to what a less capable platform offers, so a test can reach the paths that platform takes on hardware that is not that platform.Ctx::headlessreads the tier fromUNLIT3D_DEVICE_TIER, which is how the suite is run a second time against WebGL2’s shape; unset, it asks for the WebGPU baseline rather than the adapter’s own limits. On the web the device is created from a canvas and theGLbackend, since that is what a browser offers in place of a native API.init_logging— the logger backend on its own, for tests that never build aCtx. Natively it isenv_loggerreadingRUST_LOG; on the web,console_logforwards to the browser console. The default level iswarn.GpuInfo— the device a run drew with: the tier, whether an indexed draw can usebase_vertex, and the adapter, features and limits, all of them serialized as wgpu declares them. A mismatch against a stored frame is usually one driver disagreeing with another, so this is what a failing run is read against. Thegpu-infobinary prints it, andcargo xtask print-gpuruns that; the browser pass reports the same record, which is the only way to describe the device a page was given.- Texture readback through
unlit_wgpu::readback::readback_texture, plusColorTarget,Frame,texel_bytes,bg_entry,rgb,srgb_to_linear_u8andcount_pixels_off_background. - With the
snapshotfeature: snapshot assertions, described below.
log is re-exported, so a test crate needs no log dependency of its own.
§Features
| Feature | Default | Provides |
|---|---|---|
snapshot | no | snapshot assertions via SSIMULACRA2, plus image for WebP encoding and decoding |
§Snapshots
With snapshot enabled, a test can store a rendered frame or assert it against a
stored one. assert_image_snapshot compares the frame against the snapshot named
name within DEFAULT_TOLERANCE; assert_image_snapshot_with_tolerance takes
an explicit Tolerance.
A Tolerance names up to two gates, and both must pass:
- a score floor, on SSIMULACRA2’s 0–100 scale, which notices a change spread thinly over the whole frame;
- an outlier allowance, the fraction of pixels permitted to differ from the
baseline by more than
channel_delta(8 of 255 by default), which notices a change confined to a few pixels that a whole-frame average barely registers.
Either may be left unset. A snapshot that needs a looser tolerance should carry the measurements that justify it, since a bound set without them is indistinguishable from one chosen to make a failure go away.
score_frame_webp and store_frame_webp are the underlying pieces, for a caller
that wants to report the score rather than assert.
Where the baseline comes from depends on the target, and the snapshot! macro
is what arranges it:
- Natively the name is looked up under
tests/snapshots, relative to the process’s working directory, and read at comparison time so it can be rewritten. That directory is a symlink into theunlit3d_asset_filessubmodule; clone it here withgit submodule update --init --checkout. The--checkoutis what gets past the submodule’supdate = none, which spares a project that depends on this one through git from fetching snapshots it never compares against. - On the web there is no filesystem, so the bytes are embedded into the wasm
binary at compile time with
include_bytes!, resolved relative to the file the macro is invoked from. The comparison is otherwise the same one — the frame is really decoded and really scored in the browser.
A missing snapshot is stored rather than compared, and setting
SNAPSHOT_UPDATE=1 re-stores every snapshot it touches. So a snapshot that was
never committed silently passes CI by writing itself — which is why the CI
snapshot job checks the submodule out. On the web a missing snapshot cannot
arise: the build would not have compiled.
A frame that fails its comparison is kept before the assertion fails, so a run
that fails in CI leaves the frame behind for the job to upload: natively it is
written under UNLIT3D_SNAPSHOT_MISMATCH_DIR (or target/snapshot-mismatches),
and on the web it is encoded in the browser and handed to the test runner, which
is the process that has a filesystem. A mismatch is often the platform showing
through rather than a regression — the stored frames come from one GPU stack, and
another driver’s rounding is enough to put a frame below the bar — so having the
frame is what makes the difference something to look at rather than to infer from
a score.
To re-bless a snapshot after an intentional rendering change:
SNAPSHOT_UPDATE=1 cargo nextest run -p unlit_wgpu --all-features
git -C unlit3d_asset_files diff # review before committing§Usage
use unlit_wgpu_test_util::Ctx;
let ctx = unlit_wgpu_test_util::block_on(Ctx::headless());
let target = unlit_wgpu_test_util::ColorTarget::new(&ctx.device, "example", 64, 64);
// ... render into target.view ...
let bytes = unlit_wgpu::readback::readback_texture(&ctx.device, &ctx.queue, &target.texture);
#[cfg(feature = "snapshot")]
unlit_wgpu_test_util::assert_image_snapshot(
unlit_wgpu_test_util::snapshot!("example.webp"),
&bytes,
64,
64,
);§Tests
This crate’s own tests cover the harness’s internals, such as how an outlier is
counted. The workspace’s GPU tests live beside the crates they exercise, and
cargo xtask test runs them on the host while cargo xtask test-wasm runs the
same tests in a browser. Both are described in the
root README.
§License
Dual-licensed under MIT or Apache-2.0, at your option.
Re-exports§
pub use test_catalog::TestBody;pub use test_catalog::TestEntry;pub use test_catalog::TestFn;pub use native::run;Non-WebAssembly pub use proxy::WASM_TEST_ENV;Non-WebAssembly pub use proxy::run_wasm;Non-WebAssembly pub use gpu_info::GpuInfo;pub use snapshot::DEFAULT_MIN_SCORE;snapshotpub use snapshot::DEFAULT_TOLERANCE;snapshotpub use snapshot::Snapshot;snapshotpub use snapshot::SnapshotError;snapshotpub use snapshot::Tolerance;snapshotpub use snapshot::assert_image_snapshot;snapshotpub use snapshot::assert_image_snapshot_with_tolerance;snapshotpub use snapshot::DEFAULT_MISMATCH_DIR;snapshotand non-WebAssemblypub use snapshot::MISMATCH_DIR_ENV;snapshotand non-WebAssemblypub use snapshot::score_frame_webp;snapshotand non-WebAssemblypub use snapshot::snapshot_path;snapshotand non-WebAssemblypub use snapshot::store_frame_webp;snapshotand non-WebAssemblypub(crate) use std::sync::Once;🔒pub use log;
Modules§
- gpu_
info - The device a test run got, described in wgpu’s own terms.
- native 🔒
Non-WebAssembly - The native half of the harness:
libtest-mimicover the catalog. - proxy 🔒
Non-WebAssembly - The native proxy that runs each test in a browser instead of in-process.
- snapshot 🔒
snapshot - Perceptual snapshot assertions.
- test_
catalog 🔒 - The catalog that lets one test body run under
libtest-mimicnatively and in a browser through wasm.
Macros§
- gpu_
test_ main - Provides the entry point a test binary drives its catalog through.
- gpu_
tests - Declares a file’s GPU tests, and the catalog the harness runs them from.
- snapshot
snapshot - Name a snapshot baseline, embedding its bytes where there is no filesystem.
- snapshots
snapshot - The snapshots one scene’s frames verify against, in frame order.
Structs§
- Color
Target - An offscreen RGBA8 colour target plus its default view.
- Ctx
- A ready-to-use GPU context.
- Device
Capabilities - The downlevel capabilities a frame is recorded against.
- Frame
- A read-back frame: tight RGBA8 bytes plus dimensions.
Enums§
- Device
Tier - How much of the WebGPU baseline to ask a device for.
Functions§
- bg_
entry - Pair a binding slot with a resource.
- block_
on Non-WebAssembly - Drive a future to completion on the calling thread.
- count_
pixels_ off_ background - Count pixels whose colour differs from
backgroundbeyondtolerance. - init_
logging - Install the harness’s logger backend once, for the whole test process.
- new_
instance 🔒 - The instance a context’s device comes from.
- rgb
- Opaque sRGB clear/draw colour from RGB components (alpha = 1).
- srgb_
to_ linear_ u8 - Decode an sRGB-encoded byte to linear light.
- texel_
bytes - Bytes per texel block of
texture’s format.