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..fc2efe6a9214d 100644 --- a/crates/bevy_asset/src/io/wasm.rs +++ b/crates/bevy_asset/src/io/wasm.rs @@ -1,8 +1,10 @@ use crate::io::{ - get_meta_path, AssetReader, AssetReaderError, EmptyPathStream, PathStream, Reader, - ReaderRequiredFeatures, VecReader, + get_meta_path, AssetReader, AssetReaderError, AsyncRead, AsyncSeek, EmptyPathStream, + LocalStackFuture, 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::{Context, Poll}; use js_sys::{Uint8Array, JSON}; use std::path::{Path, PathBuf}; use tracing::error; @@ -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,109 @@ 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 `array`. + 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 = 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())) + } +} + +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) + }) + } +}