Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions crates/wash-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ wasi-logging = []
wasi-blobstore = []
wasi-keyvalue = []
wasi-webgpu = ["dep:wasi-webgpu-wasmtime", "dep:wasi-graphics-context-wasmtime"]
wasi-webgpu-noop = ["dep:wasi-webgpu-wasmtime", "dep:wasi-graphics-context-wasmtime"]

[dependencies]
anyhow = { workspace = true }
Expand Down Expand Up @@ -64,8 +65,8 @@ opentelemetry-semantic-conventions = { workspace = true, features = [
"semconv_experimental",
] }
opentelemetry_sdk = { workspace = true }
wasi-graphics-context-wasmtime = { git = "https://github.com/wasi-gfx/wasi-gfx-runtime.git", rev = "c9d0bf73f8db26acda2f658ad705878e2b4c3a95", optional = true }
wasi-webgpu-wasmtime = { git = "https://github.com/wasi-gfx/wasi-gfx-runtime.git", rev = "c9d0bf73f8db26acda2f658ad705878e2b4c3a95", optional = true }
wasi-graphics-context-wasmtime = { git = "https://github.com/wasi-gfx/wasi-gfx-runtime.git", rev = "fffd275ad1abb23817c720309570059d8304c61a", optional = true }
wasi-webgpu-wasmtime = { git = "https://github.com/wasi-gfx/wasi-gfx-runtime.git", rev = "fffd275ad1abb23817c720309570059d8304c61a", optional = true }

# OCI dependencies (optional, behind 'oci' feature)
docker_credential = { workspace = true, optional = true }
Expand Down
1 change: 1 addition & 0 deletions crates/wash-runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ The crate supports the following cargo features:
- `wasi-logging` (default): Logging interface
- `wasi-blobstore` (default): Blob storage interface
- `wasi-keyvalue` (default): Key-value storage interface
- `wasi-webgpu` WebGPU interface
- `oci`: OCI registry integration for pulling components

### Architecture
Expand Down
2 changes: 1 addition & 1 deletion crates/wash-runtime/src/plugin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pub mod wasi_keyvalue;
#[cfg(feature = "wasi-logging")]
pub mod wasi_logging;

#[cfg(feature = "wasi-webgpu")]
#[cfg(any(feature = "wasi-webgpu", feature = "wasi-webgpu-noop"))]
pub mod wasi_webgpu;

/// The [`HostPlugin`] trait provides an interface for implementing built-in plugins for the host.
Expand Down
27 changes: 23 additions & 4 deletions crates/wash-runtime/src/plugin/wasi_webgpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,32 @@ pub struct WasiWebGpu {

impl Default for WasiWebGpu {
fn default() -> Self {
let (backends, backend_options) = if cfg!(feature = "wasi-webgpu") {
(
wasi_webgpu_wasmtime::reexports::wgpu_types::Backends::all(),
wasi_webgpu_wasmtime::reexports::wgpu_types::BackendOptions::default(),
)
} else if cfg!(feature = "wasi-webgpu-noop") {
(
wasi_webgpu_wasmtime::reexports::wgpu_types::Backends::NOOP,
wasi_webgpu_wasmtime::reexports::wgpu_types::BackendOptions {
noop: wasi_webgpu_wasmtime::reexports::wgpu_types::NoopBackendOptions {
enable: true,
},
..Default::default()
},
)
} else {
unreachable!("wasi-webgpu or wasi-webgpu-noop feature must be enabled");
};

Self {
gpu: Arc::new(wasi_webgpu_wasmtime::reexports::wgpu_core::global::Global::new(
"webgpu",
&wasi_webgpu_wasmtime::reexports::wgpu_types::InstanceDescriptor {
backends: wasi_webgpu_wasmtime::reexports::wgpu_types::Backends::all(),
backends,
backend_options,
flags: wasi_webgpu_wasmtime::reexports::wgpu_types::InstanceFlags::from_build_config(),
backend_options: wasi_webgpu_wasmtime::reexports::wgpu_types::BackendOptions::default(),
memory_budget_thresholds: Default::default(),
},
)),
Expand All @@ -41,8 +60,8 @@ struct UiThreadSpawner;
impl wasi_webgpu_wasmtime::MainThreadSpawner for UiThreadSpawner {
async fn spawn<F, T>(&self, f: F) -> T
where
F: FnOnce() -> T + Send + Sync + 'static,
T: Send + Sync + 'static,
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
f()
}
Expand Down
197 changes: 197 additions & 0 deletions crates/wash-runtime/tests/integration_http_webgpu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
//! Integration test for HTTP and webgpu plugins working together
//!
//! This test demonstrates:
//! 1. Starting a host with HTTP and webgpu plugins
//! 2. Creating and starting a workload
//! 3. Verifying the HTTP server is listening and responding
//! 4. Confirming the component works with the webgpu plugin
//!
//! Note: This is a basic integration test that verifies plugin loading and host functionality.
//! Full component binding and request routing would require proper WIT interface configuration.
#![cfg(any(feature = "wasi-webgpu", feature = "wasi-webgpu-noop"))]

use anyhow::{Context, Result};
use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration};
use tokio::time::timeout;

mod common;
use common::find_available_port;

use wash_runtime::{
engine::Engine,
host::{
HostApi, HostBuilder,
http::{DevRouter, HttpServer},
},
plugin::wasi_webgpu::WasiWebGpu,
types::{Component, LocalResources, Workload, WorkloadStartRequest},
wit::WitInterface,
};

const HTTP_WEBGPU_WASM: &[u8] = include_bytes!("fixtures/http_webgpu.wasm");

#[tokio::test]
async fn test_http_webgpu_integration() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();

println!("Starting HTTP + Webgpu integration test");

// Create engine
let engine = Engine::builder().build()?;

// Create HTTP server plugin on a dynamically allocated port
let port = find_available_port().await?;
let addr: SocketAddr = format!("127.0.0.1:{port}").parse().unwrap();
let http_handler = DevRouter::default();
let http_plugin = HttpServer::new(http_handler, addr);

// Build host with plugins following the existing pattern from lib.rs test
let host = HostBuilder::new()
.with_engine(engine.clone())
.with_http_handler(Arc::new(http_plugin))
.with_plugin(Arc::new(WasiWebGpu::default()))?
.build()?;

println!("Created host with HTTP and webgpu plugins");

// Start the host (which starts plugins)
let host = host.start().await.context("Failed to start host")?;

println!("Host started, HTTP server listening on {addr}");

// Create a workload request with the HTTP component
let req = WorkloadStartRequest {
workload_id: uuid::Uuid::new_v4().to_string(),
workload: Workload {
namespace: "test".to_string(),
name: "test-workload".to_string(),
annotations: HashMap::new(),
service: None,
components: vec![Component {
bytes: bytes::Bytes::from_static(HTTP_WEBGPU_WASM),
local_resources: LocalResources {
memory_limit_mb: 256,
cpu_limit: 1,
config: HashMap::new(),
environment: HashMap::new(),
volume_mounts: vec![],
allowed_hosts: vec![],
},
pool_size: 1,
max_invocations: 100,
}],
host_interfaces: vec![
WitInterface {
namespace: "wasi".to_string(),
package: "http".to_string(),
interfaces: ["incoming-handler".to_string()].into_iter().collect(),
version: Some(semver::Version::parse("0.2.2").unwrap()),
config: {
let mut config = HashMap::new();
config.insert("host".to_string(), "foo".to_string());
config
},
},
WitInterface {
namespace: "wasi".to_string(),
package: "graphics-context".to_string(),
interfaces: ["graphics-context".to_string()].into_iter().collect(),
version: Some(semver::Version::parse("0.0.1").unwrap()),
config: HashMap::new(),
},
WitInterface {
namespace: "wasi".to_string(),
package: "webgpu".to_string(),
interfaces: ["webgpu".to_string()].into_iter().collect(),
version: Some(semver::Version::parse("0.0.1").unwrap()),
config: HashMap::new(),
},
],
volumes: vec![],
},
};

// Start the workload
let workload_response = host
.workload_start(req)
.await
.context("Failed to start workload")?;
println!(
"Started workload: {:?}",
workload_response.workload_status.workload_id
);

// Test 1: Make HTTP POST request with body data to the webgpu component
println!("Testing webgpu component endpoint with POST data");

let test_numbers = [1, 2, 3, 4];
let client = reqwest::Client::new();
let response = timeout(
Duration::from_secs(5),
client
.post(format!(
"http://{addr}/{}",
test_numbers
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(",")
))
.send(),
)
.await
.context("HTTP request timed out")?
.context("Failed to make HTTP request")?;

let status = response.status();
println!("HTTP Response Status: {}", status);

let response_text = response
.text()
.await
.context("Failed to read response body")?;
println!("HTTP Response Body: {}", response_text);

// The webgpu component should now work with proper plugin binding
println!("Webgpu component responded successfully");
assert!(status.is_success(), "Expected success, got {}", status);
assert!(
!response_text.trim().is_empty(),
"Expected response body content"
);
// parse the numbers from the response
let response_numbers = response_text
.chars()
.filter(|s| s == &',' || s.is_digit(10))
.collect::<String>()
.split(",")
.map(|s| s.parse::<u32>().unwrap())
.collect::<Vec<u32>>();
// expected to just double the numbers
let expected_response = expected_gpu_response(&test_numbers);
assert_eq!(
response_numbers, expected_response,
"Expected response to match the data we sent (round-trip verification)"
);
println!("Component successfully performed round-trip: POST data → webgpu → response");

// Test 2: The component itself uses webgpu, so this demonstrates both plugins working
println!("HTTP and webgpu plugins are both active and working together");

println!("All integration tests passed");
Ok(())
}

fn expected_gpu_response(numbers: &[u32]) -> Vec<u32> {
if cfg!(feature = "wasi-webgpu") {
// shader should double all numbers
numbers.iter().map(|n| n * 2).collect::<Vec<u32>>()
} else if cfg!(feature = "wasi-webgpu-noop") {
// no-op would not change the numbers
numbers.to_vec()
} else {
unreachable!("wasi-webgpu or wasi-webgpu-noop feature must be enabled");
}
}
6 changes: 6 additions & 0 deletions examples/http-webgpu/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Rust build artifacts
target/

# Wash build artifacts
build/

12 changes: 12 additions & 0 deletions examples/http-webgpu/.wash/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"build": {
"rust": {
"target": "wasm32-wasip2",
"cargo_flags": [],
"release": true,
"features": [],
"no_default_features": false
}
},
"templates": []
}
12 changes: 12 additions & 0 deletions examples/http-webgpu/.wash/wasmcloud.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# This file is automatically generated.
# It is not intended for manual editing.
version = 1

[[packages]]
name = "wasi:http"
registry = "wasi.dev"

[[packages.versions]]
requirement = "=0.2.2"
version = "0.2.2"
digest = "sha256:a1f129cdf1fde55ec2d4ae8d998c39a7e5cf7544a8bd84a831054ac0d2ac64dd"
Loading
Loading