Contributing
We welcome community contributions to this project.
Please talk with us on Discord beforehand if you’d like to contribute a larger piece of work. This is particularly important if your contribution involves adding new functionality to the host; our goal is to implement as much functionality on the guest as possible, so that the host can remain simple and enable a wide variety of use cases without being too opinionated.
Campfire
Campfire is our internal tool for working with the repository. It has several commands that can be used to help with development, which you can find by running cargo campfire --help
.
It is also aliased to cargo cf
for convenience.
Running an example can be done like this:
cargo cf run decals
By default, Campfire will build Ambient with the debug
profile. To build with the release
profile and to build the assets with --release
, use the --release
flag before the example and after:
cargo cf run --release decals -- --release
API docs
To see the latest version of the API docs, run the following command in the Ambient
repository:
cargo campfire doc api --open
Installing
As a developer, you may find yourself needing to install (a specific version of) Ambient on your system. This can be done with the following command:
cargo campfire install [--git-revision <revision>] [--git-tag <tag>] [--suffix <suffix>]
If no revision or tag is specified, the version of Ambient in the current directory will be installed as ambient-dev
. Otherwise, if specified, the relevant version will be suffixed to the executable’s name by default:
cargo campfire install --git-tag v0.1.0
This will install Ambient 0.1 as ambient-v0.1.0
.
A custom suffix can be specified with the --suffix
flag, and will override any suffix that would otherwise be used:
cargo campfire install --git-tag v0.1.0 --suffix back-in-time
This will install Ambient 0.1 as ambient-back-in-time
.
Adding to the API
Our bindings are defined in WIT, which is a language-independent interface definition language for defining WebAssembly interfaces.
They are found in the crates/wasm/wit
folder.
At present, there is only one WIT world, bindings
, in the main.wit
folder, and it is used for both the client and server bindings.
These bindings are wired up in the host using wasmtime
’s Component Model support.
The WIT interfaces generate traits, which are then implemented within crates/wasm/src/client/mod.rs and crates/wasm/src/server/mod.rs. As all interfaces need to be implemented - even when not relevant to the side of the network boundary you’re on - unused implementations go in the unused.rs
module in the same folder.
Types that are shared between interfaces should go in types.wit
; otherwise, they should go in the relevant interface file. Where possible, try to describe as much within the WIT files; the more that is specified in the WIT files, the less code needs to be written for each guest language.
If you add a new interface, you will need to expose it in main.wit
, update crates/wasm/src/shared/bindings.rs
to include it in BindingsBound
, and add implementations for the new trait in crates/wasm/src/client/mod.rs
and crates/wasm/src/server/mod.rs
.
On the host, add implementations of IntoBindgen
and FromBindgen
for your WIT type. This is typically done in a conversion.rs
. This allows you to use the type in the host code with the usual affordances, while still being able to pass it to the guest.
Guest considerations
At present, we only support Rust as a guest language, but we want to improve this in future. The following advice applies only to Rust.
The WIT bindings are automatically generated by the host’s ambient_wasm
build script. This build script runs wit-bindgen
as a library and updates guest/rust/api_core/src/internal/bindings.rs
with the generated code. You may need to build the host in order to update the guest API code after making changes to the WIT files, but running cargo check
(including through your IDE on save) should be sufficient to trigger this process.
When merging code that changes the bindings, there may be a conflict in the generated bindings.rs
file. In this case, delete the file and run cargo check -p ambient_wasm
(or a similar command that will run ambient_wasm
’s build script) to force regeneration of the file.
Where relevant/possible, use native types and convert to/from the WIT types with IntoBindgen
/FromBindgen
. This allows both API developers and users to use the type they would expect (e.g. glam::Vec3
instead of the WIT-generated Vec3
), and to extend it with additional methods where required.
This means that if you define a
record ray {
origin: vec3,
direction: vec3,
}
you should also consider defining
#![allow(unused)] fn main() { /// Some documentation struct Ray { // Per-field origin: Vec3, // Documentation direction: Vec3, } impl IntoBindgen for Ray { type Item = wit::types::Ray; fn into_bindgen(self) -> Self::Item { wit::types::Ray { origin: self.origin.into_bindgen(), direction: self.direction.into_bindgen(), } } } impl FromBindgen for wit::types::Ray { type Item = Ray; fn from_bindgen(self) -> Self::Item { Ray { origin: self.origin.from_bindgen(), direction: self.direction.from_bindgen(), } } } impl Ray { /* helper methods */ } }
so that you can provide Rust-specific features. Where possible, try to avoid exposing the WIT types to the user, and try to keep as much functionality in WIT to ensure other guest languages can benefit from it.
Adding a new supported component type
Components and their values are the core unit of data exchange in Ambient. We try to keep to a core set of types as adding more types results in some amount of bloat (especially with the amount of code generated); however, adding a new type is often the best way to represent a specific kind of data.
To do so, you will need to update the following files:
Core definitions
crates/wasm/wit/component.wit
: Add the new type to the threevalue
enums.shared_crates/primitive_component_definitions/src/lib.rs
: Add the new type to theprimitive_component_definitions
definition.
Code generation
shared_crates/package_semantic/src/value.rs
: Specify how to parse TOML for a value of the type.shared_crates/package_macro_common/src/concepts.rs
: Specify how to generate Rust code for a value of the type.
Runtime support
shared_crates/package_rt/src/message_serde.rs
: Specify how to serialize and deserialize the type to a binary stream.- If this type is defined differently between the guest and the host, use the respective files:
crates/ecs/src/message_serde.rs
guest/rust/api_core/src/message/serde.rs
- If this type is defined differently between the guest and the host, use the respective files:
Utilities
crates/wasm/src/shared/conversion.rs
: AddIntoBindgen
/FromBindgen
implementations if appropriate.guest/rust/api_core/src/internal/conversion.rs
: AddIntoBindgen
/FromBindgen
implementations if appropriate.
Documentation
CHANGELOG.md
: Document the addition of the new supported type.docs/src/reference/package.md
: Document the new type in the components section.crates/build/src/package_json.rs
: Specify how to convert the type to JSON.
Golden image tests
Golden image tests are a type of end-to-end test where a rendered image is captured and compared against an existing known-good image. This test is ran in our CI against all PRs, but you can also run it locally with cargo campfire golden-images
.
Golden images on CI
To debug why the CI fails, download the screenshots.zip
file from the build artifacts, and look in the logs of the CI.
The screenshots.zip
will show what image the CI produced.
Running golden images locally
To update golden images, run cargo campfire golden-images update
. This renders and saves a new set of golden images and replaces existing images.
To check against existing golden images, run cargo campfire golden-images check
. This renders a new set of golden images and compares against existing images using a perceptual image difference metric.
Filtering tests
Running cargo campfire golden-images --prefix ui check
will only check tests which begin with ui
prefix.
Common failures
- If your test includes anything that animates over time, this is likely to fail the golden image test because the current golden image test implementation does not attempt to exactly synchronize global time between runs. As a result, the test may never pass as the animation will never be in exactly the same pose. All tests should be static by default.
Flakiness
There are known situations where a test might fail seemingly randomly, even if the images look perceptually identical. These situations include:
- The golden image was generated on real graphics hardware - for example, on the contributor’s computer - while the CI version runs
llvmpipe
, which is a software rasterizer. This might cause small imperceptible differences. There are currently no clean solutions to this other than increasing the error threshold and/or re-generating the image usinggolden-images update
. - Timing out. Each test runs with a timeout, which may fail the test if it takes too long to produce an image. On a powerful enough local machine, this might not be an issue, but execution times are less predictable in Github Actions. In these cases, the timeout can be increased or the test can be optimized.
Releasing
- Run
cargo campfire release update-version new_version_here
to update the Ambient version across the crates and documentation. - Run
cargo campfire doc runtime
to update the documentation from the codebase. - If a new system dependency was added, ensure it is added to
docs/src/installing.md
andDockerfile
. - Run
cargo campfire package check-all
and ensure all guest packages build without errors. - Run
cargo campfire package run-all
and visually verify that they work as expected. - Use
cargo campfire release check
to check that the release is ready. - Update the
CHANGELOG.md
at the root of the repository. Copy the unreleased block, set the version and date on the copy, and then empty out the unreleased block for the next release. - Make a commit with the above changes, and create a tag
v0.X.Y
. - Push to origin.
- If this is a new major release (e.g.
0.2.0
), immediately update the version usingcargo campfire release update-version
to the next major release suffixed by dev (e.g.0.3.0-dev
) and push that up (but do not tag it). This is to disambiguate in-development major releases from stable ones. If we need to update the released version, we will branch off from the release, cherry-pick relevant hotfixes, and cut a new release from that branch. - Update the tutorial and tutorial project with the latest deployments. Ensure the version of the repository used for the website is updated, too.