diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 247e89f6e9..03413d10f1 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -438,7 +438,7 @@ set(SOURCES HighResolutionTime/Performance.cpp HighResolutionTime/TimeOrigin.cpp HTML/AbstractWorker.cpp - HTML/AnimatedBitmapDecodedImageData.cpp + HTML/BitmapDecodedImageData.cpp HTML/AnimationFrameCallbackDriver.cpp HTML/AttributeNames.cpp HTML/AudioTrack.cpp @@ -655,6 +655,7 @@ set(SOURCES HTML/SessionHistoryTraversalQueue.cpp HTML/ShadowRealmGlobalScope.cpp HTML/SharedResourceRequest.cpp + HTML/AnimatedDecodedImageData.cpp HTML/SharedWorker.cpp HTML/SharedWorkerGlobalScope.cpp HTML/SourceSet.cpp diff --git a/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp index b5a743f99b..6071efa1e4 100644 --- a/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp +++ b/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp @@ -97,6 +97,7 @@ void ImageStyleValue::animate() return; m_current_frame_index = (m_current_frame_index + 1) % image_data->frame_count(); + m_current_frame_index = image_data->notify_frame_advanced(m_current_frame_index); auto current_frame_duration = image_data->frame_duration(m_current_frame_index); if (current_frame_duration != m_timer->interval()) diff --git a/Libraries/LibWeb/HTML/AnimatedBitmapDecodedImageData.cpp b/Libraries/LibWeb/HTML/AnimatedBitmapDecodedImageData.cpp deleted file mode 100644 index c9fc21955f..0000000000 --- a/Libraries/LibWeb/HTML/AnimatedBitmapDecodedImageData.cpp +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2023, Andreas Kling - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include -#include -#include -#include -#include -#include -#include - -namespace Web::HTML { - -GC_DEFINE_ALLOCATOR(AnimatedBitmapDecodedImageData); - -ErrorOr> AnimatedBitmapDecodedImageData::create(JS::Realm& realm, Vector&& frames, size_t loop_count, bool animated) -{ - return realm.create(move(frames), loop_count, animated); -} - -AnimatedBitmapDecodedImageData::AnimatedBitmapDecodedImageData(Vector&& frames, size_t loop_count, bool animated) - : m_frames(move(frames)) - , m_loop_count(loop_count) - , m_animated(animated) -{ -} - -AnimatedBitmapDecodedImageData::~AnimatedBitmapDecodedImageData() = default; - -RefPtr AnimatedBitmapDecodedImageData::bitmap(size_t frame_index, Gfx::IntSize) const -{ - if (frame_index >= m_frames.size()) - return nullptr; - return m_frames[frame_index].bitmap; -} - -int AnimatedBitmapDecodedImageData::frame_duration(size_t frame_index) const -{ - if (frame_index >= m_frames.size()) - return 0; - return m_frames[frame_index].duration; -} - -Optional AnimatedBitmapDecodedImageData::intrinsic_width() const -{ - return m_frames.first().bitmap->width(); -} - -Optional AnimatedBitmapDecodedImageData::intrinsic_height() const -{ - return m_frames.first().bitmap->height(); -} - -Optional AnimatedBitmapDecodedImageData::intrinsic_aspect_ratio() const -{ - return CSSPixels(m_frames.first().bitmap->width()) / CSSPixels(m_frames.first().bitmap->height()); -} - -Optional AnimatedBitmapDecodedImageData::frame_rect(size_t frame_index) const -{ - return m_frames[frame_index].bitmap->rect(); -} - -void AnimatedBitmapDecodedImageData::paint(DisplayListRecordingContext& context, size_t frame_index, Gfx::IntRect dst_rect, Gfx::IntRect clip_rect, Gfx::ScalingMode scaling_mode) const -{ - context.display_list_recorder().draw_scaled_immutable_bitmap(dst_rect, clip_rect, *m_frames[frame_index].bitmap, scaling_mode); -} - -} diff --git a/Libraries/LibWeb/HTML/AnimatedDecodedImageData.cpp b/Libraries/LibWeb/HTML/AnimatedDecodedImageData.cpp new file mode 100644 index 0000000000..1fcff29fd2 --- /dev/null +++ b/Libraries/LibWeb/HTML/AnimatedDecodedImageData.cpp @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2026, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::HTML { + +GC_DEFINE_ALLOCATOR(AnimatedDecodedImageData); + +HashMap>& AnimatedDecodedImageData::session_registry() +{ + static HashMap> s_registry; + return s_registry; +} + +void AnimatedDecodedImageData::install_frame_delivery_callback() +{ + static bool s_installed = false; + if (s_installed) + return; + s_installed = true; + + Platform::ImageCodecPlugin::the().on_animation_frames_decoded = [](i64 session_id, Vector> bitmaps) { + deliver_frames_for_session(session_id, move(bitmaps)); + }; + Platform::ImageCodecPlugin::the().on_animation_decode_failed = [](i64 session_id) { + auto it = session_registry().find(session_id); + if (it != session_registry().end()) { + if (auto data = it->value) + data->m_request_in_flight = false; + } + }; +} + +void AnimatedDecodedImageData::deliver_frames_for_session(i64 session_id, Vector> bitmaps) +{ + auto it = session_registry().find(session_id); + if (it == session_registry().end()) + return; + if (auto data = it->value) + data->receive_frames(move(bitmaps), data->m_last_requested_start_frame); +} + +GC::Ref AnimatedDecodedImageData::create( + JS::Realm& realm, + i64 session_id, + u32 frame_count, + u32 loop_count, + Gfx::IntSize size, + Gfx::ColorSpace color_space, + Vector durations, + Vector> initial_bitmaps) +{ + auto data = realm.create( + session_id, frame_count, loop_count, size, move(color_space), move(durations)); + + // Place initial bitmaps into the buffer pool. + for (u32 i = 0; i < initial_bitmaps.size(); ++i) { + auto& slot = data->m_buffer_slots[i % BUFFER_POOL_SIZE]; + slot.frame_index = i; + slot.bitmap = Gfx::ImmutableBitmap::create(*initial_bitmaps[i], data->m_color_space); + slot.generation = ++data->m_write_generation; + } + + data->m_highest_requested_frame = initial_bitmaps.size(); + + if (!initial_bitmaps.is_empty()) + data->m_last_displayed_bitmap = data->m_buffer_slots[0].bitmap; + + install_frame_delivery_callback(); + session_registry().set(session_id, data.ptr()); + + return data; +} + +AnimatedDecodedImageData::AnimatedDecodedImageData( + i64 session_id, + u32 frame_count, + u32 loop_count, + Gfx::IntSize size, + Gfx::ColorSpace color_space, + Vector durations) + : m_session_id(session_id) + , m_frame_count(frame_count) + , m_loop_count(loop_count) + , m_size(size) + , m_color_space(move(color_space)) + , m_durations(move(durations)) +{ +} + +AnimatedDecodedImageData::~AnimatedDecodedImageData() = default; + +void AnimatedDecodedImageData::finalize() +{ + Base::finalize(); + session_registry().remove(m_session_id); + Platform::ImageCodecPlugin::the().stop_animation_decode(m_session_id); +} + +AnimatedDecodedImageData::BufferSlot const* AnimatedDecodedImageData::find_slot(u32 frame_index) const +{ + for (auto const& slot : m_buffer_slots) { + if (slot.frame_index == frame_index && slot.bitmap) + return &slot; + } + return nullptr; +} + +AnimatedDecodedImageData::BufferSlot& AnimatedDecodedImageData::evict_oldest_slot() +{ + BufferSlot* oldest = &m_buffer_slots[0]; + for (auto& slot : m_buffer_slots) { + if (slot.generation < oldest->generation) + oldest = &slot; + } + return *oldest; +} + +RefPtr AnimatedDecodedImageData::bitmap(size_t frame_index, Gfx::IntSize) const +{ + if (frame_index >= m_frame_count) + return m_last_displayed_bitmap; + + if (auto const* slot = find_slot(frame_index)) { + m_last_displayed_bitmap = slot->bitmap; + return slot->bitmap; + } + + // Frame not in pool; return last displayed frame as fallback. + return m_last_displayed_bitmap; +} + +int AnimatedDecodedImageData::frame_duration(size_t frame_index) const +{ + if (frame_index >= m_durations.size()) + return 0; + return m_durations[frame_index]; +} + +Optional AnimatedDecodedImageData::intrinsic_width() const +{ + return m_size.width(); +} + +Optional AnimatedDecodedImageData::intrinsic_height() const +{ + return m_size.height(); +} + +Optional AnimatedDecodedImageData::intrinsic_aspect_ratio() const +{ + return CSSPixels(m_size.width()) / CSSPixels(m_size.height()); +} + +Optional AnimatedDecodedImageData::frame_rect(size_t) const +{ + return Gfx::IntRect { {}, m_size }; +} + +void AnimatedDecodedImageData::paint(DisplayListRecordingContext& context, size_t frame_index, Gfx::IntRect dst_rect, Gfx::IntRect clip_rect, Gfx::ScalingMode scaling_mode) const +{ + auto immutable_bitmap = bitmap(frame_index); + if (!immutable_bitmap) + return; + context.display_list_recorder().draw_scaled_immutable_bitmap(dst_rect, clip_rect, *immutable_bitmap, scaling_mode); +} + +void AnimatedDecodedImageData::receive_frames(Vector> bitmaps, u32 start_frame_index) +{ + m_request_in_flight = false; + + for (u32 i = 0; i < bitmaps.size(); ++i) { + u32 frame_index = start_frame_index + i; + if (frame_index >= m_frame_count) + break; + + // Check if this frame is already in the pool. + if (find_slot(frame_index)) + continue; + + auto& slot = evict_oldest_slot(); + slot.frame_index = frame_index; + slot.bitmap = Gfx::ImmutableBitmap::create(*bitmaps[i], m_color_space); + slot.generation = ++m_write_generation; + } +} + +size_t AnimatedDecodedImageData::notify_frame_advanced(size_t caller_frame_index) +{ + // We own the frame progression. Only advance when a caller reports + // the expected next frame (this deduplicates multiple callers per tick). + size_t expected_next = (m_current_frame_index + 1) % m_frame_count; + if (caller_frame_index == expected_next) { + m_current_frame_index = expected_next; + maybe_request_more_frames(m_current_frame_index); + } + return m_current_frame_index; +} + +void AnimatedDecodedImageData::maybe_request_more_frames(size_t current_frame_index) +{ + if (m_request_in_flight) + return; + + // Count how many frames ahead of current are in the pool. + u32 frames_ahead = 0; + for (u32 offset = 1; offset <= BUFFER_POOL_SIZE; ++offset) { + u32 future_index = (current_frame_index + offset) % m_frame_count; + if (find_slot(future_index)) + ++frames_ahead; + else + break; + } + + // Request more when buffer is less than half full, giving the decoder + // time to respond while we still have frames to display. + if (frames_ahead >= REQUEST_BATCH_SIZE) + return; + + // Determine which frame to request from. + u32 request_start = (current_frame_index + frames_ahead + 1) % m_frame_count; + u32 request_count = REQUEST_BATCH_SIZE; + + m_request_in_flight = true; + m_last_requested_start_frame = request_start; + m_highest_requested_frame = max(m_highest_requested_frame, request_start + request_count); + Platform::ImageCodecPlugin::the().request_animation_frames(m_session_id, request_start, request_count); +} + +} diff --git a/Libraries/LibWeb/HTML/AnimatedDecodedImageData.h b/Libraries/LibWeb/HTML/AnimatedDecodedImageData.h new file mode 100644 index 0000000000..c36d2d8db6 --- /dev/null +++ b/Libraries/LibWeb/HTML/AnimatedDecodedImageData.h @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2026, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Web::HTML { + +class AnimatedDecodedImageData final : public DecodedImageData { + GC_CELL(AnimatedDecodedImageData, DecodedImageData); + GC_DECLARE_ALLOCATOR(AnimatedDecodedImageData); + +public: + static constexpr bool OVERRIDES_FINALIZE = true; + + static GC::Ref create( + JS::Realm&, + i64 session_id, + u32 frame_count, + u32 loop_count, + Gfx::IntSize, + Gfx::ColorSpace, + Vector durations, + Vector> initial_bitmaps); + + virtual ~AnimatedDecodedImageData() override; + virtual void finalize() override; + + virtual RefPtr bitmap(size_t frame_index, Gfx::IntSize = {}) const override; + virtual int frame_duration(size_t frame_index) const override; + + virtual size_t frame_count() const override { return m_frame_count; } + virtual size_t loop_count() const override { return m_loop_count; } + virtual bool is_animated() const override { return true; } + + virtual Optional intrinsic_width() const override; + virtual Optional intrinsic_height() const override; + virtual Optional intrinsic_aspect_ratio() const override; + + virtual Optional frame_rect(size_t frame_index) const override; + virtual void paint(DisplayListRecordingContext&, size_t frame_index, Gfx::IntRect dst_rect, Gfx::IntRect clip_rect, Gfx::ScalingMode) const override; + + virtual size_t notify_frame_advanced(size_t caller_frame_index) override; + + void receive_frames(Vector>, u32 start_frame_index); + + i64 session_id() const { return m_session_id; } + + static void deliver_frames_for_session(i64 session_id, Vector>); + static void install_frame_delivery_callback(); + +private: + static HashMap>& session_registry(); + + static constexpr u32 BUFFER_POOL_SIZE = 8; + static constexpr u32 REQUEST_BATCH_SIZE = 4; + + struct BufferSlot { + Optional frame_index; + RefPtr bitmap; + u64 generation { 0 }; + }; + + AnimatedDecodedImageData( + i64 session_id, + u32 frame_count, + u32 loop_count, + Gfx::IntSize, + Gfx::ColorSpace, + Vector durations); + + BufferSlot const* find_slot(u32 frame_index) const; + BufferSlot& evict_oldest_slot(); + void maybe_request_more_frames(size_t current_frame_index); + + i64 m_session_id; + u32 m_frame_count; + u32 m_loop_count; + Gfx::IntSize m_size; + Gfx::ColorSpace m_color_space; + Vector m_durations; + + Array m_buffer_slots; + mutable RefPtr m_last_displayed_bitmap; + u64 m_write_generation { 0 }; + bool m_request_in_flight { false }; + u32 m_current_frame_index { 0 }; + u32 m_last_requested_start_frame { 0 }; + u32 m_highest_requested_frame { 0 }; +}; + +} diff --git a/Libraries/LibWeb/HTML/BitmapDecodedImageData.cpp b/Libraries/LibWeb/HTML/BitmapDecodedImageData.cpp new file mode 100644 index 0000000000..79a1f16f97 --- /dev/null +++ b/Libraries/LibWeb/HTML/BitmapDecodedImageData.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +namespace Web::HTML { + +GC_DEFINE_ALLOCATOR(BitmapDecodedImageData); + +ErrorOr> BitmapDecodedImageData::create(JS::Realm& realm, Vector&& frames, size_t loop_count, bool animated) +{ + return realm.create(move(frames), loop_count, animated); +} + +BitmapDecodedImageData::BitmapDecodedImageData(Vector&& frames, size_t loop_count, bool animated) + : m_frames(move(frames)) + , m_loop_count(loop_count) + , m_animated(animated) +{ +} + +BitmapDecodedImageData::~BitmapDecodedImageData() = default; + +RefPtr BitmapDecodedImageData::bitmap(size_t frame_index, Gfx::IntSize) const +{ + if (frame_index >= m_frames.size()) + return nullptr; + return m_frames[frame_index].bitmap; +} + +int BitmapDecodedImageData::frame_duration(size_t frame_index) const +{ + if (frame_index >= m_frames.size()) + return 0; + return m_frames[frame_index].duration; +} + +Optional BitmapDecodedImageData::intrinsic_width() const +{ + return m_frames.first().bitmap->width(); +} + +Optional BitmapDecodedImageData::intrinsic_height() const +{ + return m_frames.first().bitmap->height(); +} + +Optional BitmapDecodedImageData::intrinsic_aspect_ratio() const +{ + return CSSPixels(m_frames.first().bitmap->width()) / CSSPixels(m_frames.first().bitmap->height()); +} + +Optional BitmapDecodedImageData::frame_rect(size_t frame_index) const +{ + return m_frames[frame_index].bitmap->rect(); +} + +void BitmapDecodedImageData::paint(DisplayListRecordingContext& context, size_t frame_index, Gfx::IntRect dst_rect, Gfx::IntRect clip_rect, Gfx::ScalingMode scaling_mode) const +{ + context.display_list_recorder().draw_scaled_immutable_bitmap(dst_rect, clip_rect, *m_frames[frame_index].bitmap, scaling_mode); +} + +} diff --git a/Libraries/LibWeb/HTML/AnimatedBitmapDecodedImageData.h b/Libraries/LibWeb/HTML/BitmapDecodedImageData.h similarity index 73% rename from Libraries/LibWeb/HTML/AnimatedBitmapDecodedImageData.h rename to Libraries/LibWeb/HTML/BitmapDecodedImageData.h index f081b9f9e8..c42549ac95 100644 --- a/Libraries/LibWeb/HTML/AnimatedBitmapDecodedImageData.h +++ b/Libraries/LibWeb/HTML/BitmapDecodedImageData.h @@ -11,9 +11,9 @@ namespace Web::HTML { -class AnimatedBitmapDecodedImageData final : public DecodedImageData { - GC_CELL(AnimatedBitmapDecodedImageData, DecodedImageData); - GC_DECLARE_ALLOCATOR(AnimatedBitmapDecodedImageData); +class BitmapDecodedImageData final : public DecodedImageData { + GC_CELL(BitmapDecodedImageData, DecodedImageData); + GC_DECLARE_ALLOCATOR(BitmapDecodedImageData); public: struct Frame { @@ -21,8 +21,8 @@ public: int duration { 0 }; }; - static ErrorOr> create(JS::Realm&, Vector&&, size_t loop_count, bool animated); - virtual ~AnimatedBitmapDecodedImageData() override; + static ErrorOr> create(JS::Realm&, Vector&&, size_t loop_count, bool animated); + virtual ~BitmapDecodedImageData() override; virtual RefPtr bitmap(size_t frame_index, Gfx::IntSize = {}) const override; virtual int frame_duration(size_t frame_index) const override; @@ -39,7 +39,7 @@ public: virtual void paint(DisplayListRecordingContext&, size_t frame_index, Gfx::IntRect dst_rect, Gfx::IntRect clip_rect, Gfx::ScalingMode scaling_mode) const override; private: - AnimatedBitmapDecodedImageData(Vector&&, size_t loop_count, bool animated); + BitmapDecodedImageData(Vector&&, size_t loop_count, bool animated); Vector m_frames; size_t m_loop_count { 0 }; diff --git a/Libraries/LibWeb/HTML/DecodedImageData.h b/Libraries/LibWeb/HTML/DecodedImageData.h index b9e36d1250..defcd6500d 100644 --- a/Libraries/LibWeb/HTML/DecodedImageData.h +++ b/Libraries/LibWeb/HTML/DecodedImageData.h @@ -32,6 +32,8 @@ public: virtual size_t loop_count() const = 0; virtual bool is_animated() const = 0; + virtual size_t notify_frame_advanced(size_t frame_index) { return frame_index; } + virtual Optional intrinsic_width() const = 0; virtual Optional intrinsic_height() const = 0; virtual Optional intrinsic_aspect_ratio() const = 0; diff --git a/Libraries/LibWeb/HTML/HTMLImageElement.cpp b/Libraries/LibWeb/HTML/HTMLImageElement.cpp index 9fec879088..ea38b92bd3 100644 --- a/Libraries/LibWeb/HTML/HTMLImageElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLImageElement.cpp @@ -22,7 +22,7 @@ #include #include #include -#include +#include #include #include #include @@ -1356,6 +1356,7 @@ void HTMLImageElement::animate() } m_current_frame_index = (m_current_frame_index + 1) % image_data->frame_count(); + m_current_frame_index = image_data->notify_frame_advanced(m_current_frame_index); auto current_frame_duration = image_data->frame_duration(m_current_frame_index); if (current_frame_duration != m_animation_timer->interval()) { diff --git a/Libraries/LibWeb/HTML/ImageRequest.cpp b/Libraries/LibWeb/HTML/ImageRequest.cpp index db308563a2..99036355d2 100644 --- a/Libraries/LibWeb/HTML/ImageRequest.cpp +++ b/Libraries/LibWeb/HTML/ImageRequest.cpp @@ -11,7 +11,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/Libraries/LibWeb/HTML/SharedResourceRequest.cpp b/Libraries/LibWeb/HTML/SharedResourceRequest.cpp index 2b9a77a664..1142b79e8c 100644 --- a/Libraries/LibWeb/HTML/SharedResourceRequest.cpp +++ b/Libraries/LibWeb/HTML/SharedResourceRequest.cpp @@ -14,7 +14,8 @@ #include #include #include -#include +#include +#include #include #include #include @@ -159,14 +160,36 @@ void SharedResourceRequest::handle_successful_fetch(URL::URL const& url_string, } auto handle_successful_bitmap_decode = [strong_this = GC::Root(*this)](Web::Platform::DecodedImage& result) -> ErrorOr { - Vector frames; - for (auto& frame : result.frames) { - frames.append(AnimatedBitmapDecodedImageData::Frame { - .bitmap = Gfx::ImmutableBitmap::create(*frame.bitmap, result.color_space), - .duration = static_cast(frame.duration), - }); + if (result.session_id != 0) { + // Streaming animated decode: create AnimatedDecodedImageData. + Vector> initial_bitmaps; + initial_bitmaps.ensure_capacity(result.frames.size()); + for (auto& frame : result.frames) + initial_bitmaps.unchecked_append(*frame.bitmap); + + auto first_bitmap = result.frames.first().bitmap; + auto size = first_bitmap->size(); + + strong_this->m_image_data = AnimatedDecodedImageData::create( + strong_this->m_document->realm(), + result.session_id, + result.frame_count, + result.loop_count, + size, + result.color_space, + move(result.all_durations), + move(initial_bitmaps)); + } else { + // Single-shot decode: create BitmapDecodedImageData as before. + Vector frames; + for (auto& frame : result.frames) { + frames.append(BitmapDecodedImageData::Frame { + .bitmap = Gfx::ImmutableBitmap::create(*frame.bitmap, result.color_space), + .duration = static_cast(frame.duration), + }); + } + strong_this->m_image_data = BitmapDecodedImageData::create(strong_this->m_document->realm(), move(frames), result.loop_count, result.is_animated).release_value_but_fixme_should_propagate_errors(); } - strong_this->m_image_data = AnimatedBitmapDecodedImageData::create(strong_this->m_document->realm(), move(frames), result.loop_count, result.is_animated).release_value_but_fixme_should_propagate_errors(); strong_this->handle_successful_resource_load(); return {}; };