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 three value enums.
  • shared_crates/primitive_component_definitions/src/lib.rs: Add the new type to the primitive_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

Utilities

  • crates/wasm/src/shared/conversion.rs: Add IntoBindgen/FromBindgen implementations if appropriate.
  • guest/rust/api_core/src/internal/conversion.rs: Add IntoBindgen/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 using golden-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

  1. Run cargo campfire release update-version new_version_here to update the Ambient version across the crates and documentation.
  2. Run cargo campfire doc runtime to update the documentation from the codebase.
  3. If a new system dependency was added, ensure it is added to docs/src/installing.md and Dockerfile.
  4. Run cargo campfire package check-all and ensure all guest packages build without errors.
  5. Run cargo campfire package run-all and visually verify that they work as expected.
  6. Use cargo campfire release check to check that the release is ready.
  7. 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.
  8. Make a commit with the above changes, and create a tag v0.X.Y.
  9. Push to origin.
  10. If this is a new major release (e.g. 0.2.0), immediately update the version using cargo 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.
  11. Update the tutorial and tutorial project with the latest deployments. Ensure the version of the repository used for the website is updated, too.