Skip to main content

Crate unlit_wgpu_test_util

Crate unlit_wgpu_test_util 

Source
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 named DeviceTier. 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::headless reads the tier from UNLIT3D_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 the GL backend, 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 a Ctx. Natively it is env_logger reading RUST_LOG; on the web, console_log forwards to the browser console. The default level is warn.
  • GpuInfo — the device a run drew with: the tier, whether an indexed draw can use base_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. The gpu-info binary prints it, and cargo xtask print-gpu runs 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, plus ColorTarget, Frame, texel_bytes, bg_entry, rgb, srgb_to_linear_u8 and count_pixels_off_background.
  • With the snapshot feature: snapshot assertions, described below.

log is re-exported, so a test crate needs no log dependency of its own.

§Features

FeatureDefaultProvides
snapshotnosnapshot 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 the unlit3d_asset_files submodule; clone it here with git submodule update --init --checkout. The --checkout is what gets past the submodule’s update = 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;snapshot
pub use snapshot::DEFAULT_TOLERANCE;snapshot
pub use snapshot::Snapshot;snapshot
pub use snapshot::SnapshotError;snapshot
pub use snapshot::Tolerance;snapshot
pub use snapshot::assert_image_snapshot;snapshot
pub use snapshot::assert_image_snapshot_with_tolerance;snapshot
pub use snapshot::DEFAULT_MISMATCH_DIR;snapshot and non-WebAssembly
pub use snapshot::MISMATCH_DIR_ENV;snapshot and non-WebAssembly
pub use snapshot::score_frame_webp;snapshot and non-WebAssembly
pub use snapshot::snapshot_path;snapshot and non-WebAssembly
pub use snapshot::store_frame_webp;snapshot and non-WebAssembly
pub(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-mimic over 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-mimic natively 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.
snapshotsnapshot
Name a snapshot baseline, embedding its bytes where there is no filesystem.
snapshotssnapshot
The snapshots one scene’s frames verify against, in frame order.

Structs§

ColorTarget
An offscreen RGBA8 colour target plus its default view.
Ctx
A ready-to-use GPU context.
DeviceCapabilities
The downlevel capabilities a frame is recorded against.
Frame
A read-back frame: tight RGBA8 bytes plus dimensions.

Enums§

DeviceTier
How much of the WebGPU baseline to ask a device for.

Functions§

bg_entry
Pair a binding slot with a resource.
block_onNon-WebAssembly
Drive a future to completion on the calling thread.
count_pixels_off_background
Count pixels whose colour differs from background beyond tolerance.
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.