From 006cefaa97a222b5db1ade0057e6670ffcd4573a Mon Sep 17 00:00:00 2001 From: Joseph <21144246+joseph-gio@users.noreply.github.com> Date: Mon, 29 Dec 2025 12:01:16 -0800 Subject: [PATCH 1/4] remove copies from HttpWasmAssetReader --- crates/bevy_asset/Cargo.toml | 2 +- .../bevy_asset/src/io/file/sync_file_asset.rs | 6 +- crates/bevy_asset/src/io/memory.rs | 6 +- crates/bevy_asset/src/io/mod.rs | 34 ++--- crates/bevy_asset/src/io/processor_gated.rs | 5 +- crates/bevy_asset/src/io/wasm.rs | 126 +++++++++++++++++- 6 files changed, 149 insertions(+), 30 deletions(-) diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index fb19e7e841d5a..646848cf29f0f 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -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 } diff --git a/crates/bevy_asset/src/io/file/sync_file_asset.rs b/crates/bevy_asset/src/io/file/sync_file_asset.rs index 28bba3d8e0b2c..ff8c93fa633a0 100644 --- a/crates/bevy_asset/src/io/file/sync_file_asset.rs +++ b/crates/bevy_asset/src/io/file/sync_file_asset.rs @@ -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}; @@ -44,9 +44,9 @@ impl Reader for FileReader { fn read_to_end<'a>( &'a mut self, buf: &'a mut Vec, - ) -> stackfuture::StackFuture<'a, std::io::Result, { crate::io::STACK_FUTURE_SIZE }> + ) -> ConditionalSendStackFuture<'a, std::io::Result, { crate::io::STACK_FUTURE_SIZE }> { - stackfuture::StackFuture::from(async { self.0.read_to_end(buf) }) + ConditionalSendStackFuture::from(async { self.0.read_to_end(buf) }) } } diff --git a/crates/bevy_asset/src/io/memory.rs b/crates/bevy_asset/src/io/memory.rs index dd0437af4c1d6..10cd67f8de378 100644 --- a/crates/bevy_asset/src/io/memory.rs +++ b/crates/bevy_asset/src/io/memory.rs @@ -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::{ @@ -351,7 +351,7 @@ impl Reader for DataReader { fn read_to_end<'a>( &'a mut self, buf: &'a mut Vec, - ) -> stackfuture::StackFuture<'a, std::io::Result, { super::STACK_FUTURE_SIZE }> { + ) -> ConditionalSendStackFuture<'a, std::io::Result, { super::STACK_FUTURE_SIZE }> { crate::io::read_to_end(self.data.value(), &mut self.bytes_read, buf) } } diff --git a/crates/bevy_asset/src/io/mod.rs b/crates/bevy_asset/src/io/mod.rs index 220b0126b56c4..ba8c4753be2f8 100644 --- a/crates/bevy_asset/src/io/mod.rs +++ b/crates/bevy_asset/src/io/mod.rs @@ -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. @@ -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. @@ -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 @@ -164,9 +168,9 @@ pub trait Reader: AsyncRead + AsyncSeek + Unpin + Send + Sync { fn read_to_end<'a>( &'a mut self, buf: &'a mut Vec, - ) -> StackFuture<'a, std::io::Result, STACK_FUTURE_SIZE> { + ) -> ConditionalSendStackFuture<'a, std::io::Result, STACK_FUTURE_SIZE> { let future = futures_lite::AsyncReadExt::read_to_end(self, buf); - StackFuture::from(future) + ConditionalSendStackFuture::from(future) } } @@ -174,7 +178,7 @@ impl Reader for Box { fn read_to_end<'a>( &'a mut self, buf: &'a mut Vec, - ) -> StackFuture<'a, std::io::Result, STACK_FUTURE_SIZE> { + ) -> ConditionalSendStackFuture<'a, std::io::Result, STACK_FUTURE_SIZE> { (**self).read_to_end(buf) } } @@ -682,7 +686,7 @@ impl Reader for VecReader { fn read_to_end<'a>( &'a mut self, buf: &'a mut Vec, - ) -> StackFuture<'a, std::io::Result, STACK_FUTURE_SIZE> { + ) -> ConditionalSendStackFuture<'a, std::io::Result, STACK_FUTURE_SIZE> { read_to_end(&self.bytes, &mut self.bytes_read, buf) } } @@ -727,7 +731,7 @@ impl Reader for SliceReader<'_> { fn read_to_end<'a>( &'a mut self, buf: &'a mut Vec, - ) -> StackFuture<'a, std::io::Result, STACK_FUTURE_SIZE> { + ) -> ConditionalSendStackFuture<'a, std::io::Result, STACK_FUTURE_SIZE> { read_to_end(self.bytes, &mut self.bytes_read, buf) } } @@ -781,8 +785,8 @@ pub(crate) fn read_to_end<'a>( source: &'a [u8], bytes_read: &'a mut usize, dest: &'a mut Vec, -) -> StackFuture<'a, std::io::Result, STACK_FUTURE_SIZE> { - StackFuture::from(async { +) -> ConditionalSendStackFuture<'a, std::io::Result, STACK_FUTURE_SIZE> { + ConditionalSendStackFuture::from(async { if *bytes_read >= source.len() { Ok(0) } else { diff --git a/crates/bevy_asset/src/io/processor_gated.rs b/crates/bevy_asset/src/io/processor_gated.rs index 47b11b34fdbb6..19462dcd297d3 100644 --- a/crates/bevy_asset/src/io/processor_gated.rs +++ b/crates/bevy_asset/src/io/processor_gated.rs @@ -1,6 +1,7 @@ use crate::{ io::{ - AssetReader, AssetReaderError, AssetSourceId, PathStream, Reader, ReaderRequiredFeatures, + AssetReader, AssetReaderError, AssetSourceId, ConditionalSendStackFuture, PathStream, + Reader, ReaderRequiredFeatures, }, processor::{ProcessStatus, ProcessingState}, AssetPath, @@ -155,7 +156,7 @@ impl Reader for TransactionLockedReader<'_> { fn read_to_end<'a>( &'a mut self, buf: &'a mut Vec, - ) -> stackfuture::StackFuture<'a, std::io::Result, { super::STACK_FUTURE_SIZE }> { + ) -> ConditionalSendStackFuture<'a, std::io::Result, { super::STACK_FUTURE_SIZE }> { self.reader.read_to_end(buf) } } diff --git a/crates/bevy_asset/src/io/wasm.rs b/crates/bevy_asset/src/io/wasm.rs index cc62017d67058..b7eae45afef36 100644 --- a/crates/bevy_asset/src/io/wasm.rs +++ b/crates/bevy_asset/src/io/wasm.rs @@ -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; @@ -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. @@ -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> { + 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> { + 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, + ) -> LocalStackFuture<'a, std::io::Result, 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) + }) + } +} From 6f868163b172e5ed718c2743969895e5f7ea9bf3 Mon Sep 17 00:00:00 2001 From: Joseph <21144246+joseph-gio@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:39:43 -0800 Subject: [PATCH 2/4] add script to build and run example for wasm --- examples/README.md | 6 ++++++ examples/wasm/run-example.sh | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100755 examples/wasm/run-example.sh diff --git a/examples/README.md b/examples/README.md index da86eac2fbdc1..c520c04a92d48 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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. diff --git a/examples/wasm/run-example.sh b/examples/wasm/run-example.sh new file mode 100755 index 0000000000000..de844273973d7 --- /dev/null +++ b/examples/wasm/run-example.sh @@ -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" From 34087bf3be9f861762a3f8feb01d0c071f52d85e Mon Sep 17 00:00:00 2001 From: Joseph <21144246+joseph-gio@users.noreply.github.com> Date: Mon, 29 Dec 2025 21:39:22 -0800 Subject: [PATCH 3/4] use new_with_byte_offset_and_length --- crates/bevy_asset/src/io/wasm.rs | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/crates/bevy_asset/src/io/wasm.rs b/crates/bevy_asset/src/io/wasm.rs index b7eae45afef36..fc2efe6a9214d 100644 --- a/crates/bevy_asset/src/io/wasm.rs +++ b/crates/bevy_asset/src/io/wasm.rs @@ -1,14 +1,14 @@ use crate::io::{ - get_meta_path, AssetReader, AssetReaderError, AsyncRead, AsyncSeek, LocalStackFuture, - EmptyPathStream, PathStream, Reader, ReaderRequiredFeatures, SeekFrom, STACK_FUTURE_SIZE, + get_meta_path, AssetReader, AssetReaderError, AsyncRead, AsyncSeek, EmptyPathStream, + LocalStackFuture, PathStream, Reader, ReaderRequiredFeatures, SeekFrom, STACK_FUTURE_SIZE, }; use alloc::{borrow::ToOwned, boxed::Box, format, vec::Vec}; use core::pin::Pin; -use core::task::{Poll, Context}; +use core::task::{Context, Poll}; use js_sys::{Uint8Array, JSON}; use std::path::{Path, PathBuf}; use tracing::error; -use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue, UnwrapThrowExt}; +use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue}; use wasm_bindgen_futures::JsFuture; use web_sys::Response; @@ -131,7 +131,7 @@ pub struct Uint8ArrayReader { } impl Uint8ArrayReader { - /// Create a new [`Uint8ArrayReader`] for `bytes`. + /// Create a new [`Uint8ArrayReader`] for `array`. pub fn new(array: Uint8Array) -> Self { Self { initial_offset: array.byte_offset(), @@ -191,17 +191,11 @@ impl AsyncSeek for Uint8ArrayReader { }; 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(); + self.array = Uint8Array::new_with_byte_offset_and_length( + self.array.buffer().unchecked_ref(), + new_array_buffer_offset, + array_buffer_end - new_array_buffer_offset, + ); Poll::Ready(Ok((new_array_buffer_offset - self.initial_offset).into())) } } From 6f4d9743b0933af86f15b044a2be131f2c67a49c Mon Sep 17 00:00:00 2001 From: Joseph <21144246+joseph-gio@users.noreply.github.com> Date: Mon, 29 Dec 2025 21:39:40 -0800 Subject: [PATCH 4/4] Revert "add script to build and run example for wasm" This reverts commit 6f868163b172e5ed718c2743969895e5f7ea9bf3. --- examples/README.md | 6 ------ examples/wasm/run-example.sh | 19 ------------------- 2 files changed, 25 deletions(-) delete mode 100755 examples/wasm/run-example.sh diff --git a/examples/README.md b/examples/README.md index c520c04a92d48..da86eac2fbdc1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -828,12 +828,6 @@ 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. diff --git a/examples/wasm/run-example.sh b/examples/wasm/run-example.sh deleted file mode 100755 index de844273973d7..0000000000000 --- a/examples/wasm/run-example.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/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"