[rust bmp] Reduce fixed overhead for small decodes This change adds a small memory-backed input path for Rusty BMP. When the `SkStream` already exposes memory and the encoded BMP is small (<=4KB), the codec passes the bytes directly to Rust/image-rs `BmpDecoder` instead of wrapping the stream in `SkStreamAdapter` + `BufReader`. Larger or incremental streams continue to use the existing streaming path. Local Chromium decoder-boundary benchmarks show this reduces small-image Rust overhead while preserving larger-image wins. For example, with the Chromium-side memory-backed stream prep: - 24bit_1x1.bmp: Rust improves from ~6.2 us to ~4.3 us - pal4_1x1.bmp: Rust improves from ~7.4 us to ~5.3 us - bmp-size-32x32-8bpp.bmp remains ~73% faster than legacy C++ - large 32bpp-topdown-320x240.bmp remains ~77% faster than legacy C++ Bug: 452666425 Change-Id: I1d8975f31b7a66b158e8402341399a47dbfe26f4 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1283656 Commit-Queue: Florin Malita <fmalita@google.com> Reviewed-by: Ćukasz Anforowicz <lukasza@google.com> Reviewed-by: Florin Malita <fmalita@google.com>
diff --git a/experimental/rust_bmp/decoder/SkBmpRustDecoder.cpp b/experimental/rust_bmp/decoder/SkBmpRustDecoder.cpp index 8f06bab..b3649f2 100644 --- a/experimental/rust_bmp/decoder/SkBmpRustDecoder.cpp +++ b/experimental/rust_bmp/decoder/SkBmpRustDecoder.cpp
@@ -13,7 +13,6 @@ #include "experimental/rust_bmp/decoder/impl/SkBmpRustCodec.h" #include "include/core/SkData.h" #include "include/core/SkStream.h" -#include "src/core/SkStreamPriv.h" namespace SkBmpRustDecoder { @@ -35,7 +34,7 @@ std::unique_ptr<SkCodec> Decode(sk_sp<const SkData> data, SkCodec::Result* result, SkCodecs::DecodeContext) { - return Decode(SkMemoryStream::Make(std::move(data)), result); + return SkBmpRustCodec::MakeFromData(std::move(data), result); } } // namespace SkBmpRustDecoder
diff --git a/experimental/rust_bmp/decoder/impl/SkBmpRustCodec.cpp b/experimental/rust_bmp/decoder/impl/SkBmpRustCodec.cpp index 0fcdd63..af49b30 100644 --- a/experimental/rust_bmp/decoder/impl/SkBmpRustCodec.cpp +++ b/experimental/rust_bmp/decoder/impl/SkBmpRustCodec.cpp
@@ -49,6 +49,8 @@ static_cast<int>(SkEncodedInfo::kUnpremul_Alpha), "BmpAlpha::Unpremul must match SkEncodedInfo::kUnpremul_Alpha"); +static constexpr size_t kInMemoryReaderThreshold = 4 * 1024; + namespace { // Helper function to map Rust DecodingResult to SkCodec::Result @@ -72,6 +74,17 @@ SK_ABORT("Unexpected `rust_bmp::DecodingResult`: %d", static_cast<int>(rustResult)); } +rust::Box<rust_bmp::ResultOfReader> NewReaderFromStream(SkStream* stream) { + auto inputAdapter = std::make_unique<rust::stream::SkStreamAdapter>(stream); + return rust_bmp::new_reader(std::move(inputAdapter)); +} + +rust::Box<rust_bmp::ResultOfReader> NewReaderFromData(const SkData* data) { + rust::Slice<const uint8_t> dataSlice(static_cast<const uint8_t*>(data->data()), + data->size()); + return rust_bmp::new_reader_from_data(dataSlice); +} + } // namespace std::unique_ptr<SkBmpRustCodec> SkBmpRustCodec::MakeFromStream(std::unique_ptr<SkStream> stream, @@ -86,9 +99,40 @@ return nullptr; } - auto inputAdapter = std::make_unique<rust::stream::SkStreamAdapter>(stream.get()); - rust::Box<rust_bmp::ResultOfReader> resultOfReader = - rust_bmp::new_reader(std::move(inputAdapter)); + rust::Box<rust_bmp::ResultOfReader> resultOfReader = NewReaderFromStream(stream.get()); + + return MakeFromStreamAndReader(std::move(stream), std::move(resultOfReader), result, nullptr); +} + +std::unique_ptr<SkBmpRustCodec> SkBmpRustCodec::MakeFromData(sk_sp<const SkData> data, + Result* result) { + Result resultStorage; + if (!result) { + result = &resultStorage; + } + + if (!data) { + *result = kInvalidInput; + return nullptr; + } + + if (!data->empty() && data->size() <= kInMemoryReaderThreshold) { + rust::Box<rust_bmp::ResultOfReader> resultOfReader = NewReaderFromData(data.get()); + std::unique_ptr<SkStream> stream = SkMemoryStream::Make(data); + return MakeFromStreamAndReader(std::move(stream), std::move(resultOfReader), result, + std::move(data)); + } + + return MakeFromStream(SkMemoryStream::Make(std::move(data)), result); +} + +std::unique_ptr<SkBmpRustCodec> SkBmpRustCodec::MakeFromStreamAndReader( + std::unique_ptr<SkStream> stream, + rust::Box<rust_bmp::ResultOfReader> resultOfReader, + Result* result, + sk_sp<const SkData> inMemoryData) { + SkASSERT_RELEASE(stream); + SkASSERT_RELEASE(result); rust_bmp::DecodingResult rustResult = resultOfReader->err(); if (rustResult != rust_bmp::DecodingResult::Success) { @@ -173,19 +217,22 @@ return std::unique_ptr<SkBmpRustCodec>(new SkBmpRustCodec( std::move(encodedInfo), std::move(stream), - std::move(reader) + std::move(reader), + std::move(inMemoryData) )); } SkBmpRustCodec::SkBmpRustCodec(SkEncodedInfo&& encodedInfo, std::unique_ptr<SkStream> stream, - rust::Box<rust_bmp::Reader> reader) + rust::Box<rust_bmp::Reader> reader, + sk_sp<const SkData> inMemoryData) : SkCodec(std::move(encodedInfo), skcms_PixelFormat_RGB_888, // TODO(crbug.com/370522089): Pass stream to SkCodec once SkCodec // avoids unnecessary rewinding (which forces re-reading entire stream). /* stream = */ nullptr) , fReader(std::move(reader)) - , fPrivStream(std::move(stream)) { + , fPrivStream(std::move(stream)) + , fInMemoryData(std::move(inMemoryData)) { SkASSERT_RELEASE(fPrivStream); } @@ -193,6 +240,9 @@ sk_sp<const SkData> SkBmpRustCodec::getEncodedData() const { SkASSERT_RELEASE(fPrivStream); + if (fInMemoryData) { + return fInMemoryData; + } sk_sp<const SkData> data = fPrivStream->getData(); if (data) { return data; @@ -230,9 +280,12 @@ return false; } - auto inputAdapter = std::make_unique<rust::stream::SkStreamAdapter>(fPrivStream.get()); - rust::Box<rust_bmp::ResultOfReader> resultOfReader = - rust_bmp::new_reader(std::move(inputAdapter)); + rust::Box<rust_bmp::ResultOfReader> resultOfReader = [&]() { + if (fInMemoryData) { + return NewReaderFromData(fInMemoryData.get()); + } + return NewReaderFromStream(fPrivStream.get()); + }(); if (resultOfReader->err() != rust_bmp::DecodingResult::Success) { return false;
diff --git a/experimental/rust_bmp/decoder/impl/SkBmpRustCodec.h b/experimental/rust_bmp/decoder/impl/SkBmpRustCodec.h index 1191519..a09a6cf 100644 --- a/experimental/rust_bmp/decoder/impl/SkBmpRustCodec.h +++ b/experimental/rust_bmp/decoder/impl/SkBmpRustCodec.h
@@ -13,10 +13,12 @@ #include "experimental/rust_bmp/ffi/FFI.rs.h" #include "include/codec/SkCodec.h" #include "include/codec/SkEncodedImageFormat.h" +#include "include/core/SkRefCnt.h" #include "include/core/SkSpan.h" #include "third_party/rust/cxx/v1/cxx.h" struct SkEncodedInfo; +class SkData; class SkStream; class SkSwizzler; @@ -28,13 +30,15 @@ class SkBmpRustCodec final : public SkCodec { public: static std::unique_ptr<SkBmpRustCodec> MakeFromStream(std::unique_ptr<SkStream>, Result*); + static std::unique_ptr<SkBmpRustCodec> MakeFromData(sk_sp<const SkData>, Result*); ~SkBmpRustCodec() override; protected: SkBmpRustCodec(SkEncodedInfo&&, std::unique_ptr<SkStream>, - rust::Box<rust_bmp::Reader>); + rust::Box<rust_bmp::Reader>, + sk_sp<const SkData> inMemoryData); SkEncodedImageFormat onGetEncodedFormat() const override { return SkEncodedImageFormat::kBMP; } @@ -43,6 +47,12 @@ bool onGetFrameInfo(int, FrameInfo*) const override; private: + static std::unique_ptr<SkBmpRustCodec> MakeFromStreamAndReader( + std::unique_ptr<SkStream>, + rust::Box<rust_bmp::ResultOfReader>, + Result*, + sk_sp<const SkData>); + // We cannot use the SkCodec implementation since we pass nullptr to the superclass out of // an abundance of caution w/r to rewinding the stream. // @@ -84,6 +94,7 @@ rust::Box<rust_bmp::Reader> fReader; std::unique_ptr<SkStream> fPrivStream; + sk_sp<const SkData> fInMemoryData; std::unique_ptr<SkSwizzler> fSwizzler; // Using 4-bytes-wide `uint32_t` for each pixel, because
diff --git a/experimental/rust_bmp/ffi/FFI.rs b/experimental/rust_bmp/ffi/FFI.rs index fd0ba39..f5aec6d 100644 --- a/experimental/rust_bmp/ffi/FFI.rs +++ b/experimental/rust_bmp/ffi/FFI.rs
@@ -59,6 +59,10 @@ fn new_reader(input: UniquePtr<SkStreamAdapter>) -> Box<ResultOfReader>; + /// Create a Reader from in-memory data, bypassing SkStreamAdapter and + /// BufReader. Use when stream data is fully available in memory. + fn new_reader_from_data(data: &[u8]) -> Box<ResultOfReader>; + type ResultOfReader; fn err(self: &ResultOfReader) -> DecodingResult; fn unwrap(self: &mut ResultOfReader) -> Box<Reader>; @@ -182,9 +186,7 @@ /// BMP decoder with resumable/streaming support. /// Both read_metadata() and read_image_data() can be retried on IncompleteInput. pub struct Reader { - decoder: Option< - image::codecs::bmp::BmpDecoder<std::io::BufReader<cxx::UniquePtr<ffi::SkStreamAdapter>>>, - >, + decoder: Option<image::codecs::bmp::BmpDecoder<DecoderInput>>, metadata_loaded: bool, width: u32, height: u32, @@ -196,19 +198,87 @@ image_data_loaded: bool, last_consumed_row_count: u32, } + +enum DecoderInput { + Stream(std::io::BufReader<cxx::UniquePtr<ffi::SkStreamAdapter>>), + Memory(std::io::Cursor<Vec<u8>>), +} + +impl std::io::Read for DecoderInput { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { + match self { + DecoderInput::Stream(s) => s.read(buf), + DecoderInput::Memory(c) => c.read(buf), + } + } +} + +impl std::io::BufRead for DecoderInput { + fn fill_buf(&mut self) -> std::io::Result<&[u8]> { + match self { + DecoderInput::Stream(s) => s.fill_buf(), + DecoderInput::Memory(c) => c.fill_buf(), + } + } + fn consume(&mut self, amt: usize) { + match self { + DecoderInput::Stream(s) => s.consume(amt), + DecoderInput::Memory(c) => c.consume(amt), + } + } +} + +impl std::io::Seek for DecoderInput { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> { + match self { + DecoderInput::Stream(s) => s.seek(pos), + DecoderInput::Memory(c) => c.seek(pos), + } + } + + fn stream_position(&mut self) -> std::io::Result<u64> { + match self { + DecoderInput::Stream(s) => s.stream_position(), + DecoderInput::Memory(c) => c.stream_position(), + } + } +} + impl Reader { fn new(input: cxx::UniquePtr<ffi::SkStreamAdapter>) -> Result<Self, BmpError> { use std::io::BufReader; - // Use a larger buffer (64KB) to reduce syscall overhead for large images. - // Default BufReader uses 8KB which causes many small reads. + // Keep the stream fallback at the previous size to avoid regressing + // large or incremental decodes. Complete small images bypass this path + // via new_reader_from_data(). const BUFFER_SIZE: usize = 64 * 1024; let buffered = BufReader::with_capacity(BUFFER_SIZE, input); + let input = DecoderInput::Stream(buffered); // new_resumable() creates the decoder without reading any data from the stream. // The caller must call read_metadata() to read the BMP headers. // This constructor is infallible - it just wraps the reader. - let decoder = image::codecs::bmp::BmpDecoder::new_resumable(buffered); + let decoder = image::codecs::bmp::BmpDecoder::new_resumable(input); + + Ok(Reader { + decoder: Some(decoder), + metadata_loaded: false, + width: 0, + height: 0, + color: BmpColor::RGB, + alpha: BmpAlpha::Opaque, + bytes_per_pixel: 0, + image_data: Vec::new(), + image_data_loaded: false, + last_consumed_row_count: 0, + }) + } + + /// Create a Reader from in-memory data. The bytes are copied into + /// Rust-owned storage because Reader outlives this FFI call. + fn from_data(data: &[u8]) -> Result<Self, BmpError> { + let input = DecoderInput::Memory(std::io::Cursor::new(data.to_vec())); + let decoder = image::codecs::bmp::BmpDecoder::new_resumable(input); Ok(Reader { decoder: Some(decoder), @@ -304,11 +374,12 @@ None => return DecodingResult::FormatError, }; - // Allocate zero-initialized buffer for the image data. - // Zero-initialization ensures that any unwritten bytes (e.g., on partial - // decode) are valid zeros rather than garbage. + // Reuse existing allocation if capacity is sufficient (e.g., after + // reset_decode_state which clears length but preserves capacity). + // This avoids a fresh heap allocation on rewind/re-decode. if self.image_data.len() < total_bytes { - self.image_data = vec![0u8; total_bytes]; + self.image_data.clear(); + self.image_data.resize(total_bytes, 0); } match decoder.read_image_data(&mut self.image_data) { @@ -415,6 +486,11 @@ Box::new(ResultOfReader { result }) } +pub fn new_reader_from_data(data: &[u8]) -> Box<ResultOfReader> { + let result = Reader::from_data(data); + Box::new(ResultOfReader { result }) +} + #[cfg(test)] mod tests { use super::*;