Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion crates/bevy_asset/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.18.0-dev", default-fea
"std",
] }

stackfuture = { version = "0.3", default-features = false }
stackfuture = { git = "https://github.com/joseph-gio/stackfuture", branch = "local-stack-future", default-features = false }
atomicow = { version = "1.1", default-features = false, features = ["std"] }
async-broadcast = { version = "0.7.2", default-features = false }
async-fs = { version = "2.0", default-features = false }
Expand Down
6 changes: 3 additions & 3 deletions crates/bevy_asset/src/io/file/sync_file_asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use futures_lite::Stream;

use crate::io::{
get_meta_path, AssetReader, AssetReaderError, AssetWriter, AssetWriterError, AsyncSeek,
PathStream, Reader, ReaderRequiredFeatures, Writer,
ConditionalSendStackFuture, PathStream, Reader, ReaderRequiredFeatures, Writer,
};

use alloc::{borrow::ToOwned, boxed::Box, vec::Vec};
Expand Down Expand Up @@ -44,9 +44,9 @@ impl Reader for FileReader {
fn read_to_end<'a>(
&'a mut self,
buf: &'a mut Vec<u8>,
) -> stackfuture::StackFuture<'a, std::io::Result<usize>, { crate::io::STACK_FUTURE_SIZE }>
) -> ConditionalSendStackFuture<'a, std::io::Result<usize>, { crate::io::STACK_FUTURE_SIZE }>
{
stackfuture::StackFuture::from(async { self.0.read_to_end(buf) })
ConditionalSendStackFuture::from(async { self.0.read_to_end(buf) })
}
}

Expand Down
6 changes: 3 additions & 3 deletions crates/bevy_asset/src/io/memory.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::io::{
AssetReader, AssetReaderError, AssetWriter, AssetWriterError, PathStream, Reader,
ReaderRequiredFeatures,
AssetReader, AssetReaderError, AssetWriter, AssetWriterError, ConditionalSendStackFuture,
PathStream, Reader, ReaderRequiredFeatures,
};
use alloc::{borrow::ToOwned, boxed::Box, sync::Arc, vec, vec::Vec};
use bevy_platform::{
Expand Down Expand Up @@ -351,7 +351,7 @@ impl Reader for DataReader {
fn read_to_end<'a>(
&'a mut self,
buf: &'a mut Vec<u8>,
) -> stackfuture::StackFuture<'a, std::io::Result<usize>, { super::STACK_FUTURE_SIZE }> {
) -> ConditionalSendStackFuture<'a, std::io::Result<usize>, { super::STACK_FUTURE_SIZE }> {
crate::io::read_to_end(self.data.value(), &mut self.bytes_read, buf)
}
}
Expand Down
34 changes: 19 additions & 15 deletions crates/bevy_asset/src/io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,19 @@ pub mod gated;

mod source;

pub use futures_io::{AsyncRead, AsyncSeek, AsyncWrite, SeekFrom};
pub use futures_lite::AsyncWriteExt;
pub use source::*;

use alloc::{boxed::Box, sync::Arc, vec::Vec};
use bevy_tasks::{BoxedFuture, ConditionalSendFuture};
use bevy_tasks::{BoxedFuture, ConditionalSend, ConditionalSendFuture};
use core::{
mem::size_of,
pin::Pin,
task::{Context, Poll},
};
use futures_io::{AsyncRead, AsyncSeek, AsyncWrite};
use futures_lite::Stream;
use std::{
io::SeekFrom,
path::{Path, PathBuf},
};
use std::path::{Path, PathBuf};
use thiserror::Error;

/// Errors that occur while loading assets.
Expand Down Expand Up @@ -131,7 +128,14 @@ pub enum SeekKind {
// a higher maximum necessary.
pub const STACK_FUTURE_SIZE: usize = 10 * size_of::<&()>();

pub use stackfuture::StackFuture;
pub use stackfuture::{LocalStackFuture, StackFuture};

#[cfg(target_arch = "wasm32")]
pub type ConditionalSendStackFuture<'a, T, const STACK_SIZE: usize> =
LocalStackFuture<'a, T, STACK_SIZE>;
#[cfg(not(target_arch = "wasm32"))]
pub type ConditionalSendStackFuture<'a, T, const STACK_SIZE: usize> =
StackFuture<'a, T, STACK_SIZE>;

/// A type returned from [`AssetReader::read`], which is used to read the contents of a file
/// (or virtual file) corresponding to an asset.
Expand All @@ -154,7 +158,7 @@ pub use stackfuture::StackFuture;
/// [`SeekKind::AnySeek`] to indicate that they may seek backward, or from the start/end. A reader
/// implementation may choose to support that, or may just detect those kinds of seeks and return an
/// error.
pub trait Reader: AsyncRead + AsyncSeek + Unpin + Send + Sync {
pub trait Reader: AsyncRead + AsyncSeek + Unpin + ConditionalSend {
/// Reads the entire contents of this reader and appends them to a vec.
///
/// # Note for implementors
Expand All @@ -164,17 +168,17 @@ pub trait Reader: AsyncRead + AsyncSeek + Unpin + Send + Sync {
fn read_to_end<'a>(
&'a mut self,
buf: &'a mut Vec<u8>,
) -> StackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
) -> ConditionalSendStackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
let future = futures_lite::AsyncReadExt::read_to_end(self, buf);
StackFuture::from(future)
ConditionalSendStackFuture::from(future)
}
}

impl Reader for Box<dyn Reader + '_> {
fn read_to_end<'a>(
&'a mut self,
buf: &'a mut Vec<u8>,
) -> StackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
) -> ConditionalSendStackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
(**self).read_to_end(buf)
}
}
Expand Down Expand Up @@ -682,7 +686,7 @@ impl Reader for VecReader {
fn read_to_end<'a>(
&'a mut self,
buf: &'a mut Vec<u8>,
) -> StackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
) -> ConditionalSendStackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
read_to_end(&self.bytes, &mut self.bytes_read, buf)
}
}
Expand Down Expand Up @@ -727,7 +731,7 @@ impl Reader for SliceReader<'_> {
fn read_to_end<'a>(
&'a mut self,
buf: &'a mut Vec<u8>,
) -> StackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
) -> ConditionalSendStackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
read_to_end(self.bytes, &mut self.bytes_read, buf)
}
}
Expand Down Expand Up @@ -781,8 +785,8 @@ pub(crate) fn read_to_end<'a>(
source: &'a [u8],
bytes_read: &'a mut usize,
dest: &'a mut Vec<u8>,
) -> StackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
StackFuture::from(async {
) -> ConditionalSendStackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
ConditionalSendStackFuture::from(async {
if *bytes_read >= source.len() {
Ok(0)
} else {
Expand Down
5 changes: 3 additions & 2 deletions crates/bevy_asset/src/io/processor_gated.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{
io::{
AssetReader, AssetReaderError, AssetSourceId, PathStream, Reader, ReaderRequiredFeatures,
AssetReader, AssetReaderError, AssetSourceId, ConditionalSendStackFuture, PathStream,
Reader, ReaderRequiredFeatures,
},
processor::{ProcessStatus, ProcessingState},
AssetPath,
Expand Down Expand Up @@ -155,7 +156,7 @@ impl Reader for TransactionLockedReader<'_> {
fn read_to_end<'a>(
&'a mut self,
buf: &'a mut Vec<u8>,
) -> stackfuture::StackFuture<'a, std::io::Result<usize>, { super::STACK_FUTURE_SIZE }> {
) -> ConditionalSendStackFuture<'a, std::io::Result<usize>, { super::STACK_FUTURE_SIZE }> {
self.reader.read_to_end(buf)
}
}
126 changes: 120 additions & 6 deletions crates/bevy_asset/src/io/wasm.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use crate::io::{
get_meta_path, AssetReader, AssetReaderError, EmptyPathStream, PathStream, Reader,
ReaderRequiredFeatures, VecReader,
get_meta_path, AssetReader, AssetReaderError, AsyncRead, AsyncSeek, LocalStackFuture,
EmptyPathStream, PathStream, Reader, ReaderRequiredFeatures, SeekFrom, STACK_FUTURE_SIZE,
};
use alloc::{borrow::ToOwned, boxed::Box, format};
use alloc::{borrow::ToOwned, boxed::Box, format, vec::Vec};
use core::pin::Pin;
use core::task::{Poll, Context};
use js_sys::{Uint8Array, JSON};
use std::path::{Path, PathBuf};
use tracing::error;
use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue};
use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue, UnwrapThrowExt};
use wasm_bindgen_futures::JsFuture;
use web_sys::Response;

Expand Down Expand Up @@ -79,8 +81,8 @@ impl HttpWasmAssetReader {
match resp.status() {
200 => {
let data = JsFuture::from(resp.array_buffer().unwrap()).await.unwrap();
let bytes = Uint8Array::new(&data).to_vec();
let reader = VecReader::new(bytes);
let bytes = Uint8Array::new(&data);
let reader = Uint8ArrayReader::new(bytes);
Ok(reader)
}
// Some web servers, including itch.io's CDN, return 403 when a requested file isn't present.
Expand Down Expand Up @@ -121,3 +123,115 @@ impl AssetReader for HttpWasmAssetReader {
Ok(false)
}
}

/// An [`AsyncRead`] implementation capable of reading a [`Uint8Array`].
pub struct Uint8ArrayReader {
array: Uint8Array,
initial_offset: u32,
}

impl Uint8ArrayReader {
/// Create a new [`Uint8ArrayReader`] for `bytes`.
pub fn new(array: Uint8Array) -> Self {
Self {
initial_offset: array.byte_offset(),
array,
}
}
}

impl AsyncRead for Uint8ArrayReader {
fn poll_read(
mut self: Pin<&mut Self>,
_cx: &mut Context,
buf: &mut [u8],
) -> Poll<futures_io::Result<usize>> {
let array_len = self.array.length();
let n = u32::min(buf.len() as u32, array_len);
self.array.subarray(0, n).copy_to(&mut buf[..n as usize]); // NOTE: copy_to will panic if the lengths do not exactly match
self.array = self.array.subarray(n, array_len);
Poll::Ready(Ok(n as usize))
}
}

impl AsyncSeek for Uint8ArrayReader {
fn poll_seek(
mut self: Pin<&mut Self>,
_cx: &mut Context,
seek_from: SeekFrom,
) -> Poll<std::io::Result<u64>> {
let array_len = self.array.length();
let current_array_buffer_offset = self.array.byte_offset();
let array_buffer_end = current_array_buffer_offset + array_len;
let new_array_buffer_offset = match seek_from {
SeekFrom::Start(from_start) => self
.initial_offset
.saturating_add(u32::try_from(from_start).unwrap_or(u32::MAX))
.min(array_buffer_end),
SeekFrom::End(from_end) => {
if from_end.is_negative() {
array_buffer_end
.saturating_sub(u32::try_from(from_end.abs()).unwrap_or(u32::MAX))
.max(self.initial_offset)
} else {
array_buffer_end
}
}
SeekFrom::Current(from_current) => {
if from_current.is_negative() {
current_array_buffer_offset
.saturating_sub(u32::try_from(from_current.abs()).unwrap_or(u32::MAX))
.max(self.initial_offset)
} else {
current_array_buffer_offset
.saturating_add(u32::try_from(from_current).unwrap_or(u32::MAX))
.min(array_buffer_end)
}
}
};
debug_assert!(new_array_buffer_offset >= self.initial_offset);
debug_assert!(new_array_buffer_offset <= array_buffer_end);
self.array = self
.array
.constructor()
.call3(
&JsValue::UNDEFINED,
&self.array.buffer(),
&new_array_buffer_offset.into(),
&array_buffer_end.into(),
)
.unwrap_throw()
.unchecked_into();
Poll::Ready(Ok((new_array_buffer_offset - self.initial_offset).into()))
}
}

impl Reader for Uint8ArrayReader {
fn read_to_end<'a>(
&'a mut self,
buf: &'a mut Vec<u8>,
) -> LocalStackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
#[expect(unsafe_code)]
LocalStackFuture::from(async {
let n = self.array.length();
let n_usize = n as usize;

buf.reserve_exact(n_usize);
let spare_capacity = buf.spare_capacity_mut();
debug_assert!(spare_capacity.len() >= n_usize);
// NOTE: `copy_to_uninit` requires the lengths to match exactly,
// and `reserve_exact` may reserve more capacity than required.
self.array.copy_to_uninit(&mut spare_capacity[..n_usize]);
// SAFETY:
// * the vector has enough spare capacity for `n` additional bytes due to `reserve_exact` above
// * the bytes have been initialized due to `copy_to_uninit` above.
unsafe {
let new_len = buf.len() + n_usize;
buf.set_len(new_len);
}
self.array = self.array.subarray(n, n);

Ok(n_usize)
})
}
}
6 changes: 6 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,12 @@ python3 -m http.server --directory examples/wasm
ruby -run -ehttpd examples/wasm
```

On Unix platforms, you can utilize the `run-example.sh` script in the wasm directory.
For example, to run the "many-foxes" example, you can run the following command from the root of the workspace:
```sh
./examples/wasm/run-example.sh many_foxes
```

##### WebGL2 and WebGPU

Bevy support for WebGPU is being worked on, but is currently experimental.
Expand Down
19 changes: 19 additions & 0 deletions examples/wasm/run-example.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/sh
set -euxo pipefail

example_name="$1"

wasm_example_dir=$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")
workspace_root=$(realpath "${wasm_example_dir}/../..")

cd "${workspace_root}"

example_output_dir="$wasm_example_dir/target/$example_name"
rm -rf "$example_output_dir"
mkdir -p "$example_output_dir"
rsync -a --exclude='target' --exclude='run-example.sh' "$wasm_example_dir/" "$example_output_dir/"

RUSTFLAGS='--cfg=web_sys_unstable_apis --cfg=getrandom_backend="wasm_js"' cargo build --release --example "$example_name" --target wasm32-unknown-unknown
wasm-bindgen --out-name wasm_example --out-dir "$example_output_dir/target" --target web "target/wasm32-unknown-unknown/release/examples/$example_name.wasm"

basic-http-server "$example_output_dir"
Loading